cmux link sync: keep snapshots and add undo, history and restore

Merged
TE
2d ago
+1377-9

Leo (2026-11-03), on the folder sync improvements: "yeah let's do alllll of those things! :DDD". This is the first one: snapshots and undo.

What changes

cmux link sync push/pull had no history, so a pull that brought back the wrong thing could not be rolled back. Now this Mac keeps snapshots of each synced folder, and three new local commands use them:

  • cmux link sync history LOCAL REMOTE lists the snapshots, newest first.
  • cmux link sync undo LOCAL REMOTE puts the folder back as it was before the last change. A second undo goes one step further back.
  • cmux link sync restore LOCAL REMOTE N puts back snapshot N.

How it works:

  • When snapshots are taken. After every push, and before and after every pull that changes something here. The newest 10 per folder are kept, in <state>/link-sync/history/<key>/<seq>.json under the same key as the base record.
  • What a snapshot holds. It is a small manifest: each working-tree path's SHA-256 and permissions, each symlink's text, and in git the branch, the HEAD commit, a digest of what is staged, and git's index file itself (kept as a blob, so nothing is ever written into the repository). The repository's own files are left out because git already has their history.
  • Where the bytes live. File contents are blobs at <state>/link-sync/blobs/<aa>/<sha256>, shared by every folder. A blob is named by the file's lowercase hex SHA-256, which is the key the content-addressed store in #34604 uses (/v1/sync/blobs/<sha256>), so a snapshot entry can be looked up in either place. Blobs are kept for files a push writes, for files a pull replaces or deletes (before), and for files it places (after). Each blob is checked against its hash before it is renamed into place. A blob that no kept snapshot names is swept once it is an hour old.
  • What a restore touches. Only paths that differ between the target snapshot and the newest one, and only when each of those paths is here exactly as the newest snapshot says. If a file changed here since the last sync, the restore is refused with sync_restore_conflict and the paths are named; nothing changes.
  • How a restore applies. It goes through the pull's place::Transaction, so a failure rolls everything back. In git the branch moves back only while it is still at the newest snapshot's commit (update-ref with the old value), and the index file is put back (through index.lock) only while what is staged is still the newest snapshot's.
  • The machine. Untouched. The next push sends what the restore put back.

Pinning or flushing snapshots with sync is not in this PR. Transport (compression, parallel sends, the GitHub seed) belongs to another lane, and nothing here touches it.

Risk

Normal to high: this is a state machine over user files. Taking a snapshot can never fail a push or pull, because every error there is ignored. A restore only replaces content whose hash it has just checked against the newest snapshot, through the same handle-based transaction the pull uses.

Tests

tests/link_sync_history.rs:

  • An undo restores exactly: every byte and mode of the folder matches its state before a pull that changed, deleted and added files. A second undo reports nothing earlier, and restore N brings the pull back.
  • An undo never overwrites a change made here: it is refused with the path named and the folder unchanged, then goes through once the file is back.
  • An undo in git moves a fast-forwarded branch back, and status comes out clean.

Command: cmux-ci test --setup rust running cargo fmt --all -- --check, cargo clippy -p cmux-tui --all-targets -- -D warnings and cargo test --no-fail-fast -p cmux-tui --test link_sync_history --test link_sync.

Observed / Not run

Changelog

Added: cmux link sync undo, history and restore put a synced folder back as an earlier sync left it.

Agent session: agent session

๐Ÿค– Generated with Claude Code


Autofix with [code]smith
Open in [code]smith ยท Need help on this PR? Tag @codesmith-bot with what you need.


Summary by cubic

Adds snapshots and undo to cmux link sync so a bad pull can be rolled back.

cmux link sync push/pull kept no history, so a pull that brought back the wrong thing could not be rolled back. Now this Mac keeps the newest 10 snapshots of each synced folder after every push and around every pull that changes it, and three new local commands use them:

  • history LOCAL REMOTE lists snapshots, newest first.
  • undo LOCAL REMOTE puts the folder back as it was before the last change; another undo steps one further back.
  • restore LOCAL REMOTE N puts back snapshot N.

Snapshots are small manifests of each path's SHA-256 and permissions (symlink text; in git, the branch, HEAD and staged tree) in <state>/link-sync/history/. File bytes live in a content-addressed blob store keyed by SHA-256 โ€” the same key the Cloud store uses โ€” shared by every folder; blob names are checked against the hash before a restore uses them, and blobs no snapshot names are swept once an hour old.

Restore safety

  • Only paths a pull or restore since the snapshot changed are touched, and only when each is still exactly as the newest snapshot says; otherwise the restore is refused with sync_restore_conflict, nothing changes, and the paths are named. An edit or new file made on this Mac is never undone.
  • Changes go through the pull's place::Transaction, so a failure rolls everything back.
  • In git, the index is locked before anything moves and what is staged is re-read under the lock; the branch moves back only while it is still at the newest snapshot's commit, and is returned when the index cannot be put back.

The machine is never touched by history, undo or restore; the next push sends what the restore put back.