GitBug Hunting with git bisect

Bug Hunting with git bisect

git bisect is a binary search over your commit history. You tell Git which commit is good (no bug) and which is bad (bug present). Git checks out the commit in the middle, you test, and you mark it good or bad. Git keeps halving the range until it pinpoints the exact commit that introduced the bug. For 1024 commits, that's about 10 tests — log2(N) instead of linear.

When to reach for bisect
  • A test that used to pass now fails — and the diff between then and now is large.

  • You noticed a regression in production and you have a known-good release tag.

  • A bug is intermittent only on certain platforms but you have a script that can detect it.

  • You inherited a codebase and need to find when a strange behaviour started.

The interactive loop

Start a session

Bash
git bisect start
git bisect bad                  # current HEAD is broken
git bisect good v1.0            # v1.0 worked
# Git now checks out a commit halfway between v1.0 and HEAD
# Bisecting: 250 revisions left to test after this (roughly 8 steps)
# [a1b2c3d] Refactor authentication middleware

Now you test the checked-out commit any way you like — run the app, run a unit test, click around in a browser. Then mark it:

Bash
git bisect good       # this commit worked, move forward in time
# or
git bisect bad        # this commit is broken, move backward in time
# or
git bisect skip       # can't test this commit (won't build, etc.)
# or
git bisect reset      # give up; restore HEAD to where you started

Repeat until Git announces the culprit:

The finishing line

Text
b3a9f00a8c4d... is the first bad commit
commit b3a9f00a8c4d...
Author: Alice <alice@example.com>
Date:   Wed May 14 09:00:00 2026 +0000

    Tighten password rules

 src/auth/login.js | 12 ++++++++----
 1 file changed, 8 insertions(+), 4 deletions(-)
Cleaning up

Bash
git bisect reset       # ALWAYS end with this — restores your previous HEAD
git bisect reset HEAD~5 # or reset to a specific commit
Skip vs bad
`bisect skip` means "I can't evaluate this commit" — maybe it won't compile, maybe a dependency isn't ready. `bisect bad` means "this commit has the bug." If you guess, you'll mislead the search. When in doubt, skip.
Automating with a script

If you can write a script that exits 0 for good and non-zero for bad, git bisect run will do the entire search hands-free.

A test script

Bash
#!/usr/bin/env bash
# test-regression.sh — exit 0 if good, 1 if bad, 125 if untestable
set -e
npm install --silent || exit 125
npm test -- --testPathPattern=login || exit 1
exit 0

Wire it up

Bash
chmod +x test-regression.sh
git bisect start HEAD v1.0
git bisect run ./test-regression.sh

What you'll see scroll by

Text
Bisecting: 250 revisions left to test after this (roughly 8 steps)
running ./test-regression.sh
Bisecting: 125 revisions left to test after this (roughly 7 steps)
running ./test-regression.sh
...
b3a9f00a8c4d... is the first bad commit

Exit code 125 is special — it tells bisect "this commit can't be tested, skip it." Use that for commits that don't build at all.

Real-world workflow recipe

It's Monday. Yesterday's release broke something. The last release was tagged v3.4.0 last Tuesday, and HEAD is broken. Here's the full session:

The full bisect

Bash
# 1. Reproduce the bug locally on HEAD to make sure your test is real
npm test -- --testPathPattern=checkout
# ✗ test fails — good, we have a repro

# 2. Verify v3.4.0 was clean
git checkout v3.4.0
npm test -- --testPathPattern=checkout
# ✓ passes — good

# 3. Start the bisect
git checkout main
git bisect start
git bisect bad             # HEAD
git bisect good v3.4.0     # last known good

# 4. Either drive it manually...
npm test -- --testPathPattern=checkout && git bisect good || git bisect bad
# ...and repeat 8-ish times.

# 5. ...or automate it
git bisect run sh -c 'npm install --silent && npm test -- --testPathPattern=checkout'

# 6. Bisect finds the culprit. Read it:
git show b3a9f00

# 7. Always end with reset
git bisect reset
Visualising the bisect

Each step roughly halves the candidate range. For N commits between good and bad, you need about log2(N) tests:

Steps versus commits

Text
commits   approximate tests
     10       4
    100       7
  1,000      10
 10,000      14
100,000      17

You can also see the current state mid-bisect:

Bash
git bisect log         # everything you&apos;ve marked so far
git bisect visualize   # gitk-style view of remaining candidates
git bisect view        # alias for visualize
Replaying a bisect

Bash
# Save and re-run a session — handy for sharing or scripting
git bisect log > my-bisect.log
git bisect reset
git bisect replay my-bisect.log
Custom terminology

Sometimes "good" and "bad" don't fit — for example when searching for the commit that fixed a bug. Rename the terms:

Bash
git bisect start --term-old=broken --term-new=fixed
git bisect fixed
git bisect broken v1.0
# now use:  git bisect fixed  /  git bisect broken
Bisect needs a linear-ish history
Bisect walks the commit graph, including merges. If a merge commit is the "first bad", the real culprit is one of the commits on the merged branch — recurse with `git bisect start <merge-commit-sha>~ <merge-commit-sha>^2` (or just rebase the suspect branch and re-run). For most projects this rarely matters.
Tip
The single biggest payoff of bisect is investing the 30 seconds to write a clear test script before you start. A script that reproduces the bug in 10 seconds turns a half-day investigation into a 90-second `git bisect run`.