fix(client): make the disaster-recovery path actually work
Review found the read path scoped to the CURRENT device's uuid, which breaks the exact case snapshot mode exists for: when the laptop dies, the replacement machine has a new id, so snapshots printed 'no restore points yet' while the backups sat on the forge, and restore errored. Reproduced, then fixed by unscoping the READ only. Writing stays device-scoped (two machines must not overwrite each other) and pruning stays device-scoped (machine A must not apply its clock to machine B's refs); the docstring now says why the three differ. Also from the same review: - mirror mode printed a %cI timestamp that restore could not accept, so copying the first column looped the user back to snapshots. It now matches the log, and refuses an ambiguous timestamp (two commits in one second) with the candidate ids instead of guessing. - the size guard advised 'add a .gitignore' while measuring with a plain walk that ignored one. It now measures what git would sync, through a throwaway git dir outside the folder so a refused add leaves no .git behind. - get --all caught only SystemExit, so a RuntimeError from any git call abandoned the remaining repos. - get --all mapped alice/notes and bob/notes to one path and reported the second as 'already present'. Clashes now clone to <owner>-<name> and say so. - the prune clock was in-memory, so watch --once under launchd pruned every run. Persisted in config. 153 tests. Live-verified on the beta forge: machine A backed up uncommitted work and was deleted; machine B, different device id, cloned the repo, listed A's snapshot and restored both files.
This commit is contained in:
@@ -333,7 +333,10 @@ deleted it again, `DELETE …/tokens/{id}` returning 204 under basic auth):
|
||||
list *is* the grant. A client-side filter would be a second opinion about
|
||||
someone else's authorisation. One repo failing does not abandon the rest,
|
||||
and a truncated listing is reported loudly — `--all` must never quietly
|
||||
mean "the first 2000".
|
||||
mean "the first 2000". `alice/notes` and `bob/notes` both want
|
||||
`<base>/notes`; the second is cloned to `<base>/bob-notes` and the clash is
|
||||
logged, because reporting it as "already present" would leave the user
|
||||
believing they had pulled both.
|
||||
* `get <repo|owner/repo> [--into DIR] [--mode M]` — the download half of
|
||||
`add`. Defaults to `snapshot` mode unless the repo carries a
|
||||
`.granthi-sync.json` marker saying otherwise, so a plain synced folder
|
||||
@@ -360,6 +363,12 @@ deleted it again, `DELETE …/tokens/{id}` returning 204 under basic auth):
|
||||
refused unless `--force`. The seeded ignore file covers `.env`, `*.key`,
|
||||
`*.pem`, `id_rsa` and friends, and it governs snapshots too — the scratch
|
||||
index honours `.gitignore` exactly as a normal commit does.
|
||||
The size guard measures what git *would* sync, ignore rules included
|
||||
(including the machine's global excludes), because its own advice is "add
|
||||
a .gitignore for what should not sync" and advice that changes nothing is
|
||||
worse than none. It asks git through a **throwaway git dir outside the
|
||||
folder**, so a refused `add` leaves no `.git` behind in a directory the
|
||||
user never agreed to turn into a repo.
|
||||
* `watch [--interval 30] [--once]` — per folder, by mode. `mirror`:
|
||||
autocommit (`sync: <ISO ts>`) → fetch → ff-pull if remote strictly ahead →
|
||||
push if local strictly ahead. `snapshot`: fetch → push a snapshot of the
|
||||
@@ -370,18 +379,31 @@ deleted it again, `DELETE …/tokens/{id}` returning 204 under basic auth):
|
||||
force, never merge** — the same policy as the mesh — **but the backup
|
||||
still happens**, because divergence is when work is most at risk.
|
||||
Retention pruning runs at most hourly. SIGTERM-clean.
|
||||
* `snapshots <folder> [--limit 20]` — restore points, newest first. Read from
|
||||
the **remote**, not a local cache: the feature exists for the case where
|
||||
this machine is gone.
|
||||
* `snapshots <folder> [--limit 20]` — restore points, newest first, **across
|
||||
every device**, with the device that took each one. Read from the
|
||||
**remote**, not a local cache: the feature exists for the case where this
|
||||
machine is gone.
|
||||
|
||||
The three scopes differ deliberately. Writing is device-scoped, so two
|
||||
machines never overwrite each other. Pruning is device-scoped, so machine A
|
||||
never applies its clock to machine B's refs. **Reading is not scoped** — a
|
||||
replacement laptop has a new id, and scoping the read to it would print
|
||||
"no restore points yet" while the backups sit on the forge. That defect
|
||||
was live in the first draft and is now pinned by a test that restores a
|
||||
dead machine's work from a fresh clone.
|
||||
* `restore <folder> --at <ts|sha> [--into DIR]` — materialise one restore
|
||||
point into a new directory; refuses a non-empty destination.
|
||||
point into a new directory; refuses a non-empty destination. Accepts what
|
||||
`snapshots` printed in either mode, including a mirror-mode `%cI`
|
||||
timestamp. Two commits inside the same second share that timestamp, so an
|
||||
ambiguous `--at` is **refused with the candidate ids** rather than
|
||||
resolved by guessing.
|
||||
* `status` — table of linked folders, mode, last sync, divergence flags.
|
||||
* Run as a daemon on macOS with `client/launchd/ai.granthi.sync.plist`
|
||||
(edit the script path, then `launchctl bootstrap gui/$UID <plist>`).
|
||||
|
||||
## Tests
|
||||
|
||||
* `python3 -m unittest discover -s tests` — 143 tests. The v1.2 additions
|
||||
* `python3 -m unittest discover -s tests` — 153 tests. The v1.2 additions
|
||||
cover: a snapshot capturing uncommitted work while HEAD, the index and the
|
||||
working tree stay byte-identical; snapshots landing outside `refs/heads`;
|
||||
an unchanged tree not being re-pushed; a diverged folder still being backed
|
||||
|
||||
Reference in New Issue
Block a user