Skip to content

1.8.0 — sync scope: exclude folders and files, or sync only what you pick

Latest

Choose a tag to compare

@yanqingwang yanqingwang released this 23 Oct 16:19
· 1 commit to main since this release

This release adds the sync scope: decide which parts of the vault a device is allowed to publish, instead of the whole vault or nothing.

1. Two exclusion lists, one selection list, four name switches

Control Meaning
Excluded folders Never staged or pushed. Folder-boundary matching: Archive covers Archive/**, never Archive-old or notes/Archive.
Excluded files Never staged or pushed, by exact vault-relative path: Inbox/token.md. For the case where the folder is fine to share and one file in it is not.
Sync only these folders Empty = the whole vault. Otherwise only what is under the listed folders is staged.
Exclude dot-prefixed files .env, .DS_Store. On by default.
Exclude dot-prefixed folders .obsidian, .archive and everything inside. On by default.
Exclude underscore-prefixed files / folders _draft.md, _templates/. Off by default, because these are usually real notes.

The four name switches are independent: turning off the dotfile rule syncs .env without syncing .obsidian.

Exclusion always outranks selection. A whitelisted folder cannot rescue a path that is excluded, and .git, .trash and this plugin's own folder stay excluded whatever you type. Both list rows take paths by hand or through a picker — the folder picker offers folders, the file picker offers files.

2. Scope is applied after staging, not through .gitignore

The obvious implementation writes your exclusions into .gitignore. That breaks moves: git sees a file appear in an ignored folder and a file vanish, so a rename becomes a deletion pushed to every other device.

So the plugin stages, then inspects the staged list (git diff --cached --name-status --find-renames -z) and resets what falls outside scope. Because the pair is still visible, a move into an excluded folder stays a move and the old remote copy survives. A heavily rewritten same-name move is caught by a second guard, and Obsidian's .trash is deliberately not an archive: moving a tracked note there publishes the deletion, while the trash copy itself is never committed.

The generated .gitignore is gone. Older versions wrote a .* block into it, which is why turning dotfile syncing back on never seemed to work. On load, that block is migrated to a comment; anything you wrote around it is preserved. A .* rule you wrote still hides dotfiles from git, because that file is yours — the plugin removes only what it generated. The root .gitignore stays syncable in every mode as a repository control file.

3. Excluding never deletes

A file that was already tracked when you excluded it — by folder or by name — stays in the repository. Its local edits stop being pushed, and a local deletion is unstaged. Publishing a removal has to be a decision:

git rm --cached -- Archive        # then let the plugin commit and push

One consequence worth knowing: those unstaged edits leave the working tree dirty, so a later pull that needs the same file can be refused by git. The plugin reports that instead of discarding your edit.

Scope decides what this device stages. It is not a download filter and not a history rewrite — commits already on the remote still arrive on other devices, and excluding a folder here does not erase it from another clone.

4. Submission requirements

  • The description in manifest.json was 296 characters. Obsidian's limit is 250, and it asks for an action statement without stray punctuation, so it is now 228.
  • One credential check asserted on the machine (does this laptop's git helper answer for github.com) rather than on the plugin. It reports a skip with the reason now; the plugin-side assertions in that check still run.
  • This release exists as a GitHub release with main.js, manifest.json and styles.css, which is what the directory needs in order to scan and install it.

Tests: 82 checks

sh tests/run.sh     # needs nothing but Node and git
File Covers Checks
tests/sync.test.js Two devices on one bare remote: local-only, remote-only, clean divergence, conflict rollback, rebase, force, blocked merge, re-entrancy 8
tests/settings.test.js Declarative definitions and search index, read/write, path-list normalization, folder and file pickers, validation, timer lifecycle, log path 20
tests/credentials.test.js Credential resolution, SecretStorage missing/empty, the generated helper holding no token, git credential fill itself, configDir shapes 17
tests/safety.test.js Repository-root refusal, force-push backups and recovery, backup pruning, credentials in a remote URL, size-skip patterns released 15
tests/scope.test.js Folder-boundary matching, excluded folders, excluded files, sync-only lists and the rule that exclusions outrank them, the four name switches, legacy .gitignore migration, tracked files, renames, .trash 22

Nothing here runs against a mocked git. A merge that silently conflicts, or a deletion that rides in on a rename, is invisible to a stub — so every scenario builds real repositories and a real bare remote, drives the real binary, and then asks git ls-files and git show HEAD:<file> what actually reached the repository.

Installing

Download main.js, manifest.json, and styles.css into <vault>/.obsidian/plugins/vault-force-sync/, then enable the plugin. minAppVersion is unchanged at 0.15.0; the scope settings are all empty/on-default, so an existing vault keeps syncing exactly what it synced before until you add a rule.

Docs: README.md · 中文:README.zh-CN.md