Everyone knows Git as a great file versioning tool. But did you know you can also use Git to debug your code?
In today's Snippet, I'll introduce two tools I use all the time--Git blame and Git bisect--and explain how they can help you find bugs hiding in your code.
Git Blame
Git blame is a well-known tool that displays which users most recently modified specific lines of code up to the last commit in a document. For example, the simple README.md file code below displays the partial commit hash followed by user, date, time, time zone, line number, and line content information.
$ git blame README.md 406b2818 (Jonathan 2017-12-05 15:49:16 -0200 1) #Git Debugging 406b2818 (Jonathan 2017-12-05 15:49:16 -0200 2) 06c430e2 (Jonathan 2017-12-05 16:14:49 -0200 3) This project is part...Avenue Code Snippets. d44487fe (Jonathan 2017-12-05 15:57:23 -0200 4) d44487fe (Jonathan 2017-12-05 15:57:23 -0200 5) ##Project content d44487fe (Jonathan 2017-12-05 15:57:23 -0200 6) 06c430e2 (Jonathan 2017-12-05 16:14:49 -0200 7) Basic Rest based crud with Spring Boot.
In the code above, we can view 3 commits (406b2818, 06c430e2, d44487fe), followed by committer data and line content information.
Git blame has many uses, which are detailed in full in the links at the end of this article, but one of the most important uses is [-L <range>]. This option allows you to isolate and check a specific set of lines within your file. For example:
$ git blame -L 2,8 README.md 406b2818 (Jonathan 2017-12-05 15:49:16 -0200 2) 06c430e2 (Jonathan 2017-12-05 16:14:49 -0200 3) This project is part...made for Avenue Code Snippets. d44487fe (Jonathan 2017-12-05 15:57:23 -0200 4) 00000000 (Not Committed Yet 2017-12-05 16:54:00 -0200 5) This is a unstaged change 00000000 (Not Committed Yet 2017-12-05 16:54:00 -0200 6) d44487fe (Jonathan 2017-12-05 15:57:23 -0200 7) ##Project content d44487fe (Jonathan 2017-12-05 15:57:23 -0200 8)
Above, we're filtering lines 2 through 8. (Note that 'blame' also shows uncommitted changes.)
Another interesting option is [-C], which is especially useful when you are refactoring a code. [-C] is helpful for finding code blocks that have been transferred from other files. Below, for example, there is a simple endpoint using Java and SpringBoot.
package edu.ac.gitdebugging; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @SpringBootApplication @RestController public class App { public static void main(String[] args) { SpringApplication.run(App.class, args); } @RequestMapping("/sayHello") public String sayHello() { return "Hello"; } }
If I decide to move the method sayHello to a new file called MainController, I get the following:
package edu.ac.gitdebugging.web; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class MainController { @RequestMapping("/sayHello") public String sayHello() { return "Hello"; } }
Now, the [-C] option displays a new column, not yet committed, that specifies the code's original location:
$ git blame -C src/main/java/edu/ac/gitdebugging/web/MainController.java
00000000 src/main/java/edu/ac/gitdebugging/web/MainController.java (Not Committed Yet 2017-12-05 18:27:29 -0200 1) package edu.ac.gitdebugging.web; 00000000 src/main/java/edu/ac/gitdebugging/web/MainController.java (Not Committed Yet 2017-12-05 18:27:29 -0200 2) 62394f5c src/main/java/edu/ac/gitdebugging/App.java (Jonathan 2017-12-05 18:20:27 -0200 3) import org.springframework.web.bind.annotation.RequestMapping; 62394f5c src/main/java/edu/ac/gitdebugging/App.java (Jonathan 2017-12-05 18:20:27 -0200 4) import org.springframework.web.bind.annotation.RestController; 62394f5c src/main/java/edu/ac/gitdebugging/App.java (Jonathan 2017-12-05 18:20:27 -0200 5) 00000000 src/main/java/edu/ac/gitdebugging/web/MainController.java (Not Committed Yet 2017-12-05 18:27:29 -0200 6) @RestController 00000000 src/main/java/edu/ac/gitdebugging/web/MainController.java (Not Committed Yet 2017-12-05 18:27:29 -0200 7) public class MainController { 62394f5c src/main/java/edu/ac/gitdebugging/App.java (Jonathan 2017-12-05 18:20:27 -0200 8) @RequestMapping("/sayHello") 62394f5c src/main/java/edu/ac/gitdebugging/App.java (Jonathan 2017-12-05 18:20:27 -0200 9) public String sayHello() { 62394f5c src/main/java/edu/ac/gitdebugging/App.java (Jonathan 2017-12-05 18:20:27 -0200 10) return "Hello"; 62394f5c src/main/java/edu/ac/gitdebugging/App.java (Jonathan 2017-12-05 18:20:27 -0200 11) } ^5f1231c src/main/java/edu/ac/gitdebugging/App.java (Jonathan 2017-12-05 15:39:16 -0200 12) }
IDEs like Eclipse (figure 01 below) and IntelliJ (figure 02 below) have a built-in 'blame' integrated with the code editor. In Eclipse, right click the line number and select 'Show revision information.' In IntelliJ, right click the line number and select 'Annotate.'
Figure 01: Blame In Eclipse
Figure 02: Blame in IntelliJ
Git Bisect
Now that we've covered Git blame, let's take a look at Git bisect. We used Git blame for discovering who changed your code, when they changed it, and which commit they used to do so, but if you're still usnure what's breaking your code, you can use Git bisect. Before discussing usage tips, let's review some basics.
"Git bisect" stands for "Git binary search." In a nutshell, Git bisect uses a binary search to find the commit that introduced a bug. Before looking at an example, let's quickly review binary searches.
First of all, why do we use binary searches instead of searching commits from newest to oldest? Very often, bugs are introduced early in projects, so if you have a code sequence with ten thousand commits and the bug is in the second, you'll waste time reviewing commits chronologically.
Binary searches simplify this process: within a collection of elements (e.g. ordered integer arrays), we start at the middle element and check if it has a bug. If it does, we check the element halfway through the preceding code; if it doesn't, we check the element halfway through the following code. We continue iterating this process within smaller groups until we identify the element, saving ourselves a lot of labor and time. (If you want to learn more about binary and other search methods, listen to this lecture from MIT Open Courseware: Binary Search, MIT Open Courseware.)
Now let's apply binary searches using Git bisect. When using the binary method to search your commit stack, you'll need to inform Git if the bug is present at each search phase until it identifies the problem commit.
Let's take an example that starts with the first check in an entire commit stack:
$ git log --pretty=oneline --abbrev-commit
548cf21 (HEAD -> master, origin/master) start to write git bisect example f848038 bisect snippet start f3851a1 Delete person service 4ed6ac5 Count persons a313218 Get All Persons 7f3ce1e Adding a BUG not covered by tests f2945f7 Bug fix in insert e64e564 Refactoring exception Handler d07ed1e Person Exist validation e70b341 Tests for update and get 7082948 Test base 9a87bc9 Find One and Update cc66f96 Find all and save endpoints 48476e9 Starting git bisect section 260722e Refactoring Hello World 62394f5 Hello World Endpoint 06c430e Adding snippet d44487f Adding spring dependencies 406b281 Readme basic info 5f1231c First commit
As you can see, the commit that added a bug in the project is "7f3ce1e Adding a BUG not covered by tests." Now let's run the Git bisect:
$ git bisect start
Next we need to define the range of the search:
$ git bisect bad HEAD $ git bisect good e70b
In the first line, we define that the HEAD commit is bad. In other words, we have a bug at this commit. Next, we need to define a commit where the bug isn't present. For this, I often use the last commit of the last release. (Note that tags can help you.)
Now Git will scan an old commit:
Bisecting: 4 revisions left to test after this (roughly 2 steps) [a3132181523726cbc77caebade9e2ae096c3f04e] Get All Persons
If you do a "Git log," you can check exactly where the Git is, and you can also check some markers:
$ git log --pretty=oneline --abbrev-commit a313218 (HEAD) Get All Persons 7f3ce1e Adding a BUG not covered by tests f2945f7 Bug fix in insert e64e564 Refactoring exception Handler d07ed1e Person Exist validation e70b341 (refs/bisect/good-e70b3416e262f4947de25d6e324c45ea301078a8) Tests for update and get 7082948 Test base 9a87bc9 Find One and Update cc66f96 Find all and save endpoints 48476e9 Starting git bisect section 260722e Refactoring Hello World 62394f5 Hello World Endpoint 06c430e Adding snippet d44487f Adding spring dependencies 406b281 Readme basic info 5f1231c First commit
Now we need to go to the project and verify if the bug is still there. You can use "Git bisect bad" or "Git bisect good" to inform Git whether the bug is there or not. If the bug is still present, we inform Git accordingly:
$ git bisect bad Bisecting: 2 revisions left to test after this (roughly 1 step) [e64e5646a348da525b17647d3d1e0834f0e4d7bc] Refactoring exception Handler
Then Git moves our HEAD to a commit before the bug. Check the Git log:
$ git log --pretty=oneline --abbrev-commit e64e564 (HEAD) Refactoring exception Handler d07ed1e Person Exist validation e70b341 (refs/bisect/good-e70b3416e262f4947de25d6e324c45ea301078a8) Tests for update and get 7082948 Test base 9a87bc9 Find One and Update cc66f96 Find all and save endpoints 48476e9 Starting git bisect section 260722e Refactoring Hello World 62394f5 Hello World Endpoint 06c430e Adding snippet d44487f Adding spring dependencies 406b281 Readme basic info 5f1231c First commit
Next, we execute Git bisect good, and Git moves the HEAD to where the bug is:
$ git bisect good Bisecting: 0 revisions left to test after this (roughly 1 step) [7f3ce1edcb4d3632bfe665d125004465a9d6d8e3] Adding a BUG not covered by tests
Checking the log, we get:
$ git log --pretty=oneline --abbrev-commit 7f3ce1e (HEAD) Adding a BUG not covered by tests f2945f7 Bug fix in insert e64e564 (refs/bisect/good-e64e5646a348da525b17647d3d1e0834f0e4d7bc) Refactoring exception Handler d07ed1e Person Exist validation e70b341 (refs/bisect/good-e70b3416e262f4947de25d6e324c45ea301078a8) Tests for update and get 7082948 Test base 9a87bc9 Find One and Update cc66f96 Find all and save endpoints 48476e9 Starting git bisect section 260722e Refactoring Hello World 62394f5 Hello World Endpoint 06c430e Adding snippet d44487f Adding spring dependencies 406b281 Readme basic info 5f1231c First commit
This time, we use "Git bisect bad." Then Git moves the HEAD one last time to verify exactly where the bug was introduced.
$ git bisect bad Bisecting: 0 revisions left to test after this (roughly 0 steps) [f2945f726299f5ce878595d0a1b821e6c09712fc] Bug fix in insert
Once more, the log shows the following:
$ git log --pretty=oneline --abbrev-commit f2945f7 (HEAD) Bug fix in insert e64e564 (refs/bisect/good-e64e5646a348da525b17647d3d1e0834f0e4d7bc) Refactoring exception Handler d07ed1e Person Exist validation e70b341 (refs/bisect/good-e70b3416e262f4947de25d6e324c45ea301078a8) Tests for update and get 7082948 Test base 9a87bc9 Find One and Update cc66f96 Find all and save endpoints 48476e9 Starting git bisect section 260722e Refactoring Hello World 62394f5 Hello World Endpoint 06c430e Adding snippet d44487f Adding spring dependencies 406b281 Readme basic info 5f1231c First commit
If you look at the first Git log before the bisect, you'll see that the commit "f2945f7" is the first commit before the bug. Now when we inform Git that the HEAD is good, we'll be able to see the commit that introduced the bug:
$ git bisect good 7f3ce1edcb4d3632bfe665d125004465a9d6d8e3 is the first bad commit commit 7f3ce1edcb4d3632bfe665d125004465a9d6d8e3 Author: Jonathan <jaraujo@avenuecode.com> Date: Fri Jan 12 14:40:00 2018 -0200 Adding a BUG not covered by tests :040000 040000 0f61c1015071199f167b1ddaee632f2368c9b99b b0e10337999203236d17756897a7f8fe071d3237 M src
To leave bisect mode, type:
$ git bisect reset
Bisect with Automated Tests
Now that we've identified our bug location, we can use a Git bisect run command to determine whether the script is good or bad:
$ git bisect run <cmd>
We can use the run command with Maven to generate results more quickly and easily. There is only one thing that we need to set up before running bisect with Maven: if you try to create a commit at the top of a stack, Git bisect will remove commits to find the bug, so you first need a broken test to check if the bug is present.
Here are two ways I do this:
1. Create the test - commit the test - create an auxiliary branch - reset the auxiliary branch to a past point (usually to the commit where you will use bisect good). Then, we rebase our source branch so that our new branch becomes the next test's first commit.
Let's review this process step by step. First, we create a new branch titled "tempBranch:"
$ git checkout -b tempBranch
Next, we reset the branch to a past point:
$ git reset --hard e70b
Then, we use the interactive rebase to change the order of our commits:
$ git rebase -i HEAD master
Make sure that you move the new commit to the first commit in the interactive rebase:
pick fc93c46 Test to Check the BUG pick d07ed1e Person Exist validation pick e64e564 Refactoring exception Handler pick f2945f7 Bug fix in insert pick 7f3ce1e Adding a BUG not covered by tests pick a313218 Get All Persons pick 4ed6ac5 Count persons pick f3851a1 Delete person service pick f848038 bisect snippet start pick 548cf21 start to write git bisect example pick 17444c7 Removing test class pick 8dc8f8a bisect step by step pick 663f57f Removing temp file
Check that the commit "fc93c46 Test to Check the BUG" is right after "e70b341 Tests for update and get."
$ git log --pretty=oneline --abbrev-commit 713de85 (HEAD -> master) Removing temp file fa8c371 bisect step by step 93bc5ea Removing test class d57a06c start to write git bisect example 21e3530 bisect snippet start 51ac2ed Delete person service e9c521a Count persons 59dae70 Get All Persons af7f157 Adding a BUG not covered by tests cffb531 Bug fix in insert 8172c65 Refactoring exception Handler 5a03476 Person Exist validation f7c2b72 Test to Check the BUG e70b341 (tempBranch) Tests for update and get 7082948 Test base 9a87bc9 Find One and Update cc66f96 Find all and save endpoints 48476e9 Starting git bisect section 260722e Refactoring Hello World 62394f5 Hello World Endpoint 06c430e Adding snippet d44487f Adding spring dependencies 406b281 Readme basic info 5f1231c First commit
2. Alternatively, we can create a new file, code the test, and keep this new file unversioned. This method is faster, but it's also easier to create a conflicting file name or lose the new test file, depending on what you do after the bisect.
If we start by creating a test that shows Git if the project is good or bad, we can use Maven to execute the next steps for us:
$ git bisect start $ git bisect bad HEAD $ git bisect good e70b $ git bisect run mvn -Dtest=BugFinderTest test
Now we can wait for Maven to complete the process on its own:
$ git bisect run mvn -Dtest=BugFinderTest test 7f3ce1edcb4d3632bfe665d125004465a9d6d8e3 is the first bad commit commit 7f3ce1edcb4d3632bfe665d125004465a9d6d8e3 Author: Jonathan <jaraujo@avenuecode.com> Date: Fri Jan 12 14:40:00 2018 -0200 Adding a BUG not covered by tests :040000 040000 0f61c1015071199f167b1ddaee632f2368c9b99b b0e10337999203236d17756897a7f8fe071d3237 M src bisect run success
You can view all these steps in the following video:
Now you know how to use Git blame and Git bisect to easily identify bugs in your code!
References:
Git Blame Docs:
BlameGit Bisect Docs:
BisectAll examples are based in following repository:
Author
Jonathan Ohara
Jonathan Ohara is a Java Engineer at Avenue Code.