Git: the basics of git bisect
git bisect
efficiently searches for a commit responsible for changing a given behaviour. git log
lets you see when a given file or line changed, but that’s often insufficient when the cause of some change is unclear. In such cases, git bisect
shines, as it lets you check your running system.
The most common use case is finding regressions: commits that broke a previously working feature. But you can also use git bisect
to find commits that accidentally fixed bugs, commits that changed performance, or any other observable effect.
A working knowledge of git bisect
can help you answer many such questions quickly. Without it, you’d need to do lots of hard manual work, perhaps enough to render the task infeasible. Also, once you understand its algorithm, you’ll likely find ways to apply it in other debugging tasks.
Let’s look through the basics of using git bisect
.
The binary search algorithm
The name “bisect” comes from the binary search algorithm that the command employs. The search repeatedly divides in two, or bisects, the list of commits until the responsible one is found.
For example, say you know that a certain bug appeared at some point between the v8
tag and the latest commit on main
:
v8 ----------------------- main
After you start git bisect
, the first step in its binary search tests the halfway point, which we’ll call commit A
:
v8 -----------A----------- main
Say the bug does not exist in commit A
. You tell git bisect
that it exhibits the old bug-free behaviour, from which it assumes that every commit between v8
and A
is clear. The search continues by testing the commit halfway between A
and main
:
v8 -----------A-----B----- main
The algorithm repeatedly subdivides until it narrows in on the single responsible commit. If B turns out to show the new buggy behaviour, the next commit will be C:
v8 -----------A--C--B----- main
And if C shows the new buggy behaviour, the search would test D:
v8 -----------A-DC--B----- main
If D turns out to be bug-free, the bisect session will be finished, concluding that C introduced the bug.
A binary search is way faster than a linear search, which tests all commits one by one. Whilst a linear search can be so slow as to be impractical, a binary search never takes that many steps. Mathematically, a binary search only needs to test log₂(n) commits:
History length | Tested by binary search |
---|---|
10 | 4 |
100 | 7 |
1,000 | 10 |
10,000 | 14 |
This slow growth makes it feasible to run git bisect
even on huge commit ranges.
Subcommands and workflow
git bisect
has several subcommands, but for basic usage, you’ll only need these four:
Subcommand | Purpose |
---|---|
git bisect start | Initialize a bisect session. |
git bisect old | Mark a commit as exhibiting the old behaviour. |
git bisect new | Mark a commit as exhibiting the new behaviour. |
git bisect reset | End the bisect session. |
The typical workflow is:
- Start
- Run
start
to initialize the session. - Run
old
to mark a previous commit as old. - Run
new
to mark a commit as new, typically the latest one.
- Run
- Binary search
git bisect
selects a commit, switches to it, and asks you to test it.- You run your test command.
- If the commit exhibits the old behaviour, run
old
, else, runnew
. - Loop to step 2a, as
git bisect
runs the binary search algorithm, until it reports the responsible commit.
- Finish
- Run
reset
to end the session and return to the branch you were on. - Decide how to act on the result. For example, you might revert the responsible commit.
- Run
Let’s look at an example, after a quick note on terms.
The “old” and “new” terms
Originally, git bisect
focused on finding regressions, so it used the terms good and bad. Those terms are confusing in other situations though, for example when finding when a bug was fixed, you’d need to use them backwards. Because of this, Git 2.7 (2016) introduced the more neutral old and new terms.
good and bad are still supported subcommands, and they often appear in other tutorials. But here, we’ll stick to old and new, since they are universal.
(It’s also possible to pick custom terms, such as slow
and fast
, but we won’t cover that.)
An example git bisect
session
Imagine you are building a website about your boat-spotting passion. You get the site up and online and proudly screenshot it:
After a couple more days of work, you look at the home page again. You realize in shock that you’ve accidentally made the footer text massive!
Oops.
Looking at the code, you struggle to see the issue. It’s not even clear which file is responsible - it could be HTML, CSS, or maybe something else.
You check the log since the first deployment, which has the tag deploy-1
:
$ git log --oneline deploy-1~..
cbd08bc (HEAD -> main, tag: deploy-3) Nav spacing
9b27483 Add 'Avoid huge ships' page
4833ad2 Extra welcome paragraph
611aa1c Add emoji
a514f80 Tweak font sizes
1378388 (tag: deploy-2) Increase <a> underline gap
e0df7a4 Add 'Anchor guide' page
8bd9715 Use blue colours
c5e04d8 Add 'Best boats' page
08b974c Add favicon
8a8d3d2 Add 'Boat or ship?' page
ced2603 (tag: deploy-1) Home page and styles
There are 11 commits to check. You decide to use git bisect
to find the responsible commit.
First, you start the session:
$ git bisect start
status: waiting for both good and bad commits
Note that Git uses the legacy terms good and bad for these initial “status” messages.
You tell Git that the commit tagged deploy-1
exhibits the old behaviour (small footer text):
$ git bisect old deploy-1
status: waiting for bad commit, 1 good commit known
Then, you tell Git that the current commit exhibits the new behaviour (large footer text):
$ git bisect new
Bisecting: 5 revisions left to test after this (roughly 3 steps)
[e0df7a4c1176d5d591dc9012b20d117ffd8e94bc] Add 'Anchor guide' page
There’s no need to provide a commit reference as new
defaults to the latest commit (HEAD
).
Second, you go through the binary search loop. After being told both the old and new commits, git bisect
has already started the search.
The above output reports that five more commits (“revisions“) will need testing, which will take about three steps. It then tells you that it switched to a commit halfway between the two points, with its full commit SHA and message “Add 'Anchor guide' page”.
You test this commit by checking the page in your browser:
The footer text is still small, so this commit exhibits the old behaviour. You tell Git this by running git bisect old
:
$ git bisect old
Bisecting: 2 revisions left to test after this (roughly 2 steps)
[611aa1c48f18c83fb69ff8b3533197df6f3847a5] Add emoji
The next step of the search is this “Add emoji” commit. You check your browser again:
Aha, large footer text! That’s the new behaviour, so you run:
$ git bisect new
Bisecting: 0 revisions left to test after this (roughly 1 step)
[a514f80fbba3b1b3b08e62d37233cdbfc8c979d0] Tweak font sizes
You check in your browser once more:
Yup, the large footer text, which you record with:
$ git bisect new
Bisecting: 0 revisions left to test after this (roughly 0 steps)
[1378388b4a6860b005f65b17de51d07fe86ba4a7] Increase <a> underline gap
Nearly there: “roughly 0 steps” left. Checking again, you see small footer text:
When you run git bisect old
this time, it finishes the search:
$ git bisect old
a514f80fbba3b1b3b08e62d37233cdbfc8c979d0 is the first new commit
commit a514f80fbba3b1b3b08e62d37233cdbfc8c979d0
Author: A Hacker <hacker9001@funmail.example>
Date: ...
Tweak font sizes
style.css | 13 +++++++------
1 file changed, 7 insertions(+), 6 deletions(-)
git bisect
reports the SHA of the “first new commit“, the one that introduced the change. It then gives more details with the output of git show --stat
for that commit.
Third, you finish the bisect session and act on the information. “Bisecting“ is a special status for your repository, shown in git status
:
$ git status
HEAD detached at 1378388
You are currently bisecting, started from branch 'main'.
nothing to commit, working tree clean
You need to exit this special status, and “reattach your HEAD“ (return to a branch), before making commits. Do so with git bisect reset
:
$ git bisect reset
Previous HEAD position was 1378388 Increase <a> underline gap
Switched to branch 'main'
Git has returned to the branch you were on before starting the bisect session. From here you can fix the issue.
One good idea is to look at the problematic commit in detail with git show
:
$ git show a514f80fbba3b1b3b08e62d37233cdbfc8c979d0
In the displayed diff, you spot the commit changed the footer’s styles like so:
@@ -39,5 +40,5 @@ h1 {
}
footer {
- font-size: 0.7em;
+ font-size: var(--size-step-1);
}
This looks like a typo, using step “1” instead of step “-1”, which requires two hyphens. You try adding the extra hyphen:
@@ -44,5 +44,5 @@ nav a {
}
footer {
- font-size: var(--size-step-1);
+ font-size: var(--size-step--1);
}
…and the footer is fixed:
Great work. Time to commit, push, and deploy.
Thanks git bisect
!
Further sections
After this introduction to the basics, the book covers more advanced git bisect
usage:
- Mark a commit as untestable with
git bisect skip
- Automate testing with
git bisect run
- Automate testing of performance regressions
Check it out to read these sections and many more.
One summary email a week, no spam, I pinky promise.
Related posts:
- Git: Improve diff generation with
diff.algorithm=histogram
- Git: Show commits that come after
- Git: Force push safely with
--force-with-lease
and--force-if-includes
Tags: git