git bisect: Systematically Finding Faulty Commits
AI generated
git
HEAD
Git · Debugging · Regression · Developer Basics
git bisect: Systematically Finding Faulty Commits
Binary search instead of endless clicking

When a bug has crept in somewhere among hundreds of commits, manually clicking through history burns valuable time. git bisect finds the guilty commit through binary search in just a handful of steps, whether tested manually or fully automated with a test script, turning hours of hunting into a matter of minutes.

13 min. read Binary Search · bisect run · Regression Git 2.x · CLI · CI/CD

1. Why linear click-through fails at finding regressions

A bug shows up that wasn't there two weeks ago. The history between the last known good state and today spans 130 commits from three different developers. The naive approach of checking out and testing each commit individually means, worst case, 130 test runs, and 65 on average. At five minutes per test run, that's over five hours of pure waiting just to find the one commit that introduced the regression.

git bisect solves exactly this problem by turning the search into a binary search over commit history. Instead of testing sequentially from start to end, Git jumps to the middle of the still-unknown range at every step and only needs you to report whether that one commit is faulty ("bad") or correct ("good"). What could have been 130 test runs turns into roughly seven on average, regardless of whether the test happens manually in a browser or an automated script reports the exit code.

2. The binary search behind git bisect: log2(n) instead of n

The mathematical core of git bisect is simple: with n commits between a known good and a known bad state, a binary search needs on average log2(n) steps to find the transition point. For 100 commits, log2(100) is roughly 6.64, so about seven test steps. For 1000 commits it's about ten steps, and for a million commits just twenty. This logarithmic scaling is why bisect stays practical even in massive repositories with years of history.

Internally, Git maintains two reference sets for this: all commits marked "good" and all commits marked "bad". After each mark, Git computes the commits that lie topologically between the most recent good boundary and the oldest bad boundary, and automatically checks out the middle commit of that set. This computation correctly handles branched history too, as long as the commit graph topology remains clearly distinguishable between good and bad. If the graph is more complex due to multiple independent merges, the number of steps can deviate slightly from the pure log2(n) formula, but stays in the same order of magnitude.

3. The manual bisect workflow step by step

The workflow always begins with git bisect start, which initializes a bisect session and remembers the current branch so you can return to it at the end. Next you mark the known faulty state, usually the current HEAD, with git bisect bad, and a known working state in the past with git bisect good <sha>, for example the last tag of a release known to predate the bug.

Once both boundaries are set, Git automatically checks out the middle commit and reports roughly how many revisions remain and how many steps are likely to follow. Now you test this state, whether by trying it manually in a browser, running a test suite, or reproducing the bug by hand, and report the result back to Git: git bisect good if the bug does not occur here, or git bisect bad if it does. Git then checks out the next candidate, and the cycle repeats until Git reports which commit was the first to introduce the fault.


# Start a bisect session, current branch is remembered
git bisect start

# Mark the current HEAD as faulty
git bisect bad

# Mark a known good commit as a reference point
git bisect good v2.4.1

# Git automatically checks out the middle commit:
# Bisecting: 63 revisions left to test after this (roughly 6 steps)
# [a1b2c3d4] Merge branch 'feature/checkout-refactor'

# Report the result after testing
git bisect good
# or
git bisect bad

4. A complete bisect session in the terminal

To make the process tangible, a realistic example helps: between tag v3.1.0 and the current main there are 118 commits, and somewhere in between a bug crept in where the cart discount is no longer calculated correctly. The terminal output below shows a full session from start to identifying the guilty commit, including the progress messages Git prints after each step.

What stands out is how quickly the remaining search space halves at every step: from 118 to about 59, then 29, 14, 7, 3, and finally 1 commit. After seven iterations, Git identifies the exact commit that introduced the regression, complete with full commit message and author, which considerably speeds up the subsequent root-cause analysis.


$ git bisect start
$ git bisect bad HEAD
$ git bisect good v3.1.0
Bisecting: 58 revisions left to test after this (roughly 6 steps)
[7f3e9a1] Refactor cart totals calculation service

$ npm test -- cart-discount.spec.js
# tests fail
$ git bisect bad
Bisecting: 29 revisions left to test after this (roughly 5 steps)
[c92d410] Add tiered discount support

$ npm test -- cart-discount.spec.js
# tests pass
$ git bisect good
Bisecting: 14 revisions left to test after this (roughly 4 steps)
[e04a882] Update discount rounding logic

$ npm test -- cart-discount.spec.js
# tests fail
$ git bisect bad
Bisecting: 6 revisions left to test after this (roughly 3 steps)
[b551f3c] Introduce percentage-based coupon codes

$ npm test -- cart-discount.spec.js
# tests pass
$ git bisect good
Bisecting: 2 revisions left to test after this (roughly 1 step)
[9a03dd7] Cache cart totals per session

$ npm test -- cart-discount.spec.js
# tests fail
$ git bisect bad
Bisecting: 0 revisions left to test after this (roughly 0 steps)
[4c81eef] Round discount before cache write instead of after

$ npm test -- cart-discount.spec.js
# tests fail
$ git bisect bad
4c81eef1a9b8c3d5e7f0a2b4c6d8e0f1a3b5c7d9 is the first bad commit
commit 4c81eef1a9b8c3d5e7f0a2b4c6d8e0f1a3b5c7d9
Author: M. Berger <m.berger@example.com>
Date:   Tue Jun 30 14:12:03 2026 +0200

    Round discount before cache write instead of after

$ git bisect reset

5. Automating with git bisect run and exit codes

Testing manually at every step is unnecessary effort for reproducible bugs. git bisect run <script> automates the entire session: Git checks out each candidate commit, runs the given script, and interprets its exit code to decide on its own whether the commit is good or bad. The convention follows the usual Unix pattern with one important extension: exit code 0 means good, any exit code between 1 and 127 except 125 means bad, and exit code 125 carries a special meaning, namely "skip this commit because it cannot be meaningfully tested".

This 125 convention matters because a bisect session inevitably runs through commits that are broken for reasons unrelated to the regression being hunted, for example because build dependencies changed in the meantime or a migration is missing. An exit code of 125 tells Git to exclude that commit from evaluation and test a neighboring commit instead, without corrupting the binary search. Exit codes above 127 are treated by bisect run as a fatal error and abort the automation immediately, since they usually indicate a problem with the test script itself rather than with the tested code.


# Start the bisect session with boundaries
git bisect start
git bisect bad HEAD
git bisect good v3.1.0

# Delegate the entire process to a script
git bisect run ./run-regression-test.sh

# Git tests every candidate automatically and reports at the end:
# 4c81eef1a9b8c3d5e7f0a2b4c6d8e0f1a3b5c7d9 is the first bad commit

git bisect reset

6. Writing a robust test script for bisect run

A good bisect test script must reliably distinguish three things: whether the current commit even builds, whether it reproduces the bug, or whether it simply isn't testable for unrelated reasons. Without that distinction, the script incorrectly reports "bad" for failed builds even though the actual failure has nothing to do with the regression being hunted, and the binary search converges on the wrong commit.

The template below shows the pattern: first it checks whether the build even succeeds, with an early return via exit code 125 on failure. Then comes the actual test, which decides through its regular exit code. It's also important that the script runs deterministically and without side effects on the repository state, since it may be executed dozens of times in a row.


#!/bin/bash
# run-regression-test.sh - test script for git bisect run
set -euo pipefail

# Install dependencies for this commit, failure means not testable
if ! npm ci --silent > /tmp/bisect-install.log 2>&1; then
  echo "Build/install failed, skipping this commit"
  exit 125
fi

# Build the application, also not testable on failure
if ! npm run build --silent > /tmp/bisect-build.log 2>&1; then
  echo "Build failed, skipping this commit"
  exit 125
fi

# Run the actual regression test
npm test -- cart-discount.spec.js --silent
# The test's exit code is passed through directly:
# 0 = good, 1 = bad (implicit via set -e)

7. Flaky tests and using git bisect skip correctly

Flaky tests that pass sometimes and fail other times without any real code change are poison for git bisect, because they violate the core assumption that the same commit always produces the same result on repeated testing. A good commit wrongly marked as "bad" can send the entire binary search toward a completely wrong region of history. The first line of defense is therefore to run the test multiple times when uncertain before committing to a decision, for example through a retry loop in the test script that only returns a final exit code after three consistent results.

When a commit simply cannot be tested reliably, for example because a function the test depends on doesn't exist yet at that intermediate state, git bisect skip is the right tool, called manually instead of good or bad. Git then picks a neighboring commit as the next candidate and still tries to narrow the bisect boundaries. If too many consecutive commits get skipped, Git can no longer uniquely isolate the guilty commit and instead reports a range of possible commits, which increases manual follow-up work but is still far better than linear testing.


# Manual: commit cannot be meaningfully tested, e.g. build broken
# for unrelated reasons
git bisect skip

# In the test script: exit code 125 has the same effect as skip
if grep -q "TODO: not implemented yet" src/discount/engine.js; then
  exit 125
fi

# Mark multiple commits as not testable at once
git bisect skip v3.2.0..v3.2.5

8. Merge commits and --first-parent while bisecting

In repositories with feature branches and regular merges, the default bisect strategy can become problematic, because Git may pick commits inside a feature branch as standalone candidates when computing the middle commit, even though those never ran in isolation in the context of the main branch. This occasionally leads to test results that never actually occurred in the overall history of main, for example because an intermediate state of the feature branch happened to be incomplete but still runnable in isolation.

The option git bisect start --first-parent solves this by following only the first parent at merge commits, meaning the linear history of the target branch, and skipping commits inside merged feature branches entirely. The result is coarser, potentially identifying the merge commit itself rather than a single commit within it, but it stays consistent with the actually deployable state of main at every point in time. In workflows using squash merges, this problem is already mitigated, since only a single commit per feature lands on the target branch anyway.


# Bisect only along the linear history of the target branch,
# feature branch commits inside merges are skipped
git bisect start --first-parent
git bisect bad HEAD
git bisect good v3.1.0
git bisect run ./run-regression-test.sh

# The result can be a merge commit instead of a single commit:
# a7c391f is the first bad commit
# Merge: 4d2a1b0 9f8e7c6
#     Merge branch 'feature/tiered-discounts' into main

9. Manual debugging vs. git bisect compared

The table below contrasts common but inefficient debugging approaches with the recommended bisect-based alternatives. The difference almost always comes down to the number of test runs required and the reliability of the result.

Situation Inefficient approach Recommended bisect approach
Finding a regression Manually scanning the commit log and guessing git bisect with good/bad boundaries
Testing 100+ commits Testing commit by commit, linearly Binary search, roughly 7 steps instead of 100+
Repeated test runs Manually retesting after every checkout git bisect run with a test script
Feature branch merges Ignoring merge commits, jumping into branches blindly git bisect --first-parent
Flaky test failure Treating any failure as "bad" outright git bisect skip / exit code 125

In every row, the difference has the same structure: the inefficient approach treats commit history as linear or ignores its topology, while git bisect deliberately exploits the structure of the commit graph to drastically cut the number of required test runs while still delivering a reproducible, documented result.

Mironsoft

Magento and Hyvä development with clean Git workflows

Need to find regressions in your Magento code fast?

We set up automated test scripts and CI pipelines for your Magento and Hyvä project so regressions can be isolated with git bisect run in minutes instead of hours.

Git workflow audit

Branching, merge, and bisect-friendly commit history

Test automation

Reproducible test scripts for bisect run and CI/CD

Regression support

Targeted root-cause analysis for acute production bugs

10. Summary

git bisect solves a problem every growing repository eventually runs into: finding a regression whose origin lies somewhere among dozens or hundreds of commits. Instead of testing linearly, bisect uses a binary search and cuts the required test runs down to about seven for 100 commits, and about ten for 1000 commits. The manual workflow of git bisect start, git bisect bad, and git bisect good <sha> is enough for occasional debugging, while git bisect run with a test script and the exit code conventions, 0 for good, nonzero and not 125 for bad, and 125 for skip, runs the entire session without manual intervention.

Two pitfalls deserve special attention: flaky tests can steer the binary search in the wrong direction, which is why git bisect skip and multiply-confirmed test runs in the script matter. And merge-heavy histories benefit from --first-parent to stay consistent with the actual state of the target branch. Every session ends with git bisect reset, which restores the working directory state from before the session started and clears the internal bisect reference list.

git bisect - The Essentials at a Glance

Binary search, not linear

log2(n) instead of n test runs: for 100 commits, about seven steps suffice instead of testing up to 100 individually.

Manual workflow

git bisect start, bisect bad, bisect good <sha>, then iteratively mark good/bad until you get a result.

Automation

git bisect run ./test.sh with exit code 0 (good), nonzero/not 125 (bad), 125 (skip).

Edge cases

Handle flaky tests with bisect skip, merges with --first-parent, end the session with bisect reset.

11. FAQ: git bisect

1What exactly does git bisect do?
Finds the commit that introduced a regression via binary search. Halves the search space at every step between a good and a bad commit instead of testing each one individually.
2How do I start a bisect session?
git bisect start, then git bisect bad for the faulty state and git bisect good sha for a known working older commit. Git then automatically checks out the middle commit.
3How many steps does git bisect need for 100 commits?
About log2(100), so roughly seven test steps instead of up to 100 individual tests. For 1000 commits it is about ten steps.
4What do the exit codes mean in git bisect run?
0 means good, any code between 1 and 127 except 125 means bad, 125 means skip. Codes above 127 abort bisect run as a fatal error.
5How does git bisect run work?
Automatically runs a script at every candidate commit and interprets its exit code as good, bad, or skip, without any manual intervention.
6How do I handle flaky tests during a bisect?
Run the test multiple times in the script and only decide on a consistent result, or use git bisect skip respectively exit code 125 when uncertain.
7What does git bisect skip do?
Marks the current commit as not meaningfully testable, for example due to a broken build, and Git picks a neighboring commit as the next candidate.
8How do merge commits affect a bisect session?
Without restriction, Git can also pick commits inside merged feature branches as candidates. --first-parent follows only the linear history of the target branch and avoids that.
9How do I end a bisect session?
With git bisect reset. Restores the working directory state from before the session started and clears the internal list of markers.
10Can I save a bisect session and resume it later?
Yes, git bisect log writes the progress to a file, git bisect replay loads it back in later to resume the session at the same point.