Hunting Bugs with git bisect
· Jerwin Arnado · 5 min read ·
“It worked last month, it’s broken now, and I have no idea which of the last few hundred
commits did it.” This is the bug that eats an afternoon of guessing — unless you let Git do
the searching. git bisect runs a binary search through your commit history: you mark
one known-good commit and one known-bad one, and Git walks you to the exact commit that
introduced the bug in about log₂(N) steps. 200 commits? Eight checks. It’s the single
biggest time-saver in Git that most people never learn.
The idea
If commit A worked and commit Z is broken, the bug was introduced somewhere in between. Instead of checking commits one by one, bisect checks the middle one: if it’s good, the bug is in the newer half; if bad, the older half. Halve, halve, halve — until one commit is left. That’s your culprit.
A bisect session by hand
Start the search and tell Git the two endpoints:
git bisect start
git bisect bad # current commit (HEAD) is broken
git bisect good v1.4.0 # this tag/commit was known to work
Git immediately checks out a commit halfway between them and tells you how many steps remain:
Bisecting: 97 revisions left to test after this (roughly 7 steps)
[a1b2c3d] feat(api): add rate limiting
Now test that commit — run the app, run the failing test, reproduce the bug or don’t — and report the result:
git bisect good # this commit works → bug is newer
# or
git bisect bad # this commit is broken → bug is older
Git checks out the next midpoint. Repeat. After ~7 rounds it announces the answer:
e0f1a2b is the first bad commit
commit e0f1a2b
refactor(cache): switch to the new Redis client
That refactor is your bug. End the session to return to where you started:
git bisect reset
Automating it with bisect run
Testing by hand a dozen times is tedious and error-prone. If you can express “is this
commit good or bad?” as a command that exits 0 for good and non-zero for bad, git
bisect run does every step for you:
git bisect start
git bisect bad
git bisect good v1.4.0
git bisect run ./test.sh
Git checks out each midpoint, runs test.sh, reads its exit code, and marks good/bad
automatically — then prints the first bad commit with zero further input from you. The
script can be anything:
git bisect run php artisan test --filter=PasswordResetTest
git bisect run npm test -- --findRelatedTests
git bisect run cargo test the_failing_case
This is the payoff of a real test suite: one failing test turns a multi-hour bug hunt into a single command you walk away from.
The special exit code 125: if a commit can’t be tested (it won’t build, a dependency
is missing), have the script exit 125 and bisect will skip it rather than
mismarking it. For everything else, exit 0 = good, exit 1–124/126–127 = bad.
Caveats and best practices
- Your good/bad endpoints must be honest. If the “good” commit was actually already broken, bisect happily converges on the wrong answer. Verify both endpoints reproduce (or don’t) the bug before you start.
- Use
git bisect skipfor untestable commits. A commit that won’t build isn’t “good” or “bad” — skip it (orexit 125in the script) so it doesn’t poison the search. - A clean, conventional history makes the result legible. When bisect lands on a commit, a good message tells you why the change was made — turning “this commit broke it” into “ah, that’s why.” Squashed, one-feature-per-commit history bisects beautifully; a wall of “wip” does not.
- Write the test first when you can. A script that reliably reproduces the bug is what
unlocks
bisect run. Five minutes writing it usually beats an hour of manual checking. git bisect resetwhen you’re done to restore HEAD — bisect leaves you on a detached commit until you do.
Conclusion
git bisect start
git bisect bad # now is broken
git bisect good <known-good-commit> # then worked
git bisect run ./your-test.sh # let Git find the culprit
git bisect reset # back to normal
git bisect converts “somewhere in hundreds of commits” into a handful of automated
checks and an exact answer. The next time something silently broke and nobody knows when,
don’t read diffs for an afternoon — give Git a good commit, a bad commit, and a test, and
let the binary search do the hunting.