Git from First PrinciplesPost 7 of 16 · 16 of 16 written
- How to use this guide
- 01Why version control exists
- 02Git's data model: a content-addressed object database
- 03References: branches, HEAD and tags are pointers
- 04The three areas: working tree, index, repository
- 05Daily commands, mapped onto the model
- 06Branching and integration: merge, rebase, cherry-pick
- 07Undoing things safely
- 08Remotes and collaboration
- 09Workflows and conventions
- 10Git for AI and ML work
- 11Git with AI coding agents
- 12Power tools
- 13Under the hood: build a commit by hand
- 14Debugging mindset and recovery playbook
- 15Practice path, fluency checklist and glossary
- 16Sources
7. Undoing things safely
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 branchStashes 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~1change, and where do the undone changes end up? - Why is
revertsafe onmainwhileresetis not? - You ran
git reset --hardand lost three commits. Which two commands get them back?