Your CI was green last Friday. Today, the payments test is failing. Somewhere between Friday's merge and now, 47 commits landed on main. Which one broke it?
Most developers answer this the wrong way: they scroll through git log, check out suspicious commits one by one, and run the test manually. An hour later, they're still guessing.
There's a command built for this exact problem. It's called git bisect, and once you learn it, you'll never debug regressions the old way again.
How bisect works
git bisect is a binary search across your commit history. You tell Git two things:
- A good commit (a known point where the bug didn't exist)
- A bad commit (a known point where the bug exists — usually
HEAD) Git then checks out the commit halfway between them. You test. You mark it as good or bad. Git narrows the range by half. Repeat.
With 47 commits between "good" and "bad", it takes at most 6 steps (log₂ 47) to find the exact commit that introduced the bug. Versus checking every commit manually, that's the difference between 5 minutes and an hour.
The manual workflow
# Start a bisect session
$ git bisect start
# Mark the current state (HEAD) as bad
$ git bisect bad
# Mark a known-good commit
$ git bisect good a3f1d22
# Bisecting: 23 revisions left to test after this (roughly 5 steps)
# [7e4b9c1] refactor: extract payment validator
# Git has checked out a commit in the middle. Run your test.
$ npm test -- --grep "payments"
# Test passed — mark this commit as good
$ git bisect good
# Bisecting: 11 revisions left to test after this (roughly 4 steps)
# [b2d8e11] feat: add retry logic to payment API
# Test failed — mark this commit as bad
$ git bisect bad
# ... continue until Git announces the first bad commit:
# b2d8e11 is the first bad commit
# commit b2d8e11
# Author: leo@company.com
# Date: Tue Apr 15 11:42:03
# feat: add retry logic to payment API
# Done — reset to where you started
$ git bisect reset
In 6 commands, you know exactly which commit broke the tests. No guessing. No archaeology through git log.
Automating bisect with a script
Here's where it gets powerful. If you have a test that can reliably detect the bug, you can hand the entire bisect to Git:
$ git bisect start
$ git bisect bad
$ git bisect good a3f1d22
# Git now runs your test automatically at each step
$ git bisect run npm test -- --grep "payments"
git bisect run expects a command that:
- Exits 0 if the commit is good (test passes)
- Exits non-zero (1-124, 126-127) if the commit is bad (test fails)
- Exits 125 if the commit can't be tested (e.g., build broken — Git will skip it) Git runs the command at each bisect step, interprets the exit code, and narrows the range automatically. You walk away, come back 5 minutes later, and Git tells you which commit broke it.
For a codebase with 50+ commits to search and a full test suite that takes 2-3 minutes per run, this is genuinely life-changing.
Writing a bisect-friendly test script
If your test isn't a simple command, wrap it in a shell script:
#!/bin/bash
# bisect-test.sh — verify the search feature
# Install dependencies (in case they changed between commits)
npm install > /dev/null 2>&1 || exit 125 # 125 = skip this commit
# Run the build
npm run build > /dev/null 2>&1 || exit 125
# Run the specific test
npm test -- --grep "search returns correct results"
# npm test returns 0 on success, 1 on failure — perfect for bisect
Now bisect it:
$ git bisect start
$ git bisect bad HEAD
$ git bisect good v2.4.0
$ git bisect run ./bisect-test.sh
The exit 125 for build failures is important. If a commit doesn't build at all, you don't want Git to mark it as "bad" — you want it to skip to another commit.
Handling flaky tests
If your test is flaky (sometimes passes, sometimes fails, even on the same commit), bisect will give you wrong answers. The fix: retry in your script.
#!/bin/bash
# Try up to 3 times, succeed if any attempt passes
for i in 1 2 3; do
npm test -- --grep "flaky test" && exit 0
done
exit 1
This is a hack, not a fix — you should eventually make your tests deterministic. But for finding a regression today, it works.
What bisect teaches you about your codebase
After running bisect a few times, you'll start making choices that make your future self's life easier:
Atomic commits matter. If one commit mixes a feature, a bugfix, and a refactor, bisect tells you "this commit broke it" — but which part? Small, focused commits make bisect precise.
Tests are investments. A codebase with good test coverage turns bisect into a fully automated tool. Without tests, bisect still works but requires manual testing at each step.
Main should stay green. If your main has frequent broken builds, git bisect run hits exit 125 everywhere and degrades into a crawl. Teams that protect main with required CI checks get the full benefit of bisect.
The mindset shift
The real insight from bisect isn't the command — it's the shift in how you debug regressions.
Without bisect: "Something changed. Let me scroll through the log and guess."
With bisect: "Something changed. I'll let Git find it for me in 6 steps."
Once you internalize that, every "when did this break?" question has a clear procedure. You stop guessing. You start finding.
This post is adapted from Git in Depth: From Solo Developer to Engineering Teams, a 658-page book covering Git the way it's actually used in real engineering teams.

Top comments (0)