The "Undo" Toolkit: Reversing AI Mistakes
"The AI will break things. Your job isn't to prevent that — it's to recover instantly."
No matter how good your AI assistant is, it will occasionally hallucinate, delete the wrong file, or introduce a subtle bug. Git gives you a full spectrum of undo tools — from gentle nudges to nuclear resets. Knowing which tool to reach for in each situation is what separates a confident developer from a panicked one.
4.1 "Discard This Mess" (Local, Not Committed)
The AI just rewrote half your file and it's completely wrong. You haven't committed anything yet. This is the simplest undo — just throw it all away and go back to your last save point.
The fix is simple -- one command wipes the slate clean and takes you back to your last commit:
git restore . # Discard edits to tracked files
git restore src/bad_file.py # Discard a single fileOne blind spot: git restore only rewinds files Git already tracks. Brand-new files the AI scaffolded are
untracked, so they survive it. Preview the leftovers with git clean -n (a dry run), then delete them with git clean -fd.
And one more trick for the road — restore doesn't have to stop at "the last commit." The
most common real-world rescue is one file, from an older commit: the AI broke src/parser.py three commits ago and you only just noticed. No reset, no history surgery:
git restore --source=HEAD~3 src/parser.py
# The file's content is now as it was 3 commits ago;
# everything else stays untouched. Review it, then commit.In VS Code, you don't need the terminal for this. In the Source Control panel, hover over the file you want to discard under "Changes" and click the curved arrow icon. To discard ALL changes at once, click the curved arrow next to the "Changes" header. You can also right-click any file and choose "Discard Changes".
"That last change broke everything — throw it all away and go back to my last commit"
"Discard all the changes you just made, they're not working"
4.2 "I Staged This by Accident" (Staged, Not Committed)
You hit git add . a bit too quickly and staged files you didn't mean to include.
No worries — unstaging is completely harmless and doesn't touch your code.
git add . and accidentally staged a file with a bad AI change. You need to "unstage" it.No worries -- unstaging is a safe operation that simply moves a file back out of the staging area:
git restore --staged src/bad_file.py
# Older equivalent: git reset HEAD src/bad_file.pyIn VS Code, this is a one-click fix. In the Source Control panel, look under "Staged Changes", hover over the file you want to unstage, and click the − (minus) button. The file moves right back to "Changes":
Try It: Unstage Dangerous Files
git add . too quickly and staged .env with credentials
and a debug file. Unstage them with git restore --staged before committing — then
remember the permanent fix from Part 2: add them to .gitignore so the next git add . can't stage them again.Loading playground...
"Unstage config.py, I don't want that in this commit"
"I accidentally staged everything — unstage all files except auth.py"
4.3 "I Forgot a File in My Last Commit"
You just committed — and immediately realized you forgot a file, or there's a typo in the message. Instead of creating a messy "oops" commit, you can quietly fix the last one.
Instead of creating a whole new commit, you can tack the missing file onto the one you just made:
git add src/forgotten_file.py # Stage the missed file
git commit --amend --no-edit # Add it to the last commit
# Or just fix the message:
git commit --amendIn VS Code, click the dropdown arrow next to the Commit button and select "Commit (Amend)". This adds your newly staged files to the last commit without needing the terminal. You can also find this under the ... menu → Commit → Commit Staged (Amend).
--force-with-lease push (see section 4.6). If teammates may have pulled the commit,
don't amend — revert instead."I forgot to include the test file in my last commit — add it without creating a new commit"
"Fix my last commit message, it should say 'fix' not 'feat'"
4.4 "Nuke This Whole Feature" (Locally, Committed)
Sometimes the AI experiment was a dead end — three commits deep, and none of it is salvageable. If you haven't pushed yet, you can erase those commits entirely and start fresh.
This is where the nuclear option comes in. A hard reset rolls your branch back as if those commits never happened:
git reset --hard HEAD~3 # Remove last 3 commits + all changes"Remove," not quite "delete": the commits vanish from your branch, but Git keeps them around
for ~30 days and the reflog (section 4.9) can bring them back. The truly unrecoverable loss
is different — see the --hard card below.
The "Safer" Resets
--soft: Keep changes staged
Deletes commits but keeps changes in the Staging Area. Useful for "squashing" commits into one.
--mixed (default): Keep changes unstaged
Deletes commits but keeps changes in the Working Directory (unstaged).
--hard: Destroy everything
Removes commits AND all code changes. Your files reset to the older commit's state —
including any uncommitted work sitting in your working tree, which is
the one thing the reflog can never bring back. Run git status first; if it isn't clean, stash or commit before you reset.
git reset rewrites history. Never use this on a branch your teammates have already pulled. This is for local cleanup only."The last 3 commits were all bad — nuke them but keep the code changes so I can redo it"
"Completely undo my last 2 commits, I want to start fresh from before them"
4.5 "I Pushed a Bug to the Team!" (Public, Pushed)
This is the "oh no" moment — you pushed a bad commit and your teammates already pulled it. You can't erase history, but you can create a new commit that perfectly reverses the damage.
main. Your teammates have already pulled it.The WRONG Solution: You cannot use git reset. It rewrites history that others have, causing repository
divergence.
The RIGHT Solution: Create a new commit that undoes the bad commit. This is a revert.
Here's how to create that revert commit in the terminal:
git log --oneline # Find the hash: a1b2c3d
git revert --no-edit a1b2c3d # Create an inverse commit
git push # Push the revertWithout --no-edit, Git opens your editor so you can customize the revert
message — fine once you expect it, startling the first time (especially if that editor turns
out to be vim).
Notice that you're not erasing anything -- you're adding a new commit on top that reverses the damage:
VS Code has this built in too. Open the ... menu in the Source Control panel -- this is your gateway to all advanced Git operations. From here you can access Commit, Changes, Pull, Push, Branch, Stash, and more:
For reverting pushed commits, use the Source Control Graph: right-click any commit and select "Revert Commit" to create the inverse commit safely.
"I pushed a broken commit to main — safely undo it without rewriting history"
"Revert commit a1b2c3d, it introduced a bug in production"
4.6 The "Break Glass" Command
You rewrote local history with a reset or amend — and now Git refuses to push because local and remote have diverged. This is the emergency tool: a force push with a built-in safety net.
git reset or git commit --amend on a branch you already pushed. Local and remote history have
diverged. Git refuses to let you push.You have two flavors of force push, and picking the right one matters a lot:
git push --force
Replaces the server unconditionally. If a teammate pushed in the last 5 minutes, you blow their commits off the server (they survive only in that teammate's local clone).
git push --force-with-lease
Conditional force push. Only succeeds if the remote branch hasn't changed since your last fetch. Always use this instead.
One subtlety: the lease compares the remote against what you last fetched — so run git fetch first, review what changed on the remote, and only then push.
git.autofetch), GitLens, and other IDE tooling
can fetch every minute — after which --force-with-lease passes even though you never looked at what arrived. The lease proves the remote hasn't
changed since the last fetch — it can't prove you reviewed it. Fetch, read, then push.! [rejected] (non-fast-forward) — hint: Updates were rejected because the tip of your current branch is behind This is Git protecting you. But careful — the full hint goes on to suggest git pull, and after a deliberate amend or reset that's exactly wrong:
pulling merges the old commit right back in, recreating the mess you just cleaned up. When you rewrote the history on purpose, the answer is the lease push, not a pull. Try it
yourself below:Try It: Reset and Force Push
git reset --hard HEAD~2 to go back, watch a plain git push get rejected, then overwrite the remote with git push --force-with-lease. Never do this on shared branches!Loading playground...
"I amended a commit I already pushed and now I can't push — help me fix it safely"
"What's the safest way to force push after rewriting history on my branch?"
4.7 The Git "Undo" Recovery Matrix
Before moving to advanced topics, here's a quick-reference matrix summarizing every undo technique and when to use it:
| Scenario | Command | What It Does | Safe? | VS Code |
|---|---|---|---|---|
| AI's change is bad, not committed | git restore . | Discards edits to tracked files (new untracked files survive — see git clean) | Safe (Local) | Right-click file → "Discard Changes" |
| One file broke several commits ago | git restore --source=HEAD~3 <file> | Brings back that file's old content; history and other files untouched | Safe (Local) | Timeline → click old commit → copy the old version |
| File staged by accident | git restore --staged <file> | Unstages a file, moving it from Staging back to Changes | Safe (Local) | Right-click staged file → "Unstage Changes" |
| Typo in last commit message | git commit --amend | Edits the message of the most recent commit | Safe (if not pushed yet) | ... → Commit → Commit (Amend) |
| Forgot a file in last commit | git add <file>git commit --amend --no-edit | Adds new files to the most recent commit | Safe (if not pushed yet) | Stage files → ... → Commit Staged (Amend) |
| Last 3 local commits are bad | git reset --hard HEAD~3 | Removes the last 3 commits and all their code changes (reflog can recover the commits) | Local Only! (Rewrites history) | ... → Commit → Undo Last Commit (once per commit) |
| Commits "vanished" after a hard reset | git refloggit reset --hard HEAD@{1} | Finds the lost commit in the reflog and moves the branch back | Safe (Local) | Terminal only (see section 4.9) |
| Pushed a bug to the team | git revert <hash> | Creates a new commit that is the inverse of the bad one | 100% Safe (Public) | Source Control Graph → Right-click commit → "Revert Commit" |
| Reset a public branch, need to push | git push --force-with-lease | Forcefully overwrites remote, only if no one else pushed | Enterprise "Break Glass" | Git: Push (Force With Lease) |
| Stuck in "detached HEAD" | git switch maingit switch -c keep-this | Reattaches HEAD to a branch — create one first if you made commits to keep (see section 4.8) | Safe (Local) | Click the branch name in the status bar |
Try It: The Undo Toolkit
git revert HEAD and git commit --amend. Try reverting the pushed bad commit, then amending after
staging a fix.Loading playground...
4.8 Detached HEAD — Time Travel Safely
You (or the agent) checked out an old commit hash to see how the code looked before a refactor — and the terminal barks back: "You are in 'detached HEAD' state." It sounds like an injury. It isn't. It's Git telling you exactly where you are: in the past.
git checkout a1b2c3d (or git checkout HEAD~2) to inspect an old version. Git warns about a "detached
HEAD" and you're not sure if you broke something.Normally, HEAD points at a branch, and the branch moves forward with every commit. When you check out a commit hash directly, HEAD points at that commit instead — no branch is along for the ride. Looking around is completely safe:
git log --oneline # Find the commit you want to visit
git checkout HEAD~2 # Detach HEAD two commits back
cat src/app.py # Look around — reading is 100% safeThe catch is committing while detached.
Any commits you make there belong to no branch — the moment you switch away, nothing points
at them anymore and they become orphaned.
They don't disappear instantly (the reflog still remembers them — see section 4.9), but they
vanish from git log and real Git will eventually garbage-collect them.
There are exactly two escape hatches, and choosing is easy:
# Made something worth keeping? Put a branch under your feet:
git switch -c inspect-v02
# Just sightseeing? Go back to the present:
git switch maingit switch -c <branch> — and even if you forget, the reflog is your safety net.Try It: Time-Travel and Escape
git checkout HEAD~2 to visit version 0.2, inspect src/app.py, then escape — git switch -c inspect-v02 to keep a
foothold, or git switch main to return to the present.Loading playground...
"Check out the commit from before yesterday's refactor so I can see how auth.py looked"
"I'm in detached HEAD state and I made a commit here — save it to a new branch and take me back to main"
4.9 The Reflog — Your Time Machine
The agent ran git reset --hard and the log now looks like two days of work never happened. Before you panic: those commits are not gone. Git keeps a private journal of
every place HEAD has ever been — the reflog — and it's about to save your week.
git log. You need them back.git log only shows commits reachable from your
branches. The reflog shows everywhere HEAD has moved, reachable or
not. Here's how to read it:
git reflog
a9f8e21 HEAD@{0}: reset: moving to HEAD~2 <- where you are NOW
4c7d3b9 HEAD@{1}: commit: feat: add caching layer <- one move ago (the "lost" work!)
8e2f1a0 HEAD@{2}: commit: feat: add ranking algorithm
1d0c5f4 HEAD@{3}: commit (initial): Initial commitHEAD@{n} means "where HEAD was n moves ago" — HEAD@{0} is now, HEAD@{1} is one move back. Each line also shows the short hash, so you have
two ways to name any point in time. Pick your recovery recipe:
# 1. Undo the reset entirely — move the branch back:
git reset --hard HEAD@{1}
# 2. Rescue a single lost commit onto your current branch
# (cherry-pick gets its own section in 5.4):
git cherry-pick 4c7d3b9
# 3. Inspect first — park a rescue branch on the lost commit:
git switch -c rescue 4c7d3b9restore for the working directory, revert for public history, reset for local history — and reflog when a reset itself was the mistake.Try It: Rescue Lost Commits
git reset --hard HEAD~2 and two commits vanished. Run git reflog to find where HEAD was before the reset, then git reset --hard HEAD@{1} to bring
everything back.Loading playground...
"The last reset deleted commits I still need — check the reflog and restore them"
"Find the commit where tests still passed in the reflog and create a rescue branch on it"
The reflog answers "where did my commits go?" Its sibling question — "which commit broke this?" — has its own tool: git bisect binary-searches your history. Mark one commit as bad and one as good, and Git checks out midpoints
for you to test — 1,000 commits take ten rounds, not a thousand. In the agent era this is the
tool for "somewhere in the bot's forty commits, the tests started failing."
Try It: Find the Bad Commit
main but passed at v1.0, eight commits
ago. Use git bisect plus the run-tests command to pin down the culprit
in three rounds instead of eight.Loading playground...
"The tests fail now but passed at v1.0 — bisect between them and tell me the first bad commit"
"Run git bisect to find which of your last 20 commits broke the login flow"
Loading challenge...