Bisect first: find the change before you read the code

An extremely dark, moody, cinematic architectural interior of an exclusive private members club, near-black shadows with faint warm muted-gold rim lighting catching the edge of a minimalist concrete and glass room. Deep atmospheric haze, almost pure black in the lower portion, subtle warm amber glow from a single hidden light source. No people, no computers, no visible tech. Editorial fashion-magazine photography, high contrast, restrained and luxurious, evoking a serious calm exclusive collective. Vast negative space, sophisticated darkness like an Aman hotel or Soho House lobby at night.
Dark, atmospheric editorial photograph of a minimalist concrete-and-glass interior at night, warm amber rim light, no people

There is a kind of bug that arrives with a sentence attached: “it worked last week.” The feature is the same, the inputs are the same, and yet something is different. The instinct is to open the file you suspect and start reading. Resist it. When a thing used to work and now doesn’t, the fastest path to the cause is almost never the code — it’s the history. Find the change first. Read the code second.

This guide is about doing that well, because the difference between a sloppy bisect and a disciplined one is the difference between an afternoon and five minutes.

Start with a reproduction you can trust — before you touch history

Bisection is only as good as the question you ask at each step, and the question is always the same: is the bug present in this version, yes or no? If you can’t answer that reliably, the search will lie to you. So the first job is a reproduction that gives the same answer every time.

That usually means turning “it looks wrong” into something mechanical. A failing test. A script that exits 0 when the behavior is good and non-zero when it’s bad. A one-line command that prints the number you expect versus the number you get. The moment you have that, you’ve bought yourself the ability to ask history a clean question — and you’ve removed yourself, and your judgment on each step, as a source of error.

The tax people skip here is flakiness. If the reproduction fails one run in five for reasons unrelated to the bug, bisection will happily converge on an innocent commit. Spend the time to make it deterministic — pin the seed, fix the clock, control the input — before you spend time searching. A shaky reproduction doesn’t just slow the search; it produces a confident, wrong answer.

Then let the machine walk the history

Git has a built-in binary search for exactly this. You mark one commit as good (the behavior was fine) and one as bad (it isn’t), and it checks out the midpoint and asks you to judge. Every answer halves what’s left. A regression hiding somewhere in a thousand commits is ten questions deep, not a thousand.

git bisect start
git bisect bad                 # current commit is broken
git bisect good v2.3.0         # this old tag was fine

Now the crucial move — the one that separates the practice from the folklore. Don’t judge each step by hand. Hand git the script:

git bisect run ./repro.sh

repro.sh exits 0 when the behavior is good and non-zero when it’s bad, and git drives the entire search unattended, printing the first bad commit when it lands. You went to get coffee; it found the change. The manual version — checking out, poking, deciding, typing good or bad — works, but it reintroduces exactly the human inconsistency the reproduction was supposed to remove. Automate the judgment or you’ve only automated the checkout.

Handle the messy middle honestly

Real history isn’t a clean line of buildable commits. Some midpoints won’t compile. Some are irrelevant to the bug. The right answer for those is git bisect skip, not a guess — and your script should signal it. By convention, exit code 125 tells git bisect run “I can’t judge this one, move on.” Guessing good or bad on a commit you can’t actually test is the most common way a bisect quietly points at the wrong place.

Two more traps worth naming. First, the bug might predate your good marker — you assumed a version was clean and it wasn’t. If the search converges somewhere absurd, widen the range and check your assumption before you trust the result. Second, the first bad commit is where the behavior changed, which is not always where the fault lives. A commit can expose a latent bug that was always there. The bisect tells you when it started mattering; you still have to read the diff to understand why.

What good looks like

A clean regression hunt has a shape. You reproduce the failure mechanically and prove the reproduction is stable. You identify one version where the behavior was correct. You let the machine bisect with a scripted judgment, skipping what it can’t evaluate. You end holding a single commit — usually small, usually surprising — and only then do you open an editor. Most of the thinking happened before you read a line of the change.

The habit this builds is more valuable than any single fix: it trains you to convert a vague “something’s off” into a yes-or-no question a computer can answer, and to make the history do the searching. Systems rarely fail at the clever line. They fail at the small change nobody connected to the symptom — and the small change is exactly what bisection is built to surface.

The next time someone says “it worked last week,” take them at their word. The word week is a boundary. Somewhere between then and now is one commit. Go get it, then read.

Leave a Reply

Discover more from Nullhaus

Subscribe now to keep reading and get access to the full archive.

Continue reading