Managing Branches With Git Worktrees
I’ve been using git worktrees to work
on multiple branches of the same repo side by side, without the usual
git stash shuffle. The setup I use comes from
this excellent article,
which uses a bare clone as the single source of truth, with each branch
checked out into its own folder next to it:
project/
├── .bare/ # bare clone — the actual git history
├── .git # one-line file pointing to .bare
├── main/ # worktree for the main branch
└── feature/
└── some-branch/ # worktree for feature/some-branch
Bootstrapping a new repo
For a fresh repo, I use a small wtree script that automates the setup:
#!/bin/bash
# wtree - bootstrap a git-worktree "pro setup" for a repository.
#
# Usage: wtree <git-repo-url>
#
# Run this in an empty directory. It will:
# 1. Bare-clone the repo into a hidden .bare folder
# 2. Point the root folder's .git file at .bare
# 3. Configure remote tracking so `git fetch` sees all remote branches
# 4. Fetch everything
#
# After that, create worktrees with: git worktree add <branch>
set -e
REPO_URL=$1
SCRIPT_NAME=$(basename "$0")
if [ -z "$REPO_URL" ]; then
echo "Error: Missing repository URL."
echo "Usage: $SCRIPT_NAME <repo-url>"
exit 1
fi
FILES_IN_DIR=$(ls -A | grep -v "^${SCRIPT_NAME}$" || true)
if [ -n "$FILES_IN_DIR" ]; then
echo "Error: Current directory is not empty!"
echo "To keep your worktree setup clean, please run this in a fresh folder."
exit 1
fi
echo "Initializing git worktree setup..."
# Clone as bare into a hidden directory
git clone --bare "$REPO_URL" .bare
# Link the root to the bare repository
printf 'gitdir: ./.bare' > .git
# Enable remote tracking (the "blind clone" fix)
git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*"
# Fetch everything
git fetch --all
echo "Setup complete!"
echo "Next step: git worktree add main"
Usage:
mkdir -p ~/Projects/my-repo && cd ~/Projects/my-repo
wtree [email protected]:org/my-repo.git
git worktree add main
This post is about what happens after that initial setup — the day-to-day of adding, updating, and removing branches.
Adding a new worktree
The general form:
git worktree add <path> [-b <new-branch>] [<start-point>]
Check out a branch that already exists (locally or on the remote) into its own folder:
git worktree add feature/better-depends-on
Create a brand new branch, based on another branch:
git worktree add improve-db -b feature/fix-db feature/improve-db
Slashes in the branch name automatically create nested folders, so
feature/my-branch becomes feature/my-branch/ on disk.
Removing a worktree
Don’t just rm -rf the folder — git still thinks the worktree exists and
will refuse to reuse that path later (it shows up as “prunable” in
git worktree list). Use:
git worktree remove <path>
If you did delete the folder manually, clean up the stale reference with:
git worktree prune
Other useful commands:
git worktree list # see every worktree and which branch it has checked out
git worktree lock <path> # protect a worktree from being pruned
git worktree unlock <path>
Keeping a branch up to date (the “I cloned this months ago” problem)
This is the part that trips people up. Say you set up the worktree repo a
while back, and today you need to open a PR against the current state of
main. Since all worktrees share one .git database, you only need to
fetch once, from any worktree:
git fetch --all
Gotcha: a bare clone by default only tracks the remote’s default branch. If
git fetchlooks like it’s doing nothing even though new branches/commits exist upstream, fix the fetch refspec once:git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*" git fetch --all
Once fetched, every worktree instantly sees the new remote refs. Now, to
start a fresh PR branch from the latest main:
git worktree add feature/my-new-thing -b feature/my-new-thing origin/main
This creates a brand-new local branch feature/my-new-thing, based on the
current origin/main — not whatever main looked like when you first
cloned.
If you already have a long-lived local branch (e.g. an existing main
worktree) that’s fallen behind, update it in place:
cd main
git fetch --all
git pull --ff-only # or: git merge origin/main / git rebase origin/main
--ff-only is a good default here since it fails loudly instead of creating
an unexpected merge commit if your local main has diverged.
Summary
- One
git fetch --allrefreshes every worktree at once — no need to fetch per-branch. - Branch off
origin/<branch>(not your possibly-stale local branch) when starting new work to guarantee you’re building on the latest changes. - Use
git worktree remove, notrm -rf, to avoid the “prunable but not removable” trap.