Skip to main content
Git & GitHub · lesson 10 of 12
Git & GitHub intermediate Lesson 10 of 12

Finding the Bad Commit with git bisect

Use git bisect run with a real automated test script to binary-search five commits and pinpoint the exact one that silently broke a function.

By SkillByExample · Published

git bisect debugging

Five commits, one of them silently changed add(2, 2) from 4 to 5. Manually checking each one is fine at five — at fifty it isn’t. git bisect run automates the whole search with a real test script.

1) A commit history with a hidden regression

$ git log --oneline -5
0b276a2 Add mathlib.divide
f70cf08 Refactor add for readability
3974231 Add mathlib.subtract
f257dd3 Add mathlib.multiply
c6e952c Add mathlib.add

One of these five silently broke add. git log -p would find it by reading every diff — bisect finds it by testing, not reading.

2) Confirm the bug exists at HEAD, and didn’t exist at the start

$ node check_add.mjs; echo "exit: $?"
exit: 1

check_add.mjs calls add(2, 2) and exits 0 if it equals 4, 1 otherwise. At the current HEAD (0b276a2) it fails. The first commit (c6e952c) is the known-good baseline — bisect needs one confirmed-bad and one confirmed-good endpoint.

3) Let bisect run the test at every step

$ git bisect start HEAD c6e952c
$ git bisect run node check_add.mjs
Bisecting: 1 revision left to test after this (roughly 1 step)
[3974231...] Add mathlib.subtract
running  'node' 'check_add.mjs'
Bisecting: 0 revisions left to test after this (roughly 0 steps)
[f70cf08...] Refactor add for readability
running  'node' 'check_add.mjs'
f70cf0825d00f3ab95bb5b4c8a18f7a3b03c0595 is the first bad commit
commit f70cf0825d00f3ab95bb5b4c8a18f7a3b03c0595
Author: Dev <dev@example.com>

    Refactor add for readability

 mathlib.mjs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
bisect found first bad commit

git bisect start HEAD c6e952c marks the bad and good endpoints in one line (bad first, good second). Bisect checked out 3974231 (“subtract”), ran the script, saw exit 0 (good), narrowed the range, checked out f70cf08 (“refactor”), saw exit 1 (bad) — and with no commits left between, declared it the culprit. Two checkouts found the bug in a five-commit history; the same halving means a 1,000-commit history takes about 10.

4) Return to normal

$ git bisect reset
Previous HEAD position was f70cf08 Refactor add for readability
Switched to branch 'main'

Bisect leaves you in a detached HEAD at the guilty commit so you can inspect it; reset returns you to the branch you started from.

Practice

  1. Run git bisect start, then git bisect bad and git bisect good <hash> as three separate commands (instead of one combined start HEAD c6e952c) to see the step-by-step form the automated version above skips past.

  2. Write a test script for a different bug — pick any earlier lesson’s scratch repo, introduce a silent regression a few commits back, and confirm git bisect run finds it without you reading a single diff by eye.

  3. Run git bisect log after a completed bisect to see the exact sequence of good/bad judgments it made, and git bisect replay with that log to rerun the same search.

    Solution
    $ git bisect log > bisect.log
    $ git bisect reset
    $ git bisect replay bisect.log

    Replaying is useful for sharing a bisect session with a teammate without them re-running the test script themselves.

  4. Mark a commit git bisect skip instead of good/bad (simulating one that doesn’t build, so it can’t be tested) and observe bisect route around it.

  5. Compare the number of steps git bisect took against how many commits exist between good and bad — confirm it’s proportional to log2(n), not n.

Frequently Asked Questions

How does git bisect decide which commit to test next?
It binary searches: given a known-good and known-bad commit, it checks out the commit roughly halfway between them by history, and waits for you (or your script) to report good or bad. Each answer halves the remaining range, which is why 5 commits take at most 2-3 checks instead of 5.
What does git bisect run actually automate?
The checkout-test-report loop. Instead of you running a check by hand and typing git bisect good or git bisect bad each time, it runs the command you give it at every step and reads the exit code: 0 means good, any nonzero (1-127, except 125) means bad.
Why does the test script's exit code matter instead of its printed output?
git bisect run reads only the process exit code to decide good/bad — nothing about stdout or stderr. This is exactly how CI test runners and Unix conventions work generally: 0 for success, nonzero for failure.
What happens to my working directory after git bisect run finishes?
You're left checked out at the first bad commit, in a detached HEAD state, so you can inspect it immediately. git bisect reset returns you to the branch and commit you started from.