Finding the Commit That Broke Things

Regression bugs are defects that surface in functionality that previously worked. When you are chasing one down, the fastest route to the root cause is often to identify exactly which change in the codebase introduced the problem. Examining that specific change can provide far better clues than staring at the current state of the code. This practice has no widely agreed-upon name, but a fitting one is diff debugging.

The prerequisite for this technique is version control, which is standard practice today. However, two additional conditions make it truly effective. First, you need a reproducible build so that you can compile and run any historical revision without trouble. Second, you benefit greatly from a history of small, frequent commits. When the guilty commit is isolated, a small commit makes it much easier to see the exact logic that changed.

The Search Procedure

The investigation starts by identifying two reference points: a known past version without the bug (the last-good) and the current broken state (the earliest-bad). Next, locate the commit that sits halfway between these two points in history and test it. If the bug is present, that commit becomes the new earliest-bad; if the bug is absent, it becomes the new last-good. Repeating this halving process—a classic binary search—will converge on the single offending commit.

With Git, the git bisect command automates most of this workflow. If you can write a test that reliably detects the bug, you can hand that test to git bisect and let it run the entire search automatically to flag the guilty commit.

Diff Debugging Inside a Session

This technique is not limited to large-scale archaeology across weeks of history; it also pays off within a single day of coding. When working with a test suite that takes minutes to run, it is common to work for a while executing only a fast subset of relevant tests. As long as you commit after every green run of that subset, you have the safety net to diff-debug if a slower test later fails. Committing this frequently—even if the changes are so small that you later squash them for the permanent history—makes it trivial to isolate the change that caused the failure. Some IDEs support this pattern further by maintaining a finer-grained local history that exists outside the formal commit log.