DEV Community

Cover image for git bisect: find the commit that broke production in minutes, not days
Matías Denda
Matías Denda

Posted on

git bisect: find the commit that broke production in minutes, not days

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.

Diagram showing three steps of git bisect. Step 1: a range of 12 commits from good on the left to bad on the right. Step 2: Git checks out the middle commit and runs the test. Step 3: the test passed, so the bug is in the right half, and Git selects a new middle commit in that half to test next.

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Now bisect it:

$ git bisect start
$ git bisect bad HEAD
$ git bisect good v2.4.0
$ git bisect run ./bisect-test.sh
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)