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

1

18

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?

Tyrelltyrian answered 23/9, 2016 at 16:32 Comment(1)
Use git switch --ignore-other-worktreesMott
H
20

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.

Homerhomere answered 23/9, 2016 at 19:49 Comment(14)
Thank you for the explanation. I thought git would notice that the checkout is no longer at the current branch commit, and it would work like in a detached HEAD state.Tyrelltyrian
No, to get a detached HEAD you have to have a raw hash ID in the file .git/HEAD. This occurs independently of any branch names: HEAD is either symbolic and hence you're on the branch whose name is in .git/HEAD, or it has a raw hash ID and hence you are detached, with that hash as the current commit. (And it's mainly git checkout that modifies .git/HEAD, although git symbolic-ref can do it too.)Homerhomere
Ok, I will try with git checkout --detach mybranch. Sometimes I want to run tests on a clean checkout of a branch and I will not change anything, so a detached HEAD is fine.Tyrelltyrian
I append a count to the end of the local branch, i.e. mybranch(1), but set it up to track the original remote branch. Thus git pull will still work and incorporate changes from the remote, but each worktree is independent in terms of local changes. This can be done with using a command like: git worktree add <path-to-new-worktree> -B mybranch(1) --track refs/remotes/origin/mybranchPahoehoe
I don't understand the reasoning of this answer. For me it seems as if worktrees are very seperate, despite of the same commit data. If you were allowed to checkout the same branch at different worktrees, it would just feel like two different users working on the same branch (pretty much the same as mybranch(1) suggested in above comment). As of now it's possible to have the same branch on several repositories only by cloning the commit data several times which is OK for small repositories but a waste of traffic/memory for big repositories.Gellman
@jifb: It's an internal issue within Git: the index and work-tree must be manipulated in concert with any change to the hash ID stored in a branch name. The git commit command writes the new commit hash ID to the branch name. Therefore, git worktree makes sure the branch name is assigned to at most one work-tree so that this coordination can take place.Homerhomere
If git worktree allowed the same name to be checked out in multiple working trees, running git commit in any one working tree would have the same disastrous effect as git push has to a checked-out branch in a non-bare repository. See also the receive.denyCurrentBranch setting in the git config documentation.Homerhomere
If git commit did the same kind of checking that receiving a push does, one could in theory allow multiple added worktrees to use the same branch: but now you'd run git commit and get an error, refusing to commit. (Also there's a bug in the push handling, although that's finally being fixed.)Homerhomere
Why can’t git simply report the old versions of the files as changes that would be recorded if you were to commit something?Mokpo
@schuelermine: the reason has to do with the way a branch name simply holds one hash ID, while the index and working tree are assumed to have come from the files associated with the commit found at that particular hash ID. If the same branch name occurs in more than one working tree, making a new commit in any one working tree would update the branch name, invalidating the index and working tree setup. This is literally the same problem that you get when receiving a push, which as I said is the reason receive.denyCurrentBranch exists in the first place.Homerhomere
As you note, you could just go ahead and let people commit from the stale index. But that doesn't do what humans want Git to do. The Git programmers discovered this (the fact that humans object to this) in the early (pre-1.5) days of Git. They won't let you make that mistake now, and have chosen this particular set of rules to prevent it.Homerhomere
I object to the fact that this is expected. I have a usecase where my proposed mechanism is expected. My use case is that I just want my git repository in two places, a working directory, and a place my programs read from (a “deploy” repo). I wanna be able to checkout one of the places to any place I wish, while working on it properly in the original directory. Any automatically commiting processes that I have running would simply refuse to run if the repository was dirty. Copying the files won’t do because my programs read git history.Mokpo
@schuelermine: just use detached HEAD mode, that's what it's for.Homerhomere
I confess I haven't digested this answer and its commentary completely, so please pardon this possibly redundant subquestion. If I'm getting this error (as I am), and if my intention in the worktree where I'm getting the error is to immediately do a git checkout -b newbranch, would the detached-HEAD workaround work properly? (I think so, but among the things I don't fully understand about git yet is detached-HEAD mode.)Terrie

© 2022 - 2024 — McMap. All rights reserved.