Git · 4 of 11
Git has four different ways to undo something, and they are not interchangeable. Using the wrong one is how "undo my last commit" turns into "where did my work go" — understanding what each command actually moves (working directory, staging area, or history) fixes that.

I once watched a teammate try to "undo" a commit on a shared branch with git reset --hard HEAD~1, push with --force, and wipe out two other people's already-pushed work in the process.
The command did exactly what it was asked. That was the problem.
Git's undo commands are precise, but each one operates on a different layer — your working files, the staging area, or commit history itself. Picking the wrong one turns "I just want to undo this" into a much bigger mess than the thing you were trying to fix.
Every change in Git passes through three places: the working directory (your actual files on disk), the staging area (also called the index — where you assemble the next commit), and the commit history (permanent snapshots). git add moves changes from working directory to staging. git commit moves them from staging into history.
Whether you want restore, reset, or revert depends entirely on which of these three you're targeting.
git restore — working directory and staging onlyrestore only touches uncommitted changes. It never rewrites history, which makes it the safest undo command.
git restore config.js
Reverts config.js back to what it looked like in the last commit. The file's content in your working directory is overwritten.
git restore --staged config.js
Moves the file from "staged" back to "modified but not staged." Your actual file content is untouched.
restore is the modern replacement for git checkout -- file (discard changes) and git reset file (unstage). Those still work but restore makes the intent much clearer.git reset — moving the branch pointerreset moves your current branch's pointer to a different commit — usually backward. The three modes decide what happens to the staging area and working directory along the way.
reset actually rewrites history (your branch tip changes), the same shared-branch caution from rebase applies: reset your own unpushed commits freely, be very careful resetting anything already pushed and pulled.--soft — un-commit, keep changes stagedgit reset --soft HEAD~1
Moves the branch pointer back one commit, but leaves everything from that commit still staged and ready to re-commit. The commit is gone from history, but every file change it contained is sitting in your staging area.
--mixed — un-commit, un-stage (default)git reset --mixed HEAD~1
# or just:
git reset HEAD~1
Moves the pointer back and un-stages the changes, but leaves them in your working directory as uncommitted edits. The commit is gone, but no work is lost.
--hard — un-commit, discard everythinggit reset --hard HEAD~1
Moves the pointer back and discards the changes from your working directory entirely. This is the only one of the three that can genuinely lose work.
--hard, the changes from the discarded commit are gone from your working directory. They're only recoverable via git reflog (covered in the next chapter) — but only if you haven't run git gc since.Memory trick: soft keeps the most (staged), mixed keeps some (in working dir), hard keeps the least (gone).
git revert — undoing without rewriting historyreset rewrites history by moving the branch pointer backward. revert does the opposite: it adds a new commit that undoes the changes from an earlier one, leaving the original commit and everything after it intact.
git revert a1b2c3d
Creates a new commit whose changes are the exact inverse of commit a1b2c3d. The original commit stays in history — nothing is rewritten, no commit hashes become invalid. History moves forward with a new commit that says "Revert [original message]."
revert is the correct choice for shared branches. If the bad commit is already on main and others have pulled it, revert is how you undo it safely. reset --hard + force-push is how you create a crisis.git revert -m 1 <merge-commit-hash>
-m 1 tells Git which parent to consider "mainline" — usually meaning "undo what this merge introduced relative to the branch it was merged into."
git stash — setting work aside temporarilyNone of the above fit this situation: you're mid-change, need to switch branches for something urgent, but the work isn't ready to commit.
git stash
Saves your uncommitted changes (staged and unstaged) onto a stack and restores your working directory to the last commit. Clean, as if nothing was in progress.
Switch branches, handle the urgent thing, then come back:
git stash pop
Reapplies the most recent stash and removes it from the stack.
git stash list # see all stashes
git stash show -p stash@{1} # see what's in stash #1
git stash apply stash@{1} # apply without removing from stack
git stash drop stash@{1} # delete a specific stash
| Situation | Command |
|---|---|
| Uncommitted file change I want to discard | git restore <file> |
| File in staging area I want to un-stage | git restore --staged <file> |
| Last commit was private, I want to re-do it | git reset --soft HEAD~1 |
| Last commit was private, I want to un-commit and keep editing | git reset HEAD~1 |
| Last commit was private, I want to throw it away | git reset --hard HEAD~1 |
| Commit is on a shared branch and needs to be undone | git revert <hash> |
| Mid-work, need to context-switch temporarily | git stash / git stash pop |
The key rule: Has anyone else already pulled the commit you're about to change? If yes → revert. Don't reset.
reset --hard and force-pushes both raise the same question: is the "lost" commit actually gone? Almost always, no. The next chapter covers git reflog — the local log that records every position your branches and HEAD have been at — along with git bisect for finding which commit introduced a bug.
Get an email when I publish the next chapter. No spam — just new technical deep-dives.
Comments
Share feedback or questions about this blog post.
No comments yet. Be the first to share your thoughts.