Git worktrees
Oct 1, 2024
I posted the following on threads and I got a few requests to post the readme I wrote at work publicly.
Post by @conradmuanView on Threads
Here's an edited version:
Or alternative title: How to win friends and influence people with superior git
knowledge
At Adobe, we use rush to manage a huge codebase with multiple packages. It's particularly useful in large-scale projects where many different packages or modules are developed together but deployed or consumed independently. It's particularly useful for its support for incremental builds.
This is also where I ran into problems. Switching branches came at a cost whenever I rebased main. Sometimes, an upstream dependency changed so drastically that rebuilding wouldn't work (we have a caching system that I still don't understand). The only way to recover from is to delete all untracked files and reinstall as if this was a fresh clone:
# The nuclear option
git clean -fdx
rush install
Git worktrees allow you to work on multiple branches simultaneously by creating separate working directories, each associated with a different branch. This feature is especially useful in large projects (like large monorepos with cumbersome build steps), as it eliminates the need to switch branches in a single directory and lets you maintain isolated work environments. Each worktree operates independently, with its own working directory and checked-out branch, enabling parallel builds.
Getting Started
Here's how we can create a worktree for our feature, feature-foo
. Assumes you are in the main
branch of your original working directory
# Assuming in main branch
git pull origin main
git worktree add ../jira-100-feature-foo
This creates a new directory in ../jira-100-feature-foo
as if you just cloned the repo with a branch name of jira-100-feature-foo
.
However, some Engineers tend to name branches a certain way (the correct way) ie myname/jira-100-feature-foo
. Here's how we'd create the same worktree but with a different branch name
# Assuming your current directory is in your project's git repo
git fetch origin main
git worktree add ../jira-100-feature-foo -b myname/jira-100-feature-foo origin/main
This creates a new directory in ../jira-100-feature-foo
with a branch myname/jira-100-feature-foo
tracking origin/main
. With this method, you can name your branch anything
First Caveat
Each worktree is like a fresh clone. You'll need to install dependencies and run a build for each worktree. As mentioned in the intro, I work with a very large monorepo utilizing rush
. My typical workflow when creating a new worktree looks like this:
cd ../jira-100-feature-foo
rush install && rush update && rush build
This usually takes 10 minutes. If this is similar to your usecase, you could just cd
into another worktree while this one installs and builds.
Example:
# While jira-100-feature-foo is installing...
cd ../jira-200-feature-bar
# Open VSCode or your IDE of choice here and do some things
code .
Second Caveat
I've been recommending opening an IDE for each worktree.
If you are working in a monorepo, chances are your codebase is huge. You could set up your IDE of choice to open your project's parent directory. However, this might wreak havoc with intellisense and slow things down. I wouldn't know, I haven't tried it 🤷
Updating from main
If you need to rebase or merge from main, you don't need to checkout main
then pull
then checkout your branch and rebase / merge.
In fact, you won't be able to checkout main
if you're in a worktree.
git checkout main
fatal: 'main' is already checked out at '/Users/myname/Work/main'
Instead, stay in your current work tree and fetch, then rebase from origin 1
# Fetch
git fetch origin main
# Now rebase from origin
git rebase origin/main
# Run your build steps. For me it's
rush update && rush build
Your worktree is now up to date
1 Otherwise you'd have to cd
to the worktree tracking main
, git pull
, cd
back to your worktree and then rebase locally via git rebase main
. This trick skips all that.
Multitasking Worktrees
The real benefit for worktrees is when you have a couple of active branches.
A common git workflow is to stash work, and then switch to another branch to continue working:
# Old way of doing things:
# Something urgent comes up, stash your work and checkout another branch
git add .
git stash
# Fetch main and checkout a fresh branch from origin/main
git fetch origin main
git checkout -b myname/urgent-bugfix
# Do some work and push up changes
git add .
git commit -m "fixed urgent bug"
git push origin myname/urgent-bugfix
# Switch back to last branch and stash pop
git checkout -
git stash pop
With git worktree
s, you just have to switch directory
# Something urgent comes up
# Fetch main
git fetch origin main
# Add a worktree and checkout a branch tracking origin/main
git worktree add ../urgent-bugfix -b myname/urgent-bugfix origin/main
# change directory and start working
cd ../urgent-bugfix
# Do work, commit and push
git add .
git commit -m "fixed urgent bug"
git push origin myname/urgent-bugfix
# Back to work, nothing to unstash
cd -
What I'm glossing over here is that if you need to build, you'd still need to install your dependencies and run your build step. For me it would be rush install && rush build
.
Alternatively, you could always have a ready and up to date worktree to switch to. I like this method a lot.
# Something urgent comes up
cd ../on-call
# do some work, commit, maybe rename the branch, push
git add .
git commit -m "fixed urgent bug"
git branch -m myname/urgent-bugfix
git push origin myname/urgent-bugfix
# back to work
cd -
# (optional) clean up and prep for the next emergency:
git remove worktree ../on-call # or "git worktree move" if you need to keep it around
git fetch origin main
git worktree add ../on-call -b myname/oncall-branch origin/main
# probably in another tab / window:
cd ../on-call
# install deps, run build steps for example
rush install && rush build
Drawbacks
Each worktree is like a fresh clone. If you're working with node packages or even using rush
like me, that means each worktree will have their own node_modules
for each of your monorepo's packages.
It's probably best to open an IDE for each worktree instead of opening a single IDE with all your worktrees in a single workspace. I haven't tried that but that sounds terrible for intellisense. I could be wrong though.
It's common to have multiple processes running a watch
or other service when developing locally. If using rush
you may end up in a scenario where you have multiple rushx build
s of a package in multiple worktrees. To keep things sane, I tend to:
- keep each worktree in its own window (or tab if your shell emulator can do that)
- split the window / tab into panes for each process to run your script ie:
rushx
- kill any
rushx
anytime I switch to another worktree
Tips and tricks
Create worktree from an existing branch
If you have an existing branch myname/no-jira-tiny-fixes
and want to create a worktree for it:
git worktree add ../worktree-name myname/no-jira-tiny-fixes
List
This lists the paths to the worktree matched with the branch it's attached to.
# List all worktrees
git worktree list
If you get into a situation where you've deleted a worktree folder, without running git worktree remove
it'll show up here as prunable
. You can prune with
git worktree prune
This will remove the reference to the worktree (does not delete the branch)
Deleting branches
You can't delete a branch if it's already attached to a worktree. Delete the worktree first (which deletes the directory)
# Remove a worktree
git worktree remove ../jira-100-feature-foo
# Delete the branch
git branch -d myname/jira-100-feature-foo
Renaming
Sometimes we don't always know how to name our branch until just before PR:
git worktree add ../fix-up
# get some work done and then find the right JIRA
git worktree move ../fix-up ../jira-300-fixes-x
# now rename the branch to match (if you want to be pendantic)
cd ../jira-300-fixes-x
git branch -m myname/JIRA-300-fixes-x
# now issue the PR
git push origin myname/JIRA-300-fixes-x