Stockio

Git Bisect: Find Bugs Fast With Regression Tests

Find the commit that introduced a regression using git bisect, a repeatable test, and safe handling of revisions that cannot be built or evaluated.

By Grace Chen

AI Tools Editor ·

On this page

A regression appeared somewhere between last month's release and today's branch. Reading every intervening commit is tempting, but it substitutes suspicion for evidence. Git bisect narrows the history by testing selected revisions until it identifies the boundary between working and broken behavior.

Define an observable regression first

Before starting, write a statement that can be checked without interpretation: requesting the fixture product returns a numeric price, the settings panel opens, or a particular unit test passes. Avoid labels such as “the application feels wrong.” A binary outcome is what makes the search useful.

For a malformed API response, use the JSON formatter to inspect sanitized good and bad fixtures. Compare field names, primitive types, and missing values. Save the resulting minimal fixture locally as test input; formatting helps inspection but does not validate your application's business contract.

Choose one regression at a time. If one revision has a rendering failure and another cannot contact your development database, those are not automatically the same bad outcome. Document the exact command, input, and expected result. The Git bisect manual describes the search mechanics, but the quality of the predicate determines whether the answer means anything.

Establish clean, verified endpoints

Commit or stash unrelated work before switching revisions. Better still, use a separate worktree when you want your main checkout untouched. Git bisect changes the checked-out revision repeatedly, usually leaving you on a detached HEAD during the search. Do not treat that temporary state as your regular development branch.

Identify a known bad commit and a known good ancestor, then test both using the same procedure. A release tag is not proof of good behavior if nobody checked this particular case. Make sure the older revision's runtime, dependencies, and required services are available.

git status --short
git bisect start
git bisect bad HEAD
git bisect good v2.4.0
#Run the regression check at the selected revision.
git bisect good
#Repeat the check after Git selects another revision.
git bisect bad

The final two commands are examples of classifications, not commands to run blindly. Mark each selected revision only after observing the test result. With a straightforward history, each answer removes roughly half the remaining candidates, which is why bisect is effective even across many commits.

For an introduction to preserving clean history and checkpoints, see the existing solo Git workflow guide. Bisect rewards small, testable commits, but it can still be useful in an imperfect repository.

Keep the experiment consistent across revisions

A historical checkout does not reset your database, caches, ignored build output, or installed dependencies. These can make an old revision seem broken for reasons unrelated to the regression. Record the test environment alongside the predicate: runtime version, package manager, fixture data, and any required feature settings.

Install from each revision's lockfile using the repository's appropriate frozen or clean-install mode. Remove only known generated output, not arbitrary files. Avoid broad destructive cleanup commands unless you understand exactly which ignored files they will delete. Local credentials and untracked research notes can otherwise disappear.

Keep your regression checker outside the changing checkout, or somewhere you deliberately preserve. A test committed only on the newest branch may vanish when Git checks out an older commit. Prefer a tiny checker that uses interfaces present throughout the range, such as an HTTP endpoint or stable module entry point.

For browser behavior, standardize browser version, viewport, input sequence, and starting state. Disable unrelated network dependencies where possible. Run a suspected flaky outcome several times before using it as evidence. Bisect cannot compensate for an unreliable test by selecting cleverer commits.

Automate classification with git bisect run

Once the manual procedure works, automate it. A bisect command reports exit status 0 for good, statuses 1 through 127 except 125 for bad, and 125 for skip. Other exit statuses abort the automated run. Account for tools that use nonzero statuses for infrastructure failures rather than regression failures.

A simple wrapper can distinguish an installation failure from a failed regression assertion:

#!/usr/bin/env bash
set -u

npm ci --ignore-scripts || exit 125
node /absolute/path/check-price.cjs
result=$?

case "$result" in
0) exit 0 ;;
1) exit 1 ;;
*) exit 125 ;;
esac

This example assumes the external checker deliberately returns 1 only for the target regression. The installation command also assumes the repository can be tested without lifecycle scripts; remove that option only after reviewing the scripts and identifying required setup. A blanket mapping of every failure to bad would contaminate the search.

Run the wrapper with git bisect run bash /absolute/path/check-regression.sh. Watch the first few iterations rather than walking away immediately. Consult the Git worktree documentation if isolation is needed, and keep the checker compatible with older dependency layouts.

Handle skips, merges, and the final suspect

Use git bisect skip when a revision cannot be evaluated, such as a temporary build break unrelated to your symptom. Skipping is honest, but too many skips can leave several possible first-bad commits. Report that ambiguity rather than declaring the first displayed hash guilty.

Merge-heavy histories deserve extra care. A failure can emerge from interactions between branches rather than one obvious changed line. Options such as first-parent traversal answer narrower questions about integration history; choose them intentionally, not as a universal speed improvement. The Git revision syntax reference helps explain which ancestry your endpoints include.

When Git identifies a candidate, inspect its diff and test its parent under the same conditions. First bad commit means the observed boundary for your predicate, not necessarily the author's conceptual mistake. A dependency update, changed fixture, or previously hidden assumption may expose an older defect.

Save the search with git bisect log, then run git bisect reset to return to your starting state. Add a regression test and record the environment and reasoning in the fix. More development guidance is useful after diagnosis, but the strongest handoff is a reproducible failing case, not merely a commit hash.

Key takeaways

  • Verify both endpoints with the same concrete test.
  • Keep dependencies, fixtures, and generated output under deliberate control.
  • Automate only after manual classification is trustworthy.
  • Skip untestable revisions instead of calling infrastructure failures bad.
  • Confirm the candidate and its parent, then preserve a regression test.
Featured sponsor

Reach designers & developers at work

Put your product next to the tools people use every day. One sponsor per placement, no tracking pixels.

Become a sponsor

Keep reading

Related articles

More in Dev Tools & AI Coding
Dev Tools & AI Coding

8 Best AI Code Review Tools

Compare eight AI-assisted code review tools for pull requests, security checks, and repository workflows, with practical cautions about false alarms, context, and human responsibility.

Includes: JSON formatterSofia MarchettiSofia Marchetti
Dev Tools & AI Coding

15 Best VS Code Extensions for Web Development

Find useful VS Code extensions for formatting, debugging, writing and remote work. This practical list explains what each add-on does, who benefits, and when built-in features are enough.

Includes: JSON formatterSofia MarchettiSofia Marchetti
Dev Tools & AI Coding

Turborepo Monorepo With pnpm Workspaces

Set up pnpm workspace boundaries and Turborepo task caching with explicit package dependencies, reliable outputs, and a structure that stays understandable as teams grow.

Includes: JSON formatterSofia MarchettiSofia Marchetti
Dev Tools & AI Coding

Chrome DevTools Performance Panel: From Trace to Fix

Record focused Chrome Performance traces, identify expensive main-thread work, and verify targeted fixes with repeatable scenarios instead of guessing from a flame chart.

Includes: JSON formatterRavi DesaiRavi Desai
Dev Tools & AI Coding

direnv and .env Files: Managing Local Environment Variables

Make local project configuration predictable with .env templates, explicit loading rules, direnv approval, and checks that catch missing values before startup.

Includes: JSON formatterRavi DesaiRavi Desai
Dev Tools & AI Coding

AI Tailwind CSS: Get Clean Styles From AI Assistants

AI assistants can produce a convincing interface quickly, but the stylesheet often reveals how little was specified. Arbitrary spacing, duplicated utilities and brittle positioning accumulate when every prompt.

Includes: JSON formatterRavi DesaiRavi Desai