SeriesPart 1 of 8 // Git Quick Hits
gitWriting
Aug 10, 2026
3 min read
git-quick-hits

Automating Regression Hunts with `git bisect run`

A test starts failing somewhere in the last two hundred commits. Instead of bisecting by hand, hand git a script and let it find the culprit while you do something else.

A magnifying glass tracing a binary search path down a branching commit graph toward a single highlighted red commit.

Someone pings you: "the totals endpoint is wrong, worked fine last sprint." You don't know which of the last hundred-odd commits broke it, and nobody wrote down when it last worked. The usual move is git bisect start, then git bisect good or git bisect bad by hand for every commit git hands you, rebuilding and re-testing each time. It works, but it eats your afternoon.

git bisect run does the same binary search without you in the loop. You give it a command that exits 0 for "good" and non-zero for "bad," and git drives the whole search itself.

Set the trap

Here's a small repo that stands in for the real thing: a shell function that adds two numbers.

# calc.sh
add() {
  echo $(($1 + $2))
}

A few commits later, someone "optimizes" it and quietly breaks it:

git log --oneline
# 8042654 add license
# b583da4 "optimize" add() with a bit shift trick
# c619324 expand README
# 844fbdb add README
# 40d2711 add test.sh
# f8157eb add calc.sh with add()

Somewhere between the first commit and HEAD, add(2, 3) stopped returning 5. That's the whole bug report.

bisect run needs one thing from you: a command that fails when the bug is present. A test script is perfect for this:

#!/bin/sh
# test.sh
. ./calc.sh
result=$(add 2 3)
if [ "$result" -eq 5 ]; then
  exit 0
else
  echo "expected 5, got $result"
  exit 1
fi

Give git the known-good and known-bad endpoints, then hand it the script:

git bisect start HEAD f8157eb
git bisect run ./test.sh

Git checks out the midpoint commit, runs test.sh, reads the exit code, and picks the next commit to check without asking you anything. Here's the real run, unedited:

Bisecting: 2 revisions left to test after this (roughly 1 step)
[844fbdb] add README
running './test.sh'
Bisecting: 0 revisions left to test after this (roughly 1 step)
[b583da4] "optimize" add() with a bit shift trick
running './test.sh'
expected 5, got 6
Bisecting: 0 revisions left to test after this (roughly 0 steps)
[c619324] expand README
running './test.sh'
b583da4 is the first bad commit
commit b583da4
    "optimize" add() with a bit shift trick
 
 calc.sh | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
bisect found first bad commit

Three test runs, and git names the exact commit — no manual good/bad typing at all. Clean up the session with git bisect reset when you're done; it puts your working tree back where you started.

Where this earns its keep

A one-line shell check is a toy example. The pattern is what matters: swap test.sh for whatever command returns non-zero on failure.

  • Unit tests: go test ./... -run TestTotals or npm test -- totals.spec.ts.
  • Build failures: a compile step that only started failing recently — bisect run make build finds the commit that introduced the broken dependency.
  • Performance regressions: a script that runs a benchmark and exits non-zero if it's slower than a threshold. This is how you find the commit that made your API twice as slow without anyone noticing for three weeks.

A couple of things worth knowing before you point bisect run at a real problem:

  • Exit code 125 means "skip." If a commit doesn't build at all, or the bug genuinely can't be tested there (a dependency didn't exist yet), have your script exit 125. Git skips that commit instead of treating it as good or bad, which would corrupt the search.
  • The script has to run standalone. It gets checked out fresh at every commit, so it can't depend on state left over from a previous run — no relying on a build cache from three commits ago, no assuming a file exists that a later commit deletes.
  • It runs on your actual working tree. For anything with side effects — writing files, hitting a database — consider pointing the script at a throwaway copy or a git worktree (more on that next week) rather than your main checkout.

The next time a regression shows up with no clue when it started, resist the urge to git blame your way through the diff by eye. Write the one check that tells git "good" from "bad," and let the binary search do the part computers are actually good at.

Series contents

01
Automating Regression Hunts with `git bisect run`
Current
02
Parallel Branches Without Stashing: `git worktree`
Coming soon // Aug 17, 2026
03
The Undo Button Nobody Uses: `git reflog`
Coming soon // Aug 24, 2026
04
`git rerere`: Stop Resolving the Same Conflict Twice
Coming soon // Aug 31, 2026
05
`.gitignore` vs `.git/info/exclude` vs Global Excludes
Coming soon // Sep 7, 2026
06
Shipping a Repo as One File with `git bundle`
Coming soon // Sep 14, 2026
07
Attaching Build Metadata with `git notes`
Coming soon // Sep 21, 2026
08
Why Shallow Clones Break Your Changelog
Coming soon // Sep 28, 2026