How to Undo Almost Anything in Git Without Panicking
by Eric Hanson, Backend Developer at Clean Systems Consulting
The Panic Is the Problem
A developer runs git reset --hard on the wrong branch. Another does a git rebase that went sideways and now the history looks wrong. A third accidentally deleted a branch with uncommitted changes on it. In each case, the first reaction is panic — and the second is a hasty command that makes things worse.
The thing to internalize: Git almost never permanently destroys work. The object store keeps everything. Reflog tracks every HEAD movement. The question is not "is my work gone?" but "where did Git put it?" This article is a decision tree for the most common undo scenarios.
Undo an Uncommitted Change (Working Tree)
You modified a file and want to discard the changes, going back to the last committed version.
# Discard changes to a specific file
git restore src/payment/processor.py
# Discard all uncommitted changes in the working tree
git restore .
# Older syntax (still works)
git checkout -- src/payment/processor.py
Warning: git restore on the working tree is one of the few Git operations that is genuinely destructive. Uncommitted changes that aren't staged go to /dev/null. There is no reflog for working tree state. If you're not sure, stash first: git stash push -m "safety stash before restore".
Undo a Staged Change (Index)
You staged a file but haven't committed yet, and want to unstage it (keep the changes, just remove from index).
# Unstage a specific file (keeps changes in working tree)
git restore --staged src/payment/processor.py
# Unstage everything
git restore --staged .
Undo the Last Commit (Not Yet Pushed)
You committed something wrong and want to fix it before pushing.
# Undo the commit, keep changes staged
git reset --soft HEAD~1
# Undo the commit, keep changes in working tree (unstaged)
git reset HEAD~1
# Undo the commit AND discard the changes (dangerous)
git reset --hard HEAD~1
To simply fix the last commit message or add a forgotten file:
git add forgotten-file.py
git commit --amend --no-edit # keeps same message, adds file
git commit --amend -m "Better message" # changes message
--amend rewrites the commit. Only do this before pushing — after pushing, amending creates history divergence.
Undo a Pushed Commit (Shared History)
This is the one that trips people up. You can't amend or reset a commit that others may have pulled — that would rewrite shared history. The safe option is a revert:
# Create a new commit that undoes the changes from HEAD
git revert HEAD --no-edit
# Revert a specific commit by SHA
git revert a3f9d24 --no-edit
# Revert a range (newest first)
git revert HEAD~3..HEAD --no-edit
Revert creates a new commit. History is preserved. Anyone who already has the original commit will see the original plus the revert when they pull. Clean, transparent, and safe.
Undo a Merge
You merged a branch and it was wrong — wrong branch, wrong time, broke things.
# If not yet pushed: reset to before the merge
git reset --hard ORIG_HEAD
# Git sets ORIG_HEAD to the pre-merge state automatically during a merge
# If already pushed: revert the merge commit
# -m 1 specifies the "mainline" (the branch you merged INTO)
git revert -m 1 <merge-commit-sha>
The -m 1 flag tells Git which parent to treat as the mainline. For a typical merge commit (where main received a feature branch), parent 1 is main. Check with git log --merges to identify the merge commit SHA.
Undo a Rebase Gone Wrong
Rebase is one of the most frequently panicked-about operations. The recovery tool is reflog.
# Find the state before the rebase
git reflog
# Output looks like:
# a3f9d24 HEAD@{0}: rebase: Fix payment processor
# b7c12e1 HEAD@{1}: rebase: Add idempotency key
# f4d8a09 HEAD@{2}: rebase (start): checkout main
# 9e1b3c7 HEAD@{3}: commit: My original commit
# ...
# The entry just before "rebase (start)" is your pre-rebase state
git reset --hard HEAD@{3}
This puts your branch back to exactly where it was before the rebase. The rebase commits still exist in the object store but are no longer reachable from HEAD.
Recover a Deleted Branch
# Find the SHA of the tip of the deleted branch
git reflog | grep 'checkout: moving from deleted-branch-name'
# or
git fsck --lost-found | grep commit
# Recreate the branch at that SHA
git checkout -b recovered-branch a3f9d24
If you remember the approximate time you last worked on it, git reflog --since="2 days ago" narrows it down.
The Nuclear Option: git stash as a Safety Net
Before any operation you're unsure about, stash your working tree:
git stash push -m "safety: before risky operation $(date)"
git stash saves the working tree and index state to a stack. It's not a substitute for commits, but it gives you a checkpoint you can return to with git stash pop if something goes wrong.
The Hierarchy of Destructiveness
Not all undo operations are equally safe. From safest to most dangerous:
git revert— creates new commits, never loses historygit reset --soft— moves HEAD, keeps changes stagedgit reset(mixed, the default) — moves HEAD, keeps changes in working treegit reset --hard— moves HEAD, discards working tree changes (but commits are still in reflog)git restoreon working tree — discards uncommitted working tree changes permanently
The practical rule: if you're not sure which one to use, start with the safest option that might solve your problem. You can always escalate to a more aggressive option if needed. You cannot un-run git restore on uncommitted changes.
When in doubt: git stash, then proceed.