/ Tags: DEVELOPER TIPS / Categories: TIPS

Git Bisect — Find the Regression-Introducing Commit in Under 10 Minutes

Something worked last month. It doesn’t work now. Between then and now there are four hundred commits, three of them yours, and nobody remembers touching that code. The instinct is to start reading diffs. Don’t. Git has a binary search built in, and it will find the exact commit that broke things in roughly log₂(n) steps — about nine checkouts for four hundred commits.

The Mental Model


git bisect is binary search over your commit history. You tell it one commit where the behaviour was good and one where it’s bad. It checks out the midpoint. You test. You tell it good or bad. It halves the range and repeats.

The only thing you supply is a yes/no answer at each step. Git handles the arithmetic. Four hundred commits becomes nine tests. Ten thousand commits becomes fourteen. The history size barely matters, which is the whole point — this is the one debugging technique that gets relatively faster as the codebase gets older.

The Manual Flow


Setup:

git bisect start
git bisect bad                  # current HEAD is broken
git bisect good v2.4.0          # this tag worked

Git responds with something like Bisecting: 203 revisions left to test after this (roughly 8 steps) and drops you on a detached HEAD at the midpoint.

Now test. Run the spec, hit the endpoint, click the button — whatever proves the behaviour. Then report back:

Example:

git bisect good     # this commit is fine, bug came later
# or
git bisect bad      # this commit is broken, bug came earlier

Repeat until git prints the verdict:

a3f8c21b9d4e5f60718293a4b5c6d7e8f9012345 is the first bad commit
commit a3f8c21b9d4e5f60718293a4b5c6d7e8f9012345
Author: someone <[email protected]>
Date:   Tue Mar 3 14:22:09 2026 +0545

    Refactor order total calculation

When you’re done, always:

git bisect reset

That returns you to the branch you started on. Forgetting this leaves you stranded on a detached HEAD, which is how people end up committing work that vanishes.

Automate It With git bisect run


Manual bisect is fine for bugs you have to eyeball. If you can express the failure as a command that exits non-zero, hand the whole thing to git and walk away.

Example:

git bisect start HEAD v2.4.0
git bisect run bundle exec rspec spec/models/order_spec.rb -e "applies bulk discount"

Git runs the command at each step, reads the exit code, and marks the commit automatically. Exit 0 means good, exit 1–124 means bad. It will chew through hundreds of commits unattended and print the culprit.

For anything more involved than a single command, write a script:

Example:

#!/usr/bin/env bash
# bisect_check.sh
set -e

bundle install --quiet
bin/rails db:migrate RAILS_ENV=test 2>/dev/null

bundle exec rspec spec/models/order_spec.rb -e "applies bulk discount"
chmod +x bisect_check.sh
git bisect run ./bisect_check.sh

Keep the script out of version control, or at least out of the commits being bisected — otherwise git checks out a revision where your script doesn’t exist yet.

The Exit Code 125 Escape Hatch


Some commits can’t be tested. The migration is mid-rename, a dependency doesn’t install, the app won’t boot for reasons unrelated to your bug. Marking those good or bad poisons the search.

Exit code 125 tells git “skip this one, I can’t judge it.”

Example:

#!/usr/bin/env bash

bundle install --quiet || exit 125
bin/rails runner 'exit 0' >/dev/null 2>&1 || exit 125

bundle exec rspec spec/models/order_spec.rb -e "applies bulk discount"

Manually, the equivalent is git bisect skip. Skip too many adjacent commits and git will tell you it can’t narrow further than a range — that’s honest, and still far better than reading four hundred diffs.

Narrowing the Search Before You Start


Bisect gets faster when you feed it a smaller range and a tighter test.

1. Find a real known-good point

Don’t guess v2.4.0 because it feels old. Check out the tag, run the test, confirm it passes. A wrong “good” endpoint sends the search down the wrong half and produces a confident, incorrect answer.

2. Scope by path

If you know the bug lives in the billing code, tell git to only consider commits that touched it:

git bisect start -- app/models/billing app/services/billing

Commits outside those paths get skipped entirely, which can cut a nine-step search to four.

3. Write the smallest possible reproduction

A full suite run at each step turns a ten-minute bisect into an hour. One focused spec, or a bin/rails runner one-liner, keeps each step under a few seconds.

When the Culprit Is a Merge Commit


Bisect sometimes lands on a merge. That’s not a dead end — it means the bug came in with a branch, and the individual commits on that branch may each have been fine in isolation while their combination is not.

Re-bisect inside the branch:

git bisect start <merge-commit> <merge-commit>^1

That searches the second parent’s lineage — the commits the branch contributed — against the mainline state before the merge.

Pro-Tip: The most common reason bisect gives a wrong answer is a flaky test used as the oracle. Binary search assumes the property is monotonic — good up to a point, bad after it. A test that fails one run in five turns that assumption into noise, and git will confidently name an innocent commit. Before bisecting, run your check three times on a known-bad commit and three times on a known-good one. If it isn’t deterministic, fix the oracle first or bisect on something else entirely — a log line, an HTTP status, a checksum. Ten minutes spent making the check reliable saves an afternoon chasing a phantom.

Recording What You Learned


git bisect log prints the full sequence of good/bad decisions. Paste it into the issue — it’s a compact record of exactly which commits were ruled out and why.

git bisect log > /tmp/bisect.log
# ... later, or on another machine
git bisect replay /tmp/bisect.log

replay reconstructs the entire search state, which is useful when you have to stop halfway through and come back, or when a colleague wants to verify your conclusion without redoing the work.

Conclusion


Bisect converts “which of these four hundred commits broke it” from an archaeology problem into nine yes/no questions. The technique costs nothing to learn and pays off every time a regression sneaks past review. The two things that make it work in practice are a genuinely verified good endpoint and a deterministic test — get those right and git bisect run will hand you the commit hash while you’re getting coffee. On a large, long-lived codebase, that is the difference between fixing a regression this morning and fixing it this week.

FAQs


Q1: Does bisect work if the history has merge commits?
Yes. Git traverses the full commit graph, not just the first-parent line. If it lands on a merge, re-bisect within that branch using the merge commit and its first parent as endpoints.

Q2: What happens to my uncommitted changes when I start bisecting?
Bisect checks out different commits, so a dirty working tree will block it or get in the way. Stash or commit your work first, then git bisect start.

Q3: Can I bisect something that isn’t a test failure?
Any command with a meaningful exit code works — a curl against a local server piped to grep, a build step, a benchmark compared against a threshold. Exit 0 for good, 1–124 for bad, 125 to skip.

Q4: How do I get back to my branch if I forget git bisect reset?
Run git bisect reset at any point; it restores the original HEAD. If you’ve already switched branches manually, git checkout <your-branch> works too, but reset is the clean exit.

Q5: Is there a limit on how far back I can bisect?
Only your history depth. Shallow clones (--depth) are the real constraint — bisect needs the full history in the range, so run git fetch --unshallow on a CI checkout before bisecting there.

cdrrazan

Rajan Bhattarai

Full Stack Software Developer! 💻 🏡 Grad. Student, MCS. 🎓 Class of '23. GitKraken Ambassador 🇳🇵 2021/22. Works with Ruby / Rails. Photography when no coding. Also tweets a lot at TW / @cdrrazan!

Read More