Undoing and recovering
Revert, reset, and restore each undo something different. The reflog means almost nothing committed is truly lost.
Four kinds of "undo"
"Undo it in Git" can mean four different operations. Choose by asking what must change and who already has the commits.
| Goal | Tool | Rewrites history? |
|---|---|---|
| Discard edits to a file on disk | git restore <file> |
No (but loses the edits) |
| Unstage a file | git restore --staged <file> |
No |
| Undo a commit others already have | git revert <commit> |
No. It adds an inverse commit |
| Move my branch to a different commit | git reset <commit> with --soft, --mixed, or --hard |
Yes, for this branch |
For anything already pushed to a shared branch, revert is the default answer. It records "we undid X, and why" as a normal commit, and it composes with the good commits that came after.
Reset, demystified
git reset <commit> always moves the current branch to <commit>. The flag decides how much else follows:
--soft: only the branch moves. The index and working tree are untouched, so the undone commit's changes appear staged. This is useful for squashing the last few local commits.--mixed(default): the branch moves and the index is reset to match. Changes now appear as unstaged edits.--hard: branch, index, and working tree are reset. Uncommitted work is overwritten.
Only --hard destroys anything, and only uncommitted work. Committed work is still in the object database.
The reflog: Git's flight recorder
Every time HEAD or a branch moves (commit, reset, checkout, rebase, merge), Git appends an entry to the reflog:
git reflog # where HEAD has been
git reflog show main # where main has been
A commit that "disappeared" after a reset is still an object, now unreachable. Find its ID in the reflog, give it a name with git branch rescue <id>, and it is safe again. Unreachable objects are only pruned by garbage collection after a grace period (two weeks for loose objects and longer for reflog entries, by default). Recovery is usually possible even after a bad afternoon. git fsck --lost-found finds dangling commits when the reflog has no entry, for example in a fresh clone.
The reflog is local. It records your repository's ref movements, not the server's. A commit that only ever existed in someone else's clone must be recovered from their reflog.
Recovering calmly
A recovery routine that never makes things worse:
- Stop and protect. Stash or commit any uncommitted work to a scratch branch. It has no other copy.
- Find.
git reflog,git log --all --oneline, andgit fsck --lost-found. - Name it.
git branch rescue/<what> <id>. Now nothing can garbage-collect it. - Inspect.
git show rescue/<what>to confirm it is the approved commit. - Restore with a reviewed merge, cherry-pick, or fast-forward, not a force-push to a shared branch.
When a revert is not one command
git revert <commit> applies the inverse of one commit on top of today's branch. Two things make it harder than that:
- A later commit touched the same lines. Git stops with a conflict, because the inverse no longer applies cleanly. Resolve it to today's file with only the bad change undone. Taking either side wholesale silently undoes the later work or keeps the bug. Finish with
git addandgit revert --continue. - The change came in through a merge.
git revert -m 1 <merge>undoes everything the merged branch brought, which is rarely what you want when one commit inside it was bad. Revert that commit instead. Reverting a merge also has a lasting effect: Git considers the branch's commits already merged, so re-merging the branch later brings nothing back until you revert the revert.
Either way, run the check that failed before you push. A protected branch accepts the fix as a fast-forward and nothing else.
Rewriting history, with care
Interactive rebase and git filter-repo can rewrite many commits: squash, reorder, or strip a file from all history. They are the right tool for unpublished work and for real emergencies such as removing a leaked secret or a huge binary. On shared branches they need coordination, because every collaborator must re-clone or reset. The “Releases, tags, and shared history” notes cover how to do that and what to do first.
Key terms
- Revert
- A new commit that applies the inverse of an earlier commit. History is preserved and safe on shared branches.
- Reset
- Moves the current branch pointer (and optionally the index and working tree) to another commit.
- Reflog
- A local log of where HEAD and each branch pointed over time. It is the safety net for "lost" commits.
- Unreachable object
- A commit no ref points to. It survives until garbage collection, which by default waits weeks.
Read further
- Pro Git, 2nd edition, 2.4 "Undoing Things" (Free to read online (CC BY-NC-SA 3.0))
commit --amend, unstaging, and unmodifying files, and the warnings about which of these lose work. - Pro Git, 2nd edition, 7.7 "Reset Demystified" (Free to read online (CC BY-NC-SA 3.0))
The three trees again, and the step-by-step diagrams of whatreset --soft,--mixed, and--hardmove. This section makes reset predictable. - Pro Git, 2nd edition, 10.7 "Maintenance and Data Recovery" (Free to read online (CC BY-NC-SA 3.0))
Recovering a lost commit withgit reflogandgit fsck --lost-found, and when Git's garbage collection actually removes unreachable objects. - Pro Git, 2nd edition, 7.6 "Rewriting History" (Free to read online (CC BY-NC-SA 3.0))
Interactive rebase andfilter-branch(and the note pointing togit filter-repo). Read it to know the tools exist and why they are dangerous on shared branches.