top of page

Bisect to Find the Cause — Debugging Systematically, Part 2

Shift Quality Editorial
Jul 10
3 min read

Updated: Oct 4

Debugging Systematically · Part 2

"It worked last week and it's broken now" is one of the most answerable questions in debugging — you just have to find the commit that flipped it, and there's a binary-search tool that does exactly that. Bisecting turns hundreds of suspect commits into a handful of yes/no checks. This walks through using git bisect (and automating it) to pin the exact change that introduced a bug.


"It used to work; now it doesn't" + 500 commits between then and now. Bisect finds the breaking commit in log(N) time.


Step 1: When to Bisect (5 min)


Bisect works when:


  • A specific behavior changed

  • You can clearly tell "broken" vs. "working"

  • You have a known-good and known-bad commit


Not useful for:


  • Intermittent bugs

  • Bugs that have always existed

  • Bugs depending on data state


Step 2: git bisect Basics (10 min)


git bisect start
git bisect bad                    # current commit is broken
git bisect good v1.0.0            # this tag was good

Git checks out the midpoint commit. Test:


# Run your tests / reproduce manually
# If broken: 
git bisect bad
# If working:
git bisect good

Repeats. Each step halves the range.


After log2(N) steps:


abc123def is the first bad commit

git bisect reset to return to where you started.


Step 3: Automate the Test (10 min)


For known reproducer:


git bisect start HEAD v1.0.0
git bisect run pytest tests/test_broken.py

bisect run runs the script at each commit. Exit 0 = good; non-zero = bad. Walks automatically.


For a custom script:


#!/bin/bash
# bisect-test.sh
make build || exit 125    # 125 = skip (can't test)
./run-test

Exit 125 means "can't test this commit; skip."


Step 4: Bisect Beyond Code (10 min)


The same logic works without git:


  • Config: bisect through config changes

  • Data: bisect through versions of input data

  • Environment: bisect through OS / library versions


Halve, test, narrow. Works for any "which change broke it."


Step 5: Be Strict About Good/Bad (5 min)


Vague "kind of works" = wrong bisect result.


For each test:


  • Run the same exact reproduction

  • Decide good/bad on the same criterion

  • If you're unsure, git bisect skip (not good/bad)


Inconsistency = wrong commit identified.


Step 6: Bisect in CI (5 min)


For automated bisects in CI, mark known good with tags:


git tag known-good

When something breaks:


git bisect start HEAD known-good
git bisect run npm test

CI can do this on a schedule for nightly regression tests.


Step 7: Once You Find the Commit (10 min)


git show abc123                  # see the diff
git log -1 --format=%B abc123    # the message
git log abc123 --stat            # files changed

Read the commit. Often the cause is obvious from the diff.


Then:


  • Revert? Patch forward?

  • Test the fix in isolation

  • Add a regression test for next time


Step 8: Bisect with Merges (10 min)


Merges complicate bisect. git bisect walks first-parent history by default.


For complex histories:


git log --oneline --first-parent

Shows merge commits along main. Bisect against these to narrow to a feature branch first; then within the branch.


Step 9: Reverse Bisect (5 min)


Sometimes you want "when was this introduced":


# A bug exists at commit X; check older commits to find when it first appeared
git bisect start X older-commit
# Each step: "is the bug present?"

Same algorithm; semantic is "first occurrence" instead of "last working."


Step 10: Discipline Pays Off (10 min)


Bisect is mechanical. The discipline:


  • Define "broken" before you start (precise reproducer)

  • Don't skip steps because "it can't be that one"

  • Trust the algorithm


A surprised "I would have never guessed this commit" is the most common outcome.


What You Just Did


Bisect: when, how, automated, beyond code, with merges. The fastest way to find regressions.


Common Failure Modes


Vague good/bad. Different criteria each step; wrong commit.


Skipping commits you "know" are fine. Sometimes they're not.


Not testing the reproducer first. Spend an hour bisecting; turns out reproducer was wrong.


Forgetting git bisect reset. Repo stuck in bisect state.


Bisecting a flaky test. Random results; meaningless.



Continue the Debugging Systematically path



Part of the Debugging Systematically learning path.

bottom of page