Recovery¶
Choose recovery by what needs to move. Start in the worktree where the unwanted operation happened.
| Situation | Command | Effect and next step |
|---|---|---|
| The latest local operation was wrong | ff undo |
Restore files and local branch state one undo step back; inspect the result. |
| You undid too far | ff redo |
Move forward on the available redo path. |
| One file needs its committed version | ff restore app.txt |
Write that file from the commit under the open change; review the diff. |
| Files need a retained earlier version | ff restore app.txt --at-op <operation> or ff restore --all --at 20m |
Write files only; HEAD, branches, and index stay where they are. |
| The whole local state needs an earlier recovery point | ff op restore <operation> |
Move this worktree's recorded local state to that point; inspect files and branches. |
| An older ref change was wrong, but later unrelated work must remain | ff op revert <operation> |
Invert that operation's ref transitions if still applicable; files, index, and HEAD selection stay in place. |
Recovery requires a successful, retained snapshot. Commands and active hooks capture at invocation boundaries, not continuously. Ignored untracked files, unsaved buffers, and working-copy content above fufu.maxFileSize (50 MiB by default) are excluded; oversized tracked files can retain older index/base content. Undo follows this worktree's chain and cannot reverse a remote update. Keep these coverage and scope limits in mind when selecting a point.
Each recipe below starts independently in a scratch repository with app.txt containing hello, a README.md, and an empty feature branch based on main. No tutorial state is needed. From the fufu source checkout, bash scripts/docs/recovery-transcript.sh reproduces all examples; pass a recipe ID such as file to run just that fixture. The script supplies the initial commit and enables fufu before the displayed commands. IDs and dates in the transcripts come from that run; use the ones your commands print.
Recover after a raw Git reset¶
Prerequisite: the desired branch state was captured before the reset. This example commits the work through ff commit, then resets it with raw Git. It has no uncaptured edits to recover.
$ printf 'feature\n' > app.txt
$ ff commit -m "app: feature"
closed 61556d9c on feature: app: feature (1 file(s))
undo: ff undo
$ git reset --hard HEAD~1
HEAD is now at 897cc69 demo: initial files
$ ff undo
ff: absorbed 1 change made outside fufu: refs/heads/feature moved to 897cc698 (reset: moving to HEAD~1)
undid (a change made outside fufu): absorbed 1 ref change(s); previous op may not have completed
now at 915a46d79f7a (commit on feature: app: feature)
refs/heads/feature → 61556d9c
1 worktree file(s) restored
back: ff redo
The next fufu command reconciles the foreign ref movement, then undo restores the retained branch tip and files. Check the files and ff status before continuing. Raw Git only gets a fresh pre-command capture when an active hook or shell alias invokes fufu first; otherwise recovery reaches the last retained state. See using fufu alongside Git.
The previous op may not have completed wording is the current reconciliation report in this fixture: the raw reset moved a ref away from the state recorded by the completed commit. The source check verifies that undo returns to that commit and restores its file.
Restore one file¶
Prerequisite: app.txt is tracked and its committed version is the desired source. Bare restore discards its working-copy edits. The example first labels a snapshot with ff trigger, so it can also demonstrate retrieving the discarded edit by its exact operation ID.
$ printf 'wrong\n' > app.txt
$ ff trigger -m "before discarding the edit"
b177304ea262 · 1 file
$ ff restore app.txt
restored from 897cc698 (demo: initial files)
restored app.txt
undo: ff undo
$ ff restore app.txt --at-op b177304ea262
restored from b177 (manual: before discarding the edit)
restored app.txt
undo: ff undo
$ ff restore app.txt --from main
restored from 897cc698 (demo: initial files)
restored app.txt
undo: ff undo
--from main selects a commit source instead. All three restores leave branches, HEAD, and the index unchanged. Restore requires its own successful pre-restore capture before writing files. For precise recovery of a discarded edit, find that capture with ff op log and use --at-op; undo groups consecutive captures and can step past the individual file state you want. Review with ff diff, then commit or keep editing.
Restore files by time or snapshot¶
Prerequisite: the desired files were captured at or before the requested time and that operation is retained. Here a named draft capture is followed by a bad commit. The timestamp and pause make the two states distinguishable in this short example.
$ printf 'keep this draft\n' > app.txt
$ ff trigger -m "known good draft"
63a3306927f3 · 1 file
$ saved_time=$(date -u +%Y-%m-%dT%H:%M:%SZ)
$ sleep 2
$ printf 'bad refactor\n' > app.txt
$ ff commit -m "app: bad refactor"
closed 3e776984 on feature: app: bad refactor (1 file(s))
undo: ff undo
$ ff restore --all --at "$saved_time"
restored from 63a3 (manual: known good draft)
restored app.txt
undo: ff undo
$ ff restore app.txt --at-op 63a3306927f3
restored from 63a3 (manual: known good draft)
(no files differed)
undo: ff undo
The files become the earlier draft, while the bad commit remains at HEAD and the index stays unchanged. Review and commit the correction if you want to keep later history. --at 20m, 2h, or a date selects the operation current at that time, not continuous file history. Use --at-op when you know the exact point. Revisions and IDs defines these sources.
Restore files and branch state together¶
Prerequisite: you want to return local state to an earlier operation, including undoing later branch movement. ff history shows this worktree's undo steps; its hexadecimal IDs address operations. Inspect the chosen record with ff op show before restoring it.
$ printf 'good\n' > app.txt
$ ff commit -m "app: good version"
closed dd0fd0cd on feature: app: good version (1 file(s))
undo: ff undo
$ printf 'bad\n' > app.txt
$ ff commit -m "app: wrong direction"
closed 685d7f03 on feature: app: wrong direction (1 file(s))
undo: ff undo
$ ff history
@ e0153c819a24 0s ago now commit on feature: app: wrong direction
↓1 b016545cb163 0s ago undo pre: ff commit -m app: wrong direction
↓2 cb1d625edebc 0s ago undo commit on feature: app: good version
↓3 89329a7eff0b 0s ago undo pre: ff commit -m app: good version
↓4 392d9dd5227c 0s ago undo mint feature at 6eabd5cd and switch from main
↓5 6fa8830fc477 0s ago undo operation log initialized from observed state; earlier operations not undoable
(the floor)
$ ff op show cb1d625edebc
cb1d625edebc op 0s ago
commit on feature: app: good version
on feature
base 6eabd5cd
refs/heads/feature → dd0fd0cd
(the worktree is unchanged across it)
$ ff op restore cb1d625edebc
undid: commit on feature: app: wrong direction
now at cb1d625edebc (commit on feature: app: good version)
refs/heads/feature → dd0fd0cd
1 worktree file(s) restored
back: ff redo
The branch and file both return to the good commit. Worktree ownership guards still apply; this does not restore every checkout in the repository at once. Inspect status and history afterward. Use a file restore instead when later branch history should stay.
Redo, or recover a forked-off operation¶
Prerequisite: a recent local operation was undone. Redo works until new work creates a different continuation. This recipe first redoes successfully, then records a different message after undoing again.
$ printf 'feature\n' > app.txt
$ ff commit -m "app: feature"
closed 271aa7ca on feature: app: feature (1 file(s))
undo: ff undo
$ ff undo
undid: commit on feature: app: feature
now at e466d7ae9876 (pre: ff commit -m app: feature)
refs/heads/feature → 6eabd5cd
back: ff redo
$ ff redo
redid: commit on feature: app: feature
now at 8998b750f354 (commit on feature: app: feature)
refs/heads/feature → 271aa7ca
back: ff undo
$ ff undo
undid: commit on feature: app: feature
now at e466d7ae9876 (pre: ff commit -m app: feature)
refs/heads/feature → 6eabd5cd
back: ff redo
$ ff commit -m "app: better message"
closed 9d098532 on feature: app: better message (1 file(s))
undo: ff undo
$ ff redo
ff: nothing to redo: work has landed since the last undo, so the log forked rather than rewound
try:
ff op log
ff history
$ ff op log --at-op 8998b750f354 -n 3
8998b750f354 0s ago op feature commit on feature: app: feature
e466d7ae9876 0s ago capture feature pre: ff commit -m app: feature
392d9dd5227c 0s ago op feature mint feature at 6eabd5cd and switch from main
$ ff op restore 8998b750f354
undid: commit on feature: app: better message
now at 8998b750f354 (commit on feature: app: feature)
2 operations stepped over
refs/heads/feature → 271aa7ca
back: ff redo
The refusal belongs to the second redo: the new commit forked the operation log. The old operation remains addressable while retained. Bare op log lists the live chain; --at-op inspects the saved old point and its predecessors. Op restore uses that same old ID to return to it. Inspect the result before adding more work; retention bounds how long these points remain available.
Reverse an older ref change after unrelated work¶
Prerequisite: the unwanted operation moved refs, those refs still equal the values it left, and the later work is on other refs. The example commits on feature, then uses ff switch to create docs from main and commits useful work there.
$ printf 'wrong branch work\n' > app.txt
$ ff commit -m "app: unwanted commit"
closed 2178bc88 on feature: app: unwanted commit (1 file(s))
undo: ff undo
$ ff switch main -b docs
minted docs (forked from main)
switched to docs
undo: ff undo
$ printf 'useful later work\n' > notes.txt
$ ff commit -m "docs: useful notes"
closed 5fe2dacc on docs: docs: useful notes (1 file(s))
undo: ff undo
$ ff op log -n 6
4ca81a2f6710 0s ago op docs commit on docs: docs: useful notes
47eb9bfbd483 0s ago capture docs pre: ff commit -m docs: useful notes
211ba99c007e 0s ago op docs mint docs at 6eabd5cd and switch from feature
2bda3463f5a9 0s ago op feature commit on feature: app: unwanted commit
ffb05cbd1a35 0s ago capture feature pre: ff commit -m app: unwanted commit
392d9dd5227c 0s ago op feature mint feature at 6eabd5cd and switch from main
$ ff op revert 2bda3463f5a9
reverted 2bda3463f5a9cc0db16e2f669a710b2f3eb67a13: commit on feature: app: unwanted commit
refs/heads/feature → 6eabd5cd
undo: ff undo
$ ff undo
undid: revert 2bda3463 (commit on feature: app: unwanted commit)
now at 4ca81a2f6710 (commit on docs: docs: useful notes)
refs/heads/feature → 2178bc88
back: ff redo
Revert moves feature back without moving docs, changing the checkout selection, or writing files or index. It records a new operation; the final undo reverses the revert. Inspect the affected branch with ff log before continuing. Reverting a current branch's ref also leaves its files in place, so those files can now differ from HEAD.
When op revert refuses¶
Prerequisite: later work moved the same ref. This independent counterexample shows the applicability check:
$ printf 'first\n' > app.txt
$ ff commit -m "app: first"
closed 29b1e91f on feature: app: first (1 file(s))
undo: ff undo
$ printf 'later\n' > app.txt
$ ff commit -m "app: later"
closed c1f070ec on feature: app: later (1 file(s))
undo: ff undo
$ ff op log -n 4
c84de588230e 0s ago op feature commit on feature: app: later
d32b79740f3e 0s ago capture feature pre: ff commit -m app: later
25435854cfc5 0s ago op feature commit on feature: app: first
4773f6b29e1d 0s ago capture feature pre: ff commit -m app: first
$ ff op revert 25435854cfc5
ff: inverting 25435854cfc5f958b4e7dc89b7973c6064cbabd1 conflicts with work done since; no ref inversion was applied: refs/heads/feature: the operation left it at 29b1e91f75bd2b09ec92558c019a1611cf13e1d5, and it now stands at c1f070ecc32116a680b03760d8ae0edbcc1e7085
try:
ff op show 25435854cfc5f958b4e7dc89b7973c6064cbabd1
ff explain held/op-revert
ff op log
held/op-revert is an applicability refusal, not a new resolution session. The target ref transitions are not applied; pre-capture and reconciliation can still write records. Captures and notes have no invertible ref transition, and revert does not selectively reverse file patches or replay later same-branch commits. Use history rewriting to change older content while preserving subsequent commits, or op restore if the whole later local state should be taken back.
Move a just-made commit to a new branch¶
Prerequisite: the unwanted close is the latest operation, and correct-branch does not exist. Undo makes its work open again; a commit with -b records it on a new branch.
$ printf 'feature\n' > app.txt
$ ff commit -m "app: feature"
closed 71e12fbc on feature: app: feature (1 file(s))
undo: ff undo
$ ff undo
undid: commit on feature: app: feature
now at b6c05ebee1c9 (pre: ff commit -m app: feature)
refs/heads/feature → 6eabd5cd
back: ff redo
$ ff commit -b correct-branch -m "app: feature"
closed 71e12fbc on correct-branch: app: feature (1 file(s))
undo: ff undo
The original feature tip stays at its prior commit. Review the new branch, then continue there. For only a wrong message, use ff describe HEAD -m "new message"; rewriting history covers older commits and moving selected files.
Undo an editing or resolution session¶
Prerequisite: you are editing an earlier commit or resolving a held rewrite. Check history to distinguish opening the session, switching into it, and finishing it.
ff editandff resolveopen rewrite sessions through creation and switching: one undo returns to the original branch, another removes the session.ff doneorff done --abandonlands or abandons in one operation. One undo brings that session back, including retained uncommitted edits.- A held parked-change arrival resolves in place; it has no session or done step. Fix the files and continue ordinary work.
The editing recipe and conflict guide show the surrounding workflow. Session edits remain in operation history when abandoned; they are not a Git stash.
These independent examples verify both opening paths. The first opens an editing session and reverses its switch and creation:
$ printf 'feature\n' > app.txt
$ ff commit -m "app: feature"
closed 99d8d6b9 on feature: app: feature (1 file(s))
undo: ff undo
$ ff edit HEAD
editing 99d8d6b9 "app: feature" on ff/dusky-reef
finish with ff done, or ff done --abandon to drop it
undo: ff undo
$ ff history
@ 8dee0084e385 0s ago now switch from feature to ff/dusky-reef
↓1 e61ecf2450a1 0s ago undo edit 99d8d6b9: session ff/dusky-reef on feature
↓2 789b60c3fe21 0s ago undo commit on feature: app: feature
↓3 e791a0a3e2df 0s ago undo pre: ff commit -m app: feature
↓4 392d9dd5227c 0s ago undo mint feature at 6eabd5cd and switch from main
↓5 6fa8830fc477 0s ago undo operation log initialized from observed state; earlier operations not undoable
(the floor)
$ ff undo
undid: switch from feature to ff/dusky-reef
now at e61ecf2450a1 (edit 99d8d6b9: session ff/dusky-reef on feature)
HEAD → refs/heads/feature
back: ff redo
$ ff undo
undid: edit 99d8d6b9: session ff/dusky-reef on feature
now at 789b60c3fe21 (commit on feature: app: feature)
refs/heads/ff/dusky-reef deleted
back: ff redo
The second creates conflicting branch edits, opens a resolution, then reverses its switch and creation. The original held request remains after the two undos; resolve it again or use ff resolve --abandon if you no longer want that replay.
$ printf 'feature greeting\n' > app.txt
$ ff commit -m "app: feature greeting"
closed f1678cb7 on feature: app: feature greeting (1 file(s))
undo: ff undo
$ ff switch main
switched to main
undo: ff undo
$ printf 'main greeting\n' > app.txt
$ ff commit -m "app: main greeting"
closed 068b64c7 on main: app: main greeting (1 file(s))
undo: ff undo
$ ff switch feature
switched to feature
undo: ff undo
$ ff restack
held: replaying f1678cb7 "app: feature greeting" conflicts in app.txt
the restack of 1 commit on feature is waiting — nothing was written
ff resolve to fix them, all at once · ff resolve --abandon to drop it
$ ff resolve
resolving 1 conflict in app.txt on ff/warm-drake
1 commits replayed
fix the markers, then ff done · ff resolve --abandon to drop it
$ ff undo
undid: switch from feature to ff/warm-drake
now at 3e644f7f640e (resolve the held restack on feature: 1 region(s))
HEAD → refs/heads/feature
1 worktree file(s) restored
back: ff redo
$ ff undo
undid: resolve the held restack on feature: 1 region(s)
now at b1e33073fe10 (hold restack of feature onto main)
refs/heads/ff/warm-drake deleted
back: ff redo
Recover from a changed remote branch¶
Prerequisite: your branch was published and someone changed its remote tip. This scratch recipe creates a same-machine origin.git and a plain-Git checkout at ../teammate. The displayed teammate commands amend and force-push only that disposable remote.
$ printf 'feature\n' > app.txt
$ ff commit -m "app: feature"
closed 5401b8e0 on feature: app: feature (1 file(s))
undo: ff undo
$ ff push
created origin/feature and set feature to track it
the push left the machine — ff undo cannot reach it
ff undo then ff push rolls the shared copy back, under a lease
$ git -C ../teammate fetch -q origin
$ git -C ../teammate switch -q feature
$ git -C ../teammate commit -q --amend -m "app: reviewed feature"
$ git -C ../teammate push -q --force origin feature
$ printf 'local follow-up\n' > notes.txt
$ ff commit -m "docs: follow-up"
closed ae08a41d on feature: docs: follow-up (1 file(s))
undo: ff undo
$ ff push
ff: origin/feature moved since you last looked, so nothing was pushed — your commits are still here, and ff pull takes in what arrived
try:
ff pull feature
ff push feature
$ ff pull
fetching from origin
took in 1 commit(s) from origin/feature
replayed 1 of yours on top
1 commit(s) to push — ff push
undo: ff undo
$ ff push
pushed feature to origin/feature
the push left the machine — ff undo cannot reach it
ff undo then ff push rolls the shared copy back, under a lease
The first retry of ff push refuses the stale lease; local work remains. ff pull fetches the changed remote history and replays the local follow-up. This raw Git amend removes the stored change-ID header, so no identity-based superseded line appears. Inspect and test the result, then push under the refreshed lease. A pull conflict needs resolution first.
Pull's local branch and file updates are undoable; fetching objects and tracking refs is separate. A push changes the remote and is outside undo's reach. Rolling it back requires another allowed push, and cannot reverse other clones, CI runs, or webhooks. Leases check the remote ref value, not ownership or a special rule for main; pulling and pushing explains seen records and team policy.
Recover parked work without fufu¶
Prerequisite: a parked change still exists at refs/fufu/open/<branch>. With fufu, switch to its branch to resume it. Without fufu, the plain-Git recovery recipe shows how to inspect that exact ref and apply its patch as uncommitted work, avoiding unrelated operation records in git log --all.
Retention and the earliest recovery point¶
Undo cannot reach before fufu's first recorded state, into another worktree's chain, or into uncaptured or expired file states. Raw file changes that move no refs can disappear before any capture sees them. Snapshot coverage applies to every recipe above.
ff op trim removes old operations according to fufu.keep (90 days by default). Automatic trimming normally runs at most daily per worktree. For a real repository, choose a window such as ff config keep 30d and preview with ff op trim -n.
The following retention recipe deliberately uses seconds in its disposable repository to show the effect. Do not use its short window for everyday work.
$ printf 'retained in branch history\n' > app.txt
$ ff commit -m "app: keep this commit"
closed ea606571 on feature: app: keep this commit (1 file(s))
undo: ff undo
$ ff config keep 2s
keep = 2s (this repo)
$ sleep 3
$ ff switch main
switched to main
undo: ff undo
$ ff switch feature
switched to feature
undo: ff undo
$ ff op trim -n
would drop 4 of 6 operations
$ ff op trim
dropped 4 of 6 operations — previous tip saved at refs/fufu/wt/main/trash/@ops until the next trim
dropped data frees after gc
$ ff history
@ 2397f30f448e 0s ago now trim: dropped 4 operation(s)
↓1 52efad6d8630 0s ago undo switch from feature to main
(the floor)
$ ff config keep 90d
keep = 90d (this repo)
Branch commits and files are unchanged; the earliest undo point moves forward. A real trim saves the previous chain tip at refs/fufu/wt/<worktree>/trash/@ops until the next trim and invokes Git's automatic garbage collection. A dry run does not drop operations, but capture and update maintenance can still run. Surviving operation IDs can change when predecessor links are rewritten. Restore or commit important work before its retention window expires, including work from removed worktrees.