Techguide · Explainer
Find the commit that broke everything — in about ten guesses.
Something used to work. Now it doesn't. Between those two facts sits a stretch of commit history — maybe a dozen commits, maybe a thousand — and one of them is the culprit. git bisect is the tool that finds it, and it finds it fast.
The mental model is the childhood guessing game: "I'm thinking of a number between 1 and 1000." You don't count up from 1. You guess 500, learn "higher," guess 750, learn "lower," and close in. Each guess halves what's left. Bisect plays exactly this game, except the "number" is the first broken commit and each "guess" is Git checking out a commit in the middle of the suspect range so you can build it and see whether the bug is present.
To play, you give Git two anchors. A good commit is one where the code behaved correctly. A bad commit is one where the bug is present — usually your current HEAD. Everything between them is the search range. Git checks out the commit halfway between good and bad; you test it; you tell Git whether that midpoint is good or bad. That answer throws away half the range. Git checks out the new midpoint, and you repeat.
Crucially, bisect assumes the bug has a single clean boundary: some commit flipped the code from working to broken, and every commit after it stays broken. Under that assumption, the boundary is unique, and binary search is guaranteed to pin it down. The words "good/bad" are just the default vocabulary — bisect is really hunting for the transition, whatever you call the two sides.
That is the whole idea. There is no magic dependency analysis, no blame heuristics, no guessing which commit "looks suspicious." Bisect is mechanical and unbiased: it only knows good, bad, and midpoint, and it converges on the answer with cold arithmetic.
Picture the alternative. You know the build was fine three weeks ago and it's broken today, with 1,000 commits in between. Without bisect, you check out a commit, build, test, and if it's still broken you check out an earlier one and try again. This is a linear scan — in the worst case you inspect all 1,000 commits, and even a lucky guess-and-drift strategy tends to wander. Worse, humans scan with bias: we gravitate toward the commit whose message mentions the subsystem we think is at fault, and we waste hours when the real culprit was an innocent-looking one-liner.
Binary search demolishes this. Each test doesn't eliminate one commit — it eliminates half of them. The number of steps is roughly log₂(N):
| Commits in range (N) | Linear worst case | Bisect steps (~log₂ N) |
|---|---|---|
| 10 | 10 | ~4 |
| 100 | 100 | ~7 |
| 1,000 | 1,000 | ~10 |
| 1,000,000 | 1,000,000 | ~20 |
Read that bottom row again. A million commits — a history larger than the Linux kernel — is bisected in about twenty tests. That is the payoff of logarithmic scaling: the range can grow astronomically while the work barely moves. Doubling the history adds exactly one step.
This is why bisect is the default reach-for tool the moment you can name a good commit and a bad commit. It replaces judgment and stamina with a procedure that cannot be fooled by a misleading commit message and cannot get tired at commit 600. You supply the yes/no answers; Git supplies the relentless halving.
The manual workflow is four commands and a loop. Start a session:
git bisect start
Tell Git the current state is broken, then point it at a commit you know was fine:
git bisect bad # HEAD is broken
git bisect good v1.4.0 # this tag/commit worked
Git now knows the search range and immediately checks out the commit halfway between v1.4.0 and HEAD, printing something like "Bisecting: 512 revisions left to test after this (roughly 9 steps)." Your working tree is now at that midpoint commit. Build it, run it, reproduce (or fail to reproduce) the bug, and report the verdict:
git bisect good # the bug is NOT here — culprit is later
# ...or...
git bisect bad # the bug IS here — culprit is here or earlier
Each verdict halves the range and Git checks out the next midpoint. You repeat: build, test, good/bad. After about log₂(N) rounds Git announces the result:
a1b2c3d is the first bad commit
— complete with the commit's author, date, message, and diff. That diff is usually your bug, staring back at you.
You are still sitting on some old midpoint commit, so finish by returning home:
git bisect reset
This restores HEAD and your working tree to exactly where you began. Skipping this step is the single most common bisect mistake — you wander off thinking you're on your branch when you're actually parked on a detached commit from the middle of history.
One convenience: you can seed both anchors on the start line — git bisect start <bad> <good> — and Git begins bisecting immediately without the two separate good/bad commands.
Testing a dozen midpoints by hand is tolerable. Testing twenty, while rebuilding a large project each time, is not. This is where bisect goes from useful to magical:
git bisect start HEAD v1.4.0
git bisect run ./test.sh
git bisect run <cmd> hands the entire loop to Git. At each midpoint, Git runs your command and reads its exit code to decide the verdict automatically — no human in the loop. The mapping is precise:
| Exit code | Meaning |
|---|---|
| 0 | good — the commit passes |
| 1–127 (except 125) | bad — the commit fails |
| 125 | skip — this commit can't be tested (e.g. won't compile) |
| 128+ | abort the whole bisect |
So any test that returns a meaningful exit code works: a full test suite (make test), a compile step, a targeted unit test, even a one-liner like ! grep -q "regression_string" output.log. Git walks the history, runs the check, advances, and eventually prints "X is the first bad commit" — all while you get coffee.
The 125 code is the quiet hero. If a midpoint commit is genuinely untestable — a broken build unrelated to your bug — returning 125 tells Git "don't count this one as good or bad, pick a nearby commit instead." Your script can detect the unbuildable state and exit 125 so the automation never derails on incidental breakage.
A practical tip: make your test script specific. If it returns "bad" for the wrong reason — a flaky network call, an unrelated failure — bisect will confidently converge on the wrong commit. Test for your bug, exit 0 otherwise, and git bisect run will scale to histories no human would ever scan by hand.
Bisect is powerful precisely because it makes one strong assumption: the breakage is monotonic. There is a single commit where good flips to bad, and it stays bad thereafter. When that holds, binary search is airtight. When it doesn't, bisect lies to you with total confidence.
The classic trap is a flaky bug — one that fails intermittently. If a "good" commit occasionally reproduces the failure, or a "bad" commit occasionally passes, you'll give bisect an inconsistent answer and it will converge on an innocent commit. Bisect is only as trustworthy as your test is reproducible. Before you start, make the bug reproduce reliably, or you're searching for a boundary that doesn't exist.
The second requirement is a genuinely good starting commit. If the bug was actually present at the commit you labeled "good" — you just didn't notice — the whole search is anchored to a lie and the result is meaningless. Verify your good anchor really is clean.
For commits that can't be judged — they won't build, or a dependency is missing — don't guess. Use:
git bisect skip
Git will route around them and pick a nearby testable commit. In automated runs, exit 125 does the same thing.
Two more tools make bisect resilient. git bisect log records every good/bad/skip decision, and git bisect replay <file> re-runs a saved session — invaluable if you make a mistake and want to restart, or hand the hunt to a colleague. And if "good/bad" is the wrong vocabulary for your problem (say you're hunting when a bug was fixed, not introduced), git bisect terms --term-old <x> --term-new <y> lets you relabel the two sides.
Whatever happens, end with git bisect reset. It's the one non-negotiable: it returns HEAD and your working tree to where you started, closing the session cleanly. Bisect leaves you in a detached-HEAD state during the hunt by design — reset is how you come home.
git bisect whenever you can name a commit that worked and one that's broken — it finds the culprit in ~log₂(N) tests.git bisect start, mark one bad and one good, test each midpoint Git hands you, then git bisect reset.git bisect run <script> to make the entire hunt fully automatic — exit code 0 is good, 125 skips, anything else is bad.git bisect reset to return HEAD and your working tree to where you started.