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.

published: reading time: 13 min read updated: March 31, 2026

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:

TermDescription
Good commitA commit where the bug is NOT present
Bad commitA commit where the bug IS present
Bisect rangeThe span between good and bad commits
MidpointThe commit bisect checks next (halfway between good and bad)
StepsNumber 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

ScenarioWhat HappensMitigation
Build fails at midpointThe commit doesn’t compile, blocking bisectUse exit 125 to skip; bisect tries a different midpoint
Flaky test resultsNon-deterministic tests give inconsistent resultsRun tests multiple times; use a deterministic test script
Merge commit confusionBisect lands on a merge commit with multiple parentsUse git bisect visualize to understand the topology
Dependencies changednpm install fails on old commitsSkip commits with incompatible dependencies; use lock files
Database migrationsOld commits expect different database schemaSkip commits that can’t run with current database
Wrong good/bad markingIncorrect marking leads to wrong culpritDouble-check each marking; use automated tests when possible

Trade-offs

AspectAdvantageDisadvantage
SpeedLogarithmic — 1000 commits in 10 stepsEach step requires building and testing
PrecisionFinds the exact commit, not just a rangeRequires a known-good starting point
Automationbisect run works unattendedWriting reliable test scripts takes effort
Manual controlYou decide good/bad at each stepManual bisect is slow for large ranges
Skip supportCan skip untestable commitsToo many skips reduce accuracy
Reset safetygit bisect reset always returns you safelyForgetting 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 log output 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

  1. Wrong Good Commit — Marking a commit as good when it actually has the bug leads to the wrong culprit. Verify the good commit thoroughly.
  2. Skipping Too Many — Skipping more than 20% of commits makes the result unreliable. Fix the build issues instead of skipping.
  3. Not Resetting — Forgetting git bisect reset leaves the repo in a detached HEAD state. Always reset when done.
  4. Ignoring Build Failures — Treating build failures as “bad” instead of skipping them leads to false positives.
  5. Non-Deterministic Tests — Flaky tests cause bisect to find the wrong commit. Make tests deterministic before bisecting.
  6. Bisecting Across Major Refactors — If the codebase changed dramatically, bisect may land on commits that can’t be tested. Use skip strategically.
  7. 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 125 to skip untestable commits
  • Write automated test scripts for git bisect run
  • Save bisect logs with git bisect log
  • Always run git bisect reset when 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 loggit bisect log > bisect-session.log for reproducibility

Interview Q&A

How does git bisect find the first bad commit?

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.

What does exit code 125 mean in git bisect run?

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.

Can git bisect work with merge 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.

How do you automate git bisect for a failing test?

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

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 #version-control #git-blame

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 #git-log #history

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.

#git #version-control #git-reflog