git bisect
binary search for the commit that broke it
Developer toolingdeveloper tooling / version control
The scenario
Friday, 5pm. A user reports the CSV export has been broken "for a while." It worked in last month's release; it's broken now. Between then and now sit 240 commits. You could read every diff (slow). You could checkout random commits and pray (random). There's a third way.
git bisect turns "240 commits to inspect" into ~8 checkouts — because log2(240) ≈ 8. It binary-searches your history for the exact commit that flipped good → bad.
The core workflow
git bisect start
git bisect bad # current HEAD is broken
git bisect good v1.4.0 # this tag was fine
# git checks out a commit halfway between them. You test it, then answer:
git bisect good # ...or: git bisect bad
# repeat — git keeps halving the range —
# until it prints: "abc123 is the first bad commit"
git bisect reset # return to where you started
Each answer halves the suspects:
240 → 120 → 60 → 30 → 15 → 8 → 4 → 2 → 1.
The superpower most people never use
If you can express "broken vs fine" as a script that exits 0 (good) or non-zero (bad):
git bisect start HEAD v1.4.0
git bisect run ./test_export.sh
git now drives the ENTIRE search unattended and names the culprit. No manual checkouts.
Where intermediate folks trip up
- "good/bad" describes the BEHAVIOR you're hunting, not code quality. Hunting where a bug got fixed instead of introduced? The labels feel backwards — rename them with
git bisect terms. - A commit in the middle won't build? Don't answer good or bad —
git bisect skipit and git picks a neighbor. - Flaky test = poisoned search. Bisect assumes a clean good→bad transition. One wrong answer sends it down the wrong half and fingers an innocent commit.
- Forgetting
git bisect resetstrands you on a detached HEAD at some random old commit. Always reset when done.
Why the log2 matters
Linear scan of 1,000 commits = up to 1,000 tests. Bisect = ~10. Same reason binary search beats scanning an array — you're just applying it to time instead of memory. The only price: history must be (roughly) a clean line where the bug is "off" then "on."
Your exercise
In any repo: intentionally break something in a commit, stack 5–6 more commits on top, then recover your sabotage commit using only git bisect — no reading diffs. Bonus: write a one-line test.sh (e.g. grep -q expected output.txt) and let git bisect run find it with zero manual checkouts. How many tests did it take vs. how many commits you had?