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: 21 min read author: Geek Workbench updated: March 31, 2026

Git Bisect for Bug Hunting: Binary Search Through Commit History

git bisect is one of Git’s most capable debugging tools, and most developers never touch it. 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

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-off Analysis

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 / 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 Questions

1. 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.

2. 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.

3. 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.

4. 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.

5. Why is binary search more efficient than linear search for bug hunting?

Binary search achieves logarithmic time complexity O(log n) versus linear O(n). For 1000 commits, linear search needs up to 1000 tests in the worst case, while binary search needs at most 10.

Each bisect step halves the search space, making it practical to find bugs in repositories with thousands of commits. The tradeoff is that binary search requires a known-good and known-bad endpoint, while linear search does not.

6. What happens if you incorrectly mark a commit as good or bad?

An incorrect marking invalidates the entire bisect result. If you mark a bad commit as good, bisect will narrow the search to the wrong range and return an incorrect culprit. Similarly, marking a good commit as bad skews the search space.

To recover, run git bisect reset to start fresh, verify your endpoints carefully, and begin again. Always confirm the good commit by testing it directly before starting.

7. How does git bisect start --first-parent change the search behavior?

The --first-parent flag tells bisect to only follow the first parent at merge commits, ignoring the merged branch history. This is useful when you want to bisect along the main branch line only.

This matters for features like "git bisect visualize" and when a bug might have been introduced in a merged branch. Without this flag, bisect follows whichever parent the internal traversal selects, which may not be deterministic.

8. Can git bisect detect bugs introduced specifically by merge commits?

Yes, but it requires care. A merge commit can introduce bugs through merge conflicts resolved incorrectly or from the merged branch's changes. Bisect will find the merge commit if it is the first bad commit in the searched range.

Use git log --graph --oneline to visualize the topology before bisecting. If the merge is suspected, start bisect from the merge commit itself as the bad endpoint.

9. What are the key limitations of manual bisect versus automated bisect?

Manual bisect becomes error-prone beyond 20 steps because humans tire and make inconsistent judgments. It also requires manually running tests at each step, making it impractical overnight or over large ranges.

Automated bisect with git bisect run eliminates human inconsistency, works unattended, and can run in CI pipelines. The cost is writing a reliable test script that handles build failures and returns consistent exit codes.

10. How do you handle non-deterministic bugs with git bisect?

Non-deterministic bugs are a fundamental problem for bisect because the same commit may test good or bad across runs. The approach depends on the cause:

  • Race conditions: Run the test script multiple times sequentially and accept the majority result
  • Flaky tests: Fix the test first using test retries or mocking to make results deterministic
  • Environmental factors: Containerize the test environment (Docker) to ensure consistency

Never bisect a known-flaky test without addressing the underlying non-determinism.

11. What is the purpose of git bisect skip and when should you use it?

git bisect skip marks the current commit as untestable, telling bisect to choose a different midpoint instead. Use it when a commit cannot be built, tested, or evaluated due to:

  • Incompatible dependencies preventing a build
  • Missing test infrastructure for that historical version
  • The bug being unrelated to the code path at that commit

Warning: Excessive skipping reduces bisect precision. If more than 20% of commits are skipped, the result may be unreliable.

12. How can you use git bisect with repositories containing submodules?

Git bisect operates on the main repository history. Submodules are tracked by the parent repo as special entries in the parent commit. When bisect checks out an old commit, the submodule stays at whatever commit the parent recorded.

To bisect inside a submodule, enter it (cd submodule) and run bisect there independently. The parent repository's bisect session does not descend into submodules automatically.

13. What strategies reduce the number of bisect steps for large codebases?

Narrow the range first by finding a closer good commit. Instead of going back months, find a tag or release from days before the bug appeared.

Use git log --oneline --since="2 weeks ago" to identify recent good commits. For very large repos, combine bisect with a build matrix that skips compilation by reusing artifacts from CI when the code paths are unchanged.

14. How does git bisect log help in reproducing and sharing bisect sessions?

git bisect log records every marking decision made during the session. This log can be:

  • Saved: git bisect log > bisect-session.log for documentation
  • Replayed: git bisect replay bisect-session.log to resume or recreate the session
  • Shared: Include in post-mortems to show exactly how the bug was isolated

The log format is a script of git bisect good/bad/skip commands that can recreate the entire search path.

15. What are the differences between running bisect on GitHub Actions versus a local machine?

CI-based bisect (GitHub Actions) offers consistent environments, parallel testing capability, and no local machine strain. However, it introduces latency between steps and may have tighter timeouts.

Local bisect is faster per step (no network latency) but consumes your machine's resources and depends on your local environment being compatible with all historical commits.

16. How do you configure bisect to handle test suites that run in parallel?

For parallel test suites, ensure your bisect script uses sequential exit codes. If tests run in parallel and any single test fails, the script should exit 1 (bad) immediately rather than waiting for all tests to complete.

Configure the test runner to exit on first failure (npm test -- --bail in Jest) so the bisect script gets a fast, deterministic signal.

17. What edge cases cause git bisect run to fail or produce incorrect results?

Edge cases that break bisect run:

  • Script returns wrong exit code: Test script exits 0 for bad or 1 for good — result is inverted
  • Empty search range: Good and bad are the same commit — bisect refuses to start
  • Infinite loop from skips: Every midpoint is skipped (e.g., due to dependency incompatibility), causing bisect to exhaust candidates
  • Environment-dependent results: Tests pass in CI but fail locally, or vice versa, indicating environmental factors
18. What does the --no-checkout flag do in git bisect?

The git bisect start --no-checkout flag performs a no-checkout bisect where Git checks out the commit under test but does not populate the working tree. Instead, it leaves the repository in adetached HEAD state without modifying files.

This is faster for large repositories and safer when the historical code might have side effects. However, it only works when the bisect script does not need to inspect or run the actual code — typically for metadata-only checks.

19. What debugging information does git bisect visualize provide?

git bisect visualize opens an interactive viewer showing the commit DAG (Directed Acyclic Graph) with good commits marked in green and bad in red. This helps you understand:

  • The search space topology
  • Where the current midpoint is positioned in history
  • The path bisect will take to narrow the range

It uses git log --graph --oneline --all --decorated under the hood. Without a graphical environment, use git bisect log for a text record of the session.

20. How do you preserve bisect results for documentation or post-mortem purposes?

Save three artifacts for documentation:

  • Bisect log: git bisect log > bisect-[date].log — the complete decision trail
  • First bad commit: Run git log -1 [commit] to get the commit message and author
  • Reproduction script: Document the exact git bisect start/good/bad commands and test script used

Include these in incident reports to identify code areas with frequent regressions and track time-to-detection metrics.

Further Reading

Conclusion

Git bisect turns bug hunting from a manual search into a logarithmic algorithm. Combined with automated testing, it pinpoints the exact commit that introduced a bug — often in seconds rather than hours.

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