Git Bisect: Find Bugs Through Binary Search

David Childs

Git bisect is a powerful debugging tool that uses binary search to find the exact commit that introduced a bug. Instead of manually checking dozens or hundreds of commits, bisect intelligently narrows down the problematic commit in logarithmic time.

How Git Bisect Works

Bisect uses binary search to find the problematic commit:

  1. You mark a known bad commit (has the bug)
  2. You mark a known good commit (doesn't have the bug)
  3. Git checks out the middle commit
  4. You test and mark it as good or bad
  5. Git narrows the range and repeats until finding the culprit

With 1000 commits to search, you'll need only about 10 tests to find the bug!

Basic Bisect Workflow

Starting a Bisect Session

# Start bisecting
git bisect start

# Mark current commit as bad
git bisect bad

# Mark a known good commit
git bisect good v2.0.0
# or
git bisect good abc1234

Testing and Marking Commits

# Git checks out a commit for you to test
# Run your tests
npm test

# If tests pass (no bug)
git bisect good

# If tests fail (bug present)
git bisect bad

# Git automatically checks out the next commit to test

Ending the Bisect

# When found, Git reports:
# abc1234 is the first bad commit

# View the problematic commit
git show abc1234

# End bisect and return to original branch
git bisect reset

Automated Bisecting

Using a Test Script

# Create a test script that returns 0 for good, non-zero for bad
cat > test-bug.sh << 'EOF'
#!/bin/bash
# Returns 0 if good, 1 if bad

npm test specific-test.js
exit $?
EOF

chmod +x test-bug.sh

# Run automated bisect
git bisect start
git bisect bad HEAD
git bisect good v2.0.0
git bisect run ./test-bug.sh

Automated Testing Examples

# Example 1: Testing for specific output
cat > check-output.sh << 'EOF'
#!/bin/bash
output=$(node app.js --test)
if [[ "$output" == *"ERROR"* ]]; then
    exit 1  # Bad commit
else
    exit 0  # Good commit
fi
EOF

git bisect run ./check-output.sh
# Example 2: Performance regression
cat > check-performance.sh << 'EOF'
#!/bin/bash
time_ms=$(node performance-test.js)
if [ "$time_ms" -gt 1000 ]; then
    exit 1  # Performance regression
else
    exit 0  # Performance acceptable
fi
EOF

git bisect run ./check-performance.sh

Practical Bisect Scenarios

Scenario 1: Finding a Visual Bug

# Bug: Button disappeared from homepage

# Start bisect
git bisect start
git bisect bad HEAD
git bisect good tags/last-release

# Manual testing at each step
# 1. Git checks out middle commit
# 2. Run the app: npm start
# 3. Check if button exists
# 4. Mark as good or bad
# 5. Repeat until found

Scenario 2: Test Suite Failure

# A test that used to pass now fails

# Create test script
echo '#!/bin/bash
npm test -- --grep "user authentication"
' > bisect-test.sh
chmod +x bisect-test.sh

# Automated bisect
git bisect start HEAD v1.0.0
git bisect run ./bisect-test.sh

# Git finds the exact commit that broke the test

Scenario 3: Build Failure

# Build used to work, now fails

git bisect start
git bisect bad
git bisect good HEAD~50

# Automated with build command
git bisect run npm run build

Advanced Bisect Techniques

Bisecting with Skipped Commits

# Some commits can't be tested (e.g., broken builds)
git bisect start
git bisect bad HEAD
git bisect good v1.0

# Can't test current commit
git bisect skip

# Skip a range
git bisect skip v2.1..v2.3

Bisecting Merge Commits

# Include first-parent only to skip feature branch commits
git bisect start --first-parent

# Or exclude merge commits
git bisect start --no-checkout

Bisecting with Terms

# Use custom terms instead of good/bad
git bisect start --term-old=working --term-new=broken
git bisect working v1.0
git bisect broken HEAD

# At each step
git bisect working  # Instead of 'good'
git bisect broken   # Instead of 'bad'

Complex Bisect Examples

Finding Performance Regressions

// performance-test.js
const startTime = Date.now();
const result = expensiveOperation();
const duration = Date.now() - startTime;

if (duration > 500) {
    console.error(`Performance regression: ${duration}ms`);
    process.exit(1);
} else {
    console.log(`Performance OK: ${duration}ms`);
    process.exit(0);
}
git bisect start HEAD v1.0.0
git bisect run node performance-test.js

Finding Memory Leaks

#!/bin/bash
# memory-test.sh

# Run app and capture memory usage
node --expose-gc app.js &
PID=$!
sleep 5

# Check memory usage
MEM=$(ps -o rss= -p $PID)
kill $PID

# Fail if memory > threshold
if [ "$MEM" -gt 100000 ]; then
    echo "Memory leak detected: ${MEM}KB"
    exit 1
else
    echo "Memory OK: ${MEM}KB"
    exit 0
fi

Finding CSS Regressions

// visual-test.js
const puppeteer = require('puppeteer');

(async () => {
    const browser = await puppeteer.launch();
    const page = await browser.newPage();
    await page.goto('http://localhost:3000');
    
    // Check if element has correct style
    const color = await page.$eval('.button', 
        el => getComputedStyle(el).backgroundColor
    );
    
    await browser.close();
    
    if (color !== 'rgb(0, 123, 255)') {
        console.error('Button color regression');
        process.exit(1);
    }
    process.exit(0);
})();

Bisect Best Practices

1. Write Reliable Test Scripts

#!/bin/bash
# Good test script practices

# Clean state before testing
git clean -fd
npm ci

# Run deterministic test
npm test specific-feature.test.js

# Capture exit code
TEST_RESULT=$?

# Cleanup
killall node 2>/dev/null

# Return result
exit $TEST_RESULT

2. Handle Build Dependencies

#!/bin/bash
# Handle changing dependencies during bisect

# Install correct dependencies for this commit
npm ci

# Build if necessary
npm run build

# Run actual test
npm test

3. Document Bisect Results

# After finding bad commit, document it
git bisect reset
git show abc1234 > bisect-result.txt

# Add to commit message when fixing
git commit -m "Fix: Resolve issue introduced in abc1234

The bug was introduced in commit abc1234 where the validation
logic was incorrectly modified. Found using git bisect.

Bisect command used:
git bisect start HEAD v2.0.0
git bisect run npm test validation.test.js"

Bisect Logging and Replay

Saving Bisect Progress

# Save bisect log
git bisect log > bisect.log

# If interrupted, restore progress
git bisect reset
git bisect start
git bisect replay bisect.log

Sharing Bisect Results

# Export bisect log for team
git bisect log > bisect-auth-bug.log

# Team member can replay
git bisect start
git bisect replay bisect-auth-bug.log

Common Pitfalls and Solutions

Non-Deterministic Tests

# Problem: Tests randomly fail
# Solution: Make tests deterministic

#!/bin/bash
# Run test multiple times
for i in {1..3}; do
    npm test
    if [ $? -ne 0 ]; then
        exit 1
    fi
done
exit 0

Environment-Specific Issues

#!/bin/bash
# Reset environment for each test

# Clear caches
rm -rf node_modules/.cache
redis-cli FLUSHALL

# Reset database
npm run db:reset

# Run test
npm test

Uncommitted Changes

# Bisect won't work with uncommitted changes
git stash
git bisect start
# ... perform bisect ...
git bisect reset
git stash pop

Integration with CI/CD

Automated Bisect in CI

# .github/workflows/bisect.yml
name: Automated Bisect
on:
  workflow_dispatch:
    inputs:
      good_commit:
        description: 'Known good commit'
        required: true
      test_command:
        description: 'Test command to run'
        required: true

jobs:
  bisect:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
        with:
          fetch-depth: 0
      
      - name: Run bisect
        run: |
          git bisect start HEAD ${{ github.event.inputs.good_commit }}
          git bisect run ${{ github.event.inputs.test_command }}
          git bisect reset

Bisect Visualization

Creating a Bisect Graph

# Visualize bisect process
git bisect start
git bisect bad HEAD
git bisect good v1.0

# View remaining commits
git rev-list --bisect-all HEAD ^v1.0 | wc -l

# Visualize search space
git log --graph --oneline --bisect-all

Real-World Case Studies

Case 1: API Response Change

#!/bin/bash
# Finding when API response format changed

response=$(curl -s http://localhost:3000/api/users)
if echo "$response" | jq -e '.data.users' > /dev/null; then
    exit 0  # Old format (good)
else
    exit 1  # New format (bad)
fi

Case 2: Database Migration Issue

#!/bin/bash
# Finding problematic migration

# Reset and run migrations
npm run db:reset
npm run db:migrate

# Check if specific table exists
if psql -U user -d database -c "\dt" | grep -q "users_table"; then
    exit 0
else
    exit 1
fi

Conclusion

Git bisect transforms debugging from a tedious manual process into an efficient automated search. By leveraging binary search, it can find bugs in large codebases remarkably quickly.

The key to successful bisecting is having reliable, automated tests that can clearly distinguish between good and bad states. With proper test scripts and bisect techniques, you can track down even the most elusive bugs in minutes rather than hours.

Remember: the better your commit history and test suite, the more powerful git bisect becomes. Make small, atomic commits and maintain comprehensive tests to maximize bisect's effectiveness in your debugging toolkit.

Share this article

DC

David Childs

Consulting Systems Engineer with over 10 years of experience building scalable infrastructure and helping organizations optimize their technology stack.

Related Articles