• Terms of Service

Learn Internet Grow

Tech Skills

  • Learn
  • Books
  • Search
  • Create
  • Measure
  • Secure
  • Share
  • Terms of Service

git bisect Explained: Binary Search Over a Partial Order

Filed Under: Search

Every engineer has run git bisect at least once and walked away with the right commit and the wrong mental model. It feels like binary search — mark “good,” mark “bad,” repeat until the range collapses — so most people assume it’s cutting a numbered list in half, the way you’d binary-search a sorted array. It isn’t. Git’s history is a directed acyclic graph: merges give commits more than one parent, topic branches run concurrently, and there is no single line to cut in half. git bisect has to compute a midpoint before it can use one, and the algorithm that computes it — the reachability scoring behind git rev-list --bisect — is the actual clever part, not the “keep halving” framing everyone remembers.

That distinction matters beyond trivia. Once you understand what bisect is really doing, you know exactly when it’s safe to trust, exactly how it degrades, and exactly why the same technique generalizes to problems that have nothing to do with source code.

git bisect explained — a branching commit graph with no single middle, one node glowing brighter as the algorithm's computed candidate

Why a DAG Doesn’t Have a Middle

In a sorted array, “the middle” is index N/2 — a position, computed by arithmetic on indices. Git’s history has no positions, only a partial order defined by ancestry: commit A precedes commit B only if A is an ancestor of B along some path through the graph. Two commits on different branches, or two commits that both feed into a later merge without either descending from the other, aren’t comparable at all — neither is “before” the other in any total sense. Ordinary binary search’s guarantee — eliminate roughly half the remaining candidates on every test — depends on a comparison operator you can trust to split the search space evenly. On a DAG there is no such < sitting around waiting to be used. Bisect has to build one, from scratch, on every step, out of reachability.

A straight numbered line beside a branching commit graph, showing why ancestry has no index arithmetic the way a sorted array does

The construction starts by narrowing the field. The candidate set is every commit that could plausibly be the first bad one: reachable from the known-bad commit, but not reachable from any known-good commit. In plumbing terms, that’s precisely git rev-list <bad> --not <good1> <good2>…, the same git-rev-list machinery Git’s revision-walking commands are built on. Call the size of that filtered set N. This is the step most explanations skip past — bisect doesn’t search “all of history,” it searches only the region genuinely still in question, which is why a wide, well-behaved history collapses fast even when the repository behind it is enormous.

git rev-list --bisect: The Scoring That Replaces “The Middle”

For every candidate commit X still in the set, Git counts how many of X’s own ancestors also fall inside the set — call that ancestors(X) — and scores X as:

score(X) = min(ancestors(X), N − ancestors(X))

Read it as: the number of commits guaranteed to be eliminated in the worst case, whichever way the next test comes back. If X turns out good, everything among X’s ancestors(X) ancestors is now known good, and drops out of the set. If X turns out bad, everything not among X’s ancestors is now known bad-or-irrelevant, eliminating N − ancestors(X) of them. The score is the smaller of those two guarantees — the worst case Git can promise you, not the best case it hopes for. Git checks out whichever candidate has the highest score, because a higher score means a tighter worst-case guarantee no matter which way the test lands.

That score can never exceed N/2 — you cannot guarantee eliminating more than half of an as-yet-unclassified set with a single yes/no test — so the moment a candidate’s score actually hits N/2, the search has found the DAG’s genuine structural middle for this step, and nothing scores higher. git rev-list --bisect is the plumbing command that runs this exact computation and hands the porcelain git bisect the single commit to check out next. Two flags make the otherwise-invisible math inspectable: --bisect-all lists every remaining candidate annotated with its own dist= score, so you can see the whole ranked field instead of trusting the winner blindly, and --bisect-vars emits the same computation pre-formatted as shell-assignable variables — bisect_rev (the next commit to test), bisect_nr and bisect_all (candidates remaining and total), bisect_good/bisect_bad, and an estimate of remaining steps — which is what the git bisect porcelain itself sources internally to drive the loop, according to gitrevisions and the git-rev-list documentation.

git rev-list --bisect ancestor scoring — a cloud of candidate commits dividing into two even groups around the highest-scored node

That’s the whole trick, stated plainly: “the middle” isn’t a position bisect looks up, it’s a property of reachability that bisect recomputes from the current candidate set on every single step.

The Bisect Command Surface

CommandWhat it does
git bisect start [<bad> <good>…]Begin a session, optionally supplying the endpoints immediately
git bisect bad [<rev>]Mark a revision (default: current checkout) as exhibiting the problem
git bisect good [<rev>]Mark a revision as not exhibiting the problem
git bisect skip [<rev>…]Mark the current or named revision untestable; Git picks a different candidate
git bisect run <cmd>Automate the good/bad loop by executing <cmd> at every checkout
git bisect visualize (view)Open the remaining candidate set in gitk or git log form
git bisect log / git bisect replay <file>Save a session to a file, or restore one verbatim from a saved log
git bisect resetEnd the session and return to the branch/commit you started from

Everything in that table is porcelain wrapped around the same git rev-list --bisect scoring described above — the commands manage state (which commits are known-good, known-bad, or skipped) and delegate the actual “what to test next” decision to the plumbing every time.

Why It Lands Near O(log₂ n) in Practice — and Why That’s Not a Proof

On a linear history the ancestor-counting scoring collapses into ordinary binary search: every commit’s ancestor count is just its position in the line, so the highest-scoring candidate really is the numeric midpoint. That’s the mental model most people carry into bisect, and on a straight line it happens to be correct.

Real repositories aren’t linear — merges branch and rejoin constantly — yet Git’s own write-up on the algorithm, git-bisect-lk2009, reports that real-world bisections at Linux-kernel scale typically resolve in 15 to 20 steps regardless of the size of the range being searched. That’s the empirical signature of roughly logarithmic behavior holding up on genuinely merge-heavy history, not just on toy linear cases.

But “close to O(log n) in practice” and “provably optimal” are different claims, and collapsing them is the single most common overstatement made about bisect. A 2023 paper by Courtiel, Dorbec, and Lecoq, Theoretical analysis of git bisect (arXiv:2312.13644), formalizes the greedy min(ancestors, N−ancestors) strategy as an algorithm and proves it is not globally optimal on general DAGs — there exist adversarially shaped DAGs where the greedy per-step choice forces exponentially more tests than an optimal strategy would need. The same paper is careful to draw the boundary where that matters: on DAGs of bounded in-degree — graphs where no commit has more than a small, fixed number of parents, which is exactly what ordinary merge history looks like, since merges essentially never combine more than a couple of branches at once — the greedy strategy stays within a constant factor of optimal, roughly 1/log₂(3/2) ≈ 1.71×, and the authors propose a provably better alternative specifically for that restricted case.

The honest framing an architect should carry away: git bisect is a very good heuristic, well matched to the shape of DAGs real version-control systems actually produce, not a proven-optimal search algorithm in the general case. That gap between “good heuristic” and “optimal algorithm” is worth internalizing well past Git — it’s the right way to reason about any plausible-sounding greedy strategy headed for production. Characterize the input distribution you actually expect; don’t assume a worst-case guarantee you never proved.

The Assumption Everything Rests On: A Monotone Predicate

Every step above assumes something bisect never checks for you: that “bad” is upward-closed from wherever it was introduced. Once a commit is bad, every descendant of it must also be bad; once a commit is good, every ancestor of it must also be good. That’s a monotone predicate over the DAG — exactly one good-to-bad transition along any path from a known-good commit to the known-bad one.

A broken monotone predicate — a commit path shifting good to bad, back to good, and back to bad instead of one clean transition

The classic way to break it: a bug is introduced, then fixed, then reintroduced later. Bisecting across that whole span no longer sees a single transition — it sees good → bad → good → bad — and the ancestor-counting scoring has no way to detect that it’s happening. Depending on which candidates the search happens to land on, bisect can converge on the reintroduction commit, the original introduction commit, or something in between that tested bad purely by which path got walked, and it reports whichever wrong answer it lands on with exactly the same confidence as a right one. The git-bisect documentation describes the workaround directly: graft in a synthetic branch via git replace that removes the confounding fix-and-reintroduce pair from the history bisect actually walks, restoring a single clean transition before you start.

Flaky tests are a related, unsolved gap. If the predicate — usually a test command — is nondeterministic, one unlucky “bad” result on an actually-good commit corrupts the entire session, and core Git has no built-in retesting or confidence tracking to catch it. That’s precisely why external tools exist purpose-built for noisy predicates (Bayesian search that tolerates uncertain results, instead of deterministic reachability scoring that assumes every test result is ground truth) — a signal that “monotone and deterministic” is a hard requirement for handing a search to bisect, not a nice-to-have.

git bisect skip: Falling Back Toward the Center, Not the Next-Best Score

Some commits genuinely can’t be tested: they don’t build, a dependency didn’t exist yet at that point in history, the environment isn’t set up for that revision. git bisect skip marks the current candidate untestable and asks for a different one to try instead. The natural assumption is that Git simply offers the next-highest-scoring commit from the same ranked list. It doesn’t. Untestable commits tend to cluster — a broken build usually stays broken across a run of consecutive commits — so handing you the next-best-scored neighbor would likely land you on another untestable commit immediately, wasting the step. Instead, git-bisect-lk2009 documents a deliberately different fallback: the choice is biased pseudo-randomly, weighted via a square-root transform of a random draw, back toward the center of the remaining candidate range — a purposeful jump clear of the suspected bad neighborhood rather than a creep along its edge.

--first-parent compounds usefully with this. Restricting bisect to first-parent history treats an entire merge as a single unit of blame, which is the practical way to avoid false “untestable” hits from a bisect step that lands mid-way inside someone else’s topic branch — commits that were never meant to build in isolation and were only ever meant to be evaluated as a finished merge.

git bisect skip fallback — an untestable cluster of commits set aside as the next candidate jumps back toward the center of the range

git bisect run: Full Automation and Its Exit-Code Contract

Once you have a scriptable reproduction of the problem — a failing test, a build check, a benchmark threshold — git bisect run <cmd> checks out every candidate the scoring picks, executes <cmd>, and interprets its exit code without a human anywhere in the loop.

Exit code from <cmd>What git bisect run does
0Treated as good — the commit does not exhibit the problem
1–124, 126, 127Treated as bad — the commit exhibits the problem
125Treated as untestable — equivalent to git bisect skip
Anything outside 0–127 (128+, e.g. killed by a signal)Aborts the bisection entirely — not recorded as a data point

125 is a deliberate, unusual choice, not an accident of implementation — it sits outside the range most build tools and shells use for their own ordinary pass/fail codes, which keeps a script’s normal exit conventions from accidentally colliding with bisect’s own. A working bisect run command is typically a small wrapper: build, then run the reproduction, exit 0 on success, exit 1 (or anything else in the bad range) on the reproduced failure, and exit 125 whenever the build itself fails, so a broken build never gets miscounted as a bad code change. Left alone, bisect run drives exactly the scoring and skip machinery described above to completion and reports the first bad commit directly — the whole algorithm this article has walked through runs unattended, purely off those exit codes, per the git-bisect documentation.

git bisect run automation — an unattended chain of commits being checked and resolved to good or bad with no human present

Once bisect has named a commit, the natural next move is finding out why it changed the behavior — which is a different query with its own machinery. git log -S<string> and -G<regex> (the “pickaxe,” documented alongside rename and copy detection in git-diff) search diff content rather than commit reachability, and pairing a bisect result with a pickaxe search over the same file is a common way to go from “which commit” to “which line, and why.” History-simplification defaults in git-log — the TREESAME pruning that governs what git log -- path shows by default — are worth knowing about here too, since a bisected commit’s own history view can quietly omit contributing commits from a merged-away branch unless you ask for --full-history.

Beyond Source Code: Bisecting Any Monotone Predicate Over a Version DAG

None of the machinery above is actually specific to source code. What git bisect automates is binary search over any monotone predicate defined on an ordered or partially ordered space, driven by a scoring function that computes which candidate splits the remaining uncertainty most evenly — instead of assuming a total order hands you a midpoint for free. Anywhere you’re tempted to binary-search a non-total-order structure, that’s the transferable move: build a “which candidate eliminates the most in the worst case” score, don’t assume sorting by some attribute gives you one automatically.

The same branching search structure repeated across three different materials, representing bisect applied beyond source code to configuration, data, and deploys

Any system that produces a version DAG and a testable predicate qualifies. Configuration history — which change to a feature flag or an infrastructure setting crossed a canary threshold. Data-pipeline history — which upstream schema or dataset revision broke a downstream job. Deployment history — which release in a chain introduced a latency regression. The moment that state is checked into Git and the predicate is scriptable, git bisect run can be pointed at any of these exactly as it would be pointed at a failing unit test. That generalization is worth sitting with: automated binary search over a version-controlled DAG is a debugging superpower most engineers only ever apply to one narrow case — source code — when the underlying technique applies to anything monotone and ordered. It pairs naturally with the discipline of reading code as its own skill: bisect tells you which commit, a careful read of the diff tells you why, and neither replaces the other. It’s also the same instinct that makes DAGs worth understanding as graph theory in the first place — a partial order is a graph problem the moment you stop assuming it’s a line.

What This Means for You

  • Treat git bisect as reachability scoring, not sorting. The “middle” it finds on any given step is a property of the DAG, recomputed from the current candidate set — not a cached position.
  • Don’t cite bisect’s speed as proof of optimality. Cite it as “close to logarithmic in practice on bounded-fan-in DAGs,” and know the 2023 counterexample exists for adversarially shaped ones.
  • Before bisecting a suspected fix-then-reintroduce bug, confirm you actually have a single good→bad transition. If you don’t, graft around the confound with git replace rather than trusting whatever commit the search happens to land on.
  • Wire bisect run predicates to exit 125 for genuinely untestable states — don’t let a broken build masquerade as a bad code change and pollute the search.
  • Look past source code. Any monotone predicate over a version DAG — config, data, deploys — is a bisect run candidate the moment it’s checked into Git and scriptable, whether or not the “commits” are code at all. If you’re automating that kind of regression hunt as part of a larger test harness, the same discipline that makes test suites trustworthy enough to bisect against is what makes the predicate itself safe to hand to a machine, the same way mutation testing checks whether a test suite would actually catch the regression bisect is hunting for.

📚 Further Reading

  • git-bisect — the command reference: subcommands, --first-parent, terminology options, and the exit-code contract for bisect run.
  • git-bisect-lk2009 (“Fighting regressions with git bisect”) — the detailed algorithm write-up this article draws its scoring and skip-fallback explanation from.
  • Courtiel, Dorbec & Lecoq — “Theoretical analysis of git bisect” (arXiv:2312.13644, 2023) — the proof that greedy bisect is not globally optimal on general DAGs, and the bounded-fan-in result that explains why it works so well in practice anyway.
  • git-rev-list — the plumbing bisect runs every step: --bisect, --bisect-all, --bisect-vars.
  • gitrevisions and git-log and commit-graph — revision syntax, history-simplification defaults, and the generation-number invariant that makes ancestry checks like bisect’s fast at scale.
  • Reading Code Is the New Superpower Skill for Engineers — what to do with the commit bisect hands you.
  • Learning the Brilliance of Graph Theory — the DAG intuition underneath everything in this article.

❓ Frequently Asked Questions

Is git bisect actually a binary search?

Yes, but not the array kind. It’s binary search over a partial order: instead of an index arithmetic midpoint, every step computes a score — min(ancestors(X), N − ancestors(X)) for each remaining candidate X — and checks out whichever commit that score says splits the current uncertainty most evenly. On a linear history the two definitions happen to coincide.

How many steps does git bisect actually take?

Close to O(log₂ n) in practice. Git’s own documentation reports real-world bisections on Linux-kernel-scale history typically resolving in 15 to 20 steps, regardless of how large the searched range is. That’s not a worst-case guarantee, though — see the next question.

Is git bisect‘s algorithm provably optimal?

No, and this is the most commonly overstated claim about it. A 2023 paper (arXiv:2312.13644) proves the greedy scoring strategy is not globally optimal on general DAGs — adversarial shapes exist where it needs exponentially more tests than an optimal strategy. On bounded-fan-in DAGs, the shape ordinary merge history actually produces, it stays within roughly a 1.71× factor of optimal, which is why it performs so well on real repositories despite the negative general-case result.

What breaks a git bisect run?

A predicate that isn’t monotone — most commonly a bug that was introduced, then fixed, then reintroduced, which creates more than one good→bad transition along the searched path. Flaky, nondeterministic tests are the other major failure mode: one wrong result corrupts the whole session, and core Git has no built-in mechanism to detect or retry it.

What does git bisect skip actually do?

It marks the current commit untestable and asks for a different candidate — but not simply the next-best-scored one. Because untestable commits cluster (a broken build tends to stay broken for a run of nearby commits), the documented fallback biases the next pick pseudo-randomly back toward the center of the remaining range, deliberately jumping clear of the suspected bad neighborhood.

What exit codes does git bisect run expect from my script?

0 for good, any code from 1 to 127 except 125 for bad, and 125 specifically for “this commit can’t be tested” (treated like git bisect skip). Any other exit code — typically a signal-killed process at 128 or above — aborts the whole bisection instead of being recorded as a result.


📚 Recommended Resources

Disclosure: some links below are Amazon affiliate links. If you buy through them, I earn a small commission — at no extra cost to you — which helps keep this site running.

Bisect is a search algorithm wearing a debugging tool’s clothes. These are the books for going deeper on both halves of that — the discipline of isolating a regression in code you didn’t write, and the internals git rev-list --bisect is actually built on.

  1. Working Effectively with Legacy Code — Michael Feathers · Book. The canonical text on getting unfamiliar, undertested code safely under control — the exact situation you’re usually in when a bisect run predicate has to be written against code you didn’t author.
  1. Refactoring: Improving the Design of Existing Code (2nd ed.) — Martin Fowler · Book. A catalog of small, named, safe transformations — the same discipline that keeps a commit history bisectable in the first place, since bisect only works cleanly against a history of small, single-purpose changes.
  1. Version Control with Git (3rd ed.) — Prem Kumar Ponuthorai & Jon Loeliger · Book. The deepest print treatment of Git’s command surface and plumbing layer, including the revision-walking machinery git bisect sits on top of.
  1. Pro Git (2nd ed.) — Scott Chacon & Ben Straub · Book. Also free in full at git-scm.com/book under CC BY-NC-SA 3.0 — read it there first. The print edition is a convenient reference if you’d rather have it off-screen; chapter 10 (“Git Internals”) is the closest match to this article’s register.

For the algorithm itself rather than the porcelain around it, James Coglan’s Building Git rebuilds Git’s internals from scratch in Ruby, chapter by chapter — no Amazon listing exists for it (it’s sold ebook-only, direct from the author). If that angle interests you, it’s worth buying directly from shop.jcoglan.com/building-git — a non-Amazon purchase, no affiliate link here.

Newsletter

Tags

Agentic AI Agentic Development AI AI Agents AI Coding Agents Artificial intelligence Backlinks Blockchain Bounded Context Browser ChatGPT Plugins Claude Code CLPR Context Engineering Cross-Chain Cryptocurrency Developer Tools Distributed Systems DLT Domain-Driven Design Email Google Search HBAR Hedera Hedera Hashgraph Internet LLM Machine Learning MCP Microservices Model Context Protocol News NLP Orchestration Photoshop Productivity Saga Pattern SEO Smart Contracts Software Architecture software engineering Twitter Typescript Web3 Writing

Recent Articles

  • QUIC and HTTP/3: The Protocol That Won Is Being Routed Around October 11, 2026
  • The Mark I Perceptron: The Machine That Learned to See October 11, 2026
  • GraphRAG vs Vector RAG: Knowledge Graphs for AI, Explained October 11, 2026
  • Cardano Tokenomics: How ADA’s Supply Actually Works October 10, 2026
  • Transformer Encoder vs Decoder: How to Read the Diagram October 10, 2026
  • Nobody Checks the Verified Badge: The Real State of Git Commit Signing October 10, 2026

Copyright © 2026 · News Pro Theme On Genesis Framework · WordPress · Log in

Content on this site is produced with the assistance of Large Language Models and edited by a human. It is intended for educational purposes — facts may occasionally be incorrect. If you spot an error, please email Larionovdmitri@learninternetgrow.com.

Affiliate Disclosure: This site contains affiliate links to Amazon and other partners. If you purchase through these links, LearnInternetGrow.com may earn a commission at no additional cost to you. We only recommend products we genuinely believe in.

Privacy Policy · Terms of Service