Tracking down the source of a bug can sometimes be really tough, especially when it’s somebody else’s bug in somebody else’s codebase.
The heart of my debugging approaches is the principle that it’s much easier to find out what piece of code introduced the bug than it is to actually understand the flow of execution around that bug. Whenever there are two ways of doing something, I choose the path that requires the smallest application of brainpower.
One of my favorite debugging techniques is to use
git bisect, a form of diff debugging.
If you’re not familiar with
git bisect, here’s how it works. You basically tell Git, “Okay, Git, I know that the bug is present now, I know that the bug was not present as of a month ago. Can you help me find out exactly which commit introduced the bug?”
Git will then take you back in time. Let’s say you happen to know that as of 32 days ago, your bug did not exist. In this case
git bisect would take you back to 16 days ago, halfway between now and when the code was good. You then tell Git whether the codebase was “good” or “bad” as of 16 days ago.
Let’s say that, as of 16 days ago, your code was good. Now we know that the bug was introduced sometime within the last 16 days. We’ve narrowed down the “mystery space” from 32 days to 16 days. The next step is that
git bisect will take you to 8 days ago, halfway between now and the most recent good state. This process repeats until you’ve identified the commit that introduced the bug. Git keeps bisecting history until there’s nothing left to bisect. This, in case you didn’t know, is called a binary search.
Once the bad commit is identified, it’s usually much easier to figure out what exactly is going wrong than it would have been to just stare at today’s code and try to ponder its innerworkings.
git revert –no-commit
git revert --no-commit less frequently than
git bisect but I still use it a fair amount. I often use
git revert --no-commit in conjunction with
git bisect after I’ve nailed down the offending commit.
The challenge with
git bisect is that sometimes the bad commit is a really big one. (Big commits are a bad idea, by the way. One reason big commits are bad is that it’s easier for a bug to hide in a big commit than in a small one.) What if 20 files changed in the bad commit? How do you know where the bug lies?
This is where
git revert --no-commit comes in. I of course assume you know that
git revert will add a new commit to your history that’s the exact opposite of whatever commit you specify. That works out great when a commit introduces a bug and nothing else. But what if there’s a commit that introduces both a bug and a set of really important features that can’t be discarded?
git revert --no-commit <SHA of bad commit> will undo all the changes of the bad commit, but it won’t actually make a new commit. (Minor note: after a
git revert --no-commit, the changes will be staged. I always do a
git reset to unstage the changes.)
Once I’ve done my revert and
git reset, I test my application to verify that the bug I’m investigating is not present. Remember, the revert commits the reverse of the bad commit, so undoing the bad commit should make the bug go away. It’s important to verify that the bug did in fact go away or else we’d be operating off of bad premises.
After I’ve verified that the bug is not present, I do a
git diff to see what exactly the commit changed. I usually discover that there are some changes that are clearly superficial or cosmetic and don’t have anything to do with the bug. If there’s a whole file that contains irrelevant changes, I
git checkout that file.
Let’s say that I have 20 modified files after I do my
git revert --no-commit, and let’s say that 15 of them contained clearly superficial changes. I would
git checkout all 15 of the irrelevant files and then verify again that the bug is not present. Now my “mystery space” has been narrowed down to 5 files.
Let’s say that this is a codebase I don’t understand at all, so studying the code is fairly useless to me, or at least a very slow way to make progress. Out of these 5 files that might contain the bad code (which we’ll call files A, B, C, D and E), I would probably do
git checkout A and see if the bug is still not present. If the bug is still not present, I’d do
git checkout B and see if that brought the bug back or not. I’d continue to do this until I found which file contains the bug-introducing code.
Let’s say that after all my investigation I’ve determined that file E contains the code that introduced the bug, but I still don’t know exactly which part. I’d then do a manual micro-bisect. I’d comment out half the file and test for the bug. Then I’d uncomment half of what’s commented. Then I’d uncomment half of that, and so on, until I found the offending code.
By this process I can usually identify and fix a bug, even if I have no understanding of the codebase.