feat(cli): add hydrate stage restore - #97
Merged
Merged
Conversation
`stage discard` has always parked the outgoing stage at
.hydrate/stage.discarded.json and told the user it was recoverable
there, but nothing ever read that file back — recovery meant
hand-copying JSON into a directory the CLI owns, the exact thing
`discard` exists to replace.
`stage restore` puts it back:
- refuses over a non-empty live stage (no silent merge or overwrite)
- a missing recovery file is a normal state, not an error
- the recovery file now records which branch it was parked from, and
restore refuses across a mismatch — a workdir can be re-bound to a
different branch between a discard and a restore, and the parked
deltas' alias table only resolves against the branch it was staged on
- a successful restore consumes the recovery file
Also fixes `discard` to thread the pulled index through its summary
(summarize_workdir, matching status/diff) instead of summarize(&stage,
None), which failed loud on any staged delta referencing a committed
node — a real discard could get stuck unable to complete.
…uards Fresh-context review of #97 found four issues, all landed here: - The branch-mismatch guard only compared when BOTH the parked record and the current binding had a branch id, so it silently no-op'd when the workdir went unbound between discard and restore — the parked deltas' alias table would then resolve against nothing. Split into its own refusal (`CliError::BranchContextMissing`), distinct from a same-branch mismatch. - A cleanup failure after a successful restore (removing the now-stale recovery file) was reported as if the whole restore had failed, inviting a retry that immediately hit the already-staged refusal without the user ever learning the first attempt worked. The wrapped error now says both: the restore landed, and which file needs manual removal. - The no-lose-both-copies invariant (a cleanup failure after a successful save must leave BOTH the restored stage and the recovery file intact) was documented at length but never pinned by a test. - The two ad hoc refusals (`live stage non-empty`, `branch mismatch`) used generic `Other`/`State` error kinds, indistinguishable from each other and from actual file corruption over `--json`. Added typed variants (`RestoreBlocked`, `BranchMismatch`, `BranchContextMissing`) with their own stable `error.kind` tokens so a driving agent can branch on the correct remediation. - Extracted the triplicated bare-file-name containment check in `state::mod` into one `validate_bare_name` helper shared by write/read/remove_state_file. The symlink-follow behavior on `remove_state_file`/`atomic_write` (noted by review as a judgement call, not a mandated fix) is left as-is with a comment recording why: it requires local write access to the working copy, which is outside this CLI's threat model. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
`stage discard` has always parked the outgoing stage at `.hydrate/stage.discarded.json` and told the user it was recoverable there, but nothing ever read that file back — recovery meant hand-copying JSON into a directory the CLI owns, the exact thing `discard` exists to replace.
`stage restore` puts it back, purely locally (no network call):
Along the way, fixed `discard` to thread the pulled index through its op summary (`summarize_workdir`, matching `status`/`diff`) instead of `summarize(&stage, None)`, which failed loud on any staged delta referencing a committed node (a cross-commit edge, an update to an earlier commit) — a real discard could get stuck unable to complete, and unable to report what it destroyed, on exactly the batch it was trying to throw away.
`hydrate guide`'s editing-in-place section now documents `stage restore` alongside `stage discard`.
Test plan
🤖 Generated with Claude Code