Guide10 min read

Git Bisect: Find the Commit That Broke Everything

You know it worked two weeks ago and it is broken now, with two hundred commits in between. Bisect finds the culprit in eight steps instead of two hundred. Here is how to run it, automate it, and avoid the traps.

Git Bisect: Find the Commit That Broke Everything | Gitoryx

A support ticket says checkout fails for customers with a saved payment method. You try it locally and reproduce it in thirty seconds. Then you check out the tag from three weeks ago and it works fine.

So the bug is in the 214 commits between those two points. That is the entire useful information you have, and testing 214 commits by hand is not a plan.

git bisect is the plan. It is a binary search over the commit range, and it will get you from 214 candidates to one in eight tests.

#The math, briefly

Each test cuts the remaining range in half. 214 becomes 107, then 54, then 27, then 14, then 7, then 4, then 2, then 1. Eight steps.

The useful property is how slowly that grows. A thousand commits is ten steps. Ten thousand is fourteen. The range being enormous is not really an argument against bisecting, which is the opposite of most people's intuition and the reason bisect is underused.

What does cost you is the time it takes to test a single commit. If reproducing the bug takes five seconds you will be done in a minute. If it requires a full build, a database migration and a manual click-through, eight steps is an afternoon. That is the number to optimise, and it is why the automated mode further down matters so much.

#The basic loop

You need two things before you start: a commit where the bug exists, and a commit where it does not. Usually the first is HEAD and the second is a release tag you know was fine.

git bisect start
git bisect bad                 # current HEAD is broken
git bisect good v1.4.0         # this tag was fine

Git responds with something like:

Bisecting: 106 revisions left to test after this (roughly 7 steps)
[8f3a91c2b4] add retry logic to the payment webhook

It has checked out a commit halfway through the range. Test it. Then answer:

git bisect good    # bug is not present here
# or
git bisect bad     # bug is present here

Git checks out the next commit and tells you how many steps remain. Repeat until it prints the verdict:

8f3a91c2b4a7d3e9f1c0b2a4d6e8f0a2c4b6d8e0 is the first bad commit
commit 8f3a91c2b4a7d3e9f1c0b2a4d6e8f0a2c4b6d8e0
Author: Dana Whitfield <dana@example.com>
Date:   Tue Aug 18 14:22:10 2026 +0200

    ♻️ normalise stored card tokens before lookup

Then clean up. This is the step people forget:

git bisect reset

That returns you to the branch you were on before you started. Without it you are left sitting on a detached HEAD in the middle of your own history, which is confusing an hour later when you have moved on to something else.

#Picking the starting points

The two commits you supply determine everything, and getting them wrong is the most common reason a bisect ends somewhere that makes no sense.

The bad end is usually easy: HEAD, or the tag that is live in production. The good end is where people guess, and guessing costs you the whole session.

# Releases, newest first, so you can pick a plausible good tag.
git tag --sort=-creatordate | head

# How many commits are actually in the range.
git rev-list --count v1.4.0..HEAD

Verify the good commit before starting. Check it out, reproduce, confirm the bug is absent. Two minutes there saves eight test cycles converging on the wrong answer.

If you have no idea how far back to go, do not binary search for the starting point by hand. Pick something you are confident about even if it is far too old. An extra 400 commits in the range costs you two additional steps, which is nothing compared to restarting because your good was not good.

The one-line version, when you are confident about both ends:

git bisect start HEAD v1.4.0        # bad first, then good

#Automating it

The moment your test can be expressed as a command that exits 0 or non-zero, you can stop answering questions.

git bisect start
git bisect bad
git bisect good v1.4.0
git bisect run npm test -- checkout.spec.ts

Git runs the command at each step and reads the exit code. Zero means good, anything from 1 to 127 means bad, with one exception described below. It works through the entire range unattended and prints the first bad commit.

For anything that is not already a test, write a three-line script. This one is a complete bisect driver:

#!/usr/bin/env bash
# bisect-check.sh
npm ci --silent || exit 125        # cannot build: skip this commit
npm run build --silent || exit 125
node ./scripts/repro-checkout.js   # exits 0 if fine, 1 if the bug reproduces
chmod +x bisect-check.sh
git bisect run ./bisect-check.sh

Exit code 125 is the special one: it tells Git the commit cannot be tested and should be skipped rather than counted. Using it for build failures is what keeps an automated bisect from derailing on a commit that never compiled in the first place.

You can also inline a test without a script file:

git bisect run sh -c 'grep -q "requestTimeout" src/config.ts'

That finds the commit where a string appeared or disappeared, which is a surprisingly common need when tracking down a config regression.

#Skip, and when to use it

Sometimes a commit genuinely cannot be judged. It does not build, the feature under test did not exist yet, or the test infrastructure was mid-migration.

git bisect skip

Git picks a different commit nearby and carries on. If a whole stretch is untestable, you can skip a range:

git bisect skip v1.4.2..v1.4.5

The one thing not to do is mark an untestable commit as good because you want to keep moving. That is not a neutral choice. It tells Git the bug is definitely in the other half, and if you were wrong, the search converges confidently on the wrong commit and you will not find out until you read the diff and it makes no sense.

#The commands nobody mentions

Four more, each of which solves a specific annoyance.

git bisect log and git bisect replay. The log records every answer you have given. If you realise halfway through that you marked something wrong, you do not have to start over:

git bisect log > /tmp/bisect.log
# edit the file, delete the wrong line
git bisect reset
git bisect replay /tmp/bisect.log

This also makes a session shareable. Hand the log to a colleague and they resume exactly where you were.

git bisect visualize. Opens the remaining candidates in your configured history viewer. Useful at the point where the range is down to a handful and you would rather read them than keep testing.

git bisect start --no-checkout. Bisects without touching your working tree, moving a ref called BISECT_HEAD instead. This matters when checking out is expensive, such as a repository with large binaries or a build system that invalidates its cache on every file change.

git bisect start -- <path>. Restricts the search to commits that touched a given path. If you already know the bug is in the billing code, this can cut a 200-commit range to 30 before the binary search even starts.

git bisect start -- src/billing/

#Bisecting things that are not yes or no

The algorithm needs a boolean, but plenty of regressions are not boolean. A page that got slower, a bundle that grew, memory that creeps. The trick is to pick a threshold and turn the measurement into an exit code.

#!/usr/bin/env bash
# bisect-bundle-size.sh
npm ci --silent   || exit 125
npm run build -s  || exit 125

size=$(wc -c < dist/main.js)
threshold=512000

if [ "$size" -gt "$threshold" ]; then
  echo "bundle is $size bytes, over threshold"
  exit 1     # bad
fi
exit 0       # good

Pick the threshold between the known-good value and the known-bad value, not at some round number that feels tidy. If good is 400 KB and bad is 900 KB, use 600 KB. Using 500 KB risks a commit that legitimately took it from 400 to 510 being marked bad when it is not the change you are hunting.

The same approach works for timings, with one caveat: measure something with low variance and run it enough times to be confident. A flaky measurement makes bisect converge confidently on the wrong commit, which is worse than not bisecting at all.

#The traps

Your good and bad are reversed. git bisect start assumes bad is newer than good. If you are looking for when something was fixed rather than broken, either invert your answers mentally or use custom terms:

git bisect start --term-old=broken --term-new=fixed
git bisect fixed
git bisect broken v1.4.0

You picked a good commit that was not actually good. The most common cause of a bisect ending on a nonsense commit. Verify your starting good before you begin, not after eight steps.

The bug is intermittent. Bisect assumes a deterministic answer. A flaky race condition will send it somewhere arbitrary. Run the reproduction several times per step, or fix the flakiness first.

You land on a merge commit. Perfectly valid, but the diff will be the whole merged branch. Take the branch's own range and bisect that instead.

The offending commit is huge. Bisect tells you which commit, not which line. A 2,000-line refactor as the answer is progress but not the destination. This is where small, focused commits pay for themselves, which is one of the practical arguments for cleaning up a branch with interactive rebase before merging it.

#Driving it visually

Bisect is one of the operations where the terminal gives you the least context. At each step you get a hash and a subject line. You do not see where that commit sits, which branch it came from, or how much of the range is left in any form you can reason about.

That information is exactly what a commit graph shows.

Starting a bisect session from the graph, then working through the range.

Gitoryx has a dedicated bisect view. You start a session from the graph itself: right-click the commit you know is broken and mark it bad, right-click one you know was fine and mark it good. Both endpoints come from the history in front of you rather than from hashes you copied out of a terminal, which is where most botched bisects begin.

From there the view lists the commits in the range, shows which one is currently checked out, and gives you good, bad and skip as buttons. Each verdict stays visible against the commit it applied to, so you can see what you have already judged instead of scrolling back through your shell to remember whether you called that one good.

That is a smaller claim than "the GUI does the search for you", and it is the honest one. Bisect is the same algorithm either way. What changes is that picking the endpoints and keeping track of your answers stop being manual. We made the broader version of that argument in when a Git GUI beats the terminal.

#After you have the commit

Finding the commit is not the same as understanding the bug.

git show 8f3a91c2b4

Read the diff. If it is small and the cause is obvious, you are done. If the commit is large, git blame on the specific file narrows it further, and checking whether related files were touched in the same commit often explains why the change had an effect nobody expected.

Then decide what to ship. Reverting is usually the fastest safe move on a shared branch, because it creates a new commit rather than rewriting anything. The git revert tutorial covers the mechanics, including what to do when the commit you want to revert is a merge.

Bisect has a reputation as an advanced command, and it is not. It is three commands and a loop. The only reason it stays unused is that people reach for it after an hour of guessing rather than in the first five minutes, which is exactly backwards. If you know a good commit and a bad commit, you already have everything the algorithm needs.

Frequently Asked Questions

How many steps does git bisect take?

Roughly log2 of the number of commits in the range. 200 commits takes about 8 tests, 1,000 takes about 10, and 10,000 takes about 14. Doubling the range adds one step, which is why bisect stays practical on very long histories.

What is the difference between git bisect skip and git bisect good?

`good` and `bad` are answers about the bug. `skip` means you cannot answer, usually because the commit does not build or the feature does not exist yet at that point. Git routes around skipped commits and picks a nearby one instead. Never mark a broken build as good just to move on, because it will send the search into the wrong half.

Can git bisect run automatically?

Yes. `git bisect run <command>` uses the exit code of your command as the answer: 0 means good, 1 to 127 (except 125) means bad, and 125 means skip. Anything you can express as a script, a test suite, a curl call, or a grep, can drive the whole search unattended.

What happens if bisect lands on a merge commit?

Nothing special, it is just another commit to test. But a merge commit as the first bad commit means the regression came from the whole merged branch rather than one change, and the diff will be large. Rerun bisect with the merged branch's own commit range to narrow it down further.

All Blog Posts