The tag v1.2 built cleanly. Today’s HEAD does not. Between them sit 412 commits from five people, and the compiler error points at a header that none of them admits to touching.

Reading 412 diffs is hopeless. Checking them by halves is not. git bisect automates that halving: it checks out a commit midway between a known good one and a known bad one, asks for a verdict, and discards half the range each time. The official git-bisect documentation describes it as using “a binary search algorithm to find which commit in your project’s history introduced a bug.” With a script supplying the verdicts, the whole hunt runs unattended.

The manual loop

Start a session by naming the bad end and the good end. Git then checks out the middle of the range and waits.

git bisect start HEAD v1.2 --
# Git checks out a commit roughly halfway between the two
make
git bisect bad      # or: git bisect good

Each verdict moves one boundary. The docs show the kind of progress line Git prints along the way, for instance “Bisecting: 675 revisions left to test after this (roughly 10 steps)”. When the range collapses to a single commit, Git names it as the first bad commit. Then git bisect reset returns the working tree to where it started, per the docs “the commit that was checked out before git bisect start.”

Testing by hand makes sense when the check takes judgment, such as whether a screen looks right. For anything a program can decide, hand the decision to a program.

Reading a real session

The Pro Git book walks through a short one. With a bad HEAD and a good v1.0, Git estimated about 12 commits in between and checked out the middle one, printing “Bisecting: 6 revisions left to test after this”. The tester marked it good. The next lines read 3 revisions left, then 1. After one more verdict Git printed the SHA-1 of “is first bad commit” along with its author, date and the files it changed. Twelve candidates, four verdicts, which matches the arithmetic in the next section: 2 cubed is only 8, so 12 needs the fourth power, 16.

The same book adds the shortcut that makes this practical at scale: with a script that exits 0 when the project is good and non-zero when it is bad, git bisect run test-error.sh runs it on every checked-out commit until the first broken one turns up. It also says make or a test target works in place of a custom script.

Letting a script decide with git bisect run

The run subcommand takes any command and uses its exit status as the verdict. The docs spell out the contract: exit 0 means the current source is good, and an exit code from 1 to 127 other than 125 means it is bad. Code 125 means the revision cannot be tested, so Git skips it. Any other code aborts the whole bisect.

Exit codeMeaning to bisect
0Good (old)
1 to 127, except 125Bad (new)
125Cannot test; skip this commit
Anything else, including values above 127Abort the bisect

The abort rule has a trap. The docs point out that a program ending with exit(-1) leaves $? at 255, because the value is masked with & 0377, so a careless exit -1 kills the search instead of marking a commit bad.

For the broken-build scenario, the docs give a three-line recipe that needs no script at all, since make already exits non-zero on failure.

git bisect start HEAD v1.2 --      # HEAD is bad, v1.2 is good
git bisect run make                # "make" builds the app
git bisect reset                   # quit the bisect session

When the build is fine at some commits and fails for unrelated reasons at others, a wrapper keeps those noise commits from poisoning the result. This version is adapted from the docs’ own example. The first line skips commits that do not build; the second runs the real check.

#!/bin/sh
make || exit 125                   # this skips broken builds
./check_test_case.sh               # exit 0 if the test case passes, 1 otherwise

The same page shows how to scope a search when the culprit is known to be recent: git bisect start HEAD HEAD~10 -- with the comment “culprit is among the last 10”. Ten candidates need at most 4 tests. Save the script outside the repository, make it executable, and point Git at it with git bisect run ~/test.sh after the usual start line. The docs’ version lives in the home directory. Keeping it out of the tree avoids having a checkout replace the script mid-search, which is the likely reason, though the docs do not say so.

The binary-search math

Each test splits the remaining suspects in half, so the number of tests needed for n candidate commits is the smallest k with 2 to the power k at least n. That is the base-2 logarithm of n, rounded up. The table below is plain arithmetic, not output from any run.

Commits in rangeTests needed (ceil of log2 n)
83
1007
4129
1,00010
1,000,00020

The scenario’s 412 commits need at most 9 tests, since 2 to the 9th is 512. A million commits need 20. The cost grows with the logarithm, which is why bisecting a long history feels almost unfair compared with reading it. Git’s own “roughly 10 steps” estimate for 675 revisions fits the same arithmetic, because 2 to the 10th is 1,024.

Two assumptions sit under that math. The bug must be monotonic: once a commit is bad, every later commit stays bad. A flaky test or a bug that appears and vanishes breaks the search silently. And the verdict for each commit must be reliable, so a test that fails one run in ten needs repeating inside the script.

Command reference

CommandWhat it does
git bisect start [<bad> [<good>...]] [--] [<pathspec>...]Begins a session, optionally naming the bad and good ends up front
git bisect bad [<rev>]Marks a revision as containing the problem
git bisect good [<rev>...]Marks revisions as free of it
git bisect old / git bisect newAlternate terms for when the sought change is not a bug; cannot be mixed with good and bad in one session
`git bisect skip []`Excludes untestable commits
git bisect run <cmd> [<arg>...]Automates verdicts from exit codes
git bisect logShows what has been marked so far
git bisect reset [<commit>]Ends the session and restores the earlier checkout

Skipping carries a warning in the docs: if you skip a commit adjacent to the one you are looking for, Git cannot tell exactly which of those commits was the first bad one. The synopsis also lists --first-parent, --no-checkout and --reset-when-found. Read their entries in the docs before relying on them.

Where it fits

Opinion: bisecting pays off most on fast-moving history where nobody remembers what changed, such as the long gap between two release candidates. When a project like the one in x9m8’s guide to testing your code on the Python 3.15 release candidate starts failing, the question shifts from “does it fail?” to “which commit made it fail?”, and bisect answers the second one. A tree as busy as the one behind the Linux 7.0 explainer is a plausible candidate, since reading every change between two releases is not realistic.

Before starting, confirm the good commit really is good by building it yourself. A wrong boundary sends the search to a meaningless answer with no warning. Afterward, read the named commit’s diff, then try reverting it on a scratch branch to confirm it is the cause.

Sources

  1. Git documentation, git-bisect: https://git-scm.com/docs/git-bisect
  2. Pro Git, Debugging with Git (Binary Search): https://git-scm.com/book/en/v2/Git-Tools-Debugging-with-Git