Undoing Mistakes — reset, revert, and checkout

9 min read

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.js

Old syntax (still works everywhere):

git reset HEAD cypress.config.js

Either way, the file leaves the staging area but your edits stay in the working directory:

git status
On 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:

FlagRepositoryStagingWorking dir
--softresetpreservedpreserved
--mixed (default)resetresetpreserved
--hardresetresetreset (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 -3
4c48901 Add cypress config for staging
8f31320 Add login regression tests
dc57b17 Update fixtures

The bad commit is 4c48901. Two-step recovery:

git revert 4c48901

A new commit appears on top, restoring the file to its previous state (no token).

[main 9f2e3b4] Revert "Add cypress config for staging"

Push:

git push

Now: 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 reflog
9f2e3b4 (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 4c48901

Or 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

GoalCommand
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 safelygit revert <hash>
Recover from a bad resetgit reflog then git reset --hard <hash>

Bookmark the Git for QA cheat sheet for this table; you'll need it.

⚠️ Common mistakes

  • git reset --hard on 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. Always revert for shared branches; reserve reset for your own laptop's local work.
  • Running git restore <file> to "undo edits" without realising it's destructive. Beginners try git restore casually, 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 revert as "delete from history." A revert undoes the effect of a commit, but the original commit and its diff still exist in git log and git 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.

  1. In your qa-sandbox repo, make and commit a small change you don't mind losing. Confirm with git log --oneline -3.
  2. Unstage: edit a file, git add it, then git restore --staged <file>. Confirm the file is back to "modified but not staged."
  3. 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).
  4. Soft reset: make a commit (echo x > a && git add a && git commit -m "soft test"). Run git reset --soft HEAD~1. Confirm the file is staged but the commit is gone from git log.
  5. Mixed reset: make another commit. Run git reset HEAD~1 (mixed is the default). Confirm the file is unstaged but its content is preserved.
  6. Revert: make a third commit and push (or just commit if local). Run git revert HEAD. Confirm a new commit appears on top with message Revert "...".
  7. Reflog rescue: make 2 commits then git reset --hard HEAD~2. Run git log — they're gone. Run git reflog, find the hash, and git reset --hard <hash>. They're back.
  8. Stretch: repeat the reflog rescue but use git cherry-pick to 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.

// tip to track lessons you complete and pick up where you left off across devices.