Scenario
Section titled “Scenario”A test that passed last week fails today. Twelve commits have landed since, most of them documentation. Someone suggests reading each diff. Someone else suggests reverting everything.
There is a third option: tell Git which commit was good, which is bad, and let it run the test at the midpoint until it finds the culprit. Twelve commits is at most four steps.
Objective
Section titled “Objective”Use git bisect run with a script to find the first bad commit automatically, understand what the
bisect log records, and fix the regression with a targeted revert.
Prerequisites
Section titled “Prerequisites”- Git installed
- A test that exits 0 on pass and non-zero on fail — the whole technique depends on that
Starting state
Section titled “Starting state”A tiny calculator, a test, and a history where a “performance” commit quietly broke negative numbers:
mkdir -p /tmp/lab-bisect && cd /tmp/lab-bisectgit init -q -b main .git config user.email "lab@example.com"git config user.name "Lab User"
cat > calc.sh <<'EOF'#!/bin/sh# prints the sum of its argumentstotal=0; for n in "$@"; do total=$((total + n)); done; echo "$total"EOFcat > test.sh <<'EOF'#!/bin/sh[ "$(./calc.sh 2 3)" = "5" ] && [ "$(./calc.sh 10 -4)" = "6" ]EOFchmod +x calc.sh test.shgit add . && git commit -q -m "Add calc and test"
for i in 1 2 3 4 5 6; do echo "# note $i" >> NOTES.md; git add . && git commit -q -m "Docs: note $i"; donesed -i 's/total=\$((total + n))/total=$((total + ${n#-}))/' calc.shgit commit -qam "Perf: simplify addition loop"for i in 7 8 9 10; do echo "# note $i" >> NOTES.md; git add . && git commit -q -m "Docs: note $i"; done
./test.sh && echo PASS || echo FAILFAIL. Twelve commits, one of them guilty.
-
Confirm the endpoints. The test fails at
HEAD. Check it passes at the first commit:Terminal window FIRST=$(git rev-list --max-parents=0 HEAD)git checkout -q "$FIRST" && ./test.sh && echo PASS; git checkout -q main -
Start the bisection with a bad and a good commit:
Terminal window git bisect start HEAD "$FIRST"Git checks out the midpoint. Read what it printed.
-
Let Git drive. Instead of testing by hand and typing
good/badeach time, hand it the test script:Terminal window git bisect run ./test.shRead the output. How many steps did it take?
-
Read the log. Everything bisect decided is recorded:
Terminal window git bisect log | tail -3 -
Return to
main— bisect leaves you detached:Terminal window git bisect reset -
Fix it with a revert of exactly the bad commit, and prove the fix:
Terminal window BAD=$(git log --format=%h --grep="Perf:")git revert --no-edit "$BAD"./test.sh && echo PASS
Validation
Section titled “Validation”Step 3 names Perf: simplify addition loop as the first bad commit in three steps. After step 6,
./test.sh prints PASS and git log --oneline -1 shows the revert.
Step 2. Bisect needs one known-good and one known-bad point. “The first commit” is a safe good point when you know nothing else; a tag or release is better when you have one.
Step 3. bisect run treats exit 0 as good, 1–127 as bad (except 125, which means “cannot
test this one, skip it”). Your test script’s exit code is the whole interface.
Step 4. The log is replayable: git bisect replay <file> reruns a saved session, which is
how you share a bisection with a colleague.
Solution
Section titled “Solution”Step 2:
Bisecting: 5 revisions left to test after this (roughly 3 steps)[fd408cd203f119fd92f75d2dffe287c01791dc4d] Docs: note 5Step 3:
Bisecting: 2 revisions left to test after this (roughly 2 steps)Bisecting: 0 revisions left to test after this (roughly 1 step)[b775e63c86ac0a7375c6e5341d6b9ad19e1adabc] Perf: simplify addition loopBisecting: 0 revisions left to test after this (roughly 0 steps)b775e63c86ac0a7375c6e5341d6b9ad19e1adabc is the first bad commit Perf: simplify addition loopbisect found first bad commitStep 4:
# good: [8e7bf880e3b86c2b1c419e298e30c4c76900dcc5] Docs: note 6git bisect good 8e7bf880e3b86c2b1c419e298e30c4c76900dcc5# first bad commit: [b775e63c86ac0a7375c6e5341d6b9ad19e1adabc] Perf: simplify addition loopStep 6:
[main d9804de] Revert "Perf: simplify addition loop"PASSYour SHAs will differ.
Explanation
Section titled “Explanation”Bisect is binary search over commits. With twelve commits between good and bad, at most four tests are needed; with a thousand, ten. Reading diffs is linear; this is logarithmic. That is the whole argument for it.
run removes the human from the loop. Manual bisection is error-prone — one wrong good
sends the search the wrong way. A script cannot mistype. Any command that exits non-zero on the
bug works: a unit test, a grep for an error in a build log, a script that starts the server
and curls it.
The bad commit is the fix’s target, not the whole history. git revert of one commit
removes exactly the regression and keeps the ten documentation commits. Reverting “everything
since last week” would have thrown away good work to remove one bad line.
The regression here was plausible. ${n#-} strips a leading minus — someone “simplifying”
a loop removed negative-number handling and the tests did not run in CI. The bisect finds it in
three steps regardless of how subtle the diff is.
Troubleshooting
Section titled “Troubleshooting”Bisect reports the wrong commit. The endpoints were wrong — “good” was actually already broken. Verify both ends by hand first, as in step 1.
bisect run stops with “bisect run failed”. The script exited with a code above 127, or
could not run at a commit (missing file). Make the script robust at every commit in the range,
or return 125 for the ones it cannot judge.
You are left on a detached HEAD. That is bisect’s working state. git bisect reset
returns you to where you started.
Clean up
Section titled “Clean up”cd /tmp && rm -rf lab-bisectRelated lessons
Section titled “Related lessons”Next lab
Section titled “Next lab”Recover from a force push — a colleague’s commit vanished from the remote; get it back from a reflog that is not yours.