Undo and Recovery¶
What You'll Learn¶
- Which undo command to use for each situation, and which ones rewrite history
- The difference between
resetandrevert, and why it matters once you've pushed - How to recover commits you think you've lost, using the reflog
- How to move commits with
cherry-pick, park work withstash, and find bugs withbisect
The Golden Rule¶
Rewrite only history nobody else has. Before pushing, you can amend, reset, and rebase freely. After others may have pulled a commit, undo it by adding a new commit with
git revert.
Pick the Right Command¶
| Situation | Command | Rewrites history? |
|---|---|---|
| Discard uncommitted edits to a file | git restore file |
No (but the edits are gone) |
| Unstage a file, keep the edits | git restore --staged file |
No |
| Change the last commit's message or add a forgotten file | git commit --amend |
Yes — last commit |
| Undo the last commit, keep its changes staged | git reset --soft HEAD~1 |
Yes |
| Undo the last commit, keep changes unstaged | git reset HEAD~1 |
Yes |
| Throw away the last commit and its changes | git reset --hard HEAD~1 |
Yes — changes gone from the working tree |
| Undo a commit that's already pushed and shared | git revert <sha> |
No — adds a new commit |
| Bring one commit to another branch | git cherry-pick <sha> |
No |
| Get back something you "lost" | git reflog |
No |
Working Tree and Staging¶
git restore src/app.py # discard unstaged edits to one file
git restore . # discard all unstaged edits (careful)
git restore --staged src/app.py # unstage, keep edits
git restore --source=HEAD~3 src/app.py # bring back the file as it was three commits ago
git clean -nd # preview untracked files that would be deleted
git clean -fd # delete them
git restore and git clean -f delete work that was never committed, and Git can't recover it. Always preview with git clean -n.
Amend the Last Commit¶
git add forgotten_test.py
git commit --amend --no-edit # add to the previous commit, keep its message
git commit --amend -m "Better message" # change the message
If you already pushed the commit to your own feature branch, follow up with git push --force-with-lease.
Reset: Move the Branch Pointer¶
git reset <commit> moves the current branch to <commit>. The mode decides what happens to the changes from the commits you moved past:
flowchart LR
subgraph soft["--soft"]
S1["Branch moves"] --> S2["Changes stay staged"]
end
subgraph mixed["--mixed (default)"]
M1["Branch moves"] --> M2["Changes stay in working tree, unstaged"]
end
subgraph hard["--hard"]
H1["Branch moves"] --> H2["Working tree and staging match the target — changes discarded"]
end
# Squash the last three local commits into one
git reset --soft HEAD~3
git commit -m "Add order retries with backoff and metrics"
# Make local main match the remote exactly, discarding local commits
git fetch
git reset --hard origin/main
Revert: Undo by Adding a Commit¶
git revert 9f3c2d1 # new commit that reverses 9f3c2d1
git revert --no-edit HEAD~2..HEAD # revert the last two commits, one revert commit each
git push
Reverting a merge commit needs to know which parent is the mainline:
Reverting a revert
If you revert a merged feature and later want it back, revert the revert commit. Merging the original branch again does nothing, because Git considers its commits already merged.
On a squash-merge workflow, each pull request is one commit, so git revert <sha> cleanly undoes one whole change — often the fastest production rollback.
The Reflog: Git's Safety Net¶
Every time a branch or HEAD moves — commit, reset, rebase, checkout — Git records it in the reflog. Commits you "lost" are usually still there.
$ git reflog
e44a7b1 (HEAD -> feature/retry) HEAD@{0}: reset: moving to HEAD~2
91c0d3e HEAD@{1}: commit: Add retry metrics
7ab21f4 HEAD@{2}: commit: Retry on 503
b7e41c9 HEAD@{3}: checkout: moving from main to feature/retry
Recover after a bad reset --hard¶
git reset --hard HEAD@{1} # move back to where you were before the reset
# or keep the recovered work on a new branch
git branch recovered 91c0d3e
Recover a deleted branch¶
git reflog | grep 'feature/search' # find the last commit it pointed to
git branch feature/search <sha>
Recover after a rebase went wrong¶
Reflog entries are local to your clone and expire (by default, reachable entries after 90 days and unreachable ones after 30). Recover promptly, and remember that a teammate's reflog can't rescue your work.
Read your own reflog¶
Paste git reflog output and choose what went wrong. The tool finds the entry from just before the reset, rebase, amend, or merge (or the last commit of a deleted branch) and gives you a safe command:
Reflog rescue (needs JavaScript). Without it: find the most recent reset:, rebase (start), or commit (amend) line in git reflog; the line just below it is where you were before. Save it with git branch rescue <sha>.
Cherry-Pick¶
Copy a commit onto the current branch as a new commit:
git switch release/2.4
git cherry-pick -x 9f3c2d1 # -x appends "(cherry picked from commit ...)"
git cherry-pick -x a1b2c3d..f6e5d4c # a range (excludes the first commit)
git cherry-pick --abort # if conflicts get out of hand
Use it for backporting fixes. If you cherry-pick often between long-lived branches, that's a sign your branching strategy needs attention.
Stash¶
Park uncommitted work to switch context:
git stash push -m "half-done retry config"
git stash push --include-untracked -m "with new files"
git stash list
git stash show -p stash@{0}
git stash pop # reapply and drop
git stash apply stash@{1} # reapply and keep
git stash drop stash@{1}
Stashes are easy to forget. For anything that lasts longer than a few minutes, commit to a branch instead.
Bisect: Find the Commit That Broke It¶
Binary search through history. With 1,000 commits between good and bad, it takes about 10 steps.
git bisect start
git bisect bad # the current commit is broken
git bisect good v2.3.0 # this release worked
# Git checks out a commit halfway; test it, then mark it:
git bisect good # or: git bisect bad
# ...repeat until Git prints "<sha> is the first bad commit"
git bisect reset
Automate it with a script that exits 0 for good and non-zero for bad:
Worktrees: Two Branches at Once¶
Instead of stashing to review a hotfix, check out another branch in a separate directory:
git worktree add ../app-hotfix hotfix/payment-timeout
cd ../app-hotfix
# ...work, test, commit, push...
cd - && git worktree remove ../app-hotfix
What Most Engineers Miss¶
- Staged work can be recovered, even after
reset --hard.git addwrites the file's content into the object database immediately. If you staged something and then lost it,git fsck --lost-foundlists dangling blobs, andgit show <blob>prints each one. - A dropped stash is a dangling commit.
git stash dropor a mistakengit stash clearleaves the commits in place until garbage collection. Find them withgit fsck --unreachable | grep commit, thengit stash apply <sha>. ORIG_HEADis a one-step undo.reset,rebase, andmergesave the previousHEADthere, sogit reset --hard ORIG_HEADundoes the last one without reading the reflog.git gc --prune=nowends recovery. It deletes unreachable objects immediately. Some "clean up the repository" scripts run it, so recover first.- On GitHub, deleted doesn't mean gone. Commits pushed to a fork stay reachable by hash across the fork network even after the fork or branch is deleted (Truffle Security). Undo can't un-publish a secret; rotation can.
Common Mistakes¶
git reset --hardon a branch others have pulled, then force-pushing — usegit revertinstead.- Running
git clean -fdorgit restore .without previewing, and deleting uncommitted work permanently. - Panicking after a bad rebase instead of checking
git reflog. - Re-merging a branch after reverting it, and being surprised nothing comes back.
- Leaving important work in a stash for weeks.
- Cherry-picking fixes to a release branch but never to
main.
Check Yourself¶
Before the rebase
Your reflog shows HEAD@{0}: rebase (finish), HEAD@{1}: rebase (pick), HEAD@{2}: rebase (start), HEAD@{3}: commit: Add cache. Which entry is your branch before the rebase?
-
HEAD@{1} -
HEAD@{2} -
HEAD@{3}
rebase (start) is the moment HEAD moved to the new base. The entry just before it is your branch as it was.
Lost staged changes
You ran git add config.yml, then git reset --hard by mistake. The file's changes were never committed. Are they recoverable?
- Yes:
git addstored the content as a blob, whichgit fsck --lost-foundcan find - No: only committed work is recoverable
- Only from the reflog
Changes that were never staged are gone for good; staged ones survive as dangling blobs until garbage collection.
Interview Questions¶
- What's the difference between
git resetandgit revert? When must you userevert? - Explain
--soft,--mixed, and--hardforgit reset. - You ran
git reset --hardand lost two commits. How do you get them back? - How do you undo a merge commit that's already on
main? - How would you find which of the last 500 commits introduced a bug?
Next¶
Continue to Collaboration and Repository Hygiene.