Git bisect and git worktree: Finding the Bad Commit and Two Branches at Once
We find the commit that introduced a bug with git bisect's binary search: marking good and bad, bisect log and reset, automatic search by exit code with git bisect run, and exit 125. Then we use git worktree to make a hotfix in a separate folder without disturbing unfinished work, and look at worktree list/remove/prune and when it beats stash.
Version note: The commands in this post were tested with Git 2.54.
Guess First
v1.0.0 worked fine. Today the cart total on main is broken. Let's run the test at two points:
$ git log --oneline v1.0.0..main
fdb91a4 (HEAD -> main) Update book titles
35606e6 Add discount function
5551b73 Document search in README
8b0b603 Add findById helper
699a57f Refactor cart total
129c4f4 Add wishlist module
$ node test/cart.test.js
FAIL: expected 95, got [object Object]35
$ git switch --detach v1.0.0
HEAD is now at b68648b Add price formatter
$ node test/cart.test.js
PASS
$ git switch main
Previous HEAD position was b68648b Add price formatter
Switched to branch 'main'
There are six commits in between. To find the commit that introduced the bug, how many commits do you need to test in the worst case? Six, three, fewer? And what if there were six hundred?
The Idea Behind Bisect: Binary Search
When you look up a word in a dictionary, you don't start at the first page. You open it in the middle, move to the first half if the word comes earlier or to the second half if it comes later, and halve the range at every step. git bisect does the same thing in history. You give it a commit known to be fine (good) and a commit known to be broken (bad). Git checks out the commit in the middle, you test it and report the result, and the range shrinks by half.
For six commits, three tests at most are enough. For six hundred commits, about ten. For a thousand, still ten. The bigger the history, the more bisect is worth; knowing which commit brought a bug in is often half the fix.
Manual Bisect: good and bad
$ git bisect start
status: waiting for both good and bad commits
$ git bisect bad
status: waiting for good commit(s), bad commit known
$ git bisect good v1.0.0
Bisecting: 2 revisions left to test after this (roughly 2 steps)
[8b0b603771b22445b3cf4da3b0bac584dd64552c] Add findById helper
Given without an argument, bad marks the current commit, meaning main, as broken. For good we gave the tag; in the previous post we saw why tags pay off. Git immediately moved to the commit in the middle. Test it and report the result:
$ node test/cart.test.js
FAIL: expected 95, got [object Object]35
$ git bisect bad
Bisecting: 0 revisions left to test after this (roughly 1 step)
[699a57f873baee70115bca5b5548eb686f612c67] Refactor cart total
$ node test/cart.test.js
FAIL: expected 95, got [object Object]35
$ git bisect bad
Bisecting: 0 revisions left to test after this (roughly 0 steps)
[129c4f48a163d1ede8f08e9d265c8a69349db754] Add wishlist module
$ node test/cart.test.js
PASS
$ git bisect good
699a57f873baee70115bca5b5548eb686f612c67 is the first bad commit
commit 699a57f873baee70115bca5b5548eb686f612c67
Author: Ada Lovelace <ada@example.com>
Date: Thu Nov 12 10:10:00 2026 +0300
Refactor cart total
src/cart.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
Found in three tests: Refactor cart total. A look with git show 699a57f reveals the cause: during the refactor, reduce's initial value 0 was dropped. Without an initial value, reduce uses the first element as the starting value, and the total turns into an object glued to a number.
While bisect is running, HEAD is detached, and git status tells you so:
$ git status
HEAD detached at 129c4f4
You are currently bisecting, started from branch 'main'.
(use "git bisect reset" to get back to the original branch)
nothing to commit, working tree clean
$ git bisect log
git bisect start
# status: waiting for both good and bad commits
# bad: [fdb91a4064fe7096cb698ad89c27046f88692b6a] Update book titles
git bisect bad fdb91a4064fe7096cb698ad89c27046f88692b6a
...
# first bad commit: [699a57f873baee70115bca5b5548eb686f612c67] Refactor cart total
$ git bisect reset
Previous HEAD position was 129c4f4 Add wishlist module
Switched to branch 'main'
git bisect log records every decision you made. If you marked a step wrong, write this output to a file, fix the bad line and replay the search from there with git bisect replay <file>. git bisect reset takes you back to the branch you were on before bisect started; don't forget it when you are done.
Automatic Search with git bisect run
Instead of running the test by hand at every step and typing the result, you can leave the job to Git. The only requirement is that the test command speaks through its exit code: 0 is good, anything from 1 to 127 is bad. 125 has a special meaning: "this commit can't be tested, skip it". 128 and above stop the bisect.
$ git bisect start main v1.0.0
Bisecting: 2 revisions left to test after this (roughly 2 steps)
[8b0b603771b22445b3cf4da3b0bac584dd64552c] Add findById helper
$ git bisect run node test/cart.test.js
running 'node' 'test/cart.test.js'
FAIL: expected 95, got [object Object]35
Bisecting: 0 revisions left to test after this (roughly 1 step)
[699a57f873baee70115bca5b5548eb686f612c67] Refactor cart total
running 'node' 'test/cart.test.js'
FAIL: expected 95, got [object Object]35
Bisecting: 0 revisions left to test after this (roughly 0 steps)
[129c4f48a163d1ede8f08e9d265c8a69349db754] Add wishlist module
running 'node' 'test/cart.test.js'
PASS
699a57f873baee70115bca5b5548eb686f612c67 is the first bad commit
commit 699a57f873baee70115bca5b5548eb686f612c67
Author: Ada Lovelace <ada@example.com>
Date: Thu Nov 12 10:10:00 2026 +0300
Refactor cart total
src/cart.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
bisect found first bad commit
$ git bisect reset
Previous HEAD position was 129c4f4 Add wishlist module
Switched to branch 'main'
git bisect start main v1.0.0 gives bad and good on one line: bad first, then good. The rest is fully automatic.
If the test only exists in newer commits, or some commits don't build, put the test script outside the repo and return exit 125 when the build fails. For manual searches, git bisect skip does the same job. The test in this post was added before v1.0.0, so it exists at every commit. That is one more benefit of running tests on every commit in CI: you always have a reliable "good" that bisect can use.
Git 2.56 added an option called --reset-when-found to git bisect. My local version is 2.54, so I did not run it here; we will cover it in the versions block.
Worktree: One Repo, Two Folders
Now a different scene. Ada is working on feature/discount-rules and has unfinished changes. Right then an urgent hotfix for the cart bug is requested. The classic solutions are familiar: stash the changes or make a half-done "wip" commit, switch branches, do the hotfix, come back, pop the stash. And meanwhile node_modules or build output can get mixed up between branches.
git worktree offers another way: checking out a second branch of the same repo into a separate folder. The two folders share a single .git database; commits, branches and stashes are shared. But each folder has its own working tree, its own index and its own HEAD. Think of two separate counters in the same kitchen: the pantry is shared, the work on each counter is separate.
Opening a Worktree for a Hotfix
$ git status -s
M src/discount.js
$ git worktree add -b hotfix/cart-total ../bookstore-hotfix main
Preparing worktree (new branch 'hotfix/cart-total')
HEAD is now at fdb91a4 Update book titles
$ git worktree list
.../bookstore fdb91a4 [feature/discount-rules]
.../bookstore-hotfix fdb91a4 [hotfix/cart-total]
(I shortened the beginning of the paths.) -b hotfix/cart-total creates a new branch, ../bookstore-hotfix is where the folder goes, and main is the starting point. You never touched your unfinished changes. Let's move to the new folder and do the hotfix:
$ cd ../bookstore-hotfix
$ git status
On branch hotfix/cart-total
nothing to commit, working tree clean
$ node test/cart.test.js
PASS
$ git commit -qam "Fix cart total for multiple items"
$ git log --oneline -2
7bfcb12 (HEAD -> hotfix/cart-total) Fix cart total for multiple items
fdb91a4 (main, feature/discount-rules) Update book titles
We added the 0 back to reduce, the test passed, and we committed. There is one rule here: the same branch cannot be checked out in two worktrees at the same time.
$ git switch feature/discount-rules
fatal: 'feature/discount-rules' is already used by worktree at '.../bookstore'
This is not a limitation, it is a safeguard. If two folders could commit to the same branch, one would move the branch out from under the other. Let's go back to the main folder:
$ cd ../bookstore
$ git status -s
M src/discount.js
$ git log --oneline -1 hotfix/cart-total
7bfcb12 (hotfix/cart-total) Fix cart total for multiple items
$ git branch
* feature/discount-rules
+ hotfix/cart-total
main
The unfinished change is still in place, and the hotfix commit is visible from this folder too, because there is only one repo. The + in the git branch output shows that the branch is checked out in another worktree. You can push the hotfix from here and open a PR.
Closing a Worktree
When the work is done, let Git remove the worktree:
$ git worktree remove ../bookstore-hotfix
$ git worktree list
.../bookstore fdb91a4 [feature/discount-rules]
The folder is gone; the branch and the commits remain. If the worktree has anything uncommitted, Git stops you:
$ git worktree add --detach ../bookstore-review2 main
Preparing worktree (detached HEAD fdb91a4)
HEAD is now at fdb91a4 Update book titles
$ git worktree remove ../bookstore-review2
fatal: '../bookstore-review2' contains modified or untracked files, use --force to delete it
--detach opens a worktree without a branch; it is perfect for just inspecting a PR or an old release. If you deleted the folder in Finder or with rm, Git keeps the record for a while; git worktree prune cleans up records of worktrees that no longer exist.
Stash or Worktree?
In the stash post we recommended stash for "a short break", and that still holds. A five-minute interruption, a one-file change, work in the same folder you will come right back to: stash is enough.
Worktree wins in these cases. When the interruption will be long and there is a chance you'll forget what you stashed. When you need to run two branches side by side and compare them, say looking at the output of the old and new version at the same time. When a long test or build runs on one branch and you want to keep working on another. And PR reviews: as a reviewer, opening a PR in a separate folder without disturbing your own work.
It has a cost too: every worktree is a full checkout, and you have to install dependencies separately (node_modules is separate in every folder). Open worktrees next to the repo folder, not inside it; otherwise the main repo sees them as untracked files.
Common Pitfalls
Marking a bisect step wrong. A single wrong good/bad sends the search to the wrong commit, and Git can't notice. If you are not sure, say git bisect skip, or review the decisions with git bisect log.
Forgetting git bisect reset. You stay on a detached HEAD, and your next commit belongs to no branch.
Not checking the exit code of the bisect run script. A script that returns 0 on failure marks every commit as good. Run the script by hand on a known bad and a known good commit first.
Deleting a worktree with rm -rf. The folder goes, but Git's record stays, and the branch keeps showing as "used by another worktree". Use git worktree remove, or git worktree prune if you forgot.
Trying to open the same branch in two worktrees. Git won't allow it. Create a new branch in the second folder or use --detach.
A Small Experiment
★☆☆ Build a ten-commit history in your bookstore repo and deliberately break a test in the seventh commit. Count how many steps manual git bisect takes to find it, and compare the result with log₂(10).
★★☆ Do the same search with git bisect run. Then move the test script outside the repo, deliberately leave a file that doesn't build in one commit, and make the script return exit 125 in that case. Find the skipped commit in the git bisect log output.
★★★ With unfinished changes in place, open a hotfix branch with git worktree add, commit the hotfix, merge it into main in the main folder and remove the worktree. Then do the same scenario with stash, and compare how many commands each way took.
With bisect and worktree, the everyday Git muscle memory is complete. In the next post we move to the side of Git that kicks in automatically: hooks that run before a commit, the config-based hooks that came with Git 2.54, and signing commits with an SSH key to get the "Verified" badge on GitHub.