From d256dca40bfcf9c305d177db5b6b79b75367da5c Mon Sep 17 00:00:00 2001 From: Mark Rhoades-Brown Date: Sat, 30 May 2026 20:48:40 +0100 Subject: [PATCH] feat: persist additional repo configs in vault file for cross-device sync --- README.md | 262 ++++++++++++++++++++++------------------ main.ts | 72 ++++++++++- src/types/settings.ts | 10 ++ tests/mocks/obsidian.ts | 21 +++- 4 files changed, 244 insertions(+), 121 deletions(-) diff --git a/README.md b/README.md index f920063..59eda98 100644 --- a/README.md +++ b/README.md @@ -1,155 +1,179 @@ -# GitHub Octokit - Obsidian Plugin +# GitHub Octokit Sync [![CI](https://github.com/rhoades-brown/obsidian-github/actions/workflows/ci.yml/badge.svg)](https://github.com/rhoades-brown/obsidian-github/actions/workflows/ci.yml) -Sync your Obsidian Vault with a GitHub repository using the official GitHub API (Octokit). - -This plugin provides a simple and efficient way to sync your Obsidian vault with a GitHub repository without external dependencies like Git CLI. Use GitHub as a remote backup and collaboration tool for your Obsidian vault on any device. +Sync your Obsidian vault with GitHub using the official Octokit API — no Git CLI required. Use GitHub as a remote backup and collaboration tool for your vault on any device, including mobile. ## Features -- **Two-way sync**: Pull changes from GitHub and push local changes -- **Conflict detection**: Automatically detects when files have been modified both locally and remotely -- **Visual diff view**: Side-by-side and inline diff comparison for conflicting files -- **Batch commits**: Efficiently commits multiple files in a single operation -- **Auto-sync options**: Sync on save, on interval, or on startup -- **Subfolder mapping**: Sync your vault to a specific subfolder in the repository -- **Ignore patterns**: Configure which files and folders to exclude from sync -- **Commit history**: View recent commits directly in Obsidian -- **Sync logging**: Debug sync operations with configurable logging -- **File status indicators**: See at a glance which files are added, modified, or deleted +### Sync + +- **Two-way sync** — pull changes from GitHub and push local changes in a single operation +- **Selective sync** — stage individual files or sync everything at once +- **Auto-sync** — sync on save, on a configurable interval, or on startup +- **Subfolder mapping** — sync your vault to a specific subfolder within the repository +- **Configuration sync** — optionally sync `.obsidian` settings (themes, snippets, hotkeys) across devices +- **Batch commits** — multiple file changes are committed in a single Git tree operation for efficiency + +### Multi-repo support + +- **Additional repositories** — sync extra GitHub repos into specific vault directories (e.g., a shared notes folder) +- **Independent sync** — each additional repo syncs independently with its own branch, subfolder, and ignore patterns +- **Shared config across devices** — additional repo configurations are stored in `.github-sync-repos.json` inside the vault, so they are synced with the primary repo and automatically picked up on other devices +- **Per-repo tokens** — use the main token or a separate PAT for each additional repo + +### Security + +- **Encrypted token storage** — all GitHub tokens (main and per-repo) are stored in Obsidian's encrypted `SecretStorage`, never in plaintext `data.json` +- **Auto-migration** — existing plaintext tokens are automatically migrated to `SecretStorage` on first load after upgrade + +### Conflict resolution + +- **Automatic detection** — files modified both locally and on GitHub are flagged as conflicts +- **Visual diff view** — side-by-side and inline diff comparison for conflicting files +- **Resolution options** — keep local, keep remote, or edit manually and re-sync + +### Sync panel + +- **File change overview** — files grouped by status (added, modified, deleted, conflict) with repo labels for multi-repo setups +- **Commit history** — view recent commits with author, message, and timestamp (collapsible) +- **Live logs** — real-time sync log viewer with filtering (collapsible) +- **Persistent UI state** — panel collapse states are remembered across restarts via per-vault local storage + +### Other + +- **Clipboard access** — the "Copy logs" and "Export logs" buttons write sync log text to the system clipboard (write-only; the plugin never reads from the clipboard) +- **Ignore patterns** — glob-based patterns to exclude files and folders from sync +- **Commit message templates** — customisable templates with `{date}` and `{action}` variables +- **Status bar** — live sync status indicator; click to open the sync panel +- **Ribbon icon** — left-click to sync, right-click for quick actions (pull, push, open panel, settings) +- **Configurable logging** — adjustable log level (debug/info/warn/error) with optional file persistence +- **Mobile compatible** — works on iOS and Android (not desktop-only) ## Prerequisites -- Obsidian -- A GitHub account +- Obsidian v1.12.7 or later +- A GitHub account with a Personal Access Token ## Installation -> **Note**: Please make a backup copy of your vault before installing this plugin. This plugin is still in early development and there may be bugs. +> **Note**: Back up your vault before installing. This plugin is in early development. -### From Obsidian Community Plugins (Coming Soon) +### From Obsidian Community Plugins -1. Open Settings → Community Plugins -2. Search for "GitHub Octokit" -3. Click Install, then Enable +1. Open **Settings → Community plugins → Browse** +2. Search for "GitHub Octokit Sync" +3. Select **Install**, then **Enable** -### BETA (BRAT) Installation +### BRAT (for beta releases, not recommended) 1. Install [BRAT](https://github.com/TfTHacker/obsidian42-brat) -2. Open Settings → Community Plugins → BRAT -3. Click "Add beta plugin" -4. Enter this repository `rhoades-brown\obsidian-github`. -5. Choose 'latest' as the version. -6. Click "Add Plugin" +2. Open **Settings → Community plugins → BRAT** +3. Select **Add beta plugin** +4. Enter `rhoades-brown/obsidian-github` +5. Choose **latest** as the version +6. Select **Add Plugin** -### Manual Installation +### Manual -1. Download `main.js`, `manifest.json`, and `styles.css` from the latest release -2. Create a folder `/.obsidian/plugins/github-octokit/` +1. Download `main.js`, `manifest.json`, and `styles.css` from the [latest release](https://github.com/rhoades-brown/obsidian-github/releases) +2. Create `/.obsidian/plugins/github-octokit/` 3. Copy the downloaded files into this folder -4. Reload Obsidian and enable the plugin in Settings → Community Plugins +4. Reload Obsidian and enable the plugin in **Settings → Community plugins** ## Setup -### 1. Generate a GitHub Personal Access Token (PAT) - -1. Go to [GitHub Settings → Developer Settings → Personal Access Tokens](https://github.com/settings/tokens) -2. Click "Generate new token (classic)" -3. Give it a descriptive name (e.g., "Obsidian Vault Sync") -4. Select scopes: - - `repo` (Full control of private repositories) -5. Click "Generate token" and copy it +### 1. Generate a GitHub Personal Access Token -### 2. Configure the Plugin +1. Go to [GitHub → Settings → Developer settings → Personal access tokens](https://github.com/settings/tokens) +2. Select **Generate new token (classic)** +3. Name it (e.g., "Obsidian Vault Sync") and select the `repo` scope +4. Select **Generate token** and copy it -1. Open Obsidian Settings → GitHub Octokit -2. Paste your Personal Access Token -3. Click "Connect" to authenticate -4. Select a repository from the dropdown -5. Optionally configure: - - Branch name (default: main) - - Vault subfolder to sync - - Sync triggers (on save, interval, startup) - - Commit message template +### 2. Configure the plugin -## Usage - -### Manual Sync +1. Open **Settings → GitHub Octokit Sync** +2. Paste your token and select **Connect** +3. Select a repository from the dropdown +4. Optionally configure branch, subfolder, sync triggers, and commit message template -- **Command Palette**: Press `Ctrl/Cmd + P` and search for: - - "GitHub Octokit: Sync now" - Full bidirectional sync - - "GitHub Octokit: Pull from GitHub" - Download remote changes - - "GitHub Octokit: Push to GitHub" - Upload local changes +### 3. Add additional repositories (optional) -- **Ribbon Icon**: Click the GitHub icon in the left ribbon, or right-click for quick actions +1. In settings, scroll to **Additional repositories** +2. Select **Add** and enter the repo owner, name, branch, and local vault directory +3. Choose whether to use the main token or a separate one +4. The configuration is saved to `.github-sync-repos.json` in your vault and synced automatically -- **Status Bar**: Click the sync status in the bottom-right to open the sync panel - -### Sync Panel +## Usage -Open the sync panel via: +### Commands -- Command: "GitHub Octokit: Open sync panel" -- Right-click the ribbon icon → "Open Sync Panel" -- Click the status bar indicator +Open the command palette (`Ctrl/Cmd + P`) and search for: -The sync panel shows: +| Command | Description | +| ------- | ----------- | +| **Sync now** | Full bidirectional sync | +| **Pull from GitHub** | Download remote changes only | +| **Push to GitHub** | Upload local changes only | +| **Open sync panel** | Open the sidebar sync panel | +| **Open sync modal** | Open the sync modal | +| **Open diff view** | Open the diff comparison view | +| **View sync conflicts** | Jump to conflicts in the sync panel | +| **Open GitHub settings** | Open plugin settings | -- Files with changes (grouped by status) -- Conflict resolution options -- Recent commit history +### Ribbon and status bar -### Resolving Conflicts +- **Left-click** the GitHub ribbon icon to sync +- **Right-click** for quick actions (pull, push, open panel, settings) +- **Click the status bar** indicator to open the sync panel -When a file has been modified both locally and on GitHub: +### Resolving conflicts -1. The file appears in the "Conflicts" section -2. Click "Diff" to see a side-by-side comparison +1. Conflicting files appear in the sync panel under **Conflicts** +2. Select **Diff** to see a side-by-side comparison 3. Choose a resolution: - - **Keep Local**: Use your local version - - **Keep Remote**: Use the GitHub version - - **Manual**: Edit the file yourself, then sync again + - **Keep local** — use your version + - **Keep remote** — use the GitHub version + - **Manual** — edit the file yourself, then sync again -## Configuration Options +## Configuration | Setting | Description | -|---------|-------------| -| **GitHub Token** | Your Personal Access Token | +| ------- | ----------- | +| **GitHub token** | Your Personal Access Token (stored encrypted) | | **Repository** | The GitHub repo to sync with | | **Branch** | Branch name (default: main) | -| **Vault Subfolder** | Only sync files in this folder | -| **Sync on Save** | Auto-sync when you save a file | -| **Sync on Interval** | Sync automatically every X minutes | -| **Sync on Startup** | Sync when Obsidian opens | -| **Commit Message** | Template for commit messages. Variables: `{date}`, `{action}` | -| **Conflict Strategy** | Default resolution: ask, keep-local, keep-remote, keep-both | -| **Status Bar** | Show/hide sync status in status bar | -| **Notifications** | Show/hide sync notifications | -| **Enable Logging** | Log sync operations for debugging | -| **Log Level** | Minimum log level: debug, info, warn, error | -| **Persist Logs** | Save logs to a file in your vault | - -## Ignore Patterns - -By default, the following are excluded from sync: - -- `.obsidian/workspace.json` -- `.obsidian/workspace-mobile.json` -- `.obsidian/github-sync-metadata.json` -- `.git/**` -- `.gitignore` - -Add custom patterns in Settings → GitHub Octokit → Ignore Patterns: - -- `*.log` - All log files -- `private/**` - Everything in the private folder -- `*.tmp` - All .tmp files -- `.obsidian/**` - All Obsidian settings (if desired) +| **Subfolder path** | Sync only a subfolder of the remote repo | +| **Sync configuration** | Include `.obsidian` settings in sync | +| **Sync on save** | Auto-sync when you modify a file (debounced) | +| **Sync on interval** | Sync every N minutes | +| **Sync on startup** | Sync when Obsidian opens | +| **Commit message** | Template with `{date}` and `{action}` variables | +| **Conflict strategy** | Default resolution: manual, keep-local, keep-remote, keep-both | +| **Status bar** | Show/hide the sync status indicator | +| **Notifications** | Show/hide sync result notices | +| **Logging** | Enable/disable, set level, optionally persist to file | + +## Ignore patterns + +The following paths are always excluded from sync: + +- `.obsidian/plugins/**` — plugin files are managed separately +- `.obsidian/workspace.json` and `workspace-mobile.json` — machine-specific +- `.git/**`, `.gitignore` + +Add custom glob patterns in **Settings → Ignore patterns**: + +```text +*.log +private/** +*.tmp +drafts/wip-* +``` ## Development & contributing -See [DEVELOPMENT.md](DEVELOPMENT.md) for build instructions, project structure, conventional commit guidelines, and the automated versioning workflow. +See [DEVELOPMENT.md](DEVELOPMENT.md) for build instructions, project structure, conventional commit guidelines, and the automated CI/CD versioning workflow. ## Troubleshooting @@ -157,31 +181,31 @@ See [DEVELOPMENT.md](DEVELOPMENT.md) for build instructions, project structure, - Verify your token has the `repo` scope - Check if the token has expired -- Try generating a new token +- Generate a new token and re-connect ### "Rate limit exceeded" -- GitHub limits API requests (5000/hour for authenticated users) -- Wait for the reset time shown in the notification +- GitHub allows 5,000 API requests per hour for authenticated users +- The notification shows when the limit resets - Reduce sync frequency in settings ### "Conflict detected" - Open the sync panel to view and resolve conflicts -- Use the diff view to compare versions +- Use the diff view to compare local and remote versions ### Files keep re-syncing -- Check if sync state is being preserved (enable logging to debug) -- Verify line endings are consistent (plugin normalizes to LF) -- Ensure ignore patterns are correctly configured +- Enable debug logging to inspect sync state +- Verify line endings are consistent (the plugin normalises to LF) +- Check that ignore patterns are correctly configured -### Debug with Logs +### Debug with logs -1. Enable logging in Settings → GitHub Octokit → Logging -2. Set log level to "Debug" for verbose output -3. Open the debug console (macOS → cmd+option+i; Windows → ctrl+shift+i) to see real-time logs -4. Or click "View Logs" in settings to see recent entries +1. Enable logging in **Settings → Logging** +2. Set log level to **Debug** for verbose output +3. Open the developer console (`Cmd+Option+I` on macOS, `Ctrl+Shift+I` on Windows) for real-time logs +4. Or select **View logs** in settings to see recent entries in-app ## License diff --git a/main.ts b/main.ts index 427951e..274bc5d 100644 --- a/main.ts +++ b/main.ts @@ -5,7 +5,7 @@ import { LoggerService } from './src/services/loggerService'; import { DiffView, DIFF_VIEW_TYPE } from './src/views/DiffView'; import { SyncView, SYNC_VIEW_TYPE } from './src/views/SyncView'; import { GitHubOctokitSettingTab, SyncModal } from './src/ui'; -import { GitHubOctokitSettings, DEFAULT_SETTINGS, AdditionalRepoConfig } from './src/types/settings'; +import { GitHubOctokitSettings, DEFAULT_SETTINGS, AdditionalRepoConfig, VaultRepoConfig, VAULT_REPOS_CONFIG_PATH } from './src/types/settings'; /** Per-repo runtime state for additional repositories */ export interface AdditionalRepoRuntime { @@ -594,6 +594,9 @@ export default class GitHubOctokitPlugin extends Plugin { void _ignored2; this.settings = Object.assign({}, DEFAULT_SETTINGS, settingsData); + // Load additional repo configs from vault file and merge + await this.loadVaultRepoConfig(); + // Migrate main token from plaintext data.json to SecretStorage const storedSecret = this.app.secretStorage.getSecret('github-pat'); if (storedSecret) { @@ -653,6 +656,73 @@ export default class GitHubOctokitPlugin extends Plugin { syncState: data.syncState, additionalRepoStates: data.additionalRepoStates, }); + + // Persist repo configs to vault file so they sync with the primary repo + await this.saveVaultRepoConfig(); + } + + /** + * Load additional repo configs from the vault file (.github-sync-repos.json). + * Merges vault-file entries with any existing entries in data.json, + * using the vault file as the source of truth for structural config. + * Tokens are resolved separately from SecretStorage. + */ + private async loadVaultRepoConfig(): Promise { + try { + const exists = await this.app.vault.adapter.exists(VAULT_REPOS_CONFIG_PATH); + if (!exists) return; + + const raw = await this.app.vault.adapter.read(VAULT_REPOS_CONFIG_PATH); + const vaultConfigs = JSON.parse(raw) as VaultRepoConfig[]; + if (!Array.isArray(vaultConfigs)) return; + + // Build a map of existing settings repos by id for token lookup + const existingById = new Map(); + for (const repo of this.settings.additionalRepos) { + existingById.set(repo.id, repo); + } + + // Merge: vault file is source of truth for config, + // existing settings provide the token (which will be resolved from SecretStorage later) + this.settings.additionalRepos = vaultConfigs.map(vc => { + const existing = existingById.get(vc.id); + return { + ...vc, + token: existing?.token ?? '', + }; + }); + } catch { + // File doesn't exist or is malformed — no-op, use data.json repos + } + } + + /** + * Save additional repo configs to the vault file (.github-sync-repos.json). + * Strips tokens before writing so secrets never end up in the vault. + * Uses the Vault API (create/modify) so the file is tracked in the vault + * index and included in sync operations. + */ + private async saveVaultRepoConfig(): Promise { + const vaultConfigs: VaultRepoConfig[] = this.settings.additionalRepos.map( + ({ token: _token, ...rest }) => { + void _token; + return rest; + } + ); + + const json = JSON.stringify(vaultConfigs, null, '\t'); + + try { + const existing = this.app.vault.getAbstractFileByPath(VAULT_REPOS_CONFIG_PATH); + if (existing instanceof TFile) { + await this.app.vault.modify(existing, json); + } else if (vaultConfigs.length > 0) { + // Only create the file if there are repos to persist + await this.app.vault.create(VAULT_REPOS_CONFIG_PATH, json); + } + } catch (error) { + console.error('Failed to save vault repo config:', error); + } } async loadSyncState() { diff --git a/src/types/settings.ts b/src/types/settings.ts index 717bee5..daf70e2 100644 --- a/src/types/settings.ts +++ b/src/types/settings.ts @@ -76,6 +76,16 @@ export interface AdditionalRepoConfig { enabled: boolean; } +/** + * Vault-persisted repo config — same as AdditionalRepoConfig but without the + * `token` field which is kept in SecretStorage per-device. + * Stored in VAULT_REPOS_CONFIG_PATH so it syncs with the primary repository. + */ +export type VaultRepoConfig = Omit; + +/** Path to the vault file that stores additional repo configurations */ +export const VAULT_REPOS_CONFIG_PATH = '.github-sync-repos.json'; + /** Main plugin settings */ export interface GitHubOctokitSettings { // Authentication diff --git a/tests/mocks/obsidian.ts b/tests/mocks/obsidian.ts index 0374587..1c3ac2d 100644 --- a/tests/mocks/obsidian.ts +++ b/tests/mocks/obsidian.ts @@ -18,8 +18,27 @@ export class App { } } +export class DataAdapter { + private rawFiles: Map = new Map(); + + async exists(path: string): Promise { + return this.rawFiles.has(path); + } + + async read(path: string): Promise { + const content = this.rawFiles.get(path); + if (content === undefined) throw new Error(`File not found: ${path}`); + return content; + } + + async write(path: string, data: string): Promise { + this.rawFiles.set(path, data); + } +} + export class Vault { private files: Map = new Map(); + adapter: DataAdapter = new DataAdapter(); async read(file: TFile): Promise { return this.files.get(file.path) || ''; @@ -49,7 +68,7 @@ export class Vault { return Array.from(this.files.keys()).map(p => new TFile(p)); } - async createFolder(path: string): Promise { + async createFolder(_path: string): Promise { // Mock folder creation } }