Tor Klingberg Tor Klingberg - 2 months ago 9
Git Question

Why can two git worktrees not check out the same branch?

Using a separate git-worktree, why can I not check out the same branch as in the main working copy? If I try, I get the error:

fatal: 'mybranch' is already checked out at '/path/to/repo'


I can see that if I check in from one worktree, the other would end up in a detached HEAD state, but is that so bad, and why can I not even check out the same branch?

Answer

I can see that if I check in from one worktree, the other would end up in a detached HEAD state

Actually, it wouldn't, and that's the problem!

Each work-tree has its own HEAD, and its own index (aka staging-area or cache). All share the actual underlying repository, and the underlying branch tip files such as .git/refs/heads/mybranch.

Suppose, then, that two different work-trees (I'll make them both separate from the main repo just so that there's no obvious "preferred" one) both have HEAD pointing to mybranch, and you make a commit from one of the two work-trees:

repo$ cd ../worktree1
worktree1$ ... hack away ...
worktree1$ git add bar1 bar2 && git commit -m 'foo some bars'

What happens now is the usual: Git writes the index to one or more trees, writes a new commit using the new tree and whatever commit mybranch resolves to as its parent commit, and updates mybranch to point to the new commit. The index for worktree1 now matches the new commit. Now we do this:

worktree1$ cd ../worktree2
worktree2$ ... modify unrelated file, not bar1 or bar2 ...
worktree2$ git add unrelated && git commit -m 'unrelated change'

What happens now is that Git writes the index ... wait, the index? Which index? Well, the index—the index in worktree2. Which does not have files modified and added from worktree1. (It does have the two bar files, unless they're totally new, but it has the old versions.) OK, so Git writes the index into one or more trees, writes a new commit using the new tree and whatever commit mybranch resolves to as its parent, and updates mybranch to point to the new commit.

The commit chain now looks like this:

...--o--1--2

where 1 is the commit made in ../worktree1, and 2 is the commit made in worktree2. The name mybranch, in both work-trees, points to commit 2. The name HEAD, in both work-trees, contains ref: refs/heads/mybranch. The index files in the two work trees are different, of course.

The contents for commit 1 are whatever is in the index in worktree1. It has the changes you made to bar1 and bar2.

The contents for commit 2 are whatever is in the index in worktree2. It has the changes you made in unrelated, but it does not have the changes made in files bar1 and bar2. In effect, the commit you made in worktree2 reverted the two files!

If you're willing to have one or both work-trees be in "detached HEAD" state, you can check them out that way, with git checkout --detach mybranch or git checkout refs/heads/mybranch. Now at least one of them will have HEAD pointing directly to a commit, rather than to the branch name, and Git should permit the two work-trees to have the same commit checked out.

Comments