7 minutes
Git bisect
Meet git bisect
When you have a large codebase and a bug has been introduced somewhere in the history, finding the exact commit that introduced the bug can be challenging. This is where git bisect comes in handy. It automates the process of binary searching through your commit history to find the offending commit.
How it works
- start the bisect process by running
git bisect start - find a known bad commit (the one with the bug) and mark it as bad using
git bisect bad <bad-commit-hash> - find a known good commit (the one without the bug) and mark it as good using
git bisect good <good-commit-hash> - Git will now checkout a commit halfway between the good and bad commits. You need to test this commit to see if the bug is present
- if the bug is present, mark the commit as bad using
git bisect bad - if the bug is not present, mark the commit as good using
git bisect good - repeat the testing and marking process until Git narrows down to the exact commit that introduced the bug
- once the bad commit is found, you can end the bisect process by running
git bisect reset
This might sound a bit tedious, but it can save you a lot of time compared to manually checking each commit. Additionally, you can automate the testing and marking process using git bisect run <script>, where <script> is a script that tests the code and returns an exit code indicating whether the commit is good or bad.
Example
Make sure you have Maven and Java installed. My setup is as follows:
mvn --version
Apache Maven 3.9.11 ...
...
Java version: 25.0.1, vendor: Oracle Corporation, runtime: /Users/vladflore/.sdkman/candidates/java/25.0.1-open
...
Note: You can use SDKMAN to easily manage multiple versions of Java and Maven on your machine. Check it out at sdkman.io.
Let’s create a simple Java project using Maven:
mvn archetype:generate \
-DgroupId=tech.vladflore.app \
-DartifactId=bisect \
-DarchetypeArtifactId=maven-archetype-quickstart \
-DarchetypeGroupId=org.apache.maven.archetypes \
-DinteractiveMode=false
Open it in your favorite IDE and implement a simple binary search algorithm in App.java. Then, write some unit tests in AppTest.java.
In App.java, implement the binary search algorithm:
package tech.vladflore.app;
public class App {
public static int binarySearch(int[] arr, int target) {
if (arr == null || arr.length == 0) {
return -1;
}
int left = 0;
int right = arr.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (arr[mid] == target) {
return mid;
} else if (arr[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return -1;
}
}
In AppTest.java, write unit tests for the binary search method:
package tech.vladflore.app;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
class AppTest {
@Test
void testBinarySearch_ElementFound() {
int[] arr = {1, 3, 5, 7, 9, 11, 13};
assertEquals(3, App.binarySearch(arr, 7));
}
@Test
void testBinarySearch_ElementNotFound() {
int[] arr = {1, 3, 5, 7, 9, 11, 13};
assertEquals(-1, App.binarySearch(arr, 6));
}
@Test
void testBinarySearch_FirstElement() {
int[] arr = {1, 3, 5, 7, 9, 11, 13};
assertEquals(0, App.binarySearch(arr, 1));
}
@Test
void testBinarySearch_LastElement() {
int[] arr = {1, 3, 5, 7, 9, 11, 13};
assertEquals(6, App.binarySearch(arr, 13));
}
@Test
void testBinarySearch_SingleElement_Found() {
int[] arr = {5};
assertEquals(0, App.binarySearch(arr, 5));
}
@Test
void testBinarySearch_SingleElement_NotFound() {
int[] arr = {5};
assertEquals(-1, App.binarySearch(arr, 3));
}
@Test
void testBinarySearch_EmptyArray() {
int[] arr = {};
assertEquals(-1, App.binarySearch(arr, 5));
}
@Test
void testBinarySearch_NullArray() {
assertEquals(-1, App.binarySearch(null, 5));
}
@Test
void testBinarySearch_LargeArray() {
int[] arr = new int[1000];
for (int i = 0; i < arr.length; i++) {
arr[i] = i * 2;
}
assertEquals(500, App.binarySearch(arr, 1000));
}
@ParameterizedTest
@CsvSource({
"1, 0",
"3, 1",
"5, 2",
"7, 3",
"9, 4",
"11, 5",
"13, 6"
})
void testBinarySearch_ParameterizedAllElements(int target, int expectedIndex) {
int[] arr = {1, 3, 5, 7, 9, 11, 13};
assertEquals(expectedIndex, App.binarySearch(arr, target));
}
}
Run mvn clean test to ensure all tests pass.
Commit everything.
Right now the git history should show one commit.
git log --oneline
827ee9a9aa5451626aec62849bd11f1f440b457b (HEAD -> main) First commit
For the current implementation to work, the array must be sorted in ascending order. Let’s implement the invariant that the array is sorted before performing the binary search. Create a new commit with this change.
Add this in App.java:
for (int i = 0; i < arr.length - 1; i++) {
if (arr[i] > arr[i + 1]) {
throw new IllegalArgumentException("Array must be sorted in ascending order");
}
}
And the test for it in AppTest.java:
@Test
void testBinarySearch_UnsortedArray_ThrowsException() {
int[] arr = {5, 3, 7, 1, 9};
Assertions.assertThrows(IllegalArgumentException.class, () -> {
App.binarySearch(arr, 7);
});
}
Right now the git history should show two commits.
git log --oneline
9a81359b0c2e67b26ffbb9e51ea3cc2ccaa5a38c (HEAD -> main) Implement invariant
827ee9a9aa5451626aec62849bd11f1f440b457b First commit
Now, let’s introduce a bug in the implementation of the binary search algorithm.
Change the condition in the while loop from left <= right to left < right in App.java, and commit the change.
Now the git history should show three commits.
git log --oneline
e7431eacefd1fafbd46e3b276b687e08e5fa987a (HEAD -> main) Introduce bug
9a81359b0c2e67b26ffbb9e51ea3cc2ccaa5a38c Implement invariant
827ee9a9aa5451626aec62849bd11f1f440b457b First commit
Let’s add some more commits to make the history longer. All of them would be bad commits. Use the following git command to add three more empty commits (⚠️ do not do this in a real project):
for i in {2..4}; do git commit --allow-empty -m "Bad commit $i"; done
Now the git history should show six commits.
git log --oneline
345d5f1ace5b93665583ec5d6e33c6a92c5cec95 (HEAD -> main) Bad commit 4
2dbac1937ed41b26103c022b6579bd855932a8d1 Bad commit 3
46f9a561f097690f86bbb1cf52a94f23294d7f41 Bad commit 2
e7431eacefd1fafbd46e3b276b687e08e5fa987a Introduce bug
9a81359b0c2e67b26ffbb9e51ea3cc2ccaa5a38c Implement invariant
827ee9a9aa5451626aec62849bd11f1f440b457b First commit
We want to find the commit that introduced the bug, i.e. e7431ea. We could do this manually, by following the steps outlined above, or we could use git bisect run to automate the process. For this we need some script that would run the tests and return exit code 0 if all tests pass, or 1 if any test fails. Create a file named test.sh with the following content:
#!/bin/bash
mvn clean test
exit_code=$?
if [ $exit_code -ne 0 ];
then
exit 1
fi
exit 0
Make the script executable by running chmod +x test.sh.
Now, run the following:
git bisect start
git bisect bad HEAD
git bisect good 9a81359b0c2e67b26ffbb
git bisect run ./test.sh
Git will now automatically checkout commits, run the tests using the provided script, and mark commits as good or bad based on the exit code of the script. After a few iterations, it will identify the commit that introduced the bug.
e7431eacefd1fafbd46e3b276b687e08e5fa987a is the first bad commit
commit e7431eacefd1fafbd46e3b276b687e08e5fa987a
Author: Vlad Flore <flore.vlad@gmail.com>
Date: Wed Jan 7 17:03:19 2026 +0100
Introduce bug
...
bisect found first bad commit
So now we know that the commit e7431ea is the one that introduced the bug. To end the bisect session and return to the original HEAD, run:
git bisect reset
We can compare the identified bad commit with the previous good commit to see what changed:
git diff 9a81359 e7431ea | cat
diff --git a/src/main/java/tech/vladflore/app/App.java b/src/main/java/tech/vladflore/app/App.java
index 4084581..588b0aa 100644
--- a/src/main/java/tech/vladflore/app/App.java
+++ b/src/main/java/tech/vladflore/app/App.java
@@ -16,7 +16,7 @@ public class App {
int left = 0;
int right = arr.length - 1;
- while (left <= right) {
+ while (left < right) {
int mid = left + (right - left) / 2;
if (arr[mid] == target) {
And here is our bug!
Conclusion
git bisect is a powerful tool that can save you a lot of time when trying to find the commit that introduced a bug. By automating the binary search process, it allows you to quickly narrow down the range of commits to investigate, making it easier to identify and fix issues in your codebase.