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!
No bug
Bug exists
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
Mark one good (working) commit and one bad (broken) commit
Git checks out the commit halfway between good and bad
Test if bug exists, mark commit as good or bad
Halve search space, repeat until exact commit found
Step-by-Step Bisect Guide
Start Bisect Session
Initialize bisect and define your known states:
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.
Test the Current Commit
Git checks out a commit for testing. Run your tests:
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.
Mark Commit as Good or Bad
Based on your test results:
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.
Repeat Until Found
Continue testing and marking commits:
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
Finish and Analyze
When bisect finds the bug-introducing commit:
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!
Advanced Bisect Techniques
Automated Bisect
Use a script to automatically test commits:
#!/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:
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:
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:
#!/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:
#!/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:
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
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
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
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
Mistakes happen! Git bisect provides several recovery options:
- Reset and start over:
git bisect reset
git bisect start
# Start fresh with correct good/bad markers - 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 - 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 - 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.
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-parentto 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:
Yes, but efficiency becomes important. Strategies for slow tests:
- 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 - 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 - 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 - 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
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:
- Use
git bisectto find which commit introduced a bug - Use
git show <bad-commit>to see what changed - Use
git blameon current file to see if bug still exists in same lines - 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.
Yes! Git bisect can focus on specific paths:
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.
# If unsure, you can skip it:
git bisect skip
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-parentor prepare a linearized history first.