A regression is a bug that appears after a change and did not exist before. You notice it when a feature that used to work now fails, and you usually know two points in history: a commit where the code worked, and a commit where it does not. Everything between those two points is a suspect.
Git bisect finds the exact commit that introduced the bug by binary search: it checks out a commit in the middle of the range, you test it, tell Git whether it is good or bad, and Git halves the search space. With 1000 commits in the range, about 10 tests are enough to isolate the culprit.
Set up a bisect session with good and bad commits
A bisect session needs two commits: a bad commit where the bug is present, and a good commit where it is not. Start a bisect session by marking the current commit as bad and naming a good commit:
git bisect start
git bisect bad
git bisect good v2.3.0
Git checks out a commit in the middle of the range and reports how many steps remain. Test that commit, then mark it with git bisect good or git bisect bad. Each answer halves the range. When one commit remains, Git reports it as the first bad commit.
Use commits you have tested. The good revision must pass the same reproduction that fails on the bad revision. Do not assume a release tag is good without checking it.
Automate bisect with git bisect run
Testing each commit by hand works, but a long range is tedious. git bisect run <command> runs a command at every candidate commit and reads the result from its exit code, the status number a command returns when it finishes. The command must exit 0 when the commit is good and non-zero when it is bad.
The git bisect documentation defines the exit code contract:
- 0 marks the commit good.
- 125 marks the commit untestable and skips it.
- Codes from 1 to 127 mark the commit bad, except 125. Codes of 128 or higher abort the run.
Start from a reproduction you can run in one command. Suppose a function add(a, b) now returns the wrong sum for some inputs. A test that asserts add(2, 3) == 5 passes on every working commit and fails on the broken one. With pytest, the automated command is:
git bisect run pytest tests/test_add.py
pytest exits 0 when the test passes and 1 when it fails, so bisect marks each commit automatically.
Two script mistakes cause wrong results. If the script runs the test and then runs another command, such as echo "done", it exits 0 no matter what the test said, so every commit looks good. End the script with the test command, or capture the status and re-exit with it:
#!/bin/sh
pytest tests/test_add.py
status=$?
echo "test finished with status $status"
exit $status
The command must also exist at every commit in the range; a test script added later is missing at older commits, so the shell exits with a non-zero status and bisect reads that as bad. A script that changes the exit code is a known cause of incorrect results.
Skip commits that cannot be tested
Not every commit in the range is testable. A commit may fail to build, or it may be a checkpoint that was never meant to run. Marking such a commit as good to keep the search moving is dangerous: it tells Git the bug is definitely in the other half, and the search converges on the wrong commit.
The correct move is to skip. In a git bisect run script, exit with 125. When testing by hand, run git bisect skip, or git bisect skip <range> to skip several commits at once.
Skipping has a limit. If you skip the commit next to the actual culprit, Git cannot tell which side of the skipped commit the bug entered on, so it may not identify the exact first bad commit.
Use git bisect skip or a test harness that handles unrelated build failures. You do not need to squash commits to use bisect.
Why flaky tests produce a false culprit
Bisect assumes every commit has a stable answer. A flaky test that sometimes passes and sometimes fails on the same commit breaks that assumption. If a good commit is marked bad, the search discards the half of history that contains the real culprit and converges on a commit that has nothing to do with the bug.
Make the reproduction reliable before you start. Run it several times on the same commit to confirm it is stable, or fix the flakiness first. During the session, if a commit gives mixed results, skip it instead of guessing. Flaky tests are a known cause of wrong bisect results.
Reset and inspect the first bad commit
During a session, Git usually leaves HEAD pointing directly to the checked-out commit rather than to a branch. A branch may still point to that commit. git bisect reset ends the session and returns to where you started. Run it when you finish or abandon the search.
Once bisect reports the first bad commit, inspect it with git show to see the diff. If the commit is a merge, the regression may have come from the entire merged branch; rerun bisect on that branch’s own range. If the commit is large, bisect identifies the commit but not the line, so follow up with git blame or a closer read of the diff.
If you need to pause or share the session, git bisect log prints the history of your marks, and git bisect replay restores the session from a saved log.
Narrow the search with paths and –first-parent
Two options, documented in the git bisect reference, make bisect faster and more accurate on real repositories.
Path limiting restricts the search to commits that touched a specific file or directory:
git bisect start HEAD v2.3.0 -- src/checkout
Git skips commits that did not modify that path, which helps when the regression is clearly tied to one area of the code.
The --first-parent option makes bisect follow only the first parent of merge commits. A merge commit has multiple parents: the main line and the merged branch. By default bisect searches all of them, so a broken commit on a merged branch can be reported as the culprit even though the merge itself was fine. Following only the first parent keeps the search on the main line and avoids those false positives.
--no-checkout updates a special reference named BISECT_HEAD instead of checking out the working tree, which helps when checkout is expensive. You can choose terms that describe your test: git bisect start --term-old fast --term-new slow replaces good and bad with your own labels.
Bisect checklist
- Verify both boundaries before starting: run the same reproduction on both revisions and check that the good one passes.
- Automate with
git bisect runand a stable test command: 0 means good, 125 skips, and 1 to 127 otherwise means bad. Codes of 128 or higher abort. - Skip, never mislabel, untestable commits: marking a commit that fails to build as good sends the search into the wrong half.
- Fix flaky tests before bisecting: a test that passes and fails on the same commit produces a false culprit.
- Run
git bisect resetwhen done: it ends the session and returns the repository to your original commit.