
Find Software Regressions Faster with Git Bisect
A regression often arrives long after the code that caused it. The current release is broken, an older release worked, and dozens or hundreds of commits sit between them. Reading every diff is slow and biased toward the files you already suspect. git bisect turns this search into a binary search and can identify the first bad commit in a small number of tests.
This tutorial covers a safe manual workflow, full automation with git bisect run, the exit codes your test script must return, and practical techniques for repositories where historical commits do not always build cleanly.
How binary search reduces the investigation
Git needs two boundaries: a known bad commit that has the problem and a known good commit from before the problem appeared. It checks out a commit near the middle. You test that version and mark it good or bad. Each result removes roughly half of the remaining candidates.
If 256 commits separate the endpoints, a linear search may require 256 tests. A clean binary search needs roughly eight decisions. The exact number varies with the commit graph, merges, skipped commits, and path restrictions, but the reduction is usually dramatic.
Prepare before starting a bisect
First, make the failure reproducible. Define an objective signal: a failing unit test, a specific HTTP response, a broken build, or a benchmark exceeding a threshold. A vague judgment such as “the page feels slower” creates inconsistent labels and can identify the wrong commit.
Commit or stash your local work because bisect repeatedly checks out historical revisions:
git status
git stash push -u -m "before regression bisect"
git fetch --all --tags
Write down the failing command and verify it fails on the current commit. Then check the proposed good revision and confirm the same command passes there. These boundary checks are essential; incorrect endpoints invalidate every later result.
Run a manual git bisect
Start with the bad revision first and the good revision second:
git bisect start HEAD v2.8.0
This is equivalent to starting the session and marking each endpoint separately:
git bisect start
git bisect bad HEAD
git bisect good v2.8.0
Git checks out a candidate commit. Install dependencies if needed, run the exact reproduction, and label the result:
npm ci
npm test -- --runInBand tests/invoice-total.test.js
Use one of these after observing the result:
git bisect good
git bisect bad
Continue until Git reports the first bad commit. Inspect it with git show, but remember that the first bad commit is evidence about when the behavior changed, not automatic proof that every line in that commit is conceptually wrong.
git show --stat bisect/bad
git show bisect/bad
When the investigation is complete, always return to your original branch:
git bisect reset
Automate the search with git bisect run
If a command can determine good versus bad, Git can run the whole search automatically. For a focused test that returns zero when it passes and nonzero when it fails:
git bisect start HEAD v2.8.0
git bisect run npm test -- --runInBand tests/invoice-total.test.js
git bisect reset
For more control, use a script. This example installs locked dependencies, skips commits that cannot be tested, and then runs the regression test:
#!/usr/bin/env bash
set -u
npm ci --ignore-scripts || exit 125
npm run build || exit 125
npm test -- --runInBand tests/invoice-total.test.js
exit $?
Make it executable and pass it to Git:
chmod +x ./scripts/bisect-invoice.sh
git bisect start HEAD v2.8.0
git bisect run ./scripts/bisect-invoice.sh
Understand the exit-code contract
For git bisect run, exit code 0 means the current commit is good. Exit codes 1 through 127, except 125, mean bad. Exit code 125 tells Git that the commit cannot be tested and should be skipped. This distinction prevents a dependency or build incompatibility from being mislabeled as the target regression.
Keep the script's output concise but useful. Log which preparation step failed and preserve test artifacts outside the working tree when possible. Git will move between commits, so generated files inside the repository can interfere with checkout.
Handle old commits that no longer build
Historical revisions may depend on an old runtime, removed package registry, or database schema. Prefer a reproducible environment pinned by the repository: a version file, lockfile, or container image. If the candidate still cannot be evaluated, mark it skipped rather than good or bad:
git bisect skip
Too many adjacent skipped commits can prevent Git from naming one exact culprit. In that case, Git reports a set of possible commits. Narrow the range by repairing the test environment or choose a different known-good boundary closer to the regression.
Bisect a performance regression
The same workflow can find a performance change. Use neutral terms so the labels remain clear:
git bisect start --term-old fast --term-new slow
git bisect slow HEAD
git bisect fast v2.8.0
Your benchmark script must produce a stable decision. Warm caches, run multiple iterations, use a median, and leave enough margin around the threshold to reduce noisy classifications:
#!/usr/bin/env bash
set -euo pipefail
duration_ms=$(node ./benchmarks/render-dashboard.mjs)
threshold_ms=450
if [ "$duration_ms" -le "$threshold_ms" ]; then
exit 0
fi
exit 1
When using custom terms, confirm how your Git version maps the script result before launching a long unattended run. Test the script manually at both endpoints first.
Use paths and first-parent history carefully
You can restrict candidates to commits that changed a relevant path:
git bisect start HEAD v2.8.0 -- src/billing tests/billing
This can save time, but it also encodes an assumption about where the bug originated. A dependency, shared utility, build setting, or database migration outside the path may be responsible. Start unrestricted unless you have strong evidence.
On merge-heavy repositories, --first-parent can focus the search on the mainline history. It is useful when you want to identify the merge that introduced a regression, but a second bisect inside the merged branch may still be needed to find the exact commit.
Preserve and review the investigation
Use git bisect log to record every decision:
git bisect log > /tmp/invoice-bisect.log
If a revision was labeled incorrectly, reset, edit the log, and replay the corrected decisions with git bisect replay. This is safer than improvising around a mistaken result in the middle of the search.
Production debugging checklist
- Create a deterministic reproduction before searching history.
- Verify the test fails at the bad endpoint and passes at the good endpoint.
- Commit or stash all local changes.
- Use a locked, reproducible dependency installation.
- Return 125 for untestable commits, not for the target failure.
- Keep benchmark thresholds far enough from normal noise.
- Save the bisect log for review and replay.
- Inspect the first bad commit and its surrounding assumptions.
- Run
git bisect resetwhen finished. - Add the reproduction as a permanent regression test.
Related reading and official sources
After converting the reproduction into a permanent test, automate it in CI. The site's Node.js CI pipeline with GitHub Actions and PostgreSQL shows a practical service-container workflow.
This tutorial follows the official git-bisect reference, the Pro Git debugging chapter, and Git's detailed bisect design and workflow discussion.