git bisect
git bisect is Git’s built-in tool for finding exactly which commit introduced a bug, using binary search instead of scrolling through history by eye. You point it at a commit you know is good and one you know is bad, and it repeatedly checks out commits in between, asking you (or an automated script) whether each one is good or bad, until it isolates the single commit responsible. For a history of a few thousand commits this narrows the search to roughly a dozen tests instead of thousands — an O(log n) search instead of an O(n) one. It works for any regression you can turn into a yes/no answer: a failing unit test, a broken build, a UI glitch, a performance drop, anything.
Overview: How git bisect Works
Under the hood a Git repository is a directed acyclic graph (DAG) of commit objects. Each commit points to a tree object (a snapshot of the project’s files and directories at that point), a pointer to its parent commit (or two parents for a merge commit), and metadata (author, committer, message, timestamp). A branch like main is nothing more than a file containing the SHA of the commit it currently points at — a lightweight, movable label. HEAD is normally a pointer to that branch pointer, one level of indirection further.
git bisect exploits this graph directly. You tell it one commit that is known-good (the bug is absent) and one that is known-bad (the bug is present, usually your current HEAD). Git computes the list of commits reachable from the bad commit but not from the good one — the same set git rev-list good..bad would produce — and treats that as the search space. It checks out the commit sitting at the midpoint of that list. Because this exploratory checkout shouldn’t disturb your actual branch, Git puts you in detached HEAD state: HEAD now points directly at a commit SHA instead of at a branch name, so your branch pointer stays exactly where it was. The working tree and the index are updated to match that commit’s tree object, so you can build and test the project exactly as it existed at that moment.
You answer git bisect good or git bisect bad based on what you observe, and Git discards the half of the remaining range that answer rules out, then checks out the new midpoint. Repeat until only one commit is left — that commit is reported as the first bad commit, along with a diffstat of what it changed. The session’s state lives in refs under .git/refs/bisect/ and a plain-text log at .git/BISECT_LOG, which is why the session survives you closing your terminal and coming back later.
One assumption is critical: bisect assumes the bug is monotonic across the range — once it appears, it stays present all the way to the bad commit; it doesn’t disappear and reappear. If a bug is intermittent, guarded by a feature flag that toggles back and forth, or the "bad" behavior you’re chasing is really two unrelated bugs, bisect’s binary-search logic breaks down and it can point at the wrong commit.
Syntax
The general form is git bisect <subcommand> [<arguments>]. The subcommands you’ll use most:
| Command | Description |
|---|---|
git bisect start [<bad> [<good>...]] |
Begin a bisect session; optionally pass the bad and good commits directly instead of marking them in separate steps. |
git bisect bad [<rev>] |
Mark a commit as bad (bug present). Defaults to the current commit (HEAD). |
git bisect good <rev> |
Mark a commit as good (bug absent). |
git bisect skip [<rev>...] |
Tell Git a commit can’t be tested (won’t build, unrelated failure) and to try a nearby commit instead. |
git bisect run <script> [args...] |
Automate the whole loop: Git runs the script at each checkout and reads its exit code (0 = good, 1–127 except 125 = bad, 125 = skip). |
git bisect reset [<branch>] |
End the session, return to the branch/commit you started from, and remove all bisect state. |
git bisect log |
Print the good/bad history of the current session, useful for saving or sharing it. |
git bisect replay <logfile> |
Replay a previously saved log to redo or resume a session, even in a different clone. |
Examples
Example 1: A manual bisect session
A team’s shopping-cart total started rendering incorrectly sometime after the v1.0 release. v1.0 is known good; the current main is bad. Start a session and mark both endpoints:
git bisect start
git bisect bad
git bisect good v1.0
# Git checks out a midpoint commit; you build and test it, then report:
git bisect bad
# test the next midpoint:
git bisect good
# test the final candidate:
git bisect bad
Output (abbreviated):
Bisecting: 6 revisions left to test after this (roughly 3 steps)
[4b1f8a0e3c2d7f6a9b5e1d4c8f0a2b3c4d5e6f70] refactor: extract calculateTotal() helper
Bisecting: 2 revisions left to test after this (roughly 2 steps)
[9c3d5f70a1b2e4d6f8a0c2e4b6d8f0a2c4e6d8f0] fix: round cart total to 2 decimal places
Bisecting: 0 revisions left to test after this (roughly 1 step)
[7a2c4e6d8f0b2c4e6d8f0a2c4e6d8f0a2c4e6d8f] style: reformat cart summary template
7a2c4e6d8f0b2c4e6d8f0a2c4e6d8f0a2c4e6d8f is the first bad commit
commit 7a2c4e6d8f0b2c4e6d8f0a2c4e6d8f0a2c4e6d8f
Author: Priya Shah <priya@example.com>
Date: Wed Jul 22 14:03:11 2026 -0700
style: reformat cart summary template
src/cart/CartSummary.jsx | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
Each call to git bisect bad or git bisect good tells Git which half of the remaining commits to discard. After three tests Git had narrowed six candidate commits down to one, and reported it directly. Finish by returning to normal state:
git bisect reset
Example 2: Fully automated with git bisect run
Manually rebuilding and testing at every step is slow. If the check can be scripted, hand it to git bisect run and let Git drive the whole loop unattended. First write a small script whose exit code reports good/bad:
#!/usr/bin/env bash
npm test -- CartSummary.test.js
npm test already exits non-zero on a failing test and zero on success, which is exactly the contract git bisect run expects. Kick off the session:
chmod +x check-total.sh
git bisect start HEAD v1.0
git bisect run ./check-total.sh
git bisect reset
Output (abbreviated):
running ./check-total.sh
Bisecting: 2 revisions left to test after this (roughly 2 steps)
running ./check-total.sh
Bisecting: 0 revisions left to test after this (roughly 1 step)
running ./check-total.sh
7a2c4e6d8f0b2c4e6d8f0a2c4e6d8f0a2c4e6d8f is the first bad commit
commit 7a2c4e6d8f0b2c4e6d8f0a2c4e6d8f0a2c4e6d8f
style: reformat cart summary template
bisect run success
Git checked out each midpoint, ran the script, read its exit code, and moved on — no human in the loop until the final report. This is the preferred approach whenever a deterministic, scriptable check exists.
Example 3: Skipping an untestable commit
Sometimes a commit in the middle of the range simply can’t be tested — it fails to build for reasons unrelated to the bug you’re chasing (a broken lockfile, a half-finished commit). Marking it good or bad would poison the search, so tell Git to skip it instead:
git bisect start
git bisect bad
git bisect good v1.0
npm install && npm test
# npm install fails here for unrelated reasons; this commit can't be tested
git bisect skip
Output:
Bisecting: 2 revisions left to test after this (roughly 2 steps)
[b2c3d4e5f6a7089b1c2d3e4f5a6b7c8d9e0f1a2b] skipped
Bisecting: 1 revision left to test after this (roughly 1 step)
[9c3d5f70a1b2e4d6f8a0c2e4b6d8f0a2c4e6d8f0] fix: round cart total to 2 decimal places
Git tries a nearby commit instead of the skipped one and continues. If too many consecutive commits get skipped, Git may run out of room to narrow the range and report something like "there are only ‘skip’ped commits left to test" along with a list of possible first-bad commits rather than a single answer — a sign you need to find a way to actually test those commits.
How it works step by step
git bisect startrecords the branch you were on (soresetcan return you there) and initializes internal bisect state.git bisect badandgit bisect good <rev>establish the two ends of the range. Git computes the commit list between them, equivalent togit rev-list good..bad.- Git checks out the commit at the midpoint of that list. This is a detached-HEAD checkout:
HEADpoints straight at a commit SHA, and your branch ref is untouched. The working tree and index are rewritten to match that commit’s tree object. - You (or a script via
git bisect run) test that exact snapshot and reportgood,bad, orskip. - Git updates internal refs (
refs/bisect/bad,refs/bisect/good-<sha>) and appends to.git/BISECT_LOG, then halves the remaining range based on your answer and checks out the new midpoint. - Steps 3–5 repeat until exactly one commit remains untested; Git reports it as the first bad commit along with its diffstat.
git bisect resetchecks your original branch back out, deletes the bisect refs and log, and returns the repository to a normal, non-bisecting state.
Common Mistakes
Forgetting to run git bisect reset
After finding the culprit it’s easy to walk away and start editing files — while still in detached HEAD from the bisect session. Any commits you make there aren’t on any branch and are easy to lose track of.
git bisect reset
Always reset as soon as you have your answer, before doing anything else in the repository.
Bisecting with a dirty working tree
If you have uncommitted changes when you run git bisect start, the repeated checkouts can fail outright, or worse, your local edits can bleed into a commit’s snapshot and give you a false test result. Commit or stash before you begin:
git stash --include-untracked
git bisect start
git bisect bad
git bisect good v1.0
# ... run the bisect session ...
git bisect reset
git stash pop
Marking a broken-build commit as bad
If a commit fails to even build for reasons unrelated to the bug you’re hunting, calling it "bad" sends bisect down the wrong half of history entirely, since it isn’t actually testing for your bug. Use git bisect skip on that commit instead, as shown in Example 3, and let Git try an adjacent one.
Bisecting from a shallow clone
CI checkouts are frequently shallow (git clone --depth 1), which means most of the history bisect needs to walk simply isn’t present locally. You’ll see errors about missing objects partway through the session. Fetch full history first:
git fetch --unshallow
Best Practices
- Prefer
git bisect runwith a deterministic, scriptable test over manual good/bad judgment — it’s faster and removes human error. - Keep commits small and logically focused (one change per commit); a bisect result is only as useful as the commit it lands on, and a giant mixed commit gives you little to act on.
- Tag releases (
v1.0,v1.1, …) so you always have convenient, well-known good/bad boundaries to bisect between. - Use
git bisect skiprather than guessing on commits you can’t actually test. - Make sure your working tree is clean before starting a session; commit or stash first.
- If your history has many merge commits and you only care about the mainline, consider
git bisect start --first-parentto bisect only first-parent history. - Save
git bisect logoutput before resetting if you might need to resume or share the session later. - Always finish with
git bisect resetso you don’t leave the repository in detached HEAD.
Practice Exercises
Exercise 1: Your team’s automated test suite (run via npm test) started failing somewhere in the last 20 commits. You know v2.0 is good and the current main is bad. Write the git bisect run sequence to find the exact offending commit automatically, and figure out which command lets you review the full session history before you reset.
Exercise 2: While bisecting, you land on a commit where npm install fails due to a temporarily broken lockfile unrelated to the bug you’re chasing. Work out the correct response so the search continues without corrupting the result.
Exercise 3: After finding a first-bad commit, save the session’s log to a file with git bisect log > bisect-cart-bug.log, reset your repository, and then figure out the command that would let a teammate replay that exact session on their own machine.
Summary
git bisectfinds the commit that introduced a bug using binary search across the commit DAG, rather than a linear scan.- You mark one known-good and one known-bad commit; Git checks out successive midpoints in detached HEAD state for you to test.
git bisect run <script>automates the entire loop using the script’s exit code (0 = good, 1–127 except 125 = bad, 125 = skip).git bisect skiphandles commits that can’t be meaningfully tested without corrupting the search.- Bisect assumes the bug’s presence is monotonic across the range; flaky or intermittent bugs can produce misleading results.
- Session state lives in
.git/refs/bisect/and.git/BISECT_LOG, which is whygit bisect log/replaycan save and resume a session. - Always finish with
git bisect resetto return to your original branch and clear the bisect state.
