"git bisect: How to Find the Commit That Broke Your Code"
git bisect finds the commit that introduced a bug using a binary search through history. Run git bisect start, mark the broken commit with git bisect bad and a known-good one with git bisect good <hash>, then test the commits Git checks out until it names the culprit; finish with git bisect reset.
git bisect finds the commit that introduced a bug using binary search. You point to one commit where the bug exists and one where it didn't — Git checks out the middle of the range and asks, "Is this good or bad?" Each answer eliminates half of the candidates, so among 1,000 commits the culprit turns up in about 10 checks.
This is especially valuable when the bug isn't obvious: a test fails, performance drops, the layout breaks, and the diffs don't tell you anything. Instead of reading hundreds of changes, you answer the same simple question a few times.
How binary search through history works
Say there are 200 commits between the working v2.3 release and the current main. Git picks the commit in the middle and you test it. If it's already broken, the bug came in during the first half; if not, in the second. The number of candidates halves at every step: 200, 100, 50, 25, 13, 7, 4, 2, 1. Eight checks instead of two hundred.
That leads to the key requirement: "broken" has to be monotonic. If the bug appeared, disappeared, and came back, bisect will find one of those transitions, but not necessarily the one you're after.
Using git bisect step by step: manual mode
1. Find a good and a bad commit
The bad one is usually the current HEAD. The good one is any commit where everything definitely worked: the tag of the last release, a commit from a week ago. git log --oneline, git tag, or git log --since="2 weeks ago" will help you find a candidate.
2. Start the session
git bisect start
git bisect bad # the current commit is broken
git bisect good v2.3 # everything worked in release v2.3
Bisecting: 99 revisions left to test after this (roughly 7 steps)
[4f5e6d7c...] Update frontend dependencies
You can write the same thing in one line: git bisect start HEAD v2.3 — bad first, then good. Git immediately checks out the commit in the middle (in a detached HEAD state).
3. Test and mark
Build the project, run the test, or reproduce the bug manually. Then report the result:
git bisect good # works here
Bisecting: 49 revisions left to test after this (roughly 6 steps)
git bisect bad # broken here
Bisecting: 24 revisions left to test after this (roughly 5 steps)
4. Get the result
When only one candidate is left, Git prints it:
a1b2c3d4e5f6... is the first bad commit
commit a1b2c3d4e5f6...
Author: John Smith <john@example.com>
Date: Tue Sep 29 11:40:12 2026 +0300
Cache the cart total calculation
src/cart.js | 12 ++++++++----
1 file changed, 8 insertions(+), 4 deletions(-)
5. End the session
git bisect reset
This takes you back to the branch you started on. Don't skip this step: until the session is closed, you stay in detached HEAD, and new commits are easy to lose. git bisect reset HEAD leaves you on the commit that was found, and git bisect reset <commit> on any commit you specify.
Automated search: git bisect run
If your check can be expressed as a command, let Git do all the work. git bisect run runs a script at every step and marks the commit based on the exit code:
| Exit code | Meaning |
|---|---|
| 0 | good — the commit is good |
| 1–127, except 125 | bad — the commit is bad |
| 125 | skip — the commit can't be tested, skip it |
| above 127 (or <0) | abort bisect (e.g. the script was killed by a signal) |
Examples:
git bisect start HEAD v2.3
git bisect run npm test
git bisect run pytest tests/test_cart.py -x
git bisect run make check
For more complex checks, write a small script. It's important to separate "didn't build" from "bug reproduced", otherwise bisect will mistake a broken build for the bug you're hunting:
#!/bin/sh
# check.sh
npm ci --silent || exit 125 # couldn't install dependencies — skip
npm run build || exit 125 # didn't build — skip
node scripts/check-cart-total.js # 0 — ok, 1 — bug
chmod +x check.sh
git bisect run ./check.sh
Keep the script out of tracked files, or in a folder that exists in every commit being tested: bisect switches the working directory, and a recently added file will vanish on older commits. The safest option is to put it in /tmp or any folder outside the repository.
A test that didn't exist back then
A common situation: you've written a test that reproduces the bug, but older commits don't have that file. The fix is the same — a copy outside the repository:
cp tests/test_regression.py /tmp/
git bisect run pytest /tmp/test_regression.py
git bisect skip: when a commit can't be tested
Sometimes the commit in the middle doesn't build or fails for an unrelated reason. Marking it good or bad would skew the result. Skip it instead:
git bisect skip
git bisect skip v2.4..v2.4.3 # skip a range
Git will pick a neighboring commit. If the skipped commits sit right on the boundary, at the end you'll see a list of "possible culprits" instead of a single one — in that case, review their diffs by hand.
Custom terms instead of good and bad
Bisect isn't only for bugs. For example, you might want to find the commit after which a page started loading faster. Calling fast commits "bad" is confusing, so there are alternative terms:
git bisect start --term-old=slow --term-new=fast
git bisect fast HEAD
git bisect slow v2.3
The built-in synonyms are old and new: git bisect new, git bisect old. There's just one rule: the "old" state must belong to the earlier commit.
Session log: log, replay, visualize
git bisect log— prints all your marks. Save it to a file if the session is long.git bisect replay file— replays a session from a log. Made a mistake at step six? Save the log, delete the wrong lines, rungit bisect reset, thenreplay.git bisect visualize(orgit bisect view) — shows the remaining candidates; with--onelineit lists them via git log.
git bisect log > /tmp/bisect.log
# edit the log
git bisect reset
git bisect replay /tmp/bisect.log
Bisect and merge commits
In a history with lots of merges, bisect can lead you into a merged branch — to commits created before that branch was synced with main, and therefore built on a different base. If you first want to know which merge brought in the bug, use:
git bisect start --first-parent
Git then follows only the first parents of merge commits, i.e. the main branch's history. Once you've found the guilty merge, you can run a second bisect inside the corresponding branch.
Common mistakes
- Mixing up good and bad. The result will be meaningless. Recover the session with
git bisect logandreplayinstead of starting over. - A dirty working directory. Uncommitted changes get in the way of switching commits and affect the check. Commit or run
git stashbefore you start. - Stale build artifacts. An outdated cache or
node_modulesfrom another commit gives false results. Do a clean build in your script. - A flaky test. If the test sometimes fails on its own, bisect will flag a random commit. Run the check several times, or fix the test before searching.
- Forgetting to reset. You stayed in detached HEAD and started working. The reflog will help you find lost commits.
Example: hunting down a regression end to end
Users report that the cart total is calculated incorrectly when a discount applies. In release v2.3 it was correct; in main it isn't. We write a minimal check and start the search:
cat > /tmp/check.sh <<'SH'
#!/bin/sh
npm ci --silent || exit 125
node -e "const c=require('./src/cart'); process.exit(c.total([{price:100}], 0.1) === 90 ? 0 : 1)"
SH
chmod +x /tmp/check.sh
git stash # set aside uncommitted changes
git bisect start HEAD v2.3
git bisect run /tmp/check.sh
...
a1b2c3d4e5f6... is the first bad commit
git bisect log > /tmp/cart-bisect.log
git bisect reset
git stash pop
The whole search across a hundred commits runs without you while you work on something else. The saved log comes in handy if you need to show your teammates in the ticket exactly how the culprit was found.
What to do with the commit you found
Once you've found the culprit, look at it in full: git show a1b2c3d. A small commit with a clear message points straight to the cause — one of the reasons teams value atomic commits: bisecting a history full of huge "fix stuff" commits is almost useless. From there you have two options: fix the bug in a new commit or, if the change was a mistake as a whole and has already been published, undo it with git revert a1b2c3d.
Summary
git bisect turns hunting for a regression from reading hundreds of diffs into a few "works / doesn't work" answers, and with git bisect run — into a single command. All you need are two reference points — a good and a bad commit — and a reproducible check. Reading history, navigating commits, and finding bugs in them are covered hands-on in the Git course.
FAQ
How many steps does git bisect take?
Roughly log2 of the number of commits in the range: about 7 steps for 100 commits and about 10 for 1,000. Git shows its own estimate as roughly N steps.
Can I run bisect if I don't know a good commit?
You need at least one. Pick a commit you know is old or a release tag and test it manually; the extra steps from a wide range cost almost nothing.
Does git bisect change the repository history?
No, it only switches the working directory between commits and stores your marks in internal refs/bisect references, which are removed by git bisect reset.
Can I specify several good commits?
Yes, you can pass several commits to git bisect good; this narrows the range, especially in a history with branches.
Is bisect only for finding bugs?
No, you can use it to find any point where a property changed: when a file appeared, when the build got faster, or when behavior changed. The old/new terms or your own via --term-old and --term-new are handy for this.
This topic is covered in depth with practice in the course “Git from Scratch to Confident Developer”.
Go to course