Working on Two Branches at Once
git worktree: several working directories over one object database. How it works underneath, the failure modes, and the patterns worth adopting — without a second clone.
git worktree: several working directories over one object database. How it works underneath, the failure modes, and the patterns worth adopting — without a second clone.
You are four hours into a refactor. Half the tests are red, three
files are renamed, a migration is half applied. Then production
breaks and the fix belongs on main. Or the calmer version of
the same problem: an integration suite that takes twenty minutes,
and twenty minutes you would rather spend writing code than
watching a progress bar.
Both are one constraint in two costumes — a repository has one
working directory, and you need two things happening in it at
once. git worktree removes the constraint. By the end of this
lesson you will have several branches checked out simultaneously
from a single object database, know exactly which files on disk
make that work, and know the four ways it bites.
git stash does not give you parallelism; it gives you a stack.
It writes your work into commit objects hanging off refs/stash —
one for the index, one for the working tree, optionally one for
untracked files — then resets you to a clean tree. You still have
exactly one checkout. The test run and the hotfix still cannot
happen at the same time, because there is still only one set of
files for them to happen in.
Stashing also throws away everything warm: your build cache
invalidates because every mtime moved, your language server
reindexes, and your open buffers point at a different version of
the file. git stash without -u leaves untracked files behind,
and restoring without --index flattens your careful staging
into one undifferentiated pile.
A second git clone does give you real parallelism, and pays for
it twice. First in bytes: a full second copy of .git/objects,
which on a mature repository is the expensive part. Then in
drift — the second clone has its own remote-tracking refs,
config, hooks, stashes, and reflogs. A commit you make in clone B
does not exist in clone A until you push it somewhere and fetch
it back. You end up maintaining two repositories that happen to
have similar contents.
A worktree takes the middle path.
A stack, not parallelism.
You still have exactly one checkout, so the test run and the hotfix still cannot happen at the same time.
And it throws away everything warm: build caches invalidate, the language server reindexes, open buffers point at a different version of the file.
Real parallelism, paid for twice.
Once in bytes — a full second copy of .git/objects — and
once in drift.
Separate refs, config, hooks, stashes and reflogs. A commit in clone B does not exist in clone A until you push and fetch it back.
One object database, several directories.
One set of refs shared between them, and each directory has
its own HEAD and its own index.
Real parallelism, and a commit made in one is immediately visible from the others.
The core form takes a directory to create and a commit-ish to check out there. Siblings of the repository are the conventional home for them, so nothing lands inside the repository itself:
git worktree add ../hotfix hotfix-2024-11 # existing branch
git worktree add -b release-2.4 ../release # create branch too
git worktree add --detach ../probe v2.3.1 # no branch at all--detach is the one people skip and then miss. When you only
want to inspect a tag, build an old revision, or run something
against a specific commit, it gives you the files without
inventing a throwaway branch name you will forget to delete.
Omit the commit-ish entirely and Git guesses from the directory
name: git worktree add ../feature-x creates a branch called
feature-x from your current HEAD. Name an existing branch and
it is checked out as-is; name a branch that exists only on a
single remote and you get a local tracking branch for free, the
same DWIM rule git switch uses.
$ git worktree add ../review-42 review-42
Preparing worktree (new branch 'review-42')
branch 'review-42' set up to track 'origin/review-42'.
HEAD is now at 981b162 Tighten retry budgetLesson 1 described a repository as three things: an object
database, refs, and a HEAD pointing into them. A worktree cuts
that structure along one specific seam. Objects and refs stay
shared and singular. HEAD and the index are duplicated per
working directory.
You can see the whole mechanism with two commands. Inside a
linked worktree, .git is not a directory but a one-line file
pointing at where the private half lives:
$ cat ../hotfix/.git
gitdir: /home/you/project/.git/worktrees/hotfix
$ ls -A .git/worktrees/hotfix
HEAD ORIG_HEAD commondir gitdir index logsHEAD is that worktree's own symref (ref: refs/heads/hotfix-2024-11).
index is its own staging area, so staged changes in one
worktree are invisible in another. logs/HEAD is its own reflog.
commondir holds ../.., the way back to the shared
.git, and gitdir holds the absolute path of that worktree's
.git file — the back-pointer git worktree prune checks when
it decides whether a worktree still exists.
Everything not in that list is shared. One .git/objects, one
refs/heads, one refs/tags, one config, one set of hooks. A
branch created in ../hotfix is visible from your main checkout
the instant it is written, because it is the same ref in the same
store. The narrow exceptions are the
per-worktree ref namespaces — HEAD, ORIG_HEAD,
refs/bisect/*, refs/worktree/*, and refs/rewritten/* —
which resolve differently depending on which worktree you are
standing in. That is what makes bisecting and rebasing in a
worktree safe.
git worktree list is the human view — path, abbreviated commit,
and either a branch or (detached HEAD), with locked and
prunable flags appended where they apply:
$ git worktree list
/home/you/project 981b162 [main]
/home/you/probe 981b162 (detached HEAD)
/home/you/review-42 4c1a09f [review-42]
/home/you/stale 981b162 [old-spike] prunableFor anything scripted, use git worktree list --porcelain. It
emits stable labelled lines with a blank line between records,
and never abbreviates:
worktree /home/you/project
HEAD 981b162790864ff24c87c8eb16df78c544237b96
branch refs/heads/main
worktree /home/you/stale
HEAD 981b162790864ff24c87c8eb16df78c544237b96
branch refs/heads/old-spike
prunable gitdir file points to non-existent locationThe labels are worktree, HEAD, branch, detached, bare,
locked, and prunable; the last two carry their reason as the
rest of the line. Add -z if you are parsing paths that might
contain newlines.
Shared refs give the first rule: the same branch cannot be checked out in two worktrees at once.
$ git worktree add ../dup hotfix-2024-11
fatal: 'hotfix-2024-11' is already used by worktree at '/home/you/hotfix'This is not fussiness. HEAD is per worktree, but the branch ref
it points at is shared. Two directories committing onto one ref
would move it under each other's feet — the second worktree's
index and HEAD then describe a commit that is no longer the
branch tip, so its git status reports fictional changes and its
next commit quietly reparents work.
--force skips the check. It does not make any of that safe; it
buys you a second read-only view of a branch — for a build, a
grep, a tool that wants a big tree — on the promise that you will
not commit there. The related -B <branch> resets an existing
branch as it creates the worktree: a different sharp knife.
The second rule costs people an afternoon. Worktrees share
tracked history, not your working directory's debris. Untracked
and ignored files are not shared, so every new worktree is a
pristine tree with no node_modules, no venv, no target/, no
.env. Plan for it: keep a bootstrap script, or point your
toolchain at a shared store where it supports one — a global pnpm
store, a CARGO_TARGET_DIR outside the tree. Symlinking
node_modules between worktrees is not that; it breaks the
moment two branches disagree about a dependency version.
Delete a worktree directory with rm -rf and Git does not
notice. The administrative directory under .git/worktrees/
survives, the branch still counts as checked out somewhere, and
list marks the entry prunable. git worktree remove does
both halves; prune cleans up after you did it by hand:
git worktree remove ../hotfix # delete files + admin data
git worktree prune -n -v # dry run: what would go
git worktree prune -v # actually clear tombstonesremove refuses when the worktree contains modified or untracked
files, which is what you want. To relocate one, use git worktree move rather than mv, so both pointers stay correct.
Locking exists for worktrees Git cannot see right now — one on a
USB drive, a network mount, an external disk currently unplugged.
A locked worktree is exempt from prune, so an unmounted volume
does not read as deleted:
git worktree lock ../on-usb --reason "external drive"
git worktree unlock ../on-usbYou can lock at creation time too, with git worktree add --lock --reason "...". Once locked, remove needs -f -f — one force
for the lock, one for the contents — or an unlock first.
By default, git worktree add ../review-42 with no commit-ish
creates a local branch review-42 from your current HEAD, even
when origin/review-42 exists. That is rarely what you meant
when the directory name matches a colleague's branch.
Set worktree.guessRemote and Git checks the remotes first:
git config --global worktree.guessRemote true$ git worktree add ../review-42
Preparing worktree (new branch 'review-42')
branch 'review-42' set up to track 'origin/review-42'.If exactly one remote has a branch of that name you get a
tracking branch; if none does, you get the old behaviour with no
error. --guess-remote and --no-guess-remote override the
setting per command, and --track / --no-track set upstream
configuration explicitly when you name the branch yourself.
Keep a permanent worktree for main and point your dev server or
smoke tests at it. You get a running, trustworthy copy of the
application next door while you tear a feature branch apart, and
comparing behaviour becomes two browser tabs rather than two
checkouts.
Give each code review its own worktree. With guessRemote on,
git worktree add ../review-1204 puts a colleague's branch in a
directory you can build and run without touching your own work.
And bisect in a worktree. Lesson 2 drove git bisect through
dozens of checkouts; because refs/bisect/* and the bisect state
files are per worktree, running the search in a throwaway
directory leaves your main checkout, your editor state, and your
long-running processes where they were.
Worktrees isolate checkouts, and nothing else. They do not
isolate processes: two worktrees running the same app still fight
over the same port, the same local database, the same container
names, the same global caches. They do not give you a second
repository — config, hooks, remotes, and refs are shared, so
"code from another project needs to appear inside this one" is a
different lesson's problem. They do not shrink anything either;
each one is a full checkout of the working tree, so if disk or
clone time is the constraint, partial clone and sparse-checkout
are the answer. And they offer no protection from
repository-wide operations: a history rewrite or an aggressive
gc from one worktree affects every other one, and can leave a
review worktree sitting on commits no branch reaches any more.
Reach for worktrees when the constraint is genuinely "one
checkout at a time"; otherwise they are extra directories to
keep tidy.
# Create
git worktree add ../hotfix hotfix-2024-11 # existing branch
git worktree add -b feat ../feat # new branch + dir
git worktree add -B feat ../feat main # reset branch, then add
git worktree add --detach ../probe v2.3.1 # no branch
git worktree add ../feature-x # branch from dir name
git worktree add --force ../ro main # 2nd view, read-only
git worktree add --lock --reason "usb" ../u main
# Inspect
git worktree list # human view
git worktree list --porcelain # stable, for scripts
git worktree list --porcelain -z # NUL-separated
git rev-parse --git-dir # this worktree's dir
git rev-parse --git-common-dir # the shared .git
cat ../hotfix/.git # the gitdir: pointer
ls -A .git/worktrees/hotfix # HEAD, index, logs...
# Maintain
git worktree remove ../hotfix # files + admin data
git worktree remove --force ../hotfix # discards local edits
git worktree move ../old ../new # relocate safely
git worktree prune -n -v # dry run
git worktree prune -v # clear tombstones
git worktree lock ../on-usb --reason "..." # exempt from prune
git worktree unlock ../on-usb # allow prune again
# Config
git config --global worktree.guessRemote true
git worktree add --guess-remote ../review-42
git worktree add --track -b fix ../fix origin/fixYou can now hold several branches open at once from a single object database, read the plumbing that makes it work, and predict the failure modes rather than discovering them at 2am.
The natural next step is submodules and subtrees. Worktrees answer one repository, several working directories; submodules and subtrees answer the opposite question — several repositories, one build — and the two get confused constantly. The contrast is sharp once you have this lesson: a worktree shares one object database, while a submodule deliberately keeps a second one, pinned by a single gitlink entry in your tree.
Before moving on, add a permanent main worktree to a project
you work on daily and live with it for a week. Then run your next
bisect in a throwaway worktree and notice what your editor does
not do. These stop feeling like tricks around the third time you
reach for them without thinking.