How-to

"git bisect: How to Find the Commit That Broke Your Code"

Updated: · Skillaria.Top

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 codeMeaning
0good — the commit is good
1–127, except 125bad — the commit is bad
125skip — 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, run git bisect reset, then replay.
  • git bisect visualize (or git bisect view) — shows the remaining candidates; with --oneline it 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 log and replay instead of starting over.
  • A dirty working directory. Uncommitted changes get in the way of switching commits and affect the check. Commit or run git stash before you start.
  • Stale build artifacts. An outdated cache or node_modules from 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.

Learn this topic on video

This topic is covered in depth with practice in the course “Git from Scratch to Confident Developer”.

Go to course