Undoing Things & Recovery
Nothing Committed Is Ever Really Lost
The most reassuring thing I've learned about Git: once something is committed, it is very hard to truly lose it. Git keeps "deleted" commits around for weeks, and the reflog remembers every place HEAD has ever been. Most Git disasters are ten seconds away from a full recovery — if you know which command to reach for.
This page covers the full undo toolbox, from the harmless to the history-rewriting:
git restore— throw away uncommitted changesgit commit --amend— fix the last commitgit reset— move a branch pointer backwardsgit revert— undo a pushed commit safelygit reflog— the safety net that recovers everything else
Which Undo Do I Need?
The rule of thumb: anything not pushed yet can be rewritten freely. Anything pushed to a shared branch should only be undone with git revert.
Discarding Uncommitted Changes: git restore
git restore (Git 2.23+) is the modern, purpose-built command for throwing away changes. Before it existed, git checkout -- file did the same job — you'll still see that form in older tutorials.
Discard changes in the working directory
git restore app.js
The file goes back to how it looks in the last commit. The edits are gone — this is one of the few Git operations with no undo, so double-check with git diff first.
git restore .
Unstage a file
You ran git add . and caught a file that shouldn't be in the commit:
git restore --staged .env.local
The file leaves the staging area but your edits stay in the working directory. Nothing is lost.
Restore a file from an older commit
git restore --source HEAD~3 config/database.js
The old version lands in your working directory as an uncommitted change, so you can review it before committing.
git restore file replaces git checkout -- file, and git restore --staged file replaces git reset HEAD file. The old forms still work everywhere; the new ones just say what they mean.
Fixing the Last Commit: git commit --amend
Committed too early, forgot a file, or made a typo in the message? Amend rewrites the most recent commit in place:
git commit --amend -m "Add password reset endpoint"
git add tests/reset.test.js
git commit --amend --no-edit
--no-edit keeps the existing message. Amending creates a new commit with a new hash — so only amend commits that haven't been pushed. If you already pushed, either push with git push --force-with-lease (on your own feature branch only) or just make a follow-up commit.
git reset vs git revert
These two get confused constantly, and the difference matters:
git reset | git revert | |
|---|---|---|
| What it does | Moves the branch pointer backwards, discarding commits from the branch | Creates a new commit that applies the opposite changes |
| History | Rewritten | Preserved |
| Safe on shared branches | No | Yes |
| Typical use | Cleaning up local commits before pushing | Undoing something already on main |
Reset: rewind local commits
git reset --soft HEAD~1
git reset HEAD~1
git reset --hard HEAD~1
The three modes only differ in what happens to the changes from the discarded commits:
--soft— changes stay staged, ready to recommit--mixed(default) — changes stay in the working directory, unstaged--hard— changes are discarded entirely
--hard is the dangerous onegit reset --hard also wipes any uncommitted work in your working directory, and that part is unrecoverable. Committed work that you reset away can still be rescued through the reflog — uncommitted work cannot. Run git status before every hard reset.
Revert: undo without rewriting
When the bad commit is already on a shared branch, resetting would rewrite history that teammates have built on. Revert instead:
git revert a1b2c3d
History stays intact, everyone's clones stay valid, and the revert commit documents exactly what was undone and why. To undo several commits, revert each one (newest first), or use a range:
git revert HEAD~3..HEAD
The Reflog: Git's Black Box Recorder
git reflog logs every position HEAD has been in on your machine — every commit, checkout, reset, rebase, and merge. It's local-only and entries expire — after 90 days by default for commits still reachable from a branch, but only 30 days for unreachable ones (the reset-away and deleted-branch commits you'd actually want back). Within that window it makes almost any mistake reversible.
git reflog
a1b2c3d HEAD@{0}: reset: moving to HEAD~3
9d4b7e2 HEAD@{1}: commit: Add export to CSV
3c1a9f0 HEAD@{2}: commit: Add report filters
b7d2e85 HEAD@{3}: commit: Add reports page
a1b2c3d HEAD@{4}: checkout: moving from main to feature/reports
Recovering from a bad hard reset
Real scenario: Zakariya ran git reset --hard HEAD~3 on the wrong branch and "lost" three commits of work. The commits are still in the object database — only the branch pointer moved. The reflog shows the commit that was HEAD before the reset (9d4b7e2 above), so:
git reset --hard 9d4b7e2
All three commits are back. Total recovery time: under a minute.
Recovering a deleted branch
Deleted a branch with git branch -D before it was merged? Find its tip commit in the reflog, then recreate the branch:
git reflog
# look for the last commit made on the deleted branch, e.g. 3c1a9f0
git branch feature/reports 3c1a9f0
Recovering after a botched rebase
A rebase that went wrong is the same recovery: the reflog records the pre-rebase state as something like HEAD@{5}: rebase (start). Reset to the entry just before the rebase started:
git reset --hard HEAD@{5}
If a rebase or merge is going badly while it's still in progress, you don't need the reflog at all — git rebase --abort or git merge --abort returns you to the exact starting state.
What Recovery Cannot Save
Being honest about the limits:
- Uncommitted changes destroyed by
git reset --hardorgit restore— gone. Commit early and often; even messy WIP commits are recoverable, uncommitted work is not. - Dropped stashes — not in the reflog, but often findable with
git fsck --unreachable | grep commitwhile the objects still exist. - Expired reflog entries — unreachable commits are pruned after 30 days by default (90 for reachable ones), then garbage-collected.
Quick Reference
# Uncommitted changes
git restore file.js # discard edits to a file
git restore --staged file.js # unstage, keep edits
git restore --source HEAD~2 f.js # pull an old version into working dir
# Last commit (not pushed)
git commit --amend --no-edit # add staged changes to last commit
git reset --soft HEAD~1 # uncommit, keep staged
git reset HEAD~1 # uncommit, keep unstaged
git reset --hard HEAD~1 # uncommit and discard
# Pushed commits
git revert <hash> # undo with a new commit
# Recovery
git reflog # find the lost commit
git branch rescue <hash> # resurrect it on a new branch
git reset --hard <hash> # or move the current branch back