Skip to content

← Writing

engineering

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 skip for untestable commits. A commit that won’t build isn’t “good” or “bad” — skip it (or exit 125 in 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 reset when 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.