Every line tagged with the commit that last changed it. The hash is the payload — feed it to git show and you get the diff, the author, and the message that says why.
blame shows only the last touch. To see what a line was before that, blame the parent: git blame c584509~1 -- invoice.py. Or git log -L :total:invoice.py to watch one function evolve.
When did this text arrive or vanish? — the pickaxe
$ git log -S "TAX_RATE" --oneline # commits that add or remove the stringc584509 Add tax calculation$ git log -p -- invoice.py # every change to one file, diffs inline$ git show c584509 # one commit in full: diff, author, message
-S finds where a string was introduced or deleted — "who added this call", "where did this constant go". -G takes a regex instead of a literal.
Blame answers "what's on this line now". The pickaxe answers "which commit changed this text, ever" — even if later commits moved or reindented it.
$ git reflog
9d4e1a7 HEAD@{0}: commit: Cap discount at subtotal2b8c6f0 HEAD@{1}: reset: moving to HEAD~17a1f9c3 HEAD@{2}: commit (amend): Tidy discount helper1f9a7d2 HEAD@{3}: rebase (finish): returning to refs/heads/feature/discounta30f5c8 HEAD@{4}: pull --rebase: Fast-forward8e3b4a6 HEAD@{5}: checkout: moving from main to feature/discount6d0c2f1 HEAD@{6}: merge feature/tax: Merge made by the 'ort' strategyf6c6987 HEAD@{7}: checkout: moving from feature/tax to mainc584509 HEAD@{8}: commit: Add tax calculationb71d3e8 HEAD@{9}: clone: from github.com:acme/pricing.git
Every HEAD movement is journaled — not just commit and reset, but checkout, pull/clone, merge/rebase too. Newest on top; HEAD@{n} counts movements, not commits.
Two things that scale with a real repo: each branch keeps its own reflog (git reflog show feature/discount, or feature/discount@{1}), and you can index by time — HEAD@{yesterday}, HEAD@{2.hours.ago}.
6.2
bisect
It worked in March. It's broken now.
Four hundred commits in between. Which one did it?
Reading four hundred diffs: hours. Binary search: nine checks.
Marking good and bad
$ git bisect start
$ git bisect bad # current commit is broken$ git bisect good v2.1 # this tag was fineBisecting: 187 revisions left to test (roughly 8 steps)
Git checks out a commit halfway between. You test it, and tell git what you found.
$ git bisect start
$ git bisect bad # HEAD 8b3f1a9 is broken$ git bisect good v2.1 # 91ac5d3 was fineBisecting: 5 left (~3 steps) — now testing d5f1a83 → git bisect goodBisecting: 2 left (~2 steps) — now testing f6c6987 → git bisect badBisecting: 1 left (~1 step) — now testing 1a2b3c4 → git bisect goodBisecting: 0 left (~1 step) — now testing c584509 → git bisect badc584509 is the first bad commit
Mark the ends — bad HEAD, good v2.1 — then each round is two moves: git checks out a commit and asks, you test it and answer. Four rounds pin the culprit; nine would pin one in four hundred.
The culprit, and getting out
c584509 is the first bad commit Add tax calculation$ git bisect reset # back to where you started
If you can script the test, git will do the whole thing unattended:
$ git stash
Saved working directory and index state WIP on main$ git switch hotfix # deal with the fire$ git switch main
$ git stash pop # pick up where you left off
Stash is a stack. git stash list shows them; pop takes the most recent.
Plain git stash only tucks away tracked changes. A brand-new file you haven't added is untracked — it stays in your working tree and follows you to the other branch.
$ git stash -u # --include-untracked: sweep up new files too$ git stash -a # --all: untracked and ignored (rarely what you want)
Git merges automatically when the two sides changed different regions relative to the common ancestor.
A conflict is git saying: both of you changed the same region, and I will not guess which one you meant.
It's a request for a decision, not an error.
Which side is ours?
ours = the base. theirs = the commits stacked on top of it.
Same shape both times: main is the base (ours), feature sits on top (theirs). Only your standing point changes — and a rebase runs from feature, yet feature is still theirs. Unsure? Skip the flags and resolve in the editor.
The same two sides, in the markers
<<<<<<< HEAD ← ours · the base (main)
TAX_RATE = 0.20
=======
TAX_RATE = 0.175
>>>>>>> feature ← theirs · stacked on top
The top block, up to =======, is ours (HEAD). The bottom block is theirs. Exactly the two sides from the graph — teal base, rose on top — now in the file.
HEAD is main in both a merge and a rebase. So in a rebase the top block is main's line and >>>>>>> names your own commit — "HEAD" is not "the branch you started on". Read the position, not the label: top is the base, bottom is what's arriving.
The default markers hide the useful part
All you're shown are the two tips — main's 0.20 and feature's 0.175. The ancestor they both diverged from is dropped, so you can't tell which side actually changed the line.
Did someone raise it from 0.175, or lower it from 0.20? The answer changes the decision entirely.
Both new lines land in the same spot — the end — so git can't tell them apart. Conflict.
✓ kept alphabetical
deps = [
"aiohttp", # A: sorts to top
"flask",
"requests",
"urllib3", # B: sorts to bottom
]
Alphabetical order sends each addition to a different line. Git drops both in and merges without a word.
Reduce conflicts — more habits
Same principle as the commas and the sorting: keep every change small, local, and canonical.
Habit
Why it cuts conflicts
One item per line
Each change is its own line, so two edits can't overlap
A shared auto-formatter on save (prettier · black · gofmt)
Everyone's output is byte-identical — no whitespace or reflow conflicts, and minimal diffs
Never hand-edit generated files or lockfiles
Regenerate them — the tool owns the format, not you
Union-merge append-only files (CHANGELOG.md merge=union in .gitattributes)
Git keeps both sides automatically instead of conflicting
Block 5's rule, one more time: the cheapest conflict is the one you never create. Short branches + code that merges cleanly removes most of them before they start.
Stop resolving the same conflict twice
$ git config --global rerere.enabled true
Reuse recorded resolution. Git remembers how you resolved a conflict and reapplies it automatically next time it sees the same one.
Useful during a long rebase where the same conflict recurs commit after commit. Set it and forget it.
Escape hatches
$ git merge --abort # put everything back$ git rebase --abort # same, mid-rebase$ git checkout --ours file # take our whole version$ git checkout --theirs file # take theirs
During a rebase, "ours" and "theirs" are reversed from what you'd expect — you're replaying your commits onto their base, so "ours" is the branch you're rebasing onto.
That's the course
Block
The one-line version
1 — Local model
Three trees, snapshots, pointers
2 — Recovery
Almost nothing is ever really lost
3 — Rewriting
New commits, not edited ones
4 — Remotes
origin/main is your note about the server
5 — Team workflow
Short branches, small PRs, authors own them
6 — Investigation
Tools for when it goes sideways
Handout: the 13 commandments. Slides and katas are in the repo — the speaker notes have every command.
The 13 commandments
01Commit early, commit often.
02One logical change per commit. If the message needs an "and", it's two.
03Read your own diff before you commit.
04Never rewrite history someone else has pulled.
05Force push only your own branch — with --force-with-lease.
06Revert in public, reset in private.
07Keep your branch current with main.
08Never commit secrets, build output, or dependencies.
09When it looks broken, stop. reflog, then ask.
10The PR description is the permanent record.
11Keep PRs small enough to review in one sitting.
12You opened it, you own it.
13After a squash merge, delete the branch and re-branch from main.
Nine follow from how git works. Four are ours to change. The printable handout is in the repo.