git bisect performs a binary search through your commit history to find the commit that first introduced a regression. You give it one commit you know is good and one you know is bad, and it walks you through the midpoints, halving the search space each time. With N commits between good and bad, it finds the culprit in roughly log₂(N) steps.
This is the right tool when you know "it worked at some point and now it doesn't" but the change might be hundreds of commits ago.
The basic flow
git bisect start
git bisect bad # current commit is broken
git bisect good v1.4.0 # this tag was fine
# git checks out a commit halfway between
# build / run your reproducer
git bisect good # or: git bisect bad
# git checks out a new midpoint, repeat...
# eventually:
# abc1234 is the first bad commit
git bisect reset # return to where you were
git bisect reset is mandatory at the end (or any time you want to abort). It puts your working tree back where it was before bisect started.
Choosing your good and bad commits
The good commit should be as recent as possible while still being free of the bug. The further apart good and bad are, the more steps bisect takes — and you'll have to test more commits.
Useful sources for a known-good commit:
- A release tag (
v1.4.0) - A commit on
mainfrom before a feature was merged - The last commit you remember running successfully (find it in
git logor your CI history)
If you only know the bug appeared "recently", start with bad = HEAD and good = HEAD~50. Bisect at most 6 steps over 50 commits.
Each step: the rhythm
For every commit bisect checks out:
- Build the project (if needed).
- Run the reproducer — the smallest test that distinguishes good from bad.
- Tell git which it was:
git bisect good git bisect bad
Don't skip step 2 or trust your memory. Bisect's only job is to halve the search; you have to provide the good/bad signal.
Skipping commits that can't be tested
Some commits in the range may not even build (broken merges, half-finished work, missing dependencies). Skip them:
git bisect skip
Bisect picks a different midpoint nearby. If many adjacent commits skip, bisect may report a range of suspects instead of a single commit, and you'll have to investigate that range manually.
To skip a range up front:
git bisect skip <sha1>..<sha2>
Automating bisect with a script
If you can write a one-liner that returns 0 for good and non-zero for bad, you can hand the whole bisect to git:
git bisect start HEAD v1.4.0
git bisect run npm test
Or with a custom script:
git bisect run ./scripts/repro.sh
The script's exit code is what bisect reads:
- 0 — good
- 1–124, 126–127 — bad
- 125 — skip (can't be tested; common when build fails)
- 128+ — abort the bisect
A typical bisect script:
#!/usr/bin/env bash
set -e
npm install --silent || exit 125 # build failure → skip
node ./repro.js # 0 if bug NOT present, non-zero if present
Note the inversion: repro.js should fail on the bad version, since bisect treats non-zero as bad.
Practical recipes
"It works on v1.4.0 but is broken on main"
git bisect start
git bisect bad main
git bisect good v1.4.0
# test, mark good/bad until done
git bisect reset
"Find when this test started failing"
git bisect start HEAD v1.4.0
git bisect run npm test -- --testPathPattern=login
git bisect reset
"Find when a function was renamed"
You can bisect on anything testable, not just bugs. Reverse the meaning to find when something appeared:
git bisect start HEAD v1.4.0
git bisect run sh -c '! grep -q "newFunctionName" src/'
# now "bad" = "newFunctionName exists" — bisect finds first appearance
"I want a transcript of the bisect"
git bisect log > bisect.log
# share it with a colleague
git bisect replay bisect.log
git bisect replay lets someone else (or future-you) re-run the same bisect.
Visualizing where you are
git bisect visualize # opens gitk if available
git bisect visualize --oneline # shows remaining suspects
Useful when you suspect bisect is converging on the wrong neighborhood and want to see the candidate set.
After bisect identifies a commit
Once bisect declares the first bad commit, do not assume the fix is "revert that commit". Inspect it:
git show <bad-sha>
git log --oneline <bad-sha>~5..<bad-sha>+5
The commit may be:
- The actual bug — fix or revert.
- A correct change that exposed a latent bug elsewhere.
- A merge commit — bisect reports the merge, not the underlying commit. Use
git bisect start --first-parentif you want bisect to follow merges along the mainline only, or run a sub-bisect inside the merged branch.
Etiquette and edge cases
- Bisect rewrites your working tree on every step. Commit or stash uncommitted work before starting.
- Bisect leaves a marker in
.git/. A failed bisect that wasn'tresetwill block other operations. Alwaysgit bisect resetto clean up. - Submodules don't update automatically. If your project has submodules and the bug depends on submodule state, run
git submodule updateafter each step (or in thebisect runscript). - Be careful with database migrations. If your reproducer migrates the DB, bisect can leave you with an out-of-sync schema. Use a clean database per step.
Summary
git bisect start→bad→good, then mark each midpoint.- Use
git bisect skipfor unbuildable commits. - Use
git bisect run <command>to fully automate when you have a deterministic test. - Always end with
git bisect reset. - The first-bad commit isn't always the root cause — read it, don't just revert.