Why Not Step Back One Commit at a Time
If the bug hides among 200 commits, testing one by one in order takes up to 200 runs in the worst case. git bisect uses a binary search, cutting to the middle commit each time and asking you good or bad, converging in about log2(200)≈8 steps.
It exploits Git's smart commit selection, jumping straight to the most information-gaining commit in your good/bad interval, and you only answer whether each middle commit is good or bad.
One precondition: the history must not suffer from flaky, environment-driven failures (random test failures, external service dependence), or the good/bad answers contradict and mislead the bisect.
git bisect start
git bisect bad # Current HEAD is bad
git bisect good v1.2.0 # A known good point in history
# Bisecting: 99 revisions left to test after this (roughly 7 steps)Automate the Good/Bad Call: Script Instead of Clicking
Running the app by hand and eyeballing the result at each middle commit gets tiring after twenty steps. A better pattern is packaging "is this commit bad" as an idempotent command — exit code 0 means good, non-zero means bad — then letting git bisect run run it automatically to completion.
A typical script checks out into a clean temp working copy, builds, and runs a minimal reproducer (pytest, or npm test -- a single case), then returns its exit code. The point is to make the test actually return non-zero on failure, with set -e or the framework's failure code.
The script must treat build failure and test failure both as bad — otherwise a one-off compile hiccup is mislabeled good and steers the bisect astray.
cat > bisect_test.sh <<"EOF"
#!/usr/bin/env bash
set -euo pipefail
# Build cleanly at the current commit and run the minimal repro case
rm -rf /tmp/bw && git worktree add /tmp/bw HEAD
(cd /tmp/bw && npm ci && npm run build)
(cd /tmp/bw && npm run test -- --grep "balance overflow")
EOF
chmod +x bisect_test.sh
git bisect start
git bisect bad HEAD
git bisect good v1.2.0
git bisect run ./bisect_test.shHandling the Common Failure: Contradictory Labels
If bisect suddenly reports "skipped or contradicts", the interval probably has environment-dependent flakiness, or the very first "good" you picked was not actually good.
Debug in this order: dump the process with git bisect log; double-check whether good and bad are truly good and bad — a historically "sort of works" tag poisons the result; if honestly flaky, switch to a more deterministic reproducer (pinned deps, fixed ports, mocked external HTTP).
A legitimate tactic is git bisect skip for commits you cannot judge, e.g. one that does not even compile and is unrelated. skip lets bisect advance to a judgeable commit, at the cost of a few more steps but with more trust.
# Review current progress and double-check the labels
git bisect log
# Skip commits that cannot be judged
git bisect skip
# Add another good point to narrow the range
git bisect good v1.1.0
git bisect reset # Always exit bisect mode when doneBetter Habits: Make Bisect Hit Every Time
Keep commits small and single-purpose so the culprit that bisect returns reads clearly and the offending line pops out at a glance. One commit packing twenty unrelated changes means even after bisect finds it you still hand-split it further.
Keep every commit compiling and green against your main tests; do not commit half-works. The whole bisect span should ideally build, or middle steps waste judging on build failure.
Write a fast, minimal reproducer for critical paths so git bisect run gives sub-second feedback; otherwise each step runs the full integration suite and eight steps become a whole day.
git log --oneline HEAD~10 # Small-commit history looks like this:
# 7ad3 Add cache header to image route
# 61f2 Fix round calculation in quote
# 98aa Rename billing module files
# ...
# Pair with a single case for a fast repro:
# package.json "scripts": { "test:bw": "vitest run tests/repro.spec.ts" }
git bisect run ./bisect_test.sh