Git Bisect - Hunt Regressions Fast
The Problem Bisect Solves
Ayesha reports that CSV export is broken. It definitely worked in the v1.4.0 release two weeks ago — and 180 commits have landed since. Reading 180 diffs is a day of work. Checking out commits one by one is worse.
git bisect finds the guilty commit by binary search: it checks out the midpoint of the suspect range, you tell it whether the bug exists there, and it halves the range again. 180 commits take about 8 tests. A thousand commits take 10.
Manual Bisect Walkthrough
Three commands drive the whole session:
git bisect start
git bisect bad # current HEAD has the bug
git bisect good v1.4.0 # this tag was fine
Git immediately checks out the middle commit and reports its progress:
Bisecting: 90 revisions left to test after this (roughly 7 steps)
Now test — run the app, run one test file, whatever reproduces the bug — and report the verdict:
git bisect bad # bug present at this commit
# or
git bisect good # bug absent at this commit
Repeat. After a handful of rounds Git announces the culprit:
b7d2e85 is the first bad commit
commit b7d2e85
Author: Rizwan Ashiq <mrizwanashiq@outlook.com>
Date: Mon Jun 22 14:03:11 2026
refactor(export): stream rows instead of buffering
Read that one diff, find the bug, and end the session:
git bisect reset
reset is mandatory — bisect leaves you in a detached HEAD state on some historical commit until you run it.
Sometimes the midpoint commit doesn't even build for unrelated reasons. Skip it and Git picks a neighbor instead:
git bisect skip
Automated Bisect: git bisect run
If the bug is reproducible by a command, you don't need to answer manually at all. Any script that exits 0 for good and non-zero for bad can drive the entire search:
git bisect start HEAD v1.4.0
git bisect run npm test -- --testPathPattern export
git bisect reset
Git checks out commits, runs the command, interprets the exit codes, and prints the first bad commit — completely unattended. This is my favorite party trick in Git: a regression hunt across two weeks of history reduced to two commands and a coffee break.
Exit code semantics for the script:
| Exit code | Meaning |
|---|---|
0 | Good — bug absent |
1–124, 126–127 | Bad — bug present |
125 | Untestable — same as git bisect skip |
A custom script works too:
#!/bin/sh
npm ci --silent || exit 125
node scripts/export-check.js
git bisect run ./bisect-check.sh
The exit 125 line marks commits where dependencies won't even install as "skip" instead of falsely blaming them.
Making Your History Bisect-Friendly
Bisect is only as good as the history it searches. Two habits pay off:
- Every commit should build and pass tests. A history full of
wipcommits that don't compile forces constant skipping and can make the result ambiguous. This is a strong argument for squashing messy branches before merge — see Cherry-pick & Interactive Rebase. - Small, focused commits. Bisect points at one commit — if that commit changes 40 files across three features, you've only narrowed the search, not finished it.
Quick Reference
git bisect start # begin a session
git bisect bad [commit] # mark a commit as broken (default HEAD)
git bisect good <commit> # mark a known-good commit
git bisect skip # this commit is untestable
git bisect run <cmd> # automate with a command's exit code
git bisect log # show verdicts so far
git bisect reset # end session, return to start