Once upon a time, there was a young developer who worked on a small side project, after a few hours he created a working version and started to refactor the code to make it nice, tidy, and clean. He refactored the code for a couple of hours and kept working neatly in small and meaningful commits. When he was pleased with his work he ran the code again but oh no, something terrible happened… the code didn’t work.
We have all been there, for me it happened a week ago, I sat in my bed and stared at my IDE rat me out that it was I who wrote that buggy code, “I got only myself to git blame that’s for sure” I grinned with myself but after a second a light bulb popped with a new idea — what if I can use Git to traverse through all of my commits and hunt down my bug? Fortunately, after a short search on Google I discovered that it is possible - using the simple git bisect command.
In this tutorial, we will explore the git bisect command: how it works, and why and how to use it -hopefully you will find it useful and powerful as I do!
🔮 Debugging with Git
As you may know for git everything is about commits, a branch for that matter is nothing but a pointer to a single commit. This allowed git to traverse through the commits and provide a couple of debugging tools to help us debug our code: File Annotation(a.k.a ‘git blame’) and Binary Search.
File Annotation — git blame
File annotation shows the last commit to modify each line of any file and done using the git blame command, this is often the most useful tool for tracking down a bug and know who is the one to blame. If you are not familiar with git’s file annotation I highly recommend you read about it form git documentation.
Binary Search — git bisect
A way to locate which commit introduced an issue using binary search through all the commits history of a particular branch. It’s done with the git bisectcommand which checks out each commit allowing you to run a test to see if the issue exists in this commit. When you’re hunting down a bug but you don’t know where to look for it and there were hundreds of commits since the last good commit you remember - the git bisect command is your answer.
📽 Simple Scenario
Let’s say you worked on a new branch…
🥳 You worked for some time until you finally reach a good point with a good working product.
😌 You are also a responsible developer who wishes to keep his tree clean so you probably added a few commits along the way and had the following commits history:
💎 You decided it’s time to start refactoring the code, make it nicer and clean, eventually you ended up with the following commits history:
😰 Unfortunately, your code isn’t working properly at the current commit, apparently somewhere along the way a part of the code got broken.
🧐 You know that it had definitely worked before the refactor — when you added commit 271cb1c — meaning there is a bug in the code between commit 271cb1c and 9c07a95, the only question is…. How do you find it?
👑 Enter git bisect.
🛠 How to use git bisect
Inspired by Clint Eastwood I like to think about this part as the story of ‘The Good, the Bad and the Wizard’:
🧙🏼♂️ The Wizard
Run the git bisect start command to startup the git bisectwizard.
😈 The Bad
In git the binary search works by repeatedly dividing in half the commits that could contain the bug and narrowing down the possible commits to one, this makes the scope of the search essential. The way to define the git bisect wizard what is the searching scope is by running the git bisect bad command which tells git that the current commit is broken and contains a contagious bug.
😇 The Good
The bad commit is only one end of the scope now we need to tell git when the last known good commit was, we do that by running git bisect <good_commit> command, for example in our case, the good commit was commit 271cb1c so we’ll run the following command:
After running this command git will start checkout commits using binary search and allow at each commit to build the app and check whether or not the bug exists, all left for you to do is to make sure you tell git the status of the last git bisect command with either git bisect bad or git bisect good.
This way, it will continue to narrow down the possible commits, repeat until it finds the commit where the bug was introduced.
Let’s assume we checked out on commit66e0075 and that we found out that the bug occurred before it so we run:
🙌🏻 Git is kind
Now git has all it needs to determine that the commit between commit 66e0075 and commit 271cb1cis the commit where the bug introduced. When git finally finds the commit with the bug it prints all the information it has about it to help you figure out what happened that may have introduced this bug, for instance:
Like every time-travel journey its important to go back to where it all began otherwise we might be checked out on a commit from the past and end up in a weird branch state. We go back to the present by running the simple git bisect reset command which shuts down the git bisect wizard and reset the HEAD to where we were before we started.
This is the part when git bisect really shines - one of git bisect subcommand is the git bisect run which accepts a command as an argument and allows to fully automate git bisect!
After running this command git automatically runs cmd on each checked-out commit and uses the return value of that call to decide if the commit is good or bad until it will find the first broken commit.
How to automate git bisect
First, you tell git the scope of the bisect by providing the bad and good commits:
Then you git bisect run with your command and let git do all the work for you, for example:
The command can be any script that will exit 0 for “good commit” or non-0 “bad-commit”.
In this tutorial, you learned how to track down a bug using thegit bisect command. Moreover, you learned how to automate it and by that reduce debugging time significantly. I hope you’ll try git bisect in the future when you’ll look for a nice solution for hunting down a bug.
Special thanks for my teammates at datreeio 🙏