Skip to content

1.7.0 — data safety: never touch the wrong repository, force pushes are recoverable

Choose a tag to compare

@yanqingwang yanqingwang released this 17 Oct 08:22
· 4 commits to main since this release

This release is about the one thing that has to hold: pointing this plugin at a vault should never be the reason you lose notes. Four ways it could still cost data are closed, each with a test that drives the real git binary.

1. It could operate on the wrong repository entirely

git rev-parse --is-inside-work-tree returns true for a vault merely nested inside a project, and git add -A run from a subdirectory stages the entire repository, not the subdirectory.

Measured on a real nested layout, the two together staged files outside the vault — and staged deletions among them. Anyone who keeps their vault inside a notes or projects repository, which is common, would have had that whole project committed and pushed to whatever origin it happened to have. Worse, the plugin's existing check passed, so nothing looked wrong.

The vault must now be the root of its repository. Anything else is refused, with both paths named:

Refusing to sync: the vault is not the root of its git repository.

  vault:      /path/to/vault
  repository: /path/to/project

This plugin stages and commits the whole repository, so syncing here would
publish every file in that project -- including files outside the vault.
Give the vault its own repository (`git init` inside it) and sync that instead.

The check runs before .gitignore is written, so refusing leaves the directory exactly as it was found. Pull remote changes only and Show sync status refuse too — neither should read or write a foreign repository.

If you are upgrading and syncing stops. That is the check firing. Your vault is inside another repository; give it its own with git init in the vault and point Remote URL at that repository.

2. A force push could discard commits nothing else pointed at

--force-with-lease guards against a remote that moved unexpectedly. It does not make the commits you are about to discard recoverable — they stayed only until git next ran gc.

The remote's state is now snapshotted to refs/vault-force-sync/<timestamp> before any force push. Last 5 kept, local only, never pushed:

git -C <vault> for-each-ref refs/vault-force-sync/
git -C <vault> branch recovered refs/vault-force-sync/<timestamp>

If the backup cannot be written, the force push is aborted rather than proceeding unprotected.

3. A size-skip pattern could outlive its reason

Oversized files are listed in .git/info/exclude so they do not slow every sync. That list was only ever appended to — so raising the limit, shrinking the file, or deleting it left the pattern behind, keeping that file out of the repository forever, with nothing on screen explaining why it never reached the other device.

The list is now a marked, managed block, rewritten on every run and released the moment it stops applying. Anything you wrote in .git/info/exclude yourself is preserved byte for byte, and that is asserted.

4. A credential could still reach disk

A token pasted into the remote URL is copied verbatim into .git/config — the one hole left in "no secret on disk". It is now flagged once per session. The URL is not rewritten: it is yours, and guessing at it would be worse than telling you.

Tests are now in the repository

They lived outside it and nobody else could run them. tests/ ships with a dependency-free runner:

sh tests/run.sh     # 57 checks, needs only 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, validation, timer lifecycle, log path 17
tests/credentials.test.js Credential resolution, SecretStorage missing/empty, helper holding no token, git credential fill itself, configDir shapes 17
tests/safety.test.js All four fixes above, including that the parent project is left byte-for-byte untouched 15

These are not unit tests over a mocked git. A merge that silently conflicts, or a push that runs before the pull, is invisible to a stub — so every scenario creates real repositories, a real bare remote, and runs the real binary.

Also

  • Log path fixed. It joined Vault#configDir, which is a folder name rather than a path, so fs resolved it against process.cwd() — the Obsidian install directory, not the vault.
  • README rewritten around these guarantees, with a Chinese translation: README.zh-CN.md.

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.