Last time we looked at what actually happens between git add and git commit — the mechanics of the moment a commit gets created. This time we shift focus to a different kind of question: given a long history of past commits, how do you track down the one that introduced a specific bug? git blame and git bisect both dig into that history, but they’re suited to very different situations.
git blame: following one line’s history directly
Running git blame <file> lists, for every line in that file, the most recent commit that touched it — hash, author, and date included. If you already suspect a particular line is causing a problem, blame tells you exactly when it was introduced and by which commit.
git blame -L 40,60 core/db_backup_diagnostics.py
Narrowing the range with -L keeps the search fast since you don’t have to scan the whole file. From there, feeding the commit hash into git show <hash> reveals the full commit message and diff that line was part of. The limitation of blame is that it tells you when a line was written, not whether that line is actually responsible for the bug you’re chasing. If you don’t yet know which line is at fault, blame isn’t the right starting point.
git bisect: binary-searching between a known-good point and a known-bad one
git bisect is the tool for when you don’t know which commit — or even which file — caused the regression. The idea is simple: mark one commit you know was working (“good”) and one you know is currently broken (“bad”). Git then checks out the commit sitting roughly halfway between them. You test that commit and tell Git whether it’s still broken or not, and the remaining range gets cut in half again.
Note: binary search means repeatedly checking the midpoint of a range and discarding the half that can’t contain the answer. Even with 100 candidates, this narrows things down to one in roughly seven checks (2 to the 7th power is 128).
git bisect start
git bisect bad # the current commit (HEAD) is broken
git bisect good v1.6.8 # this tag was known to work
# → Git checks out a commit roughly halfway between the two
# test it, then report the result:
git bisect good # if it still works
git bisect bad # if it's broken
# this repeats automatically until one commit remains
git bisect reset # return to your original branch when done
Even across hundreds of candidate commits, this cuts the number of commits you actually need to check down sharply compared with testing them one by one in order.
Automating it: git bisect run hands the verdict to a script
You don’t have to eyeball good/bad by hand every time. If you have a script or test that reliably reproduces the bug, pass it to git bisect run <command>, and Git will execute that command at each candidate commit automatically — treating exit code 0 as good and anything else as bad.
git bisect start
git bisect bad HEAD
git bisect good v1.6.8
git bisect run python3 -m unittest tests.test_db_backup_diagnostics
Give it a valid starting good/bad pair, and Git drives the whole search to the culprit commit on its own. The catch is that your test needs to reliably reproduce the exact failure you’re chasing. A flaky check (one that sometimes fails for unrelated reasons) breaks the core assumption of binary search — that one side of the range is definitively good and the other definitively bad — and can lead bisect to converge on the wrong commit.
A real example: tracking down a shell-dependent parsing bug
There was a past issue where DB backups reliably failed on hosts using csh-family login shells, such as Sakura Internet’s. The cause turned out to be a progress-log output routine added at some point that implicitly assumed a bash-style environment; under csh, the log format came out differently and parsing broke. This is a textbook case for bisect: you know exactly when the symptom appeared (once a csh-environment report came in), but the actual commit responsible could be anywhere across dozens or hundreds of commits further back. With a test that reproduces the csh environment, git bisect run can chase down the offending commit without manual intervention at every step; without one, you’d answer good/bad by hand after testing on that environment each time. Once bisect narrows things to a single commit, git blame (or git show on that commit) is the natural next step to understand what the change was trying to do and how to fix it properly.
When to reach for which
| Situation | Command |
|---|---|
| You already suspect a specific line and want to know when it was written | git blame |
| Something is broken, but you don’t know which line or commit caused it | git bisect |
bisect narrowed it to one commit and you want to understand the intent behind it |
git blame / git show <commit> |
Once bisect narrows a regression down to a single commit, reading that commit’s diff with git show — and tracing the same lines further back with blame if needed — is the natural next step. It helps to think of bisect as narrowing the range and blame as digging into one specific point; they cover different stages of the same investigation.
Summary
git blame tells you exactly when a specific line last changed, but it’s no help if you don’t yet know which line is at fault. git bisect only needs a known-good and a known-bad commit to binary-search its way to the culprit, and pairing it with a reliable test via git bisect run lets the whole search run unattended. This connects back to last time’s point about staging — commits are fixed-in-time snapshots, and it’s precisely because of that property that you can binary-search backward through them to find which snapshot introduced the problem.