Boost Productivity with Git Worktrees

A short & concise guide on getting productive with Git Worktrees.
Article Preview (Terminal)
# Start a repository using Git Worktrees
$ git clone REPOSITORY_NAME --bare

# Add a worktree (worktree name matches branch name)
$ git worktree add WORKTREE_NAME

# Navigate to a worktree
$ cd WORKTREE_NAME

# Remove a worktree
$ git worktree remove WORKTREE_NAME

# List all worktrees
$ git worktree list

# Create a worktree with better-commits
$ npm install -g better-commits
$ better-branch

What are Worktrees

Manage multiple working trees attached to the same repository

In other words, they allow you have multiple commits checked out at the same time in the same repository.

Why Worktrees?

This is best explained with a common scenario.

You’re working on a web project that has dependencies like node_modules. As you work on feature_a, an urgent bug ticket comes in. You need to switch to branch bugfix_b.

When switching branches and serving locally, you often have to do this dependency dance:

Example flow
Checkout new branch BUGFIX_B
install dependencies
fix the bug
Checkout previous branch FEATURE_A
install dependencies
resume working on feature

Worktrees allow you to have multiple working trees that each have their own state. I use the term state broadly because a worktree is essentially just a folder on your machine. Thus, even editor state (such as marks, recent files, harpoon, etc…) can be maintained per branch via worktrees.

This is ideal when working on multiple branches at once, having multiple PRs open at the same time, or being able to pivot between tasks quickly.

Getting Started with Worktrees

To use worktrees, it’s recommended to use a bare repository

Terminal
# --> Clone repository ✅
$ git clone YOUR_REPOSITORY --bare

# --> Create worktree 🌳
$ git worktree add main

This will checkout a new worktree (named main) at main’s HEAD.

This is a little confusing, because the worktree name and the branch name will match. — If the branch exists, it will checkout that branch. If it doesn’t exist, it will create a new branch that starts at the HEAD of main/master.

Worktree Examples
# main branch
$ git worktree add main
Preparing worktree (checking out 'main')
HEAD is now at c69d66d fix: improve docs (#9)

# existing feature branch
$ git worktree add about-me
Preparing worktree (checking out 'about-me')
HEAD is now at 7d6a74b Fix paths in prod

# new feature branch -- notice commit is same as main
$ git worktree add does-not-exist
Preparing worktree (new branch 'does-not-exist')
HEAD is now at c69d66d fix: improve docs (#9)

Running ls you can see each worktree is represented by a folder, which are essentially separate checkouts with their own state.

Concrete example of state. If you run npm install in each folder. Each worktree will have it’s own node_modules.

Worktree Pain Points

There’s a few things that come to mind that are somewhat inconvenient when using worktrees.

  • Worktree names get confusing when using a branch naming convention with slashes (everduin94/feat/X-123-example)
  • You have to remember to install dependencies / build / whatever your app requires to run locally, per worktree.
  • Keeping the local main branch up to date is not as intuitive as its branch counterpart.

To alleviate some of these pain points, I wrote a CLI that optimizes a few operations around working with git. (Originally, it was just for formatting commits, hence the name, better-commits).

Terminal
# --> Install ✅
$ npm install -g better-commits

# --> Create worktree 🌳
$ better-branch

This runs through a list of prompts to create a common & explicit branch naming convention. If you want to use a branch naming convention like this without better-commits, the command would translate to: git worktree add WORKTREE_NAME BRANCH_NAME. This is because worktrees will create a new folder for each slash in the worktree name (which is probably not what you want).

However, we still have a couple inconveniences. Our dependencies aren’t automatically installed, and our main HEAD commit we create worktrees from may be out of sync with the remote. To resolve these issues we can update our .better-commits.json.

If you’ve already ran the tool once, the file should live in your $HOME directory. Otherwise, create the file there.

We need these two lines of configuration (see all of the options here). This will run git fetch origin main:main, which will update the commit that our local main is pointing to with the latest commit on the remote, before creating the worktree.

{
  "worktree_pre_commands": ["git fetch origin main:main"],
  "worktree_post_commands": ["npm install"]
}

In the post commands, we’ll run any necessary commands after the worktree is created. npm install is specific to certain web projects. You can add whatever is appropriate for building / serving your project locally. Any valid shell command works.

Conclusion

Worktrees can be a fantastic tool to work with when setup properly. Another lib/plugin you may consider using to improve your experience is something like git-worktree.nvim (or it’s equivalent in vscode, intellij, etc). This allows you to list, fuzzy find, and switch worktrees easily.

And if you like better-commits consider giving it a star on Github ⭐! Thanks for reading!

Wed Dec 27 2023