Run git reset --hard on a commit your team already pulled and you’ve just made your history disagree with theirs. Run git revert on a typo you haven’t pushed and you’ve added a pointless commit to the log. Git has four undo commands, restore, reset, commit --amend and revert, and they are not interchangeable. Pick the wrong one and it either does nothing or does far more than you wanted.
What decides it is where the change lives right now: in your working tree, in the staging area, in a commit only you have, or in a commit you already pushed.
Where a change lives in Git: working tree, index, history
Every undo command operates on one or more of three layers:
- Working tree: the files on disk, as you’re editing them.
- Index (the staging area): the snapshot you’ve built with
git add, waiting to become the next commit. - History: the commits already recorded, reachable from
HEAD.
A change starts in the working tree, moves to the index on git add, and moves to history on git commit. Undoing it means walking it back through the same layers. The further it has gone, the more deliberate the command.
| |
How to discard unstaged changes with git restore
You’ve edited config.py, broken it, and want Git’s copy back:
| |
This overwrites the file in your working tree with the version in the index, which is the same as HEAD unless you’ve staged something. The edit is gone. It isn’t stashed, and no Git command will bring it back. To discard every unstaged change at once:
| |
git restore replaces the older git checkout -- <file>. Both still work, but restore exists so that “give me this file back” no longer shares a command with “switch branches”.
How to unstage a file without losing your edits
You ran git add on three files, then noticed one of them doesn’t belong in this commit. The edit itself is fine:
| |
migration.sql leaves the index and goes back to “modified, not staged”. The file on disk is untouched. This is the same as the older git reset HEAD -- migration.sql, and it’s the one place where reset and restore overlap: reset with a pathspec only touches the index, which is all restore --staged does.
How to undo a commit with git reset: soft, mixed, and hard
git restore can’t touch a commit. For that you need git reset, and the flag controls how much it undoes:
| Flag | Working tree | Index | History (HEAD) |
|---|---|---|---|
--soft | unchanged | unchanged | moved |
--mixed (default) | unchanged | reset to match target | moved |
--hard | reset to match target | reset to match target | moved |
All three move the branch pointer. The difference is how much of your work follows it backward.
--soft rewinds the commits but leaves their changes staged. That makes it the quickest way to squash the last three commits into one:
| |
--mixed is what you get with a plain git reset HEAD~1. It rewinds the commit and drops its changes back into the working tree as unstaged edits, so you can split them into smaller commits or throw part of them away.
--hard discards the diff entirely. The working tree ends up as if the commit never happened:
| |
There’s no prompt. The commits you rewound past can still be found through git reflog (see below), but any uncommitted edits in your working tree at that moment are gone for good, so check git status first. And never run it on commits someone else has already pulled: moving the branch backward on a shared branch leaves your history and theirs disagreeing about what happened, the same problem behind the golden rule of rebasing.
How to fix the last commit with git commit –amend
If the mistake is in the most recent commit, a reset is overkill. --amend replaces the tip commit instead of rewinding past it:
| |
Stage more changes first and they fold into the same commit, on the same parent:
| |
The amended commit is a new object with a new hash, just like a rebased one. So the rule is the same as for rebase: amend only what you haven’t pushed. Amend a commit that’s already on a shared branch and everyone who pulled it has to reconcile two commits that claim to be the same change.
How to undo a commit that’s already been pushed with git revert
Once a commit is on a shared branch, reset and --amend are off the table. Rewriting history other people have fetched is how you get diverged branches and duplicated commits. git revert takes the other route: it leaves the bad commit where it is and adds a new one that does the opposite.
| |
Git opens an editor for the revert commit’s message, then records it:
| |
History now holds both the original commit and its inverse. Nothing was rewritten, so a plain git pull on every other clone picks up the fix. To revert a range as a single commit:
| |
--no-commit (-n) applies the inverse of each commit to the working tree and index without committing, so three reverts land as one commit instead of three. If a revert conflicts with changes made since, Git stops the way it does mid-merge: resolve the conflict markers, git add the file, then run git revert --continue.
So “undo a push” means: revert the commit, then push the revert. Don’t force-push rewritten history to a branch other people pull from.
How to recover from a bad reset with git reflog
After reset --hard, the commits disappear from git log. They still exist. git reflog lists every position HEAD has held on your machine, including the one right before the reset:
| |
| |
git reset --hard HEAD@{1} puts you back on 7d4e1b8. How long reflog entries live, and why the reflog is local-only, is covered in the recovery section of Git Rebase vs Merge. It works the same whichever command moved HEAD.
The reflog gets you back to where you were. It won’t tell you which older commit was the last good one. For that, git bisect narrows hundreds of commits down to the one that introduced the bug.
Git reset vs revert vs restore: which to use when
| The change is… | Command |
|---|---|
| Edited, not staged, and you want it gone | git restore <file> |
| Staged, and you want it unstaged (edit kept) | git restore --staged <file> |
| Committed, unpushed, and it’s the last commit | git commit --amend |
| Committed, unpushed, and you want to rewind further | git reset --soft / --mixed / --hard HEAD~n |
| Committed and already pushed or shared | git revert <commit> |
Gone after a reset --hard you regret | git reflog + git reset --hard HEAD@{n} |
For anything already committed, one question picks the row: has anyone else seen this commit? If not, reset or amend and move on. If they have, revert. It costs one extra commit in the log, and none of your teammates has to untangle a history that no longer matches theirs.