DevOpsInterviewPrep logo
← 🚀 Delivery & GitOps
Foundational

Git bisect: find a regression with a trustworthy test

Find the first bad Git revision with a repeatable predicate. Run a disposable bisect exercise and handle flaky tests, skipped commits and merge boundaries.

TL;DR: Git bisect narrows a known-good to known-bad history using a test that consistently classifies each checked revision. Stabilize that test first; a fast search over unreliable labels produces an unreliable culprit.

Define the regression as an observable predicate

Start with one behavior and two revisions. For example, an invoice calculation returned the agreed value before a deployment and returns a different value afterward. Reproduce that same input against both revisions using comparable dependencies and configuration. A production graph that changed near a commit is evidence for investigation, not yet a bisect predicate.

Bisect repeatedly selects revisions between the bounds and records whether the behavior is good or bad. The Git bisect manual specifies the command and automated exit-status contract. For git bisect run, zero means good; statuses 1 through 127 mean bad except 125, which means the revision cannot be tested. Other exit statuses abort the search.

Missing commands can return 126 or 127, which Git treats as bad. Validate the test environment before trusting automation.

Run an isolated regression exercise

This Bash example creates its own temporary repository. It leaves your current project untouched. Python checks one deliberately simple calculation, and revision five introduces a changed multiplier.

lab=$(mktemp -d)
git init -q "$lab/repo"
cd "$lab/repo"
git config user.name 'Example Engineer'
git config user.email '[email protected]'
cat > ../check.py <<'CHECK'
import runpy
quote = runpy.run_path("price.py")["quote"]
raise SystemExit(0 if quote(4) == 8 else 1)
CHECK
for revision in 1 2 3 4 5 6 7 8; do
  factor=2
  if [ "$revision" -ge 5 ]; then factor=3; fi
  printf 'def quote(units):\n    return units * %s\n' "$factor" > price.py
  git add price.py
  git commit -q --allow-empty -m "revision $revision"
done
good=$(git rev-list --max-parents=0 HEAD)
git bisect start HEAD "$good"
git bisect run python3 ../check.py
git bisect reset

The expected first bad commit is the one named revision 5. The checker lives outside the repository so checkout changes do not replace it. The example uses empty commits to keep the history easy to inspect; real histories contain other changes that the chosen behavior may not exercise.

Do not infer that every line in the identified commit caused the failure. Bisect establishes a behavioral boundary under the test's assumptions. Inspect the diff and validate the proposed fix with a focused regression test.

rendering diagram…

Keep infrastructure failure separate from product failure

ObservationSuitable handlingWhy
Expected calculation is wrongMark badThis is the defined regression
Dependency mirror is unavailableRepair the environment or skipNetwork failure does not classify the code
Revision predates the test interfaceAdapt a compatible checker or skipTest cannot evaluate the chosen behavior
Same revision alternates outcomesInvestigate nondeterminismSingle-run labels are unreliable
Several adjacent revisions cannot buildReport an ambiguous candidate rangeSkips may prevent an exact boundary

A wrapper can map a known build incompatibility to 125, but do not map every test exception to skip. That would hide real failures. Log why a revision was skipped and retain git bisect log so another engineer can reproduce the classification history.

For a flaky regression, define a sampling procedure and state its limits. Repeating a test several times may reduce uncertainty, but a finite run cannot prove that a low-probability failure is absent. A deterministic reproducer is preferable when you can isolate one.

Choose the history that matches the question

A full-history bisect can identify a change developed on a side branch. A first-parent search can instead identify the integration point where the mainline began exhibiting the failure. Those answer different questions. The Git book's debugging chapter illustrates how history-based diagnosis complements inspecting individual changes.

State whether you are finding the originating change or the release integration boundary. Preserve exact dependency versions and build inputs where possible; otherwise rebuilding an old commit with today's dependencies may reproduce a failure that was never present in the original artifact.

Self-check: bisect identifies a commit that changes only a lock file. Can you dismiss it because no application code changed?

No. A dependency change can alter runtime behavior. Reproduce with the selected dependency versions, inspect the lock-file difference and test a minimal correction. Connect the result to backport discipline if an older release needs the fix.

RELATED CONCEPTS
PRACTICE THIS IN REAL QUESTIONS