What is git bisect?
git bisect is a Git command that uses binary search to find the exact commit that introduced a bug, by checking out midpoints between a known-good and a known-bad commit until it narrows down to one.
Theory
TL;DR
- Binary search cuts the commit range in half each step: 1000 commits = ~10 tests
- You mark commits as
good(working) orbad(broken); Git picks each midpoint automatically - Always run
git bisect badon the broken commit first, thengit bisect good <older-commit> git bisect run ./test.shautomates everything: Git runs your script at each midpoint without any input from you- Rule of thumb: more than 30 commits since the last good state? Use bisect. Fewer?
git log -pis faster
Quick example
git bisect start
git bisect bad # HEAD is broken
git bisect good v1.0 # v1.0 worked fine
# Output: "Bisecting: 512 revisions left to test (roughly 9 steps)"
# Git checks out the midpoint commit automatically
# Test it, then mark the result:
git bisect good # or: git bisect bad
# After ~9 more marks:
# "abc123 is the first bad commit"
git bisect reset # Return to original branch1000 commits, 10 tests. That is the whole idea.
Why binary search changes the math
Checking commits one by one takes up to N checks. Binary search takes log2(N). For 1000 commits that is ~10. For 100,000 commits that is ~17. The difference matters a lot when each test run takes two minutes.
Git builds a temporary ref at refs/bisect/ to track good/bad boundaries, sorts commits topologically, and picks the midpoint using an exponential search in bisect--helper.c. It does not simply divide the commit count by two. The algorithm accounts for the DAG shape, which is why the selected midpoint sometimes looks like it jumped ahead or back in a counterintuitive way.
When to use git bisect
- Bug appeared after a large merge or PR: bisect between the merge base and HEAD
- Production broke with no obvious recent changes: bisect from the last release tag
- A test fails and you can reproduce it consistently: use
bisect runwith a script - Someone changed behavior without a clear commit message: bisect finds the commit,
git showshows the diff
One case where you should pause first: if other engineers are actively pushing to the same branch during your session, history can shift. Start bisect on a local copy or a dedicated branch.
Automating with bisect run
git bisect start
git bisect bad
git bisect good v1.0
git bisect run node test/auth.test.js
# Exit codes: 0 = good, 1-127 = bad, 125 = skip this commit, 128+ = abortExit code 125 tells Git to skip a commit entirely. This covers commits that do not compile, have missing dependencies, or predate a file your test expects. Git skips them and continues searching.
How it works internally
Git stores bisect state in .git/refs/bisect/ and .git/BISECT_LOG. The log records every mark so you can replay a session with git bisect replay bisect.log. Useful when you need to pause, or share the debugging session with a teammate.
Merge commits get deprioritized during midpoint selection by default, since they do not introduce code directly. If you suspect a merge commit is the source, git bisect visualize shows the remaining candidates as a graph so you can spot it.
Common mistakes
Running bisect good HEAD when HEAD is already broken. This creates overlapping boundaries and Git aborts the session. Start with bisect bad first, always. Then mark a known-good older commit.
Forgetting bisect reset. After finding the bad commit, you are in a detached HEAD state. Commits, pushes, and most branch operations behave unexpectedly until you reset. Run git bisect reset or git checkout main.
Testing manually with inconsistent results. Bisect assumes your test is deterministic. If you are eyeballing "does this look right?" you will eventually mark the wrong commit. Write a script and use bisect run.
Starting with uncommitted changes. Git refuses to check out commits when the working tree is dirty.
# This fails:
echo "debug" >> index.js
git bisect start # fatal: cannot bisect on dirty working tree
# Do this instead:
git stash
git bisect start
# ...
git bisect reset
git stash popBisecting across a rebase. Rewriting history changes commit SHAs. If someone rebases the repo mid-session, bisect loses its reference points. Save progress first: git bisect log > session.log, then restore with git bisect replay session.log.
Real-world usage
- Linux kernel: contributors use
git bisect run make testto locate driver regressions; documented in the official contribution guide at kernel.org - Node.js:
git bisect run node test/parallel/test-http.jsfor regressions in the HTTP core module - Chromium: automated bisect bot triggers on every CI build failure
- React: bisect between release tags when a renderer regression appears between versions
In practice, the hardest part is not running bisect itself but writing the test script. If the bug only shows up under load or in a specific environment, a plain npm test will not catch it. The script has to reproduce the exact failure condition.
Follow-up questions
Q: How do you bisect a bug that was introduced inside a merge commit?
A: Git skips merge commits by default because they do not add code directly. Use git bisect visualize to check whether a merge commit appears in the remaining candidate range. If it does, test it manually via git checkout -b test-merge <sha>, then mark the result in your bisect session.
Q: How does Git pick the "middle" commit?
A: It does not split the commit count in half. bisect--helper.c runs an exponential search from the bad end to find the commit that minimizes worst-case remaining steps, given the actual DAG topology. In repos with many merges, the midpoint can look non-obvious.
Q: What exit codes does bisect run expect?
A: 0 means good, 1 to 127 (except 125) means bad, 125 means skip this commit, 128 or above aborts the entire session. Use 125 when a commit cannot be tested at all.
Q: What is the time complexity of git bisect?
A: O(log N) on average. For 1 million commits, roughly 20 tests. Git picks the midpoint greedily each time to minimize remaining steps, so it stays efficient even with unusual history shapes.
Q: How do you limit bisect to changes in a specific file?
A: git bisect operates per commit, not per file. Narrow the range first with git log --follow -- path/to/file to identify commits that touched the file, then use those as good/bad boundaries. For function-level history, git log -L :functionName:file.js shows line-by-line changes.
Examples
Simple repo: finding a bad commit step by step
git init bisect-demo && cd bisect-demo
# 10 clean commits, then one that breaks things
for i in {1..10}; do echo "v$i" > file.txt; git add .; git commit -m "commit $i"; done
echo "BUG" >> file.txt && git commit -am "commit 11"
git bisect start
git bisect bad # commit 11 is broken
git bisect good HEAD~5 # 5 commits back was fine
# Git checks out ~commit 8
cat file.txt # No BUG line - working
git bisect good
# Git checks out ~commit 10
cat file.txt # No BUG line - working
git bisect good
# Git checks out commit 11
git bisect bad
# Output: "commit 11 is the first bad commit"
git bisect reset4 checks for 11 commits. Without bisect you would check each one.
Production bug: automated auth regression hunt
Your Express app stopped setting req.user somewhere between v4.18.0 and v4.19.2. Write a test that exits 0 on pass and 1 on fail, then let bisect run it at each midpoint automatically:
cd express
git bisect start
git bisect bad # v4.19.2 fails the auth test
git bisect good v4.18.0 # v4.18.0 passed
git bisect run node test/auth.test.js
# Git runs the test at each midpoint
# After ~7 iterations: "8f4d2a1 is the first bad commit"
# git show 8f4d2a1 reveals the PR that changed route handling
git bisect resetNo reading through 50 diffs by hand. Git does the narrowing, you read exactly one diff.
Advanced: skipping commits that cannot be tested
In a long repo history some old commits will not compile or are missing dependencies. Handle them with exit code 125:
#!/bin/bash
# test.sh
npm install 2>/dev/null
if [ $? -ne 0 ]; then
exit 125 # Cannot test this commit, skip it
fi
npm test
exit $? # 0 = good, non-zero = badgit bisect start
git bisect bad
git bisect good v2.0.0
git bisect run ./test.sh
# Skips broken commits, still finds the culprit
# "a1b2c3 is the first bad commit"
git bisect resetThis pattern holds up in monorepos where old commits predate a package.json restructure.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.