Finding the commit that broke it with git bisect
Binary search across commit history to find the first bad commit. Works manually or fully automated with a script — turning a needle-in-a-haystack search into log2(n) steps.
On this page
The bug is in production. The last release was fine. Somewhere in the 200 commits between then and now, someone broke it. git bisect turns that search into binary chop: O(log n) steps instead of O(n). For 200 commits, that’s 8 tests instead of 200.
The catch — and it’s a real one — is that you need a deterministic, reproducible test for “is this commit broken?”. Without that, bisect just halves your way to a confidently wrong answer.
The mental model
Bisect is a state machine over your commit history. You give it a bad commit and a good commit; it picks the middle and checks it out; you tell it good or bad; it picks the middle of the remaining range; and so on until exactly one commit is left.
Bisect across 100 commits
One run of binary search across 100 commits. Each step halves the candidate range — log2(100) ≈ 7 tests.
The manual walkthrough
-
Open the session
git bisect start— Git is now in bisect mode.HEADwill move around without ceremony. -
Mark a bad and a good commit
git bisect bad(with no argument, means “this commit, HEAD, is bad”) andgit bisect good <sha>(the last known-good commit). Git checks out a commit roughly halfway between them. -
Test the current checkout
Run whatever reproduces the bug — a unit test, a manual reproduction, a script. Decide: does this commit have the bug?
-
Mark and repeat
git bisect badif the bug is present at this commit,git bisect goodif it isn’t. Git checks out the midpoint of the new range. Repeat. -
End the session
When the range collapses, Git prints
<sha> is the first bad commit. Rungit bisect resetto returnHEADto where you started.
Automating with git bisect run
When the test is scriptable, hand the script to bisect and walk away.
git bisect start
git bisect bad HEAD
git bisect good v1.4.0
git bisect run ./scripts/repro.sh
bisect run invokes the script at each step. The script’s exit code is the verdict:
| Exit code | Meaning |
|---|---|
0 | Good commit. |
1–124, 126–127 | Bad commit. |
125 | Skip — commit cannot be tested (e.g., build broken for unrelated reasons). |
>= 128 | Abort the bisect. |
A 200-commit bisect with a 30-second test runs to completion in about four minutes of wall-clock time, with no human in the loop.
What the test script needs to do
#!/usr/bin/env bash
set -e
# Rebuild — bisect just checked out a different commit.
./gradlew assemble > /dev/null 2>&1 || exit 125 # build broken: skip
# Run the specific failing case.
./gradlew test --tests com.example.CheckoutTest.handlesEmptyCart > /dev/null 2>&1
# exit 0 if the test passed (good), non-zero if it failed (bad).Three rules for a script that won’t lead you astray:
- Rebuild for every commit. Bisect changes the working tree silently between invocations. A stale build will give the same answer across very different commits.
- Test the exact failure mode. If the script returns “bad” for any test failure, an unrelated regression introduced halfway through your range will throw the bisect off entirely.
- Skip what you can’t test. Use exit 125 generously for “this commit is broken for reasons unrelated to my bug” — better to skip than to mismark.
When bisect doesn’t help
“I think it’s broken — the page sometimes feels slow.” Bisecting a flaky test or a non-deterministic symptom.
Bisect is a satisfying tool — it makes you feel like progress is happening. So it gets reached for before the reproduction is solid.
Get the repro to be deterministic first. Bisect across flaky signals gives you a confident wrong answer; the binary search structure obscures the uncertainty. If you can’t write a script that returns the same verdict 10 times in a row at the same commit, you can’t bisect.
Other limitations to know:
- Squash merges erase intermediate commits, so bisecting across a heavily-squashed history may land on a commit containing dozens of changes — useful but coarse.
- Bisect assumes monotonic regression: the bug went from absent to present and stayed present. A bug introduced and then accidentally masked by a later commit will give a misleading result.
- Git’s
refs/bisectnamespace is per-worktree — running two bisects in two worktrees on the same repo is safe.
Quick reference
git bisect start # open a session
git bisect bad [<sha>] # mark current or named commit as bad
git bisect good <sha> # mark as good
git bisect skip # current commit untestable
git bisect run <cmd> # automate; <cmd> exit code = verdict
git bisect log # show the session's marks so far
git bisect replay <file> # replay a previous session from `git bisect log` output
git bisect reset # exit; return HEAD to the starting point
Related: 0025 Git rebase without losing work · Git for the rebase-and-rewrite operations that can complicate a bisect by reshaping history.