Conrad Aidan Muan

← Home

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 @conradmuan
View 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 worktrees, 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 builds 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