Techguide · Explainer

git bisect basics

Find the commit that broke everything — in about ten guesses.

Thesis git bisect is a binary search over your commit history that finds the exact commit which introduced a bug in a logarithmic number of steps instead of a linear hunt — turning "somewhere in the last 1000 commits" into roughly 10 quick yes/no tests.

01What git bisect is

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.

good bad (HEAD) test midpoint now "good" half discarded next midpoint
Each test collapses the suspect range by half: mark the midpoint good and the entire left half is eliminated at once.

02The problem it solves

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 caseBisect steps (~log₂ N)
1010~4
100100~7
1,0001,000~10
1,000,0001,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.

Steps to find the culprit ■ linear ■ bisect N=1,000 1000 10 N=1,000,000 1000000 20 1000× more commits costs bisect only 2× the steps — logarithmic scaling.
Linear scan grows with N; bisect grows with log₂(N). A thousand-fold larger history barely doubles the work.

03How it works step by step

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.

git bisect start bisect bad bisect good v1.4.0 Git checks out the midpoint commit Build & test bug here? first bad commit found git bisect reset good / bad → narrow, repeat range=1
Start → mark one bad and one good → Git checks out the midpoint → you test → good/bad narrows the range and loops → the culprit is found → reset returns you home.

04Automating it with git bisect run

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 codeMeaning
0good — the commit passes
1–127 (except 125)bad — the commit fails
125skip — 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.

Git checks out next midpoint ./test.sh exit code → 0 good → search later half 1–127 bad → search earlier half 125 skip → try a neighbor 128+ abort the bisect Git auto-advances — no human — until the culprit is printed
In git bisect run, Git maps each test-script exit code to good, bad, skip, or abort and advances on its own until it prints the first bad commit.

05Pitfalls and boundaries

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.

✓ Works well • Reproducible, reliable test • One clean good → bad transition • A genuinely good anchor commit • Specific check (your bug only) ✕ Trips it up • Flaky / intermittent failures • No truly good commit exists • Commits that won't build • Test fails for unrelated reasons Escape hatches git bisect skip unbuildable commit git bisect reset always finish here
Bisect rewards a reproducible test and a clean transition; flaky bugs and bad anchors mislead it. skip routes around untestable commits and reset always returns you home.

Takeaways

  1. Reach for git bisect whenever you can name a commit that worked and one that's broken — it finds the culprit in ~log₂(N) tests.
  2. The manual loop is just: git bisect start, mark one bad and one good, test each midpoint Git hands you, then git bisect reset.
  3. Wrap your test in git bisect run <script> to make the entire hunt fully automatic — exit code 0 is good, 125 skips, anything else is bad.
  4. Bisect only works if your test is reproducible and the breakage is a single clean good-to-bad transition.
  5. Always finish with git bisect reset to return HEAD and your working tree to where you started.

References

  1. git-bisect Documentation — Git Reference Manual. git-scm.com/docs/git-bisect
  2. Chacon, S. & Straub, B. Pro Git (2nd ed.), "Git Tools — Debugging with Git" (Binary Search / Bisect). git-scm.com/book/en/v2/Git-Tools-Debugging-with-Git