HAMZA KAREEM / CONCEPTS hamzawi.dev

Lesson 132 min read

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 skip it 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 reset strands 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?