Git from First PrinciplesPost 7 of 16 · 16 of 16 written

7. Undoing things safely

Git · 7 of 164 min readOct 2026

Every undo in Git is one of three moves: add a new commit that cancels an old one (revert), move a branch pointer (reset), or copy a stored version over your files (restore). Pick by asking two questions: has this been shared, and which of the three areas should change?

Pick the right tool

I want to… Command Rewrites history?
Fix the last commit’s message, or add a forgotten file to it git commit --amend Yes, the last commit
Discard unstaged edits to a file git restore <file> No, but the edits are gone for good
Unstage a file and keep the edits git restore --staged <file> No
Bring back a file as it was in an old commit git restore --source <sha> <file> No
Cancel a commit that is already pushed git revert <sha> No
Uncommit the last 3 local commits, keeping the changes git reset --soft HEAD~3 Yes
Make the branch exactly match a commit, discarding the rest git reset --hard <sha> Yes, and discards uncommitted work
Set unfinished work aside for a moment git stash No
Find something you think you lost git reflog No
Delete untracked files git clean -n, then git clean -f No, but deletes files

Amend

git commit --amend builds a new commit from the current index, with the same parent as the old HEAD, and moves the branch to it. The old commit is not edited - objects are immutable - it is replaced, so the hash changes. Fine before pushing; a rewrite after.

Reset and its three modes

git reset <commit> always moves the current branch, and HEAD with it, to the target commit. The mode decides how many of the three areas follow it.

Mode Branch and HEAD Index Working tree Typical use
--soft Moved Unchanged Unchanged Combine the last few commits into one
--mixed (default) Moved Reset to target Unchanged Uncommit and unstage; edits stay on disk
--hard Moved Reset to target Reset to target Throw away everything after the target

An easy way to remember: soft touches one area, mixed two, hard all three. Combining three commits by hand looks like this:

git reset --soft HEAD~3
git commit -m "Implement data pipeline"

With a path, git reset <file> does not move any branch. It copies HEAD’s version into the index, which is the older spelling of git restore --staged <file>.

Undoing a bad reset: git reset --hard ORIG_HEAD, or find the old position in git reflog. Commits always come back; unstaged edits wiped by --hard never do.

Revert

git revert <sha> computes the inverse of that commit’s change and records it as a new commit. History only grows, so revert is the safe undo on shared branches like main.

To revert a merge, say which parent’s side to keep: git revert -m 1 <merge-sha>. One trap: merging the same branch again later will not bring the reverted changes back, because Git considers them already merged. You revert the revert instead.

Restore

restore copies file content from a source into the working tree, the index, or both. By default it copies index → working tree. --staged copies HEAD → index. --source=<sha> uses any commit. --staged --worktree updates both.

Stash

stash saves your staged and unstaged changes as hidden commits under refs/stash, then resets your files to HEAD. Use it when you must switch branches mid-task.

git stash push -m "wip: tokenizer"   # add -u to include untracked files
git stash list
git stash show -p stash@{0}           # inspect before applying
git stash pop                         # apply and remove from the list
git stash apply stash@{1}             # apply and keep it
git stash branch tokenizer-wip        # turn a stash into a branch

Stashes are a stack and easy to forget. For anything longer than a coffee break, a work-in-progress commit on its own branch is clearer.

Clean

git clean deletes untracked files. -n is a dry run, -f actually deletes, -d includes folders, and -x also deletes ignored files - which in an ML project can mean your local datasets and checkpoints. Always dry-run first.

Recovering with the reflog

git reflog
# a1b2c3d HEAD@{0}: reset: moving to HEAD~3
# 9f8e7d6 HEAD@{1}: commit: Add eval script

git branch rescue 9f8e7d6      # safest: put a label on it first
# or: git reset --hard HEAD@{1}

The rule of thumb: if it was committed on this machine in the last month or so, you can get it back.

Local or shared: the decision rule

  • Not pushed yet: amend, reset and rebase freely. It is your private draft.
  • Pushed to your own feature branch: you may rewrite it, then push with --force-with-lease.
  • Pushed to a branch others use: only revert.

Check yourself

  • Which areas does git reset --mixed HEAD~1 change, and where do the undone changes end up?
  • Why is revert safe on main while reset is not?
  • You ran git reset --hard and lost three commits. Which two commands get them back?