Managing Multi-branch Workflows With Git Worktrees

March 22, 2022

You're working on a branch and have made some changes. Something comes up and you now need to put your work on this branch on hold and jump to another branch, make some changes there and then jump back to your other branch. The need for the switch may be brought on by e.g. a hotfix, a pair programming session, checking out a PR branch. These sorts of need-for-parallelization scenarios are particularly common when working on collaborative projects with tons of feature branches. The challenge is finding a way to preserve the work in progress in each branch and switch between them with as little friction as possible.

Problem: working efficiently on multiple branches

There are several ways of switching between branches mid work:

  • Stashing changes
  • Using WIP commits
  • Using separate repositories for branches

Stashing changes

You can stash all your changes and checkout the other branch by running git stash and git stash pop to retrieve the changes when switching back to your WIP branch. The problem is, stashed changes can get stashed away for good — I often forget to pop them back. Also, sometimes you need to stash multiple times if switching between multiple branches — managing Git stash becomes another problem in and of itself.

Using WIP commits

Another solution is to commit all changes under what I like to label a 'WIP' commit and later use git reset HEAD~1to restore these changes. I prefer this over stash, but theres the slight problem of WIP commits getting included in push operations and ending up in the remote repository.

Both stashing and using WIP commits suffer from a more frustrating issue — the project's node modules need to be reinstalled when switching between branches unless there have been no changes to dependencies. This, for me, is a massive headache.

Maintaining a separate Git repository

A way to make node modules independent of the branch you're working on is by creating multiple clones of the repository — a Git directory for each branch. Although better than stashing or using WIP commits, this approach is somewhat heavy and involves quite a bit of friction. To make use of it, you need to clone a repo for each branch that you need to jump to and then manually delete the cloned repos when you're done. Since Git clone copies down the whole repository each time its run, the process can take a long time with large repositories. It also means that hard drive space is wasted. Further, all cloned repositories need to be kept in sync with the remote repository they're clones of.

Git worktrees: the ultimate solution:

I recently discovered Git worktrees, a native git feature which solves all of the issues with the other above-mentioned workflows.

By default, Git only allows a single working tree to be attached to a repository. A working tree (also called working directory) contains the contents of the commit that HEAD is pointing to in addition to any local changes.

For example, if I run git worktree list in my current repository, I see the following:

<path_to_my_repo>/joelpersonalsite  cc82669 [git-worktree-post]

which can be represented with the following diagram:

project_git_repository
 ├─ [ git-worktree-post ] (HEAD commit + local changes)

where cc82669 is the commit hash that HEAD is pointing to, and git-worktree-post the name of the branch.

If I need to keep my local changes, and pivot to working in the main branch, I can use git worktrees to run:

git worktree add ../joelpersonalsite-main main

This creates a new working tree (directory) called joelpersonalsite-main with the latest commit of the main branch checked out.

We now have the following structure for our repository:

project_git_repository
 ├─ [ git-worktree-post ](HEAD commit contents + local changes)
 ├─ [ main ](HEAD commit contents + local changes)

Now, all I need to do to switch to main while keeping my local changes in the current branch is to cd into the main directory. In this case, joelpersonalsite-main.

The great thing about Git uses hardlinks for worktrees, which means that we don't need to re-download the project for our new worktree.

Removing worktrees

Once you're done with a worktree, removing it is as simple as

git worktree remove <path_to_worktree>