Git Bisect

Binary search for bugs — find the exact commit that broke something in O(log n) steps.

Core Bisect Workflow

Start a bisect session
git bisect start
Mark the current commit as broken
git bisect bad
Mark a known-good commit — the last point where things worked
git bisect good v0.3.0

You can use a tag, branch name, or commit hash. Git calculates the midpoint and checks it out. You test, mark good or bad, and repeat. For N commits, bisect takes at most log2(N) steps — 1000 commits narrows to 10 steps.

After each checkout, test and mark
git bisect good
# or
git bisect bad
End the session and return to your original branch
git bisect reset

Always reset when done. During bisect you’re in detached HEAD state — committing or branching here leads to confusion.

Automated Bisect — The Power Move

Bisect automatically with a test script
git bisect start HEAD v0.3.0
git bisect run pytest tests/test_auth.py -x

Git runs the command at each midpoint. Exit code 0 means "good," non-zero means "bad." The -x flag tells pytest to stop on first failure. Bisect finishes without human interaction and prints the first bad commit.

Bisect with a custom shell command
git bisect start HEAD abc1234
git bisect run sh -c 'make build && ./run-smoke-tests.sh'

The sh -c wrapper lets you chain commands. If the build itself fails on some commits, the non-zero exit marks them bad — which is usually what you want.

Bisect with a script that distinguishes "bad" from "untestable"
git bisect start HEAD v0.2.0
git bisect run sh -c '
    make build 2>/dev/null || exit 125
    pytest tests/test_graph.py -x
'

Exit code 125 is special: it means "skip this commit — I can’t test it." Git moves to an adjacent commit instead. Use this when some commits don’t compile, have missing dependencies, or are otherwise untestable.

Skip — Handling Untestable Commits

Skip the current commit during manual bisect
git bisect skip

When a commit is broken for unrelated reasons (won’t compile, missing migration, incomplete merge), skip it. Git picks a nearby commit instead.

Skip a range of known-broken commits
git bisect skip abc1234..def5678

If you know an entire range is untestable (e.g., a botched refactor that was fixed later), skip the whole range upfront to avoid wasting steps.

Bisect Log & Replay

View the bisect log — every decision you’ve made
git bisect log
Save the bisect log to replay later
git bisect log > /tmp/bisect-session.log
Replay a saved bisect session
git bisect start
git bisect replay /tmp/bisect-session.log

Replay is useful when you want to demonstrate the debugging process to someone else, or when you need to redo a bisect after realizing your test was wrong. Edit the log file to correct any mis-marked commits, then replay.

Visualization

See the remaining bisect range in gitk
git bisect visualize

Opens gitk (or git log if gitk isn’t available) showing only the commits still under consideration. Useful for spotting obvious culprits by commit message before continuing the binary search.

Visualize with a specific tool
git bisect visualize --oneline

When you don’t have a GUI, --oneline gives you a compact list of remaining suspects in the terminal.

Using Old/New Instead of Good/Bad

Bisect for non-bug changes — use old/new terminology
git bisect start --term-old=fast --term-new=slow
git bisect slow HEAD
git bisect fast v0.2.0

Not every bisect is hunting a bug. If you’re finding which commit introduced a performance regression, "good/bad" is confusing — the commit isn’t broken, it’s just slower. Custom terms make the session self-documenting.

Real-World Example

Finding which commit broke a test — full workflow
# 1. Start: HEAD is broken, v0.5.0 was the last known-good release
git bisect start HEAD v0.5.0

# 2. Run automatically against the failing test
git bisect run pytest tests/integration/test_api_endpoints.py::test_create_association -x

# 3. Git outputs something like:
#    abc1234 is the first bad commit
#    Author: Evan <evan@example.com>
#    Date: Mon Apr 7 14:22:00 2026
#    refactor: change association dict to use frozenset keys

# 4. Inspect the offending commit
git show abc1234

# 5. Clean up
git bisect reset

The entire workflow — across 200 commits — takes under a minute. Manual debugging would have taken hours. This is the single highest-leverage debugging technique in git.

Bisect a build failure — when only some commits compile
git bisect start HEAD v0.4.0
git bisect run sh -c '
    python -m py_compile src/domus_api/main.py || exit 125
    pytest tests/test_graph.py -x
'

The py_compile check catches syntax errors. If the file doesn’t even parse, exit 125 skips that commit. Otherwise, run the real test.

See Also

  • History — log search and blame

  • Reflog — recover from bad bisect resets