Git bisect: find a regression with a trustworthy test
Find the first bad Git revision with a repeatable predicate. Run a disposable bisect exercise and handle flaky tests, skipped commits and merge boundaries.
TL;DR: Git bisect narrows a known-good to known-bad history using a test that consistently classifies each checked revision. Stabilize that test first; a fast search over unreliable labels produces an unreliable culprit.
Define the regression as an observable predicate
Start with one behavior and two revisions. For example, an invoice calculation returned the agreed value before a deployment and returns a different value afterward. Reproduce that same input against both revisions using comparable dependencies and configuration. A production graph that changed near a commit is evidence for investigation, not yet a bisect predicate.
Bisect repeatedly selects revisions between the bounds and records whether the behavior is good or bad. The Git bisect manual specifies the command and automated exit-status contract. For git bisect run, zero means good; statuses 1 through 127 mean bad except 125, which means the revision cannot be tested. Other exit statuses abort the search.
Missing commands can return 126 or 127, which Git treats as bad. Validate the test environment before trusting automation.
Run an isolated regression exercise
This Bash example creates its own temporary repository. It leaves your current project untouched. Python checks one deliberately simple calculation, and revision five introduces a changed multiplier.
lab=$(mktemp -d)
git init -q "$lab/repo"
cd "$lab/repo"
git config user.name 'Example Engineer'
git config user.email '[email protected]'
cat > ../check.py <<'CHECK'
import runpy
quote = runpy.run_path("price.py")["quote"]
raise SystemExit(0 if quote(4) == 8 else 1)
CHECK
for revision in 1 2 3 4 5 6 7 8; do
factor=2
if [ "$revision" -ge 5 ]; then factor=3; fi
printf 'def quote(units):\n return units * %s\n' "$factor" > price.py
git add price.py
git commit -q --allow-empty -m "revision $revision"
done
good=$(git rev-list --max-parents=0 HEAD)
git bisect start HEAD "$good"
git bisect run python3 ../check.py
git bisect reset
The expected first bad commit is the one named revision 5. The checker lives outside the repository so checkout changes do not replace it. The example uses empty commits to keep the history easy to inspect; real histories contain other changes that the chosen behavior may not exercise.
Do not infer that every line in the identified commit caused the failure. Bisect establishes a behavioral boundary under the test's assumptions. Inspect the diff and validate the proposed fix with a focused regression test.
Keep infrastructure failure separate from product failure
| Observation | Suitable handling | Why |
|---|---|---|
| Expected calculation is wrong | Mark bad | This is the defined regression |
| Dependency mirror is unavailable | Repair the environment or skip | Network failure does not classify the code |
| Revision predates the test interface | Adapt a compatible checker or skip | Test cannot evaluate the chosen behavior |
| Same revision alternates outcomes | Investigate nondeterminism | Single-run labels are unreliable |
| Several adjacent revisions cannot build | Report an ambiguous candidate range | Skips may prevent an exact boundary |
A wrapper can map a known build incompatibility to 125, but do not map every test exception to skip. That would hide real failures. Log why a revision was skipped and retain git bisect log so another engineer can reproduce the classification history.
For a flaky regression, define a sampling procedure and state its limits. Repeating a test several times may reduce uncertainty, but a finite run cannot prove that a low-probability failure is absent. A deterministic reproducer is preferable when you can isolate one.
Choose the history that matches the question
A full-history bisect can identify a change developed on a side branch. A first-parent search can instead identify the integration point where the mainline began exhibiting the failure. Those answer different questions. The Git book's debugging chapter illustrates how history-based diagnosis complements inspecting individual changes.
State whether you are finding the originating change or the release integration boundary. Preserve exact dependency versions and build inputs where possible; otherwise rebuilding an old commit with today's dependencies may reproduce a failure that was never present in the original artifact.
Self-check: bisect identifies a commit that changes only a lock file. Can you dismiss it because no application code changed?
No. A dependency change can alter runtime behavior. Reproduce with the selected dependency versions, inspect the lock-file difference and test a minimal correction. Connect the result to backport discipline if an older release needs the fix.