Skip to main content

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 changes
  • git commit --amend — fix the last commit
  • git reset — move a branch pointer backwards
  • git revert — undo a pushed commit safely
  • git 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

Throw away uncommitted edits to a file
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.

Discard all uncommitted changes
git restore .

Unstage a file

You ran git add . and caught a file that shouldn't be in the commit:

Unstage a file, keeping the edits
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

Bring back the version of a file from three commits ago
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.

Legacy equivalents

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:

Fix the last commit message
git commit --amend -m "Add password reset endpoint"
Add a forgotten file to the last commit
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 resetgit revert
What it doesMoves the branch pointer backwards, discarding commits from the branchCreates a new commit that applies the opposite changes
HistoryRewrittenPreserved
Safe on shared branchesNoYes
Typical useCleaning up local commits before pushingUndoing something already on main

Reset: rewind local commits

Undo the last commit, keep changes staged
git reset --soft HEAD~1
Undo the last commit, keep changes unstaged - the default
git reset HEAD~1
Undo the last commit and throw the changes away
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 one

git 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:

Undo a specific commit with a new commit
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:

Revert the last three commits
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.

Show where HEAD has been
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:

Point the branch back at the lost commit
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}
Aborting is easier than recovering

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 --hard or git 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 commit while 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