Git Bisect: Binary Search for Bugs That Changed Everything About How I Debug¶
The story of how a 2000-commit regression hunt that would have taken days was solved in 11 steps.
The Moment I Discovered Bisect¶
It was a Friday afternoon. A performance regression had crept into our service — response times doubled, but nobody knew when it started. The commit log showed 2000+ commits since the last known good release. Manual inspection was impossible. A senior colleague walked over, typed git bisect start, and 11 commits later we had the culprit. I was stunned. Binary search on commit history. Of course.
How Git Bisect Works¶
The concept is elegant: you tell git one commit that's "good" (the bug isn't there) and one that's "bad" (the bug is present). Git checks out the midpoint. You test. You report good or bad. Git narrows the range by half. Repeat until the first bad commit is found.
With N commits between good and bad, you need at most log₂(N) steps. For 2000 commits, that's about 11 tests. For 1000, about 10. The efficiency is staggering.
# Start bisecting
git bisect start
# Mark the current commit as bad (has the bug)
git bisect bad
# Mark the last known good commit
git bisect good v2.1.0
# Git checks out a middle commit. Test it.
# Then report:
git bisect good # if the bug is NOT present
git bisect bad # if the bug IS present
# Repeat until git identifies the first bad commit
# When done:
git bisect reset # return to your original branch
Automating Bisect With a Test Script¶
Manual testing works for visual bugs, but for anything you can verify programmatically, git bisect run is transformative:
git bisect start
git bisect bad HEAD
git bisect good v2.1.0
# Run a script that exits 0 for good, non-zero for bad
git bisect run ./test-performance.sh
My test script for the performance regression:
#!/bin/bash
# test-performance.sh — exits 0 if response time < 200ms, 1 otherwise
# Build the service
make build 2>/dev/null || exit 125 # 125 = skip this commit
# Start service, wait for ready
./service &
SERVICE_PID=$!
sleep 2
# Measure response time
RESPONSE_TIME=$(curl -o /dev/null -s -w '%{time_total}' http://localhost:8080/health)
# Clean up
kill $SERVICE_PID 2>/dev/null
# Compare (using bc for floating point)
RESULT=$(echo "$RESPONSE_TIME < 0.200" | bc)
[ "$RESULT" -eq 1 ]
Exit code 125 is special — it tells bisect to skip this commit (useful when a commit doesn't build). Git will try adjacent commits instead.
Real-World Bisect Strategies¶
Over the years, I've developed patterns for different types of regressions:
For test failures:
The -x flag stops on first failure, giving a clean exit code.
For compilation errors:
Simple — if it builds, it's good.
For behavior changes that need custom logic:
git bisect run bash -c '
make build 2>/dev/null || exit 125
OUTPUT=$(./cli --version)
echo "$OUTPUT" | grep -q "expected-pattern"
'
Bisect With Merge Commits: The Skip Strategy¶
In repositories with many merge commits, some midpoints may land on broken or irrelevant commits. I handle this by skipping:
# If the current commit can't be tested
git bisect skip
# Skip a range of known-broken commits
git bisect skip v2.2.0..v2.2.5
For repos with a linear history (thanks to rebase workflows), bisect works perfectly. This is another reason I advocate for linear history — debugging tools work better with it.
Bisect Logs: Documenting the Hunt¶
Every bisect session can be recorded and replayed:
# Save the bisect log
git bisect log > bisect-session.log
# Replay a saved session (useful for showing colleagues)
git bisect replay bisect-session.log
I often save these logs in issue trackers. They document not just what introduced a bug, but the exact process used to find it. Future developers appreciate this breadcrumb trail.
Beyond Bugs: Creative Uses of Bisect¶
I've used bisect for things beyond traditional debugging:
- Finding when a dependency size increased — test script checks binary size
- Locating when a config format changed — script validates YAML structure
- Identifying when documentation went stale — script checks for broken internal links
- Pinpointing performance regressions — script runs benchmarks with threshold checks
Any property that can be expressed as a binary test (pass/fail) across commits is bisectable.
Key Takeaway¶
Git bisect turns an O(n) debugging problem into O(log n). The only investment is defining what "good" and "bad" mean in a way a script can verify. Once you internalize this tool, you'll never manually scan commit logs for regressions again. Write the test script first, then let git do the searching.
Tags: git, bisect, debugging, binary-search, automation