Git gives you several different "undo" commands because there are genuinely several different things you might want to undo - an uncommitted edit, a bad commit that's only local, or a bad commit that's already been pushed and pulled by other people. Reaching for the wrong one is how people lose work, so it's worth knowing exactly what each does before you need one in a hurry.
Undoing uncommitted changes: git restore
git restore <file> discards uncommitted changes in your working directory, putting the file back to how it looked at your last commit. git restore --staged <file> unstages a file without touching its actual content - useful when you ran git add on something you didn't mean to include yet. (Older tutorials use git checkout -- <file> for the same thing; restore is the newer, less overloaded command for this specific job.)
git reset: moving where a branch points
git reset moves your current branch's pointer to a different commit, and optionally changes your staging area and working files to match. It has three modes, and the difference between them matters a lot:
| Mode | Branch pointer | Staging area | Working files |
|---|---|---|---|
--soft | Moves | Unchanged (stays staged) | Unchanged |
--mixed (default) | Moves | Reset to match target commit | Unchanged |
--hard | Moves | Reset to match target commit | Reset to match - uncommitted work is gone |
reset --hard is the one command in this series that can genuinely destroy uncommitted work with no confirmation prompt. Double-check git status before running it, and know that it only rewrites history locally - never run it on commits you've already pushed and that someone else might have pulled.git revert: undoing by adding, not removing
git revert <commit> takes a different approach entirely: instead of moving pointers or deleting history, it creates a brand new commit that applies the exact opposite of the target commit's changes. The bad commit is still there in the log, but its effects are cancelled out by the new one. This matters because it's non-destructive and safe on shared branches - anyone who's already pulled the bad commit will simply pull the revert commit too, and end up in the same correct state, with no rewritten history to conflict with.
git checkout for looking at old commits
git checkout <commit-hash> (without a branch name) puts you in what Git calls a "detached HEAD" state - your working directory now shows exactly what things looked like at that commit, but you're not on any branch. It's a safe way to look around old history or test something from the past. If you make commits here and want to keep them, you'll need to create a branch from this point (git switch -c new-branch-name) before switching away, or they'll become hard to find again.
The rule of thumb
If a commit has already been pushed and anyone else might have it: revert, never reset or force-push. If a commit only exists on your own machine and nobody else has seen it: reset is fine, and often cleaner. When in doubt about whether something's been seen by anyone else, treat it as if it has.
reset --hard, your old commits usually aren't gone immediately - Git keeps a local log of where your branches have pointed called the reflog, which can often recover "lost" work. It's covered properly in the last guide in this series.