reset, revert, checkout, restore
Three (now four) commands that all “undo” something but operate at different levels: working tree, staging area, branch pointer, history.
The mental model
A git operation can affect three places:
[ working tree ] ← what your editor sees
↓ git add
[ staging area / index ] ← what's staged for next commit
↓ git commit
[ committed history (HEAD pointer) ]
Each undo command operates at a different combination of these.
git reset — moves the branch pointer
git reset <commit> moves the current branch’s HEAD to <commit>. The flag controls what happens to the working tree and index.
| Flag | HEAD | Index | Working tree | Use |
|---|---|---|---|---|
--soft |
reset | unchanged | unchanged | “uncommit but keep everything staged” |
--mixed (default) |
reset | reset to HEAD | unchanged | “uncommit and unstage, keep file changes” |
--hard |
reset | reset to HEAD | reset to HEAD | “throw it all away” |
git reset --soft HEAD~ # undo last commit, keep changes staged
git reset HEAD~ # undo last commit, keep changes in working dir but unstaged (--mixed default)
git reset --hard HEAD~ # undo last commit AND throw away the changes
git reset --hard origin/main # make local match remote main exactly (destructive)
reset --hard is irreversible from working state, but the dropped commits stay in reflog for ~90 days. See 05_reflog_recovery.md.
git revert — make a new commit that undoes an old one
git revert abc1234 # creates a new commit that inverts abc1234's changes
git revert HEAD # revert the most recent commit
git revert -n abc1234 # apply the inverse but don't commit yet
History stays intact. The “undo” is a new commit on top.
Before: A───B───C───D
git revert C
After: A───B───C───D───!C (!C is a new commit reversing C)
Always safe on shared branches because nothing is rewritten.
revert vs reset — when to use which
revert |
reset |
|
|---|---|---|
| Rewrites history? | no | yes |
| Safe on pushed branches? | yes | no (unless you own the branch) |
| Result | new commit reversing the change | branch pointer moves; commits “disappear” (recoverable from reflog) |
| Use for | undoing something already on main/shared | cleaning up local commits before pushing |
Production rule: if it’s been pushed to a shared branch, revert. If it’s only local, reset.
git checkout — the overloaded command
Historically checkout did two unrelated things:
- Switch branches:
git checkout branch-name. - Restore files from a commit:
git checkout HEAD -- file.py.
This confused everyone, so git split it:
| Old | New (git 2.23+) |
|---|---|
git checkout <branch> |
git switch <branch> |
git checkout -b <new> |
git switch -c <new> |
git checkout -- <file> |
git restore <file> |
git checkout <commit> -- <file> |
git restore --source=<commit> <file> |
The old checkout syntax still works. Modern teams default to switch + restore for clarity.
git restore — modern file undo
git restore file.py # discard working-tree changes (default --worktree)
git restore --staged file.py # unstage but keep working-tree changes
git restore --staged --worktree file.py # both: undo back to HEAD
git restore --source=HEAD~3 file.py # bring file from 3 commits ago into working tree
Replaces the confusing git checkout -- file.py and git reset HEAD file.py.
Common scenarios
“I committed too early, want to add one more file”
git add forgotten.py
git commit --amend --no-edit # amend last commit, keep its message
Or:
git reset --soft HEAD~ # uncommit, keep staged
git add forgotten.py
git commit -m "..."
“I committed to the wrong branch”
git log -1 --format=%H # note the SHA of the commit
git reset --hard HEAD~ # remove it from current branch
git checkout target-branch
git cherry-pick <sha> # apply it on the right branch
“I want to undo git add file.py”
git restore --staged file.py # modern
git reset HEAD file.py # legacy
“I want to throw away local changes to a file”
git restore file.py # modern
git checkout -- file.py # legacy
“I made a mess, want to match remote main exactly”
git fetch
git reset --hard origin/main # destroys local commits and changes — make sure you've pushed first
“I pushed a bad commit to main, need to undo”
git revert <sha>
git push # forward-only; safe for everyone
Don’t git reset --hard and git push --force to “fix” pushed commits — anyone who pulled them will get phantom duplicates.
git clean — remove untracked files
reset only touches tracked files. To delete untracked files (e.g. accidentally created build artifacts):
git clean -n # dry run — show what would be deleted
git clean -f # force delete untracked files
git clean -fd # also remove untracked directories
git clean -fdx # also remove .gitignored files (very destructive)
-n first, every time. -fdx will erase your node_modules, .venv, .env — make sure that’s what you want.
Common interview confusions
- “
reset --harddeletes commits forever.” — they stay in reflog (~90 days). Usegit reflogandgit reset --hard <sha>to recover. See 05_reflog_recovery.md. - “
revertundoes a commit by removing it.” — it adds a new commit that reverses the changes. Original commit stays in history. - “
git checkoutandgit switchare different.” —switchis a clearer alias for the branch-switching part ofcheckout. Same effect. - “
reset --soft HEADdoes nothing.” — correct. You’re moving HEAD to where it already is. People meanreset --soft HEAD~.
Interview angle
- “
reset --softvs--mixedvs--hard?” — soft moves only HEAD (changes stay staged), mixed (default) also unstages, hard also wipes the working tree. Hard is destructive but recoverable via reflog within ~90 days. - “Difference between
git resetandgit revert?” — reset rewrites history (moves the branch pointer); revert adds a new commit that inverts the change. Reset for local cleanup, revert for shared branches. - “How do you undo a pushed commit safely?” —
git revert <sha>and push the new commit. Don’t reset + force-push pushed commits. - “What’s
git restoreand how does it relate tocheckout?” — modern command for file-level undo.git restore file=git checkout -- file;git restore --staged file=git reset HEAD file. Splitting the overloadedcheckout. - “Difference between
git reset HEAD fileandgit rm --cached file?” — reset unstages a modified file.rm --cachedremoves a file from tracking entirely (next commit deletes it from the repo, but keeps it in working tree). - “You force-pushed and lost commits — how do you recover?” —
git reflogshows your local HEAD history;git reset --hard <reflog-sha>restores. If only the remote was wiped, the local clone still has them.