A regression report often gives two useful facts: the current revision fails, and an older release worked. Testing every commit between them wastes time and encourages guesses. git bisect uses those endpoints to select intermediate commits, but the result is only as reliable as the test used to label each revision.
The real task is therefore not running a binary search. It is designing a stable yes-or-no predicate, controlling the test environment, and preserving enough evidence to challenge the reported culprit.
Define one observable property
Choose a property that each candidate commit can be classified against. “The application is broken” is too broad. Better predicates include:
- a named test exits successfully;
- a request returns a defined status and response field;
- a build produces a particular artifact;
- a deterministic input produces the expected output;
- a measured value stays on one side of a documented threshold.
Keep the environment constant across commits: runtime version, operating system, dependency source, feature flags, test data, and relevant services. If dependency installation resolves new versions at each step, the bisection is testing history plus a moving ecosystem.
Prove the predicate at both endpoints before starting. A “good” endpoint that fails intermittently invalidates the search, while a “bad” endpoint that passes under the test means the predicate does not represent the reported regression.
Start with verified endpoints
Use a clean disposable worktree or clone because bisect checks out many commits in a detached HEAD state. Preserve uncommitted work elsewhere before beginning.
git status --short --branch
git bisect start
git bisect bad HEAD
git bisect good v2.4.0
The tag is a synthetic example; select a commit that you actually tested. The official bisect documentation explains that Git repeatedly chooses a commit between known bad and good points until it narrows the history to the change that introduced the property.
At each selected commit, prepare only what that revision requires, run the same predicate, then mark it:
git bisect good
git bisect bad
Do not classify a commit based on compilation failure if the property under investigation is an application behavior and that historical revision requires a different supported build setup. That commit may be untestable rather than bad.
Automate the predicate with meaningful exit codes
Automation removes operator variation, but it can also repeat a flawed test quickly. Run the script against the two endpoints manually before giving it to Git.
git bisect start HEAD v2.4.0 --
git bisect run ./scripts/check-invoice-rounding.sh
For git bisect run, exit code 0 means good. Codes from 1 through 127, except 125, mean bad. Code 125 means the current commit cannot be tested and should be skipped. Other exit codes abort the run. Those semantics are defined by the Git manual, so a generic test runner may need a small adapter.
A useful adapter distinguishes three outcomes:
- Environment or build incompatibility for that historical commit: exit
125. - The target behavior is correct: exit
0. - The target behavior is reproduced: exit
1.
Do not turn every setup error into 125. Skipping a large or strategically placed part of history can leave Git unable to name one first bad commit. Investigate repeated skips instead of accepting a vague result.
Control state that survives between candidates
Bisect changes tracked files, but external state can persist. A previous candidate may leave a database row, cache entry, generated file, background process, compiled object, or service response that affects the next test.
Make the predicate self-contained:
- create uniquely named disposable test data;
- delete or recreate generated output before the test;
- stop background processes on every exit path;
- use lockfile-respecting dependency installation;
- set explicit locale and timezone when output depends on them;
- bound network calls or replace them with a controlled fixture;
- verify the test actually executed rather than passing because no cases were discovered.
If an old commit cannot run with the current database schema, a versioned fixture or migration-aware setup may make it testable. Avoid applying an unrelated patch silently: the result would describe the historical commit plus an undocumented modification.
Handle merges and noisy history deliberately
The first commit where the predicate fails is not automatically the conceptual origin of the defect. A merge commit can combine individually passing parents into a failing result. Generated files can obscure the functional change. A broad predicate may also be triggered by an earlier refactor and a later configuration change.
Use git show <candidate> and inspect both the patch and its parents. If merge topology is central, compare the merge result with each parent. The --first-parent option can be useful when the question is which integration into the mainline introduced the regression, but it answers a different question from searching every reachable commit.
Path-limited bisection can reduce candidates when the affected subsystem is certain:
git bisect start HEAD v2.4.0 -- src/billing tests/billing
Do not add a path limit merely because the visible symptom occurs there. The cause may be in shared serialization, configuration, authentication, or dependency code outside that path.
Preserve and verify the result
Before leaving the session, capture the decisions:
git bisect log
git show --stat --format=fuller BISECT_HEAD
git bisect reset
git bisect reset returns to the pre-bisect checkout by default. Save the log in the issue or investigation record if it contains no sensitive paths or data. Record the endpoints, environment, predicate command, skipped commits, and final candidate.
Then test the suspected commit and its parent again in fresh state. Reverting or repairing the candidate should make the predicate pass for the expected reason. That final causal check separates a useful bisection from a coincidental green test.
Was this guide useful?
Your rating helps us prioritize clearer, more practical technical content.
Review diffs, run checks, and prepare the release in Arezgit.
Keep Git review, security scanning, API checks, database inspection, and release preparation together in one local desktop application.
Explore Arezgit