Lesson 03 · solo safety
Every "I made a mistake" fix, sorted by which tree or pointer it touches. This is the lesson that kills the panic.
This is the payoff of the first mission goal: never lose work, undo anything without googling. It works because you already have the map. Every command below is just moving a pointer or overwriting one of the three trees — nothing new, only recombination. The trick is matching the mistake to the tree it lives in.
git status and git log --oneline --graph --all. Know where you are before you move. Every fix below is reversible except throwing away uncommitted working-directory changes — so that's the only category to slow down for.
"I staged a file I didn't mean to."
Change lives in the staging area. Move it back to working dir — edits on disk untouched.
git restore --staged <file> # unstage, keep the edit
"I want to throw away my edits to a file and go back to the last commit."
Change lives in the working directory. This one destroys the edit — there's no undo. Be sure.
git restore <file> # discard working-dir edits — irreversible
"My last commit message has a typo / I forgot to add a file."
The commit is fine, you just want to redo it. --amend replaces the top commit with a new one.
git commit --amend -m "better message"
# or: stage the forgotten file first, then:
git add forgotten.js
git commit --amend --no-edit # keep the old message
"I committed too early — I want the commit undone but my work kept."
Move the branch pointer back one, leave the trees. That's reset — covered in full next.
git reset --soft HEAD~1 # uncommit, changes stay STAGED
git reset HEAD~1 # uncommit, changes go to working dir (unstaged)
"I already pushed / shared the commit and want to reverse it."
Don't rewrite shared history. Add a new commit that undoes the old one. Safe on a team.
git revert <sha> # new commit that inverts <sha>; history preserved
reset rewrites history (moves the pointer, old commits fall off). Great before you push. revert adds a new inverse commit, leaving history intact — the right tool after a commit is shared. Rewriting history someone else has already pulled is how you create the merge nightmares your teammates blame Git for.
reset is the command people fear, because --soft, --mixed, --hard look arbitrary. They're not. Pro Git frames reset as walking down three steps, stopping where you tell it:
--soft stops here--mixed (default) stops here--hard goes all the wayPro Git, 7.7 — Reset Demystified reset "overwrites these three trees in a specific order, stopping when you tell it to: (1) Move the branch HEAD points to — stop here if--soft. (2) Make the index look like HEAD — stop here unless--hard. (3) Make the working directory look like the index."
Reset Demystified →
| Command | Branch ptr | Staging | Working dir | Use when |
|---|---|---|---|---|
reset --soft | moves | — | — | Uncommit; keep everything staged, ready to recommit. |
reset (--mixed) | moves | reset | — | Uncommit and unstage; edits stay on disk. The default. |
reset --hard | moves | reset | reset | Nuke it all back to a commit. Destroys uncommitted work. |
--hard is "the only way to make the reset command dangerous, and one of the very few cases where Git will actually destroy data." --soft and --mixed never touch your working directory — your edits are safe. Say the flag out loud before you hit enter.
Here's the fact that should let you sleep. Git almost never truly deletes a commit. When you reset "away" a commit, the commit object still exists — you've only moved the branch label off it. The reflog is a log of everywhere HEAD has been, so you can find that orphaned commit and jump back.
git reflog # every position HEAD has held, newest first
# find the line before your mistake, e.g. "d4e5f6 HEAD@{2}: commit: good work"
git reset --hard d4e5f6 # or: git switch -c rescue d4e5f6
reset --hard feels like disaster. But the commit you reset off is still in reflog for weeks (until garbage collection). "I lost a commit" is almost always "I moved a pointer and can move it back." The one thing reflog can not save: uncommitted edits that were never a commit. Which is the whole argument for committing early and often.
| CLI | VS Code |
|---|---|
git restore --staged <f> | − on the staged row (Unstage Changes) |
git restore <f> | right-click a changed file ▸ Discard Changes |
git commit --amend | Source Control ▸ ⋯ ▸ Commit ▸ Commit (Amend) |
git revert <sha> | right-click a commit in history ▸ Revert Commit |
reset / reflog | no safe GUI button — use the terminal for these |
git init undo-drill && cd undo-drill
echo one > f.txt && git add f.txt && git commit -m "c1"
echo two >> f.txt && git commit -am "c2 — the mistake"
git log --oneline # two commits
git reset --soft HEAD~1 # undo c2
git status # "two" change is STAGED again — soft kept it
git commit -m "c2 redone" # recommit cleanly
git reset --hard HEAD~1 # nuke back to c1 — the change is gone from disk
cat f.txt # just "one"
git reflog # see c2 still listed — pick its SHA...
git reset --hard <sha-of-c2> # ...and it's back. Nothing was ever truly lost.
The whole lesson is in those last two lines: you destroyed a commit with --hard, then walked it back out of reflog. Do that once and reset stops being scary forever.
Primary source — read this one: Pro Git §7.7, "Reset Demystified". The best explanation of reset that exists, with tree-by-tree diagrams. Worth a slow read — it's the command most worth truly understanding.
Also skim §2.4, "Undoing Things" for --amend and restore in the book's own words.