Lesson 28 of 34 intermediate 6 min read

Before this:Reproducing a bug

Bisecting history

Key takeaways When something used to work, version history holds the answer: some single commit broke it. git bisect binary-searches history — mark a good commit and a bad one, test the midpoint, halve the range, repeat — finding the culprit among ~1,000 commits in ~10 tests. With a script that exits 0/non-zero, git bisect run does the whole hunt unattended. The prerequisites are the module’s other disciplines: a reproduction you can run per commit, and the small, always-building commits good practice produces.

Some bugs you diagnose forward, from the code. This lesson’s tool diagnoses backward, from history — and for one common situation it beats everything else in this unit: the regression, where the only thing you know is that it worked before and doesn’t now.

The insight: history is a sorted array

“It worked three weeks ago” plus “it’s broken now” brackets the failure: in the commit sequence between those two points, there’s a first bad commit, and every commit after it is bad too (good-good-good-bad-bad-bad). A sorted boundary like that is binary-searchable: test the middle commit — if it’s good, the break is in the newer half; if bad, the older half. Either way the suspect range just halved. Powers of two do the rest:

Commits in range Tests needed
16 4
250 8
1,000 ~10
16,000 ~14

A month of a busy repo’s history, narrowed to one commit, in a dozen checks. And the payoff of finding it is huge: the diff of one commit is a shortlist of suspect lines, plus the author, the message, and the intent — often the diagnosis practically reads itself.

Driving git bisect

Git automates the bookkeeping — the checkout choreography, the range tracking — while you supply verdicts:

git bisect start
git bisect bad                  # current commit is broken
git bisect good v1.4.0          # this tag was fine

# git checks out the midpoint. Test it (run your reproduction), then:
git bisect good                 # ...if it worked
git bisect bad                  # ...if it failed
# repeat: git keeps halving until —

# 3f2a91c is the first bad commit
#   decoder: rework burst length handling

git bisect reset                # return to where you started

Each round: git checks out a commit, you run your reproduction, you pronounce good or bad. That’s the whole interface — plus git bisect skip for a commit that won’t build, and the Git module’s lesson for the surrounding mechanics.

The killer feature: git bisect run

If your reproduction is a command — and after this module it should be: a test is exactly that — the human leaves the loop entirely. Any script that exits 0 for good and non-zero for bad can drive the search:

git bisect start
git bisect bad
git bisect good v1.4.0
git bisect run go test ./decoder/ -run TestDecodeBurst_TruncatedFrame

Git now checks out, runs, judges, and halves — unattended — and prints the first bad commit a few minutes later. This is the module’s ideas compounding: the failing-first regression test you wrote to pin the bug is also the oracle that finds the commit that caused it. Write once, use as reproduction, bisect oracle, and permanent guard.

# The reproduction can be anything executable — a replay of a capture:
git bisect run ./scripts/check_capture_decodes.sh site_capture.raw

What bisect quietly assumes — and repays

Bisect’s preconditions are the very habits earlier lessons argued for, which is why it doubles as an argument for them:

  • A runnable reproduction. Bisect asks “good or bad?” dozens of times; only an on-demand check can answer. No reproduction, no bisect.
  • Commits that build. A history where half the commits don’t compile yields skip after skip, and wide skip-gaps can leave a range of candidates instead of one culprit. This is the concrete payoff of the Git module’s “every commit should build” discipline.
  • Small, single-purpose commits. Bisect names a commit; the diagnosis quality depends on its size. A 40-file “misc changes” culprit leaves you nearly where you started — a 3-file focused commit hands you the bug. (The same property that made review effective.)
  • A deterministic check. A flaky oracle poisons the search: one wrong verdict sends the binary search into the wrong half, and the final answer is confidently wrong. If the failure is probabilistic, make the script run the test N times and judge the batch.

Rule of thumb: when the report says “it used to work,” reach for bisect before reading code — ten minutes of mechanical search often replaces an afternoon of theorizing, and hands you the diff, the author, and the intent.

Quick check: roughly how many test runs does git bisect need to find one bad commit among 1,000?

Recap

  • A regression means history contains a first bad commit — a sorted boundary, findable by binary search.
  • git bisect runs the search: mark good and bad, judge midpoints, ~10 tests per 1,000 commits.
  • git bisect run <cmd> automates the verdicts — your failing regression test doubles as the oracle.
  • The found commit’s diff, author, and message turn “somewhere this month” into a near-complete diagnosis.
  • Bisect repays the module’s habits: runnable reproductions, deterministic checks, and small commits that build.

Next up: Root-cause analysis