Skip to content

feat(snapshots): one-command restore recovery#10

Merged
suzuke merged 3 commits into
mainfrom
feat/snapshots-restore
Jul 5, 2026
Merged

feat(snapshots): one-command restore recovery#10
suzuke merged 3 commits into
mainfrom
feat/snapshots-restore

Conversation

@suzuke

@suzuke suzuke commented Jul 5, 2026

Copy link
Copy Markdown
Owner

What

Adds agentic-git snapshots restore [<ref>] [--repo <path>] [--yes] [--staged] — the one-command form of the previously-documented manual git checkout <snapshot-ref> -- ..

Why

Adversarial product review (fugu) flagged the git-native restore as a top-3 onboarding drop-off: a product trier who doesn't understand snapshot refs abandons at the recovery step. This makes recovery a single command that needs no git internals.

Design (settled against an adversarial design pass)

  • Non-destructive — writes the snapshot's paths back; never deletes files created after it. It's a recovery tool, not a tree rollback.
  • Unstaged by default — recovering files does not stage them into your next commit; --staged opts into the classic index side effect.
  • Fail-closed pre-restore snapshot — restore overwrites the working tree, so it snapshots current state first (undo target) and aborts if that fails. (Unlike maybe_snapshot, which fails open because it must never block the git op it protects — this is our own command.)
  • Refuses to guess — no ref + exactly one snapshot → uses it; several → exit 2 + candidate list unless --yes (then the genuinely-newest, tie-broken by ref <seq> since forced dates are only second-granular).
  • Our refs only — restores from refs/agentic-git/snapshots/…, never an arbitrary branch/tag.

Path resolution via diff --name-status -z (spaces/newlines safe; A = newer files, excluded).

Tests

6 end-to-end tests (real shim creates the snapshot, real CLI restores):
restore_cli_recovers_tracked_and_untracked_unstaged, _staged_flag_leaves_index_staged, _refuses_to_guess_then_yes_takes_newest, _rejects_non_snapshot_ref, _no_snapshots_exit_1, _is_nondestructive_and_undoable (undo round-trip via the pre-restore snapshot).

Full workspace suite green (14 binaries), clippy --all-targets -D warnings clean.

🤖 Generated with Claude Code

suzuke and others added 3 commits July 5, 2026 15:56
… onboarding drop-off)

`git checkout <snapshot-ref> -- .` was the only documented recovery — a
git-native step that a product trier who doesn't understand snapshot refs
abandons on (adversarial review, fugu drop-off #3). Replace it with:

    agentic-git snapshots restore [<ref>] [--repo <path>] [--yes] [--staged]

Design settled against an adversarial design pass:
- Non-destructive: writes the snapshot's paths back; NEVER deletes files
  created after it (this is a *recovery* tool, not a tree rollback).
- Lands UNSTAGED by default (a user recovering files does not expect them
  silently staged into the next commit); `--staged` opts into the classic
  `checkout -- .` index side effect.
- Fail-CLOSED pre-restore snapshot: restore overwrites the working tree, so
  it snapshots current state first (undo target) and ABORTS if that fails —
  unlike `maybe_snapshot`, which fails open because it must never block the
  git op it protects. This is our own command; no reason to overwrite
  unsaved work with the net down.
- No-ref resolution refuses to guess: exactly one snapshot → use it; several
  → exit 2 and list candidates unless `--yes` (then the genuinely-newest,
  tie-broken by ref `<seq>` since dates are only second-granular).
- Only restores from `refs/agentic-git/snapshots/…` — never an arbitrary
  branch/tag.

Paths resolved via `diff --name-status -z` (spaces/newlines safe; `A` entries
= newer files, excluded). 6 end-to-end tests (real shim snapshot + real CLI
restore): unstaged recovery, `--staged`, ambiguous+`--yes`-newest, bad-ref
reject, no-snapshots, and a non-destructive undo round-trip. README updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…BIG on large snapshots)

fugu's adversarial review (PR #10) repro'd a real blocker: a `clean -fd`
snapshot of 60,000 long filenames restored ZERO files — `git checkout`
received every changed path as argv and died with `Argument list too long`
(E2BIG) before touching the tree. One-command recovery was unusable for
exactly the case it's for: large generated trees / vendored dirs.

Fix: feed pathspecs to checkout AND reset over stdin via
`--pathspec-from-file=- --pathspec-file-nul`. No argv limit; one invocation
handles any count. Verified against git 2.39.

Bonus: paths now stay RAW BYTES end-to-end (diff `-z` → `Vec<u8>` → NUL-joined
stdin), dropping the lossy `String::from_utf8_lossy` conversion — a non-UTF-8
filename round-trips unchanged (fugu's secondary concern).

Regression test R7: 5000 files with 230-char names (>1 MB of pathspec,
past macOS ARG_MAX) lost and fully recovered in one command. Full workspace
suite green (23 snapshot tests), clippy -D warnings clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… git magic)

fugu re-review (PR #10): a legal filename that begins with git's pathspec
sigil — `:(glob)literal.txt`, `:/foo`, `:x` — was snapshotted faithfully but
could NOT be recovered: `git checkout` parsed the fed pathspec as a magic
expression (`pathspec ... did not match`) instead of a literal path. The
recovery promise broke for an entire class of legal names.

Fix: set `GIT_LITERAL_PATHSPECS=1` on the pathspec-stdin checkout AND reset,
so every fed path is treated literally, never as magic. Verified against
git 2.39 (repro recovers `:(glob)literal.txt` byte-for-byte).

This closes the weird-filename class comprehensively: bulk/E2BIG (stdin),
spaces+newlines (-z/NUL), non-UTF-8 bytes (raw Vec<u8>), and now magic
sigils (literal pathspecs).

Regression test R8: a `:(glob)…`-named file lost and recovered. 24 snapshot
tests green, clippy -D warnings clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@suzuke suzuke merged commit a84f69d into main Jul 5, 2026
3 of 4 checks passed
@suzuke suzuke mentioned this pull request Jul 5, 2026
suzuke added a commit that referenced this pull request Jul 5, 2026
Bumps the shim to 0.2.0 for the one-command recovery (`snapshots restore`,
#10) and run-first onboarding (#11). agentic-git-core stays 0.1.0 — the
contract crate is unchanged, so the two versions move independently (the shim's
`version` is now explicit rather than inherited from the workspace).

Publishing: the release workflow publishes core first (0.1.0, already on
crates.io → tolerated) then the shim 0.2.0.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant