Git Bisect Debugging: Binary Search for Bugs

What is Git Bisect?

Git bisect is a powerful debugging tool that uses binary search to efficiently find the commit that introduced a bug. Instead of manually testing each commit, bisect automates the process by halving the search space with each test.

Simple Analogy: Imagine you have 1000 pages of code and a bug appears somewhere. Instead of reading page by page, you'd check page 500. If the bug exists, it's in the first half; if not, it's in the second half. Repeat until you find the exact page. That's bisect!

Good Commit
No bug
Bad Commit
Bug exists
git bisect start
git bisect good v1.0
git bisect bad HEAD
# Git checks middle commit, you test it
git bisect good # or git bisect bad

Why Use Git Bisect?

  • Efficiency: Finds bug-introducing commit in O(log n) time
  • Automation: Can be fully automated with test scripts
  • Accuracy: Precisely identifies the problematic commit
  • Time-saving: Saves hours of manual debugging
  • Complex bugs: Works for bugs that appear over time

Pro Tip: Bisect isn't just for bugs! Use it to find when any change occurred: performance regression, feature addition, API change, etc.

The Binary Search Algorithm

1
Define Range

Mark one good (working) commit and one bad (broken) commit

2
Check Middle

Git checks out the commit halfway between good and bad

3
Test & Classify

Test if bug exists, mark commit as good or bad

4
Repeat

Halve search space, repeat until exact commit found

Step-by-Step Bisect Guide

1

Start Bisect Session

Initialize bisect and define your known states:

# Start bisect session
git bisect start

# Mark current HEAD as bad (bug exists)
git bisect bad

# Mark a known good commit (bug doesn't exist)
git bisect good v1.0
# Or use commit hash, tag, or branch

Git will automatically check out a commit halfway between good and bad.

2

Test the Current Commit

Git checks out a commit for testing. Run your tests:

# Check which commit you're on
git log --oneline -1

# Run your tests
npm test
# or
python test_bug.py
# or manually test the feature

Determine if the bug exists in this commit.

3

Mark Commit as Good or Bad

Based on your test results:

# If bug DOES NOT exist in this commit
git bisect good

# If bug DOES exist in this commit
git bisect bad

# If you can't test this commit (skip it)
git bisect skip

Git will automatically check out the next commit to test.

4

Repeat Until Found

Continue testing and marking commits:

# Git shows progress
Bisecting: 12 revisions left to test after this (roughly 4 steps)
[abc1234] Commit message here

# Keep testing and marking good/bad
# Git will eventually find the first bad commit
5

Finish and Analyze

When bisect finds the bug-introducing commit:

# Git announces the result
abc1234 is the first bad commit
abc1234 Commit message that introduced the bug

# End bisect session
git bisect reset

# Examine the problematic commit
git show abc1234
git log --oneline -5 abc1234

Interactive Bisect Demo

Try It: Find the Buggy Commit

We have 15 commits. Commit 1 is good, commit 15 is bad. Use bisect to find the first bad commit!

Commits tested: 0
Remaining range: 1-15
Estimated steps left: ~4

Advanced Bisect Techniques

Automated Bisect

Use a script to automatically test commits:

# Create test script (returns 0 for good, 1-127 for bad, 125 for skip)
#!/bin/bash
npm test 2>&1 | grep -q "Test passed"
# Exit code 0 = good, non-zero = bad

# Run automated bisect
git bisect start HEAD v1.0
git bisect run ./test-script.sh

Bisect with Branches

Find when bug appeared between branches:

# Start from merge base
git merge-base main feature > /tmp/mergebase
git bisect start main feature
git bisect good $(cat /tmp/mergebase)
git bisect bad feature

# Or bisect only specific path
git bisect start -- path/to/file.js

Visual Bisect Log

View and replay bisect sessions:

# View bisect log
git bisect log
# git bisect start
# git bisect bad HEAD
# git bisect good abc1234
# ...

# Replay from log file
git bisect replay bisect.log

# Visualize with git log
git log --oneline --graph --bisect

Practical Use Cases

1. Finding Performance Regressions

Use bisect with performance tests:

# Performance test script
#!/bin/bash
start=$(date +%s%N)
./run-operation # Your performance-critical operation
end=$(date +%s%N)
duration=$(( (end - start) / 1000000 )) # milliseconds

# Mark as bad if slower than threshold
if [ $duration -gt 100 ]; then
  exit 1 # Bad (too slow)
else
  exit 0 # Good
fi

# Run bisect
git bisect run ./perf-test.sh

2. Debugging Intermittent Bugs

For bugs that don't always reproduce:

# Test script that runs multiple times
#!/bin/bash
for i in {1..10}; do
  if ./flakey-test; then
    echo "Test $i passed"
  else
    echo "Test $i failed"
    exit 1 # Bad if any failure
  fi
done
exit 0 # Good if all passed

# Conservative approach: mark as bad only if consistently failing

3. Finding When Feature Was Added

Reverse bisect: find when something started working:

# Reverse logic: good = feature works, bad = feature doesn't work
git bisect start
git bisect good HEAD # Feature works now
git bisect bad v1.0 # Feature didn't exist then

# Test script checks if feature exists
#!/bin/bash
if grep -q "newFeature" code.js; then
  exit 0 # Good (feature exists)
else
  exit 1 # Bad (feature doesn't exist)
fi

Command Reference

Basic Bisect Commands

# Start bisect session
git bisect start

# Mark current commit as bad
git bisect bad [commit]

# Mark commit as good
git bisect good [commit]

# Skip current commit
git bisect skip [commit]

# Reset to original state
git bisect reset [commit]

Advanced Commands

# Run automated bisect with script
git bisect run ./test-script.sh

# View bisect log
git bisect log

# Replay bisect from log
git bisect replay logfile

# Visualize bisect state
git bisect visualize

# View current bisect terms
git bisect terms

Helper Commands

# See what commit is being tested
git show --oneline HEAD

# Check bisect status
git bisect status

# List remaining commits to test
git bisect view

# Create bisect terms file
echo "fixed broken" > .git/BISECT_TERMS

Best Practices

Test Quality

  • Ensure tests are reliable and deterministic
  • Test in consistent environment
  • Clear cache between tests if needed
  • Handle test setup/teardown properly
  • Log test results for debugging

Session Management

  • Save bisect log before reset
  • Use descriptive commit markers
  • Consider using temporary branches
  • Document found commits thoroughly
  • Clean up test artifacts

Automation Tips

  • Start with simple manual bisect
  • Gradually automate repetitive tests
  • Use exit codes properly (0=good, 1-127=bad, 125=skip)
  • Handle edge cases in test scripts
  • Parallelize tests when possible

Frequently Asked Questions

What if I make a mistake marking a commit as good/bad?

Mistakes happen! Git bisect provides several recovery options:

  1. Reset and start over:
    git bisect reset
    git bisect start
    # Start fresh with correct good/bad markers
  2. Use bisect log: Save your progress, edit, and replay:
    # Save current bisect state
    git bisect log > bisect.log
    # Edit the log file, fix the mistake
    git bisect reset
    git bisect replay bisect.log
  3. Manual correction: Use reset to specific commit:
    # Go back to a known state
    git bisect reset abc1234
    # Then continue from there
    git bisect good abc1234
  4. Skip problematic commits: If unsure, skip and continue:
    git bisect skip
    # Git will choose another commit to test

Prevention: Test carefully before marking. For automated bisect, ensure your test script is reliable.

How does bisect handle merge commits?

Merge commits can be tricky with bisect. Git has specific behavior:

  • Default behavior: Git tries to avoid testing merge commits when possible
  • First-parent: Use --first-parent to follow only the main branch:
    git bisect start --first-parent
    # Only test commits on the main branch lineage
  • Manual handling: When bisect lands on a merge commit:
    # You need to test if the bug exists
    # If unsure, you can skip it:
    git bisect skip
  • Complex merges: For complex merge histories, consider:
    # Bisect only on a specific branch
    git bisect start branch-start branch-end

    # Or use git log to simplify range
    git log --oneline --no-merges good..bad > commits.txt
    # Then bisect using these commits

Best practice: For projects with complex merge history, use --first-parent or prepare a linearized history first.

Can I use bisect with a test that takes a long time to run?

Yes, but efficiency becomes important. Strategies for slow tests:

  1. Optimize test selection: Start with broader range, then narrow:
    # First, find approximate range with quick tests
    git bisect start
    git bisect bad HEAD
    git bisect good HEAD~100
    # Use quick smoke tests first

    # Then refine with detailed tests
    git bisect reset
    git bisect start
    git bisect bad found-commit
    git bisect good found-commit~20
    # Now run comprehensive tests
  2. Parallel testing: Test multiple commits simultaneously (advanced):
    # Use git worktree to test multiple commits
    git worktree add ../test1 commit1
    git worktree add ../test2 commit2
    # Run tests in parallel, then mark results
  3. Batch testing: Test groups of commits:
    # Script that tests multiple commits
    #!/bin/bash
    for commit in $(git rev-list start..end); do
      git checkout $commit
      if ./long-test; then
        echo "$commit good"
      else
        echo "$commit bad"
        break
      fi
    done
  4. Cache results: Store test results to avoid re-running:
    # Test script with caching
    #!/bin/bash
    commit=$(git rev-parse HEAD)
    cache_file="/tmp/bisect-cache-$commit"

    if [ -f "$cache_file" ]; then
      exit $(cat "$cache_file")
    fi

    # Run test and cache result
    ./long-test
    result=$?
    echo $result > "$cache_file"
    exit $result
What's the difference between git bisect and git blame?

Both help find problematic code, but in different ways:

Aspect git bisect git blame
Purpose Find commit that introduced a bug/change Show who last modified each line
Method Binary search through history Line-by-line annotation
When to use When you know something worked before When examining current code
Time complexity O(log n) - efficient for long history O(1) - instant for current file
Best for Finding when bug appeared Understanding current code ownership

Example workflow:

  1. Use git bisect to find which commit introduced a bug
  2. Use git show <bad-commit> to see what changed
  3. Use git blame on current file to see if bug still exists in same lines
  4. Use git log -p <file> to see evolution of problematic code

Pro tip: Combine both! Use bisect to find the problematic commit, then blame to understand the current state.

Can I bisect only specific files or directories?

Yes! Git bisect can focus on specific paths:

# Bisect only changes in specific directory
git bisect start -- src/components/
git bisect bad
git bisect good v1.0

# Or specify multiple paths
git bisect start -- src/ tests/

# For automated bisect with paths
git bisect run ./test-script.sh -- src/

# Check which paths are being considered
git bisect view --stat

How it works: When you specify paths, Git only considers commits that touched those paths. This can significantly reduce the search space.

Use cases for path-limited bisect:

  • Bug is isolated to a specific module
  • You recently refactored a directory
  • Test only applies to certain files
  • Performance issue in specific component

Warning: If the bug involves interactions between files, path-limited bisect might not find the root cause.

Previous: Git Cherry-Pick Next: Git Hooks