How I Use Git Worktrees
There are a bunch of posts on the internet about using git worktree command. As far as I can tell,<br>most of them are primarily about using worktrees as a replacement of, or a supplement to git<br>branches. Instead of switching branches, you just change directories. This is also how I originally<br>had used worktrees, but that didn’t stick, and I abandoned them. But recently worktrees grew<br>on me, though my new use-case is unlike branching.
When a Branch is Enough
If you use worktrees as a replacement for branching, that’s great, no need to change anything! But<br>let me start with explaining why that workflow isn’t for me.
The principal problem with using branches is that it’s hard to context switch in the middle of doing<br>something. You have your branch, your commit, a bunch of changes in the work tree, some of them<br>might be stages and some unstaged. You can’t really tell Git “save all this context and restore it<br>later.” The solution that Git suggests here is to use stashing, but that’s awkward, as it is too<br>easy to get lost when stashing several things at the same time, and then applying the stash on top<br>of the wrong branch.
Managing Git state became much easier for me when I realized that the staging area and the stash are just bad<br>features, and life is easier if I avoid them. Instead, I just commit whatever and deal with<br>it later. So, when I need to switch a branch in the middle of things, what I do is, basically:
$ git add .<br>$ git commit -m.<br>$ git switch another-branch
And, to switch back,
$ git switch -
# Undo the last commit, but keep its changes in the working tree<br>$ git reset HEAD~
To make this more streamlined, I have a ggc utility which does “commit all with a trivial message”<br>atomically.
Reminder: Git is not a version control system, Git is a toolbox for building a VCS. Do have a<br>low-friction way to add your own scripts for common git operations.
And I don’t always reset HEAD~ — I usually just continue hacking with . in my Git log and then amend the commit<br>once I am satisfied with subset of changes
Reminder: magit, for Emacs and VS Code, is<br>excellent for making such commit surgery easy. In particular, instant fixup is excellent. Even<br>if you don’t use magit, you should have an equivalent of instant fixup among your Git scripts.
So that’s how I deal with switching branches. But why worktrees then?
Worktree Per Concurrent Activity
It’s a bit hard to describe, but:
I have a fixed number of worktrees (5, to be exact)
worktrees are mostly uncorrelated to branches
but instead correspond to my concurrent activities during coding.
Specifically:
The main worktree is a readonly worktree that contains a recent snapshot of the remote main<br>branch. I use this tree to compare the code I am currently working on and/or reviewing with the<br>master version (this includes things like “how long the build takes”, “what is the behavior of<br>this test” and the like, so not just the actual source code).
The work worktree, where I write most of the code. I often need to write new code and compare it<br>with old code at the same time. But can’t actually work on two different things in parallel.<br>That’s why main and work are different worktrees, but work also constantly switches branches.
The review worktree, where I checkout code for code review. While I can’t review code and write<br>code at the same time, there is one thing I am implementing, and one thing I am reviewing, but the<br>review and implementation proceed concurrently.
Then, there’s the fuzz tree, where I run long-running fuzzing jobs for the code I am actively working<br>on. My overall idealized feature workflow looks like this:
# go to the `work` worktree<br>$ cd ~/projects/tigerbeetle/work
# Create a new branch. As we work with a centralized repo,<br># rather than personal forks, I tend to prefix my branch names<br># with `matklad/`<br>$ git switch -c matklad/awesome-feature
# Start with a reasonably clean slate.<br># In reality, I have yet another script to start a branch off<br># fresh from the main remote, but this reset is a good enough approximation.<br>$ git reset --hard origin/main
# For more complicated features, I start with an empty commit<br># and write the commit message _first_, before starting the work.<br># That's a good way to collect your thoughts and discover dead<br># ends more gracefully than hitting a brick wall coding at 80 WPM.<br>$ git commit --allow-empty
# Hack furiously writing throughway code.<br>$ code .
# At this point, I have something that I hope works<br># but would be embarrassed to share with anyone!<br># So that's the good place to kick off fuzzing.
# First, I commit everything so far.<br># Remember, I have `ggc` one liner for this:<br>$ git add . && git commit -m.
# Now I go to my `fuzz` worktree and kick off fuzzing.<br># I usually split screen here.<br># On the left, I copy the current commit hash.<br># On the right, I switch to the fuzzing worktree,<br># switch to the copied commit, and start fuzzing:
$ git add . && git commit -m. |<br>$ git...