You will mess up in Git. Everyone does. The skill that separates juniors from fluent users isn't avoiding mistakes — it's confidently undoing them. This lesson covers the four common "undo" situations: unstaging an accidental git add, discarding edits in your working directory, undoing a commit you've already pushed, and undoing a commit you haven't pushed yet. The right tool for each is different, and using the wrong one (git reset --hard on a shared branch, say) is how juniors lose a week of work.
The three zones, again
Recall from Chapter 1: changes live in three zones — working directory, staging area, repository. Every undo command targets one or more of those zones. Knowing which zone you're trying to clean up tells you which command to use.
Unstaging — undo a git add
You ran git add cypress.config.js but realised you didn't actually want it in the next commit.
Modern syntax (preferred):
git restore --staged cypress.config.jsOld syntax (still works everywhere):
git reset HEAD cypress.config.jsEither way, the file leaves the staging area but your edits stay in the working directory:
git statusOn branch feature/checkout-tests
Changes not staged for commit:
modified: cypress.config.js
You haven't lost any work — the file is just back to "modified but not staged."
Discarding working-directory edits
You edited tests/login.spec.ts and decided the change was wrong. You want to throw the edits away and go back to the last committed version:
git restore tests/login.spec.ts(Or older: git checkout -- tests/login.spec.ts.)
This permanently discards your changes. There is no undo for this undo. Be sure before you run it. If you might want them back later, git stash first.
git revert — safe undo for shared branches
You committed and pushed something bad. A test you removed was actually load-bearing; a config tweak broke CI; you accidentally committed a file with a credential. Anything that's already on the remote (and especially on main) needs the safe undo: git revert.
git revert abc1234[main 9f2e3b4] Revert "Remove deprecated login flow test"
1 file changed, 24 insertions(+)
git revert creates a new commit that undoes the changes from abc1234. The bad commit is still in the history; the revert sits on top of it, cancelling its effect. Nothing is rewritten. Anyone who pulled the bad commit will get the revert too — no diverged histories, no force-pushes, no panic.
This is the only safe undo for shared branches. git revert is the answer when in doubt.
git reset — the local-only history rewriter
git reset moves the branch pointer backwards in time, as if those commits never happened. It comes in three flavours that differ in what they keep:
git reset --soft HEAD~1 # undo last commit, keep changes STAGED
git reset --mixed HEAD~1 # undo last commit, keep changes in WORKING DIRECTORY (the default)
git reset --hard HEAD~1 # undo last commit, DISCARD all changes (dangerous)What each preserves:
| Flag | Repository | Staging | Working dir |
|---|---|---|---|
--soft | reset | preserved | preserved |
--mixed (default) | reset | reset | preserved |
--hard | reset | reset | reset (gone) |
HEAD~1 means "one commit before HEAD." Use HEAD~3 to undo the last three commits, etc.
The killer rule: git reset rewrites local history. If those commits have been pushed and other people have based work on them, resetting and force-pushing creates divergence chaos. Use reset only on commits that have never left your laptop.
revert vs reset — pick the right one
git revert vs git reset — same goal, very different blast radius
git revert <commit>
Creates a NEW commit that undoes the target
The bad commit stays in history; the revert cancels its effect
Safe on pushed/shared branches
No history rewrite — teammates' clones don't diverge
Reversible
You can revert the revert if needed
Always the right choice for main
When in doubt, revert
git reset --hard HEAD~N
Moves the branch pointer backward
The discarded commits effectively disappear from this branch
Rewrites history
Force-push required to push it; teammates' clones diverge if they pulled
--hard discards working-directory changes too
No undo. Stash first if uncertain
Only on local, unpushed commits
Never on shared branches
A QA scenario — accidentally committed credentials
You ran git add . and cypress.config.js had a real API token in it. You committed and pushed before you noticed.
git log --oneline -34c48901 Add cypress config for staging
8f31320 Add login regression tests
dc57b17 Update fixtures
The bad commit is 4c48901. Two-step recovery:
git revert 4c48901A new commit appears on top, restoring the file to its previous state (no token).
[main 9f2e3b4] Revert "Add cypress config for staging"
Push:
git pushNow: the secret is still in history. A git revert doesn't erase it — anyone who cloned can git show 4c48901 and see the leak. Treat the credential as compromised: rotate it immediately. (For genuinely public repos with secrets, follow the recovery flow in Chapter 5, Lesson 1 — and rotate first, always.)
Then add the file to .gitignore (Chapter 5) so it can never be committed again, and create a cypress.config.example.js with a placeholder.
Recovering from a bad reset — git reflog
You ran git reset --hard HEAD~3 and immediately realised you needed those commits. They're not in git log anymore — but Git keeps a hidden record of every move in git reflog:
git reflog9f2e3b4 (HEAD -> main) HEAD@{0}: reset: moving to HEAD~3
4c48901 HEAD@{1}: commit: Add cart fixture for discount-code edge cases
8f31320 HEAD@{2}: commit: Add login regression tests
dc57b17 HEAD@{3}: commit: Update fixtures
Every move HEAD has made — commits, resets, checkouts — is logged. To recover the commits, jump back to where you were before the reset:
git reset --hard 4c48901Or cherry-pick individual commits (next lesson). The reflog only lives locally and only for ~90 days, but in the moment of "I just lost a week of work," it's the safety net.
A quick reference
| Goal | Command |
|---|---|
| Unstage a file (keep edits) | git restore --staged <file> |
| Discard edits in working dir (no recovery) | git restore <file> |
| Undo last commit (keep edits in working dir) | git reset HEAD~1 |
| Undo last commit (discard everything) | git reset --hard HEAD~1 |
| Undo a pushed commit safely | git revert <hash> |
| Recover from a bad reset | git reflog then git reset --hard <hash> |
Bookmark the Git for QA cheat sheet for this table; you'll need it.
⚠️ Common mistakes
git reset --hardon shared branches followed by force-push. This is the classic teammate-rage move. Anyone who pulled before your reset now has commits that no longer exist on the remote — their next push will conflict, their next pull will create duplicate commits. Alwaysrevertfor shared branches; reserveresetfor your own laptop's local work.- Running
git restore <file>to "undo edits" without realising it's destructive. Beginners trygit restorecasually, expecting an undo button, and find their hour of work gone with no recovery. Build a habit: stash first (git stash push -m "before discard"), then restore. The stash is your safety net. - Treating
revertas "delete from history." A revert undoes the effect of a commit, but the original commit and its diff still exist ingit logandgit show. For genuine secrets, history rewrites (git filter-repo, BFG) are needed in addition — and credentials must be rotated regardless.
🎯 Practice task
Practice every undo on a sandbox where mistakes don't matter. 25-30 minutes.
- In your
qa-sandboxrepo, make and commit a small change you don't mind losing. Confirm withgit log --oneline -3. - Unstage: edit a file,
git addit, thengit restore --staged <file>. Confirm the file is back to "modified but not staged." - Discard: edit the file again. Run
git restore <file>. Confirm the edit is gone (and feel the pang of "wait, was that recoverable?" — it isn't). - Soft reset: make a commit (
echo x > a && git add a && git commit -m "soft test"). Rungit reset --soft HEAD~1. Confirm the file is staged but the commit is gone fromgit log. - Mixed reset: make another commit. Run
git reset HEAD~1(mixed is the default). Confirm the file is unstaged but its content is preserved. - Revert: make a third commit and push (or just commit if local). Run
git revert HEAD. Confirm a new commit appears on top with messageRevert "...". - Reflog rescue: make 2 commits then
git reset --hard HEAD~2. Rungit log— they're gone. Rungit reflog, find the hash, andgit reset --hard <hash>. They're back. - Stretch: repeat the reflog rescue but use
git cherry-pickto bring back only one of the two commits (next lesson covers cherry-pick — try it now and see the docs say what they say).
The next lesson covers cherry-pick and rebase — the more advanced moves for shaping history.