For almost any non-trivial software development project, bugs are an inevitable fact of life. Although software teams can, and should, use unit and acceptance testing to reduce the frequency of bugs, from time to time a developer in the team will introduce a bug to the codebase.
The reasons that bugs exist are numerous and varied. Save for teams working on the most mission-critical software projects, that will use formal verification and perform extensive reviews of any dependencies, most teams need a practical approach to discovering and fixing bugs during a project’s lifetime.
In my experience, most bugs are simple to fix. The difficult part is finding the root cause of a bug and understanding why a particular behaviour manifests itself. This is made even more difficult if a bug is introduced as an unwanted and undocumented side effect of a dependency.
Typically, when a bug is discovered, team members will start hypothesising on its cause. A developer will try to replicate the bug and find its root cause in the most recent state of the codebase. In other words, she will do her investigative work on the most recent commit on the master, develop or other branches where she sees the bug. This strategy is not ideal since developers often waste time chasing the wrong leads.
Rather than working on gut instinct, a developer should first find the commit that introduces a bug to the codebase. She should identify the single commit where the bug first appeared. Fortunately, git comes with an awesome but underused tool called git bisect. This tool uses a binary search between the last known ‘good commit’ and the most recent ‘bad commit’ to pinpoint the precise commit in a project’s history that introduces the bug.
Using a binary search to locate a bug sounds scary at first, but git bisect makes it very simple. You start a new ‘bisect session’ with the ‘git bisect start’ command. Mark your current commit as bad with ‘git bisect bad’ and mark a previous commit as good with ‘git bisect good <rev>’.
git bisect uses a divide and conquer strategy to discover the first bad commit in the project history. Roughly speaking, it will choose a commit halfway between the newest ‘good’ commit and oldest ‘bad’ commit and ask you whether you can observe the bug. You mark the commit as good or bad, which divides the search space in half, and the process continues until it finds the first bad commit. If you land on a commit where you cannot build the project, you can also use ‘git bisect skip’.
Because of the binary search nature of the algorithm, the number of commits you have to check scales logarithmically. If there are 100 commits between the good and bad commit, you only have to check 6 or 7 commits to find the one that introduced the bug. Even if there were 1,000 commits, you only have to check 9 or 10.
The benefit of git bisect is obvious. A developer can use the tool to quickly track down when and where a bug was introduced to the codebase. By comparing a small number of commits, she can quickly identify the single commit and look at the diff to understand the bug’s cause.
A developer can use git bisect, as I show above, by manually checking each commit. By using the ‘start’, ‘bad’ and ‘good’ commands, she can quickly isolate the commit that introduces the bug. Since it only involves 6 or 7 steps, the process is quick and easy.
But in many cases, it is better software development practice to write a failing test first. A developer should write a failing test, not least because this ensures they can replicate the problem.
For developers that subscribe to the test-driven methodology, git bisect comes with a ‘run’ command. ‘git bisect run’ connects git bisect to the project’s test runner. Relying on the test runner’s exit code, git bisect traverses the commit history to automatically isolate the first bad commit. You simply need to configure your test runner to only run your single failing test.
git bisect is one of my favourite software development tools. Whenever I need to investigate a bug, it is my go-to tool for tracking down a bad commit. It saves a huge amount of time and makes it easy to isolate tricky bugs that have unexpected causes.