Git Bisect for Bug Hunting: Binary Search Through Commit History
Master git bisect to find the exact commit that introduced a bug using binary search. Automate bug hunting with scripts and handle complex scenarios.
Git Bisect for Bug Hunting: Binary Search Through Commit History
git bisect is the most powerful debugging tool in Git that most developers never use. It performs a binary search through your commit history to find the exact commit that introduced a bug. Instead of manually checking dozens of commits, bisect narrows it down in logarithmic time.
A bug appeared sometime in the last 200 commits. Manually checking each one would take hours. With git bisect, you’ll find the culprit in about 8 steps. That’s the power of binary search applied to version control.
This post covers the complete bisect workflow: manual and automated bisect, handling complex scenarios, and the failure modes that can send you down the wrong path.
When to Use / When Not to Use
Use Git Bisect When
- Regression bugs — A feature worked before but broke after some commit
- Performance regressions — Something got slower and you need to find when
- Test failures — A test that used to pass now fails
- Large commit history — Hundreds of commits since the bug was introduced
- Automated detection — You can write a script that detects the bug
Do Not Use Git Bisect When
- The bug always existed — Bisect needs a known-good starting point
- Non-deterministic bugs — Race conditions and flaky tests confuse bisect
- External dependencies changed — If the bug is in a library, not your code
- Very few commits — If there are only 5-10 commits, manual checking is faster
- Merge commits complicate history — Bisect can struggle with complex merge topology
Core Concepts
Git bisect works by repeatedly halving the search space:
| Term | Description |
|---|---|
| Good commit | A commit where the bug is NOT present |
| Bad commit | A commit where the bug IS present |
| Bisect range | The span between good and bad commits |
| Midpoint | The commit bisect checks next (halfway between good and bad) |
| Steps | Number of iterations needed (log₂ of commits in range) |
The algorithm: start with a good and bad commit, check the midpoint, mark it as good or bad, repeat until the first bad commit is found.
graph LR
A[Good Commit] --> B[Midpoint 1]
B --> C[Midpoint 2]
C --> D[Midpoint 3]
D --> E[First Bad Commit]
E --> F[Bad Commit]
Architecture and Flow Diagram
The complete git bisect workflow from start to finding the culprit:
graph TD
A[Start Bisect] --> B[Mark Bad Commit HEAD]
B --> C[Mark Good Commit known-working]
C --> D[Bisect checks midpoint]
D --> E{Test the code}
E -->|Bug present| F[git bisect bad]
E -->|Bug absent| G[git bisect good]
E -->|Skip| H[git bisect skip]
F --> I{One commit left?}
G --> I
H --> D
I -->|Yes| J[First Bad Commit Found]
I -->|No| D
J --> K[git bisect reset]
Step-by-Step Guide
1. Manual Bisect
The interactive workflow where you test each midpoint yourself:
# Start bisect
git bisect start
# Mark the current commit as bad (bug is present)
git bisect bad
# Mark a known-good commit (from 2 weeks ago, or a specific tag)
git bisect good v1.2.0
# Or by date: git bisect good @{2.weeks.ago}
# Or by commit hash: git bisect good abc1234
# Git checks out the midpoint commit
# Bisecting: 128 revisions left to test after this (roughly 7 steps)
# Test the code manually
# Run the application, check if the bug is present
# If the bug is present:
git bisect bad
# If the bug is NOT present:
git bisect good
# Git checks out the next midpoint
# Repeat until the first bad commit is found
# Bisect complete!
# abc1234 is the first bad commit
# commit abc1234567890abcdef1234567890abcdef12345678
# Author: Jane Doe <jane@example.com>
# Date: Mon Mar 15 14:30:00 2026 -0500
#
# feat: refactor authentication middleware
# Reset bisect and return to your original branch
git bisect reset
2. Automated Bisect
When you can write a script to detect the bug, bisect runs it automatically:
# Start bisect
git bisect start
git bisect bad HEAD
git bisect good v1.2.0
# Run automated bisect with a test script
# The script should exit 0 for good, 1-127 for bad (except 125 for skip)
git bisect run ./scripts/test-bug.sh
# Bisect completes automatically
git bisect reset
Example test script:
#!/bin/bash
# scripts/test-bug.sh
# Exit 0 if good (bug NOT present), exit 1 if bad (bug IS present)
# Build the project
npm ci 2>/dev/null || exit 125 # Skip if dependencies fail
npm run build 2>/dev/null || exit 125
# Run the specific test that fails
npm test -- --testNamePattern="user authentication" 2>/dev/null
# npm test exits 0 on pass (good), 1 on fail (bad)
# This is exactly what bisect expects
3. Bisect with Visual Inspection
For UI bugs or visual regressions:
# Start bisect
git bisect start HEAD v1.0.0
# For each midpoint, visually inspect the UI
# Then mark good or bad
# Pro tip: use a checklist for consistent evaluation
# 1. Open the page in browser
# 2. Click the button
# 3. Check if the modal appears
# 4. Mark good or bad accordingly
4. Bisect Run with Complex Scripts
For complex detection scenarios:
#!/bin/bash
# scripts/bisect-test.sh
# Complex bisect test with multiple checks
# Skip if the project doesn't build at this commit
npm ci 2>/dev/null || exit 125
npm run build 2>/dev/null || exit 125
# Check 1: Unit tests must pass
npm test -- --testPathPattern=auth 2>/dev/null || exit 1
# Check 2: API endpoint must respond correctly
RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/api/health 2>/dev/null)
if [ "$RESPONSE" != "200" ]; then
exit 1 # Bad
fi
# Check 3: Performance threshold
START=$(date +%s%N)
curl -s http://localhost:3000/api/users > /dev/null 2>&1
END=$(date +%s%N)
ELAPSED=$(( (END - START) / 1000000 ))
if [ "$ELAPSED" -gt 500 ]; then
exit 1 # Bad - too slow
fi
exit 0 # Good
Production Failure Scenarios + Mitigations
| Scenario | What Happens | Mitigation |
|---|---|---|
| Build fails at midpoint | The commit doesn’t compile, blocking bisect | Use exit 125 to skip; bisect tries a different midpoint |
| Flaky test results | Non-deterministic tests give inconsistent results | Run tests multiple times; use a deterministic test script |
| Merge commit confusion | Bisect lands on a merge commit with multiple parents | Use git bisect visualize to understand the topology |
| Dependencies changed | npm install fails on old commits | Skip commits with incompatible dependencies; use lock files |
| Database migrations | Old commits expect different database schema | Skip commits that can’t run with current database |
| Wrong good/bad marking | Incorrect marking leads to wrong culprit | Double-check each marking; use automated tests when possible |
Trade-offs
| Aspect | Advantage | Disadvantage |
|---|---|---|
| Speed | Logarithmic — 1000 commits in 10 steps | Each step requires building and testing |
| Precision | Finds the exact commit, not just a range | Requires a known-good starting point |
| Automation | bisect run works unattended | Writing reliable test scripts takes effort |
| Manual control | You decide good/bad at each step | Manual bisect is slow for large ranges |
| Skip support | Can skip untestable commits | Too many skips reduce accuracy |
| Reset safety | git bisect reset always returns you safely | Forgetting to reset leaves repo in bisect state |
Implementation Snippets
Bisect Helper Aliases
# Add to ~/.gitconfig
[alias]
bs-start = "!f() { git bisect start; git bisect bad HEAD; git bisect good \"$1\"; }; f"
bs-good = git bisect good
bs-bad = git bisect bad
bs-skip = git bisect skip
bs-reset = git bisect reset
bs-log = git bisect log
bs-visualize = git bisect visualize
bs-run = git bisect run
Automated Bisect with CI
#!/bin/bash
# scripts/automated-bisect.sh
# Run bisect using CI pipeline results
GOOD_TAG=$1
BAD_COMMIT=${2:-HEAD}
if [ -z "$GOOD_TAG" ]; then
echo "Usage: automated-bisect.sh <good-tag> [bad-commit]"
exit 1
fi
echo "Starting bisect from $GOOD_TAG to $BAD_COMMIT"
git bisect start
git bisect bad "$BAD_COMMIT"
git bisect good "$GOOD_TAG"
# Use CI test results as the bisect script
git bisect run bash -c '
npm ci 2>/dev/null || exit 125
npm run build 2>/dev/null || exit 125
npm test 2>/dev/null
'
echo "Bisect complete. Run git bisect log for details."
git bisect reset
Bisect Log Analysis
# Save bisect log for analysis
git bisect log > bisect-session.log
# The log can be replayed later
git bisect replay bisect-session.log
# Example bisect log:
# git bisect start
# status: waiting for both good and bad commits
# good: [abc1234] Release v1.2.0
# git bisect good abc1234
# bad: [def5678] Latest commit
# git bisect bad def5678
# good: [1234567] Midpoint commit
# git bisect good 1234567
# ...
Bisect with Docker
#!/bin/bash
# scripts/bisect-docker.sh
# Run bisect tests in Docker for consistent environment
git bisect start HEAD v1.0.0
git bisect run bash -c '
# Build and test in Docker
docker build -t bisect-test . 2>/dev/null || exit 125
docker run --rm bisect-test npm test 2>/dev/null
'
git bisect reset
Observability Checklist
- Logs: Save
git bisect logoutput for every bisect session - Metrics: Track bisect session duration, number of skips, and success rate
- Alerts: Not typically applicable — bisect is a developer tool
- Dashboards: Display average bisect time and most common bug sources
- Post-mortem: Include bisect findings in incident reports to identify problematic areas
Security and Compliance Notes
- Code access: Bisect checks out historical commits — ensure developers have access to all commits in the range
- Audit trail: Bisect sessions should be logged for compliance in regulated environments
- Sensitive data: Old commits may contain secrets that were later removed — bisect exposes them temporarily
- Branch protection: Bisect works on any commit, including those from deleted branches
Common Pitfalls and Anti-Patterns
- Wrong Good Commit — Marking a commit as good when it actually has the bug leads to the wrong culprit. Verify the good commit thoroughly.
- Skipping Too Many — Skipping more than 20% of commits makes the result unreliable. Fix the build issues instead of skipping.
- Not Resetting — Forgetting
git bisect resetleaves the repo in a detached HEAD state. Always reset when done. - Ignoring Build Failures — Treating build failures as “bad” instead of skipping them leads to false positives.
- Non-Deterministic Tests — Flaky tests cause bisect to find the wrong commit. Make tests deterministic before bisecting.
- Bisecting Across Major Refactors — If the codebase changed dramatically, bisect may land on commits that can’t be tested. Use skip strategically.
- Not Automating — Manual bisect for more than 20 steps is error-prone. Write a test script and use
bisect run.
Quick Recap Checklist
- Identify a known-good commit where the bug was NOT present
- Mark the current commit as bad with
git bisect bad - Mark the known-good commit with
git bisect good <commit> - Test each midpoint and mark good or bad
- Use
exit 125to skip untestable commits - Write automated test scripts for
git bisect run - Save bisect logs with
git bisect log - Always run
git bisect resetwhen done - Verify the found commit actually introduced the bug
- Include bisect findings in incident post-mortems
Automated Bisect Script
#!/bin/bash
# scripts/bisect-automated.sh
# Automated bisect with build skip and test script
GOOD=$1
BAD=${2:-HEAD}
if [ -z "$GOOD" ]; then
echo "Usage: bisect-automated.sh <good-commit> [bad-commit]"
exit 1
fi
git bisect start
git bisect bad "$BAD"
git bisect good "$GOOD"
# Run with a test script that handles build failures
git bisect run bash -c '
# Skip if build fails
npm ci 2>/dev/null || exit 125
npm run build 2>/dev/null || exit 125
# Run the specific failing test
npm test -- --testPathPattern="regression" 2>/dev/null
'
echo "=== Bisect Results ==="
git bisect log
git bisect reset
Bisect Preparation Checklist
- Identify good commit — Find a known-working version (tag, date, or SHA)
- Identify bad commit — Usually HEAD or the current broken state
- Write reproducible test — Script that exits 0 for good, 1 for bad
- Handle build failures — Script should exit 125 if the commit can’t build
- Check dependency compatibility — Old commits may need different Node/Python versions
- Verify test determinism — Run the test 3 times on the same commit to confirm consistency
- Save bisect log —
git bisect log > bisect-session.logfor reproducibility
Interview Q&A
Git bisect uses binary search. Given a range of N commits between a known-good and known-bad commit, it checks the midpoint commit. If the midpoint is bad, the first bad commit is in the first half. If good, it's in the second half. This halves the search space each step.
For 1000 commits, bisect finds the culprit in log₂(1000) ≈ 10 steps. Each step requires building and testing the code at that commit. The process continues until only one commit remains — the first bad commit.
Exit code 125 tells bisect to skip the current commit. This is used when the commit can't be tested — for example, if the build fails, dependencies are incompatible, or the test doesn't apply to that version of the code.
Exit code 0 means good (bug not present), exit codes 1-127 (except 125) mean bad (bug present), and 125 means skip. Using 125 correctly prevents bisect from being blocked by untestable commits.
Yes, but with caveats. Git bisect traverses merge commits by following the first parent by default. This means it follows the main branch history, not the merged feature branch history.
If the bug was introduced in a merged feature branch, bisect may not find it correctly. In this case, use git bisect start --first-parent to explicitly follow first-parent history, or bisect the feature branch separately.
Write a script that runs the test and exits with the correct code, then use git bisect run:
git bisect start HEAD v1.0.0
git bisect run npm test -- --testNamePattern="failing-test"
The test command exits 0 when passing (good) and 1 when failing (bad), which is exactly what bisect expects. The entire process runs unattended until the first bad commit is found.
Resources
- Git bisect documentation — Official Git documentation
- Git bisect run examples — Automated bisect guide
- Finding regressions with git bisect — Beginner-friendly tutorial
- Git bisect and Docker — Using bisect with containers
- Binary search algorithm — The algorithm behind bisect
Category
Related Posts
Git Blame and Annotate: Line-by-Line Code Attribution
Master git blame for line-by-line code attribution, understanding code history, finding when code changed, and using blame effectively for code comprehension.
git log: Master Commit History Navigation and Filtering
Master git log formatting, filtering, searching history, and navigating commit history effectively for version control debugging and auditing.
Git Reflog and Recovery: Your Safety Net for Destructive Operations
Master git reflog to recover lost commits, undo destructive operations, and understand Git's safety net. Learn recovery techniques for reset, rebase, and merge disasters.