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.
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:
| Term | Description |
|---|---|
| Good commit | A commit where the bug is NOT present |
| Bad commit | A commit where the bug IS present |
| Bisect range | The span between good and bad commits |
| Midpoint | The commit bisect checks next (halfway between good and bad) |
| Steps | Number 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
| Scenario | What Happens | Mitigation |
|---|---|---|
| Build fails at midpoint | The commit doesn’t compile, blocking bisect | Use exit 125 to skip; bisect tries a different midpoint |
| Flaky test results | Non-deterministic tests give inconsistent results | Run tests multiple times; use a deterministic test script |
| Merge commit confusion | Bisect lands on a merge commit with multiple parents | Use git bisect visualize to understand the topology |
| Dependencies changed | npm install fails on old commits | Skip commits with incompatible dependencies; use lock files |
| Database migrations | Old commits expect different database schema | Skip commits that can’t run with current database |
| Wrong good/bad marking | Incorrect marking leads to wrong culprit | Double-check each marking; use automated tests when possible |
Trade-off Analysis
| Aspect | Advantage | Disadvantage |
|---|---|---|
| Speed | Logarithmic — 1000 commits in 10 steps | Each step requires building and testing |
| Precision | Finds the exact commit, not just a range | Requires a known-good starting point |
| Automation | bisect run works unattended | Writing reliable test scripts takes effort |
| Manual control | You decide good/bad at each step | Manual bisect is slow for large ranges |
| Skip support | Can skip untestable commits | Too many skips reduce accuracy |
| Reset safety | git bisect reset always returns you safely | Forgetting 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 logoutput 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
- Wrong Good Commit — Marking a commit as good when it actually has the bug leads to the wrong culprit. Verify the good commit thoroughly.
- Skipping Too Many — Skipping more than 20% of commits makes the result unreliable. Fix the build issues instead of skipping.
- Not Resetting — Forgetting
git bisect resetleaves the repo in a detached HEAD state. Always reset when done. - Ignoring Build Failures — Treating build failures as “bad” instead of skipping them leads to false positives.
- Non-Deterministic Tests — Flaky tests cause bisect to find the wrong commit. Make tests deterministic before bisecting.
- Bisecting Across Major Refactors — If the codebase changed dramatically, bisect may land on commits that can’t be tested. Use skip strategically.
- 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 125to skip untestable commits - Write automated test scripts for
git bisect run - Save bisect logs with
git bisect log - Always run
git bisect resetwhen 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 log —
git bisect log > bisect-session.logfor reproducibility
Interview Questions
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
git bisect log records every marking decision made during the session. This log can be:
- Saved:
git bisect log > bisect-session.logfor documentation - Replayed:
git bisect replay bisect-session.logto 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.
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.
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.
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
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.
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.
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/badcommands and test script used
Include these in incident reports to identify code areas with frequent regressions and track time-to-detection metrics.
Further Reading
- Git bisect documentation — Official Git documentation
- Git bisect run examples — Automated bisect guide
- Finding regressions with git bisect — Beginner-friendly tutorial
- Git bisect and Docker — Using bisect with containers
- Binary search algorithm — The algorithm behind bisect
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 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 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.