git bisect: find the commit that broke it in log(n) steps
Use git bisect to find the exact commit that introduced a regression or bug with binary search over history — O(log n) instead of O(n).
A regression appeared in production. The feature worked three weeks ago. Since then, four engineers have pushed two hundred commits. Which one broke it?
The naive approach is to checkout increasingly older commits one by one, test, and walk backward until the behavior changes. That is a linear search — O(n) in the number of commits. If you have two hundred commits and the bug is old, you might test a hundred of them before you find it.
There is a better approach built into git and overlooked by most developers: git bisect. It applies binary search to your commit history, cutting the search space in half at every step. Two hundred commits need at most eight tests. A thousand commits need at most ten. The savings compound dramatically at scale.
why linear search loses
When you chase a regression by hand, you are effectively implementing a linked-list traversal: start at HEAD, step back one commit at a time, test each one. The worst case visits every commit between the known-good revision and today.
Binary search works differently. Pick the midpoint of the range. Is the bug present? If yes, the culprit is in the older half. If no, it is in the newer half. Each test halves the problem. The worst-case number of tests is ⌈log₂(n)⌉, where n is the number of commits in the range.
For practical context:
| Commits in range | Max tests (linear) | Max tests (bisect) |
|---|---|---|
| 10 | 10 | 4 |
| 100 | 100 | 7 |
| 1,000 | 1,000 | 10 |
| 10,000 | 10,000 | 14 |
That last row is the one that matters for long-lived repositories. Ten minutes of systematic testing versus days of guessing.
the manual workflow
Start by identifying two commits: one where the behavior was definitely correct, and HEAD (or any recent commit) where it is definitely broken.
git bisect start
git bisect bad # current HEAD is broken
git bisect good v2.4.1 # this tag was known good
At this point git checks out the commit exactly halfway between the good and bad boundaries and prints something like:
Bisecting: 49 revisions left to test after this (roughly 6 steps)
[a1b2c3d] feat: add caching layer to user profile queries
You are now on commit a1b2c3d. Test whether the bug is present — run your test suite, reproduce the issue manually, whatever your verification method is — and then tell git the verdict:
git bisect bad # bug is present here
or
git bisect good # works fine here
Git adjusts the search window and checks out the next midpoint. Repeat until git prints:
a1b2c3d is the first bad commit
commit a1b2c3d...
Author: ...
Date: ...
feat: add caching layer to user profile queries
When you have your answer, clean up and return to your original branch:
git bisect reset
This is the complete manual loop. See the git branches guide for a refresher on how git moves the HEAD pointer during this process.
the automation superpower: git bisect run
The manual loop works, but you have to sit there for every test. If your project has a reliable, automated way to check whether a given commit is good or bad — a unit test, a shell command, a build check — you can hand the entire bisect session to git.
The command is:
git bisect run <script> [args...]
Git will checkout each midpoint commit, run your script, and interpret the exit code:
- 0 — this commit is good
- 1–124 (except 125) — this commit is bad
- 125 — skip this commit (untestable: it does not compile, or a flaky test fired, or the environment is wrong)
- 126 or 127 — script itself could not be found or run; bisect aborts
Those semantics map exactly to git bisect good, git bisect bad, and git bisect skip in the manual workflow. Exit code 125 is the escape hatch for commits where a verdict is impossible — perhaps a dependency is missing or the build is broken for unrelated reasons.
A practical example: suppose the regression is in a specific function and you already have a unit test for it.
git bisect start
git bisect bad HEAD
git bisect good v3.1.0
git bisect run npm test -- --testPathPattern="auth.test.ts"
Git will now run that test against every midpoint automatically. When it finishes, it prints the first bad commit and stops. You did not have to watch it.
If you prefer a one-liner without a dedicated script file:
git bisect run sh -c 'make build && ./bin/myapp --smoke-test'
The sh -c form works anywhere you can express the check as a shell command. Failed builds should exit with a code other than 125 if they are genuinely bad; use 125 only for “I cannot tell.”
handling messy histories
Real repositories are not clean binary-search inputs. Some commits might not compile because a dependency was temporarily broken. Some might have flaky tests that give inconsistent results. git bisect skip addresses both:
git bisect skip <commit-hash>
Or in the automated path, exit 125 from your script. Git will skip that commit and pick a neighboring midpoint instead. This works well as long as skipped commits are sparse — if too many commits are skipped in the search window, git may not be able to isolate a single culprit and will tell you so, offering a small set of candidates.
Tips for clean bisect sessions:
- Lock your test environment. If the test depends on an external service or random seed, results will be inconsistent. Pin dependencies, seed randomness, and mock external calls before running bisect.
- Use a minimal reproduction. If your full test suite takes eight minutes, write a targeted test that exercises only the regressed behavior. You want sub-ten-second round trips.
- Write a real test before bisecting. Turning “it feels slow” into
exit 1 if response_time_ms > 200 else exit 0gives bisect something deterministic to work with. - Keep
git bisect resetin your muscle memory. If you interrupt a session or something goes wrong, this command returns you to the branch and commit you started from.
You can also use git bisect log to see the decisions made so far, and git bisect replay to replay a saved log — useful when you want to reproduce the session or share it with a colleague.
See the glossary for a refresher on terms like “HEAD”, “ref”, and “detached HEAD state”, which bisect triggers when it checks out midpoint commits.
why it is underused
Most developers know git log, git blame, and git diff. Bisect is less visible because it solves a problem that feels rare until it is not: tracking down a regression across a large history. When that situation arises, many teams default to reading commit messages, poking at suspects by intuition, or asking the author. Those methods work, but they do not scale.
The deeper reason bisect is underused is that it demands a reliable test. If you cannot write a script that exits 0 or 1 based on whether the regression is present, bisect cannot automate for you. This is actually a valuable constraint — it forces you to articulate what “broken” means precisely, which is useful even if you never run bisect.
Teams that invest in fast, deterministic regression tests get bisect for free. A test suite that runs in under a minute and reliably catches the bug you are hunting lets you run git bisect run npm test and walk away. On a well-tested codebase, what used to take a day takes ten minutes.
For further context on how branch history and commits are structured in git, the git branches reference is a good companion to this workflow. Developers preparing for technical interviews will also find this a common topic — see the interview prep section for examples of how bisect questions appear in system debugging discussions.
frequently asked questions
Can I use git bisect on a repository with merge commits?
Yes. Git bisect traverses the commit graph, not a linear list, so merge commits are handled correctly. Git picks midpoints based on graph distance. If a merge commit itself is the first bad commit, bisect will identify it. The one edge case to watch is when the same change appears in multiple branches and was merged at different times — bisect will find where the change was first integrated into your main line.
What if I accidentally mark the wrong commit as good or bad?
Run git bisect log to see the current session history. You can save the log, then run git bisect reset to end the session, and git bisect replay <logfile> after manually editing the log to correct the mistake. There is no in-place undo for a single bad mark.
Does git bisect work across renames and moved files?
Yes, but your test script needs to be resilient to repository layout changes across history. If your test references a file path that did not exist in older commits, the script will error. Either write tests that tolerate missing files gracefully, use exit 125 for those commits, or narrow the bisect range to a window where the layout is stable.
How is git bisect different from git blame?
git blame shows you who last modified each line of a file, which helps when the bug is a specific line you can already see. git bisect helps when you know something is broken but do not know where to look — it tells you which commit introduced the change, after which you can run git show or git diff on that commit to see exactly what changed. They answer complementary questions: blame says “who touched this line”, bisect says “which commit caused this behavior.”