Git worktrees for fun and profit

I’ve always been a mental multi-tasker when it comes to juggling the things I’m working on. I like having multiple bugs and features in flight at the same time and bouncing between different ideas. Prior to learning about git worktrees, maintaining multiple clones of a repo was my way of managing my multi-tasker workflow. This got really hard to do as my career progressed and I started juggling not just multiple features and bugs — but multiple prototypes and code reviews. About a year ago, I finally decided enough was enough and I needed to invest some energy into my source control workflow so that managing all these different workstreams was somewhat sane. Enter: git worktrees.

Git worktrees allow you to check out multiple branches from a repo at a time, each isolated in its own directory, and all sharing the same Git repository. This has some space-saving benefits (there’s only one .git directory for multiple branches) and some hygiene benefits (no juggling stashes or checkouts between branches). In my workflow, I like to pair git worktrees with another Git feature to maximize structure: bare clones.

Bare clones are initiated by running git clone --bare $REPO_URL and effectively create a clone that contains only the repo’s .git directory and no actual source code. If you couple them with Git worktrees, you can get a nice workflow where one directory contains just the .git repository and other directories contain actual source code that’s been checked out.

To structure things in my workflow, I create a parent directory that will contain the bare clone and any worktrees.

$ mkdir aspire
$ cd aspire
$ git clone --bare https://github.com/dotnet/aspire.git
$ ls
aspire.git

In this case, aspire.git contains only the git repository. To checkout the actual source code from the main branch:

$ cd aspire.git
$ git worktree add ../main

This creates a sibling directory to aspire.git that contains source code that can be built/debugged/etc. And that forms the basis of my workflow. Each repo gets its own directory with a bare clone and sibling directories containing the Git worktrees.

Some things to note about worktrees and bare clones:

$ git rev-parse --abbrev-ref --symbolic-full-name @{u}
fatal: no upstream configured for branch 'main'

This means that keeping your local worktree-based branch in sync with a remote can become difficult. If we try to set the remote with the usual commands, we’ll see:

$ git branch -u origin/main
fatal: the requested upstream branch 'origin/main' does not exist

To fix this issue, we need to configure the bare clone to properly fetch remote branches. By default, bare clones don’t set up the standard refspec that maps remote branches to local remote-tracking branches. We can do this by running the following command to fetch the correct refspecs for the “origin” remote.

$ git config remote.origin.fetch '+refs/heads/*:refs/remotes/origin/*'

This tells Git to fetch all branches from the remote and create corresponding remote-tracking branches under refs/remotes/origin/. The + prefix allows non-fast-forward updates. Next, we can fetch the remote branches to populate the remote-tracking branches:

$ git fetch origin

Now, we can see the remote-tracking branches:

$ git branch -r
  origin/main
  origin/feature-branch

Add set up upstream tracking for the other

$ git branch -u origin/main
Branch 'main' set up to track 'origin/main'.

At this point, you can use standard Git commands like git pull, git push, and git status will show you whether your branch is ahead, behind, or in sync with the remote.

For new worktrees created from remote branches, you can now use the -t flag to automatically set up tracking:

$ git worktree add -t ../feature-work origin/feature-branch

This creates a new worktree and automatically configures it to track the remote branch, so now we can pull/push from and to the tracking branch the same way we would from a non-worktree based setup. Nice!