Using git bisect to Track Down a Bad Commit
Use git bisect to pinpoint the commit behind a regression. Set reliable good and bad refs, automate tests, handle flaky results and skips, and verify the culprit.
git bisect tracks down the commit that caused a regression with a binary search. You mark one known-good commit and one known-bad commit, and each test cuts the remaining range in half, so 300 commits take about eight or nine checks.
The feature worked last month. Since then, 300 commits have landed on main, nobody remembers touching the cart code, and reading every diff would take all afternoon.
5 Git Commands Beyond Commit and Push introduces start, good, bad, run, skip and reset. This article goes further: how to pick a good commit you can trust, how to write a git bisect run script whose exit codes can’t mislead Git, and what to do when a flaky test or a wrong mark sends the search down the wrong path.
Key Takeaways
- Each doubling of a bisect range adds one check, so the real cost of a distant good commit is old commits that no longer build and have to be skipped.
- With
git bisect run, exit code 0 marks a commit good, 125 skips it, any other code from 1 to 127 marks it bad, and 128 or higher aborts the bisect. - For an intermittent failure, mark a commit bad if any of several runs fails and good only if every run passes.
- A wrong mark doesn’t mean starting over: save
git bisect log, delete the wrong line and everything after it, then rungit bisect resetandgit bisect replay. - Confirm a result by testing the named commit and its parent. The parent should pass and the named commit should fail.
The Manual git bisect Loop
The manual loop takes three commands to set up: git bisect start, then git bisect bad on a broken commit, then git bisect good <ref> on a working one. From there you test each commit Git checks out and mark it until Git names the culprit. The Pro Git chapter on debugging with Git covers the same flow. If Git is still new to you, 10 Git commands every developer should know covers the everyday basics.
git bisect start
git bisect bad # HEAD has the bug
git bisect good v4.12.0 # last release known to work
Bisecting: 149 revisions left to test after this (roughly 7 steps)
[9e41c07b2d85a3f16c0e7b94d2a58f3e1c6b0d27] Refactor cart line-item formatter
Git has checked out the midpoint. Run your test and report what happened:
git bisect bad
Bisecting: 74 revisions left to test after this (roughly 6 steps)
[2b7f0d9a4c16e83b5f2a07d9c4e1b68a3f05c9d1] Update currency util types
Keep testing and marking. When one candidate is left, Git prints the result (SHAs and names below are placeholders):
3c9d2e7a51f04b8e96d1a2c7f05e4b3d8a6c1f92 is the first bad commit
commit 3c9d2e7a51f04b8e96d1a2c7f05e4b3d8a6c1f92
Author: Example Dev <dev@example.com>
Date: <date>
Round line totals before applying discount
src/cart/total.ts | 4 ++--
How Do You Pick the Good Commit?
The best good commit for git bisect is the most recent release tag that you know shipped without the bug, tested before you mark it. A good ref that is actually bad still produces a confident answer, usually a commit just after your good ref, and that answer is wrong.
Test the tag with the same check bisect will use (Vitest here is only an example runner):
git switch --detach v4.12.0
npm ci && npx vitest run src/cart/total.test.ts # must pass
git switch -
Don’t try to shrink the range at the expense of certainty. Each doubling adds only one check: about 8 to 9 checks for 300 commits, 9 to 10 for 600, and 11 to 12 for 2,400. Going far back costs you something else. Old commits may need an older Node version, a dependency that has since been unpublished, or a different lockfile format. Each commit that won’t build has to be skipped, and a long skipped stretch can hide the culprit.
Session replays show when a regression first reached real users, which narrows the good commit to the release deployed just before it.
How Does git bisect run Automate the Search?
git bisect run <script> runs your script on every candidate commit and reads its exit code as the verdict. The git-bisect documentation defines the mapping:
| Exit code | Meaning |
|---|---|
| 0 | Good |
| 1–127, except 125 | Bad |
| 125 | Skip (cannot test) |
| 128 and above | Abort the bisect |
Keep the script outside the repository. That way checking out older commits can’t change it, and cleanup commands such as git clean -fdx can’t delete it:
cat > ../bisect-test.sh <<'EOF'
#!/usr/bin/env bash
npm ci --silent || exit 125 # can't install: skip
npm run build --silent || exit 125 # can't build: skip
npx vitest run src/cart/total.test.ts || exit 1
EOF
chmod +x ../bisect-test.sh
git bisect run ../bisect-test.sh
A failed install or build exits with 125, so an unrelated breakage is skipped instead of being counted as bad. The final || exit 1 maps any test failure to 1, so a test runner that happens to return 125 or 128+ can’t skip a commit or abort the run by accident.
Exit codes 126 (not executable) and 127 (command not found) normally count as bad. So a wrong script path or a missing execute bit could mark every commit bad. Git’s 2.36 release notes describe the safeguard: Git tries to spot a script that can’t run and stops early rather than naming a culprit. If git bisect run stops early, check the script path and permissions before you look at the code.
Common Snags: Skip, Reset and a Dirty Tree
Most interruptions come from three things: a commit you can’t test, a session you need to end, or local changes that are in the way.
git bisect skip
git bisect skip sets aside the current commit and Git moves to a nearby one. If the culprit ends up inside a run of skipped commits, Git lists the candidates rather than naming one.
git bisect reset # back to the branch you started on
git bisect reset 3c9d2e7 # end the session on a specific commit
Commit or stash local changes before git bisect start. You can limit the search to certain paths with git bisect start -- src/cart/. git bisect visualize opens gitk on the remaining suspects, or falls back to git log when Git finds no graphical desktop session.
How Do You Handle Flaky Tests and Wrong Marks?
To bisect an intermittent failure, run the test several times on each commit. Mark it bad if any run fails and good only if every run passes. The reason is that a single wrong mark sends the search into the wrong half, and Git still reports a first bad commit. A truly bad commit can pass by luck, but a good commit should never fail this test.
#!/usr/bin/env bash
npm ci --silent || exit 125
npm run build --silent || exit 125
for i in $(seq 1 10); do
npx vitest run src/cart/total.test.ts || exit 1 # any failure = bad
done
exit 0 # all passed = good
Here is the arithmetic for a hypothetical case: if a bad commit fails 20% of the time, ten clean runs happen by chance with probability 0.8¹⁰ ≈ 0.11. More runs shrink that risk. Skipping ambiguous commits doesn’t fix anything, because the unreliable result is still there and the final answer just gets wider. This rule assumes the flakiness is new. If the test was already flaky before the regression, stabilize it or write a narrower check first, or it will mark good commits bad.
If you already made a wrong mark, you can fix it without starting over:
git bisect log > ../bisect.log
# edit ../bisect.log
git bisect reset
git bisect replay ../bisect.log
The saved log includes comment lines as well. Only the command lines are shown here. Before editing:
git bisect start
git bisect bad 8d1f...
git bisect good 5a0c...
git bisect good 9e41... <- wrong: this commit was flaky
git bisect bad 2b7f...
After editing, delete the wrong line and everything after it:
git bisect start
git bisect bad 8d1f...
git bisect good 5a0c...
git bisect replay restores the session up to the last correct mark, and you carry on from there.
How Do You Confirm the Result?
Treat the commit git bisect names as a lead until you have checked it. Read the diff with git show 3c9d2e7, then test the commit’s parent and the commit itself:
git switch --detach 3c9d2e7^ # parent: test should pass
git switch --detach 3c9d2e7 # culprit: test should fail
If the parent passes and the commit fails, you have found the regression. From here, use git blame to see why the surrounding lines changed, and git history to fix up commits that haven’t been pushed yet. On the next regression, start from a tested release tag and a script that runs the test several times on each commit.
FAQs
Can git bisect find the commit that fixed a bug instead of the one that broke it?
Yes. Use the terms old and new instead of good and bad. Run git bisect start, mark a fixed commit with git bisect new and an older broken commit with git bisect old, and Git reports the first new commit, which is the fix. For clearer labels, run git bisect start --term-old broken --term-new fixed, then mark commits with git bisect fixed and git bisect broken.
How does git bisect handle merge commits from feature branches?
By default, git bisect also tests commits from inside merged branches, including work-in-progress commits that may not build. Running git bisect start --first-parent, added in Git 2.29, keeps the search on the main line of history. On main, it stops at the merge that brought the bug in and never tests the branch's own commits. A second bisect over that branch finds the exact commit.
What happens if the good commit is not an ancestor of the bad commit?
Git checks out a merge base of the two commits and asks you to test it first. If the merge base is good, bisection carries on normally over the commits between it and the bad commit. If the merge base is already bad, Git stops instead of searching, because the good commit sits on a separate line of history where the bug is absent or was fixed.
What is the difference between git bisect and git blame?
git blame tells you, line by line, which commit most recently changed a file, so it only answers who last edited a given line. git bisect tests behaviour across commits, so it finds a regression even when the cause is a dependency bump, a config change, or a file other than the one showing the symptom. Use bisect to find the commit, then blame to trace the lines it changed.