Git Worktree: Managing Multiple Working Directories

What is a Git Worktree?

A Git worktree is a powerful Git feature that allows you to have multiple working directories associated with a single Git repository. Instead of cloning the same repository multiple times, you can check out different branches in separate directories while sharing the same .git directory.

This makes it easy to work on multiple features, bug fixes, or code reviews simultaneously without the overhead of multiple clones.

And it is also great for spinning some LLM processes to experiment with different branches without messing up your main working directory.

Folder structure

Here is how you would structure your worktrees for different branches. This example shows three worktrees for different branches: feature/new-user-api, chore/dependency-update, and fix/security-patch.

gitGraph
    commit id: "Initial"
    commit id: "Base setup"
    branch feature/new-user-api
    checkout feature/new-user-api
    commit id: "Add user model"
    commit id: "Add user service"
    commit id: "Add user endpoint"
    checkout main
    branch chore/dependency-update
    checkout chore/dependency-update
    commit id: "Update dependency"
    checkout main
    branch fix/security-patch
    checkout fix/security-patch
    commit id: "Fix vulnerability"
    checkout main
    merge feature/new-user-api
    merge chore/dependency-update

Each worktree has its own working directory, index (for staged changes), and HEAD (current branch reference), but they all share the same object database (stores the actual data like commits, files, and history, same stashes), refs (pointers to branches and tags), and configuration (repository settings like remotes and hooks).

It would look like this in the filesystem:

./my-project/
├── my-projec.git/               # Bare repository (Git data only)
├── new-user-api/              # Worktree for the `new-user-api` branch
   ├── .git                   
   ├── <project files>        
├── dependency-update/         # Worktree for the `dependency-update` branch
   ├── .git                   
   ├── <project files>        
└── security-patch/            # Worktree for the `security-patch` branch
    ├── .git                   
    └── <project files>        

When you fetch origin/main, all worktrees see the updated commits because they share the same object database.

Setup and Usage

Initial Setup with Bare Repository

First make a bare clone of your project.

# Clone as bare repository (recommended for worktrees)
git clone --bare git@github.com:user/my-project.git my-project
cd my-project

This creates a bare repository that contains only Git data without any working files.

While worktrees work with normal repository you want to use a bare repository because it offers better separation. It’s just a pure Git data storage while the worktrees handle file operations. But most importantly, it prevents conflicts since you can’t accidentally work in the “main” directory since it has no files.

The bare repository serves as the central storage for all branches and commits.

# Configure remote tracking and fetch all branches
git config --add remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*"
git fetch origin

This configuration ensures that all remote branches are fetched and available in your bare repository, allowing you to create worktrees based on these branches. It is done automatically when you clone normally, but not with a bare repository.

Creating Worktrees

Now that you have your repository set up, you can create worktrees for different branches.

# Create worktrees for different tasks
 git worktree add ../new-user-api -b feature/new-user-api
git worktree add ../dependency-update -b chore/dependency-update
git worktree add ../security-patch -b fix/security-patch

This will create three separate directories outside the bare repository, each checked out to the specified branch. Exactly like the diagram above. Each worktree will have its own copy of the project files for that specific branch. We used the -b to create the branch if it didn’t exist.

Development Workflow Example

Let’s say you’re working on three different tasks simultaneously: implementing a new user API, updating project dependencies, and addressing a critical security vulnerability. Here’s how you’d manage this with worktrees:

cd ../new-user-api
git add .
git commit -m "Add user model"
git push origin feature/new-user-api

But you could do the same in parallel on the other branches:

# Work on dependency update in parallel
cd ../dependency-update
git add .
git commit -m "Update dependencies"
git push origin chore/dependency-update

# Handle urgent security patch
cd ../security-patch
git add .
git commit -m "Fix security vulnerability"
git push origin fix/security-patch

Management Commands

Now that you have worked with multiple worktree, you might want to manage them.

# List all worktrees
git worktree list

# /my-project/my-project.git     1a2b3c4 (bare)
# /my-project/new-user-api       a1b2c3d [feature/new-user-api]
# /my-project/dependency-update  b1c2d3e [chore/dependency-update]
# /my-project/security-patch     c1d2e3f [fix/security-patch]

You can also remove a worktree when you’re done with it. Cleaning up the not only the directory but also the references in the main repository.

# Remove completed worktrees
git worktree remove ./my-project/new-user-api

If that doesn’t work you could try --force or delete the folder directly. But in that case with just folder deleted, the worktree still exists and becomes stale, and you should prune it to remove the references.

# Clean up stale worktrees
git worktree prune

Stacked Diff with Git Worktrees

Git worktrees are particularly useful when working with stacked diffs, where multiple dependent changes are developed in sequence.

For example, if we are working on some new API feature, and we have a new UI that’ll depend on it, we could create a new worktree for the UI branch that is based on the API branch.

# Create a stacked branch for additional changes
git worktree add -b feature/new-user-ui ../new-user-ui feature/new-user-api

With the start point of the worktree set as feature/new-user-api, the feature/new-user-ui branch will be stacked on top of the feature/new-user-api branch. Its worktree is located at /my-project/new-user-ui.

This allows you to work on new-user-ui while keeping it dependent on the changes in new-user-api. Once the base branch is ready, you can merge it and rebase the stacked branch as needed.

gitGraph
    commit id: "Initial Commit"
    commit id: "Base Setup"
    branch feature/new-user-api
    checkout feature/new-user-api
    commit id: "Add User Model"
    commit id: "Add User Service"
    commit id: "Add User Endpoint"
    branch feature/new-user-ui
    checkout feature/new-user-ui
    commit id: "Add UI Components"
    commit id: "Integrate with API"
    checkout main
    merge feature/new-user-api
    merge feature/new-user-ui

However, you should not abuse stacked diffs as breaking refactorings in the base branch can cause significant headaches when rebasing. Depending on the type of changes, the rebase complexity will compound with each stacked branch.

Due to the nature of stacked diff, squashing commits is not recommended as it will force a complex rebasing. As the commit history will be rewritten, you will rebase the final state of the branch with each previous commit. It becomes tedious very quickly, for a 10 commit branch times 3 stacked branches, you will have to do 30 rebase instead of 3 normally.