Skip to content

Bisection: binary search on commits, input, config and time

Write the check, then let halving find where it stopped passing.

Predict first

Before you read: predict

A payments ledger's daily settlement total was right at tag v7.0 and is a few cents high on main, 2,000 commits later. check-total.mjs, kept outside the repository, settles a fixed day of payments and exits 0 when the total matches the bank's file and 1 when it does not. The team lead suspects this week's merges and starts reading the newest diffs. You run git bisect start main v7.0, then git bisect run node ../check-total.mjs. The cause turns out to be change 1,300.

How many times does git bisect run run the check?

Pick one

The 30-second version

When something used to work, halve the history instead of reading it. Give git bisect a good commit, a bad one and a check; it tests the middle and keeps the half holding the change, so a thousand commits cost about ten checks. The same halving finds a bad input, flag or minute. It needs only a check that never lies.

Write the check, then let halving find where it stopped passing.

How it works

Step 1 of 11

Two ends. Mark a bad commit and an older good one. Bisect checks out the middle commit; your verdict keeps the half holding the change.
In the kitchen
  • the first bar in tune, the last bar flat, one recording between → a good commit, a bad commit and the history between them (step 1)
  • she plays the middle bar against the piano's note and marks it in tune or flat → checking out the middle commit and running the check (steps 1, 4)

Analogy: A choir director finding where a rehearsal went flat

She recorded the whole rehearsal of a 64-bar anthem: in tune at the first bar, flat by the last.

In the rehearsal In the repository
the first bar in tune, the last bar flat, one recording between a good commit, a bad commit and the history between them (step 1)
she plays the middle bar against the piano's note and marks it in tune or flat checking out the middle commit and running the check (steps 1, 4)
each listen halves the bars still in doubt the cost is the number of halvings (step 3)
the bar she ends on is flat, right after one in tune the first bad commit (step 2)
a bar of rest gives her nothing to judge, so she plays its neighbour a skipped commit, exit 125 (steps 4, 5)
the piano's note does not drift with the singers a check kept outside the history it judges (steps 6, 7)
she needs no idea why the altos sagged no theory, only the check (step 6)

Breaks down when: a recording's bars all exist at once and never change, while each commit must be checked out and built; and a choir sags by degrees, while a commit flips the check in one step.

Contrasting case: playing the recording backwards from the last bar, one bar at a time, until it sounds in tune: every bar between costs a listen.

In real terms: when something used to work, write the check first; halving does the rest.

Examples, run for real

4.1 ExamHall EXH-4420: a 70 that now fails

Since this term, a candidate who scores exactly 70 fails ECON-201, whose pass mark is 70. Change 1 was good; change 16 is the tip. check.mjs, outside the repository, exits 0 when isPass(70, 70) is true:

4.1 EXH-4420 16 commits, change 1 good, change 16 bad · first: "7 revisions left to test after this (roughly 3 steps)" · change 11 named after 4 runs
4.1 1,000 commits · cause at change 637: 10 runs · cause at change 534: 9 runs

Nobody read a diff. Over 1,000 commits the run takes 10 checks, or 9 where the halves fall luckily (step 3).

4.2 MR !4720: the fix and its guard

Change 11 turned >= into >. MR !4720 restores >= and keeps the bug's case as a test that fails before the fix and passes after it:

4.2 MR !4720 grading.test.mjs (70 at pass mark 70) · with change 11's > : fails · after the fix restores >= : passes

4.3 A commit that cannot be tested

Had change 10 also left a syntax error, a check that exits 125 when grading.mjs cannot load would skip it, and Git could not choose between it and change 11 (step 5):

4.3 change 10 cannot be parsed, its check exits 125 · after 5 runs git lists the first bad commit as any of: change 10, change 11

Try it

Answer first, then check. Nothing is saved.

Question 1 of 7

A ledger's ledger.mjs imports fees.mjs. Changes 9 to 16 left a stray parenthesis in fees.mjs, so on those commits ledger.mjs cannot be loaded and node exits 1; change 17 repaired it. Change 20 truncates the fee, the cause of the wrong total. From v7.0 (good) to change 24 (bad) you run git bisect run node ../check-total.mjs, which loads ledger.mjs and exits 0 when the total is right.

What does the run report?

Pick one

Question 2 of 7

A ledger repository's tests run with node --test, which runs every *.test.mjs file in the checked-out tree. fee.test.mjs has been in the repository since v7.0 and passes with or without the bug. After the bug report, change 20, the tip, adds settlement.test.mjs, which fails on the wrong total. The cause turns out to be change 12. From v7.0 (good) to change 20 (bad) you run git bisect run node --test.

Which commit does the run report?

Pick one

Question 3 of 7

One of 40 feature flags breaks a ledger's settlement: with all 40 on, the settlement total is wrong; with all off, it is right. Each run, you turn on half of the flags still in doubt (the rest off), run the check, and keep the half that breaks the total.

Whichever flag it is, at most how many runs does that take on this lesson's halving model?

Pick one

Question 4 of 7

A ledger's branch refund-export (5 commits, export 1 to export 5) left main after change 1 and was merged after change 5. On the branch, export 2 truncated the fee, and nothing later restored it: the merge and every commit after it settle wrong. Main's own changes never touch the fee. From v7.0 (good) to change 10 (bad) you start with git bisect start --first-parent, then run git bisect run node ../check-total.mjs.

What does the run report as the first bad commit?

Pick one

Question 5 of 7

A second ledger repository: ledger.mjs imports fees.mjs, changes 6 to 10 cannot load fees.mjs, and change 13 truncates the fee. Write the check that git bisect run should call so that the run names change 13. The runner builds the 16-commit repository in a throwaway folder, puts your script next to it (outside the repository), runs git bisect from v7.0 (good) to change 16 (bad) and prints the commit it names.

Answer in one or two lines.

Question 6 of 7

A chat service's count of undelivered messages was clean at 08:00, and at 16:00 it is not; once it starts it does not stop. Each check reads the count for one minute.

Checking the minute in the middle of the window still in doubt each time, how many checks narrow the start to a window of 15 minutes or less on this lesson's halving model?

Pick one

Question 7 of 7

An import of 1,024 ticket-order lines fails: the importer rejects a file in which any order id appears twice. By mistake, line 300 and line 900 both carry order id T-0300. A halving search imports lines 1 to 512 alone, then lines 513 to 1,024 alone, keeps whichever half the importer rejects, and repeats on it.

What does the search end with on this lesson's halving model?

Pick one

Go deeper

Pro tips
  1. Before git bisect run, run the check once on each end: it must pass on the good commit and fail on the bad one, or the run cannot find anything. When: before any bisect, and when the row's typical question "When does bisect mislead you?" comes up (steps 4, 6). Verify:
    tip the check on the good end (change 1) exits 0 and on the bad end (change 16) exits 1: it can tell them apart
    Status: RAN.
Source

quote-check.mjs finds every passage below verbatim in the saved text. Valid as of 2026-10-15 (the page says "git-bisect last updated in 2.56.0"; the machine runs git 2.56.0).

  • Git project, git-bisect documentation: https://git-scm.com/docs/git-bisect (git-bisect.txt). The row's source. Quoted: "This command uses a binary search algorithm to find which commit in your project’s history introduced a bug." · "Then git bisect picks a commit between those two endpoints and asks you whether the selected commit is "good" or "bad". It continues narrowing down the range until it finds the exact commit that introduced the change." · "Bisecting: 675 revisions left to test after this (roughly 10 steps)" · "should exit with code 0 if the current source code is good/old, and exit with a code between 1 and 127 (inclusive), except 125, if the current source code is bad/new." · "Any other exit code will abort the bisect process." · "a program that terminates via exit ( -1 ) leaves $? = 255" · "The special exit code 125 should be used when the current source code cannot be tested. If the script exits with this code, the current revision will be skipped" · "if you skip a commit adjacent to the one you are looking for, Git will be unable to tell exactly which of those commits was the first bad one." · "It is safer if both test.sh and check_test_case.sh are outside the repository to prevent interactions between the bisect, make and test processes and the scripts." · "Follow only the first parent commit upon seeing a merge commit." Supports steps 1–8.
  • Steps 9–11 (input, flags, time, one-way changes) apply step 1's halving outside git; their numbers come from the executed models, and no item rests on a source for them.

Checked against the tools and docs as of 2026-10-15.