TL;DR:你的问题实际上很简单,只要你的 Git 至少是 2.15 版:只要git worktree add
正确使用,创建两个使用相同远程跟踪名称的分支作为上游。
如果没有,您使用两个存储库的方法可能是最好的。git worktree add
只要您避免一个主要问题(我将在下面讨论),您仍然可以使用2.5 和 2.15 之间的版本。
长
我希望当我在本地分支之间切换时,即使一个分支的未分级更改也不应该在另一个分支中可见。
Git 不支持这种期望。
这里真正的问题是没有“未分级更改”之类的东西,也没有“分级更改”之类的东西。您所看到的两者都是动态创建的错觉,因为这种错觉往往对人类程序员更有用。Git 向您显示的更改是按需计算的,通过比较三个项目中的两个:当前提交、索引和工作树。但实际上,只有存储在工作树和索引中的文件是无常的和可变的;加上存储在存储库中的提交,它们是永久的——嗯,大部分是永久的——并且永远冻结。查看我最近的回答为什么 git diff 与 git diff --staged 下的输出不同?了解更多信息。
存储库中有(可能)许多提交,但每个存储库只有一 (1) 个工作树 + 索引对。1 您可以添加更多对 index-and-work-tree using git worktree add
,您已经尝试过。这应该适用于您的情况,只要您的 Git 至少是 2.15 版本(从 Git 2.5 到但不包括 Git 2.15,git worktree add
有一个潜在的严重错误,具体取决于您如何使用它)。
1裸存储库(使用git clone --bare
or创建git init --bare
)有一个索引并且没有工作树,但假设您没有使用裸存储库似乎是安全的。
... git worktree 似乎需要 2 个不同的远程分支
不是这种情况。
什么git worktree add
是添加一个索引和工作树对。添加的工作树位于一个单独的工作树目录中(主工作树目录位于主存储库目录旁边.git
;该.git
目录包含所有索引以及 Git 需要的所有其他辅助信息)。添加的工作树也有自己HEAD
的,但共享所有分支名称和远程跟踪名称。
强加的约束git worktree add
是每个工作树必须使用不同的分支名称,或者根本没有分支名称HEAD
。为了正确定义它是如何工作的,我们需要离题一下HEAD
和分支名称。我稍后会谈到这个,但首先,让我们
注意:没有远程分支之类的东西。Git 确实有一个术语,它称为远程跟踪分支名称。我现在更喜欢称这些远程跟踪名称,因为它们缺少分支名称所具有的一个关键属性。远程跟踪名称通常看起来像origin/master
or origin/develop
: 即以 . 开头的名称origin/
。2
2您可以定义多个遥控器,或者将您可能已经拥有的一个遥控器的默认名称更改为origin
. 例如,您可以添加第二个名为upstream
. 在这种情况下,您可能还拥有upstream/master
和/或upstream/develop
. 这些都是有效的远程跟踪名称的缩写形式。
提交、分支名称和 HEAD
任何 Git 存储库中的永久存储单元都是提交。正如您现在所看到的,提交是由一个大的、丑陋的、明显随机的(根本不是随机的)、每个提交唯一的哈希 ID 标识的,例如5d826e972970a784bd7a7bdf587512510097b8c7
. 这些东西对人类没有用,我们通常只通过剪切粘贴或间接使用它们,但哈希 ID 是真实名称。如果你有5d826e972970a784bd7a7bdf587512510097b8c7
(在 Git 的 Git 存储库中提交),它总是那个特定的提交。如果您没有它,您可以获取 Git 存储库的副本(或更新您现有的副本),现在您确实拥有它,它就是那个提交——它是 Git 版本 2.20。(名字v2.20.0
是这个提交更人性化的名称,也是我们通常使用的名称。Git 存储了一个标签名称到哈希 ID 的转换表,这就是v2.20.0
这个提交的人类可读名称。)
提交包含有人指示 Git 进行提交时索引中所有文件的完整快照。但是,它还包含一些额外的元数据——关于提交的数据,例如提交者、时间和原因(用户名、电子邮件地址、时间戳和日志消息)。在同一个元数据部分,Git 存储了前一次提交的确切哈希 ID。Git 将先前的提交称为提交的父级。
通过这种方式,存储库中的每个提交都会连接回同一存储库中的早期提交。这是存储库中的历史记录:提交字符串,从结尾开始,然后向后工作。在非常简单的情况下,例如在一个非常新的存储库中,我们可能只需要在一个非常简单的行中进行一些提交,如下所示:
A <-B <-C
在这里,大写字母代表实际的哈希 ID(记住,它很大、很丑,而且显然是随机的)。我们——和/或 Git——将做的是从最后开始,在提交C
时,然后向后工作。CommitC
存储了 commit 其 parent 的实际 hash ID B
,以便C
我们可以从中找到B
。同时B
存储 parent 的哈希 ID A
。由于A
是第一次提交,它没有父级,这就是 Git 告诉我们已经到达历史起点的方式:无处可去。
不过,诀窍是我们需要找到commit C
,其哈希 ID 显然是随机的。这就是分支名称的来源。我们选择一个类似的名称master
并使用它来存储 的实际哈希 ID C
:
A <-B <-C <--master
我们之前提到过,一旦提交,就永远不会改变。这意味着我们真的不需要画出所有的内部箭头:我们知道一个提交不能记住它的子节点,因为当我们提交时它们不存在,但是一个提交可以记住它的父节点,因为父节点当时确实存在。Git 将永远冻结父哈希到新的提交中。因此,如果我们想向我们的三个字符串添加一个新的提交A-B-C
,我们只需这样做:
A--B--C--D
为了记住D
的哈希 ID,Git 立即将新提交的哈希 ID 写入 name master
:
A--B--C--D <-- master
因此,提交一直是固定的,但分支名称一直在移动!
现在,假设我们添加一个新的分支名称develop
. Git 中的分支名称必须准确指向一个 commit。我们希望它指向的一个提交可能是最新的,D
:
A--B--C--D <-- develop, master
请注意,两个名称都指向同一个提交。这是完全正常的!所有四个提交都在两个分支上。
现在让我们添加一个新的提交,并调用它E
:
A--B--C--D
\
E
我们应该更新两个分支名称中的哪一个?这就是HEAD
进来的地方。
在我们制作之前E
,我们告诉 Git要附加HEAD
到哪个名称。我们使用git checkout
. 如果我们git checkout master
,Git 将附加HEAD
名称master
。如果我们git checkout develop
,Git 将附加HEAD
名称develop
。让我们在 make 之前做后者E
,这样我们就可以开始:
A--B--C--D <-- develop (HEAD), master
现在我们将 make E
,Git 将更新HEAD
附加的名称,即develop
:
A--B--C--D <-- master
\
E <-- develop (HEAD)
简而言之,这就是树枝的生长方式。Git 创建一个新提交,其父提交是当前提交,通过 name 找到HEAD
,它附加到某个分支名称。在创建新的提交之后——这给它一个新的、唯一的、又大又丑的哈希 ID——Git 将新提交的新哈希 ID 写入同一个分支名称,这样分支名称现在指向新提交。新提交继续指向旧提交。
添加的工作树要求您将它们的 HEAD 附加到不同的分支
出于马上有意义的原因,git worktree add
要求新添加的工作树为该工作树使用不同的分支名称HEAD
。也就是说,当我们绘制提交和分支名称并附HEAD
加到某个分支名称时,我们实际上是附加了这个工作树的HEAD
,因为现在有不止一个HEAD
。
所以现在我们有两个名称master
和develop
,我们可以使用这两个不同的分支名称创建两个不同的工作树:
A--B--C--D <-- master (HEAD) # in work-tree M
\
E <-- develop
与:
A--B--C--D <-- master
\
E <-- develop (HEAD) # in work-tree D
工作树的内容及其索引通常会与其HEAD
提交的内容匹配。我们将修改工作树中的一些文件,将git add
它们修改到该工作树的索引处,git commit
然后更新该工作树的HEAD
. 这就是为什么这两个需要使用不同的分支名称。观察我们在工作树 M(对于 master)中工作时会发生什么。我们开始:
A--B--C--D <-- master (HEAD) # in work-tree M
\
E <-- develop
索引和工作树匹配 commit D
。我们做了一些工作,git add
并git commit
做出新的承诺。新提交的哈希 ID 是新的且唯一的;让我们在这里调用它F
,并把它画进去,更新名称master
:
A--B--C--D--F <-- master (HEAD) # in work-tree M
\
E <-- develop
现在让我们导航到另一个工作树(D 表示开发,但这听起来很像 commit D
,所以让我们停止这样命名它)。这有它自己的HEAD
,所以图片是:
A--B--C--D--F <-- master
\
E <-- develop (HEAD)
请注意,master
已经发生了变化——分支名称在所有工作树之间共享——并且F
出现了新的提交,因为提交也是共享的。但是develop
仍然指向 commit E
,并且我们的索引和工作树在这里,在这个工作树中,与E
. 现在我们修改一些文件,git add
将它们复制回索引中,并git commit
进行我们可以调用的新提交G
:
A--B--C--D--F <-- master
\
E--G <-- develop (HEAD)
CommitG
将出现在另一个工作树中,并且另一个工作树develop
将标识 commit G
,但由于另一个工作树已master
/ commitF
签出,另一个工作树的索引和工作树仍将匹配 commit F
。
任何分支名称的上游设置都是您控制的
当您使用或创建新分支名称时,您可以控制:git checkout -b
git branch
- 该新分支是否有任何上游设置,以及
- 如果是这样,那么什么名称——<code>origin/whatever 是典型的,但它可以是任何名称——存储在该设置中。
master
你用origin/master
它作为它的上游名称,你develop
用它作为它的上游名称是很正常的origin/develop
,但这里根本没有任何限制。例如,您可以将所有分支共享 origin/master
为它们的上游。或者,您可以拥有没有上游集的分支。请参阅为什么我必须“git push --set-upstream origin <branch>”?讨论上游设置。
有一个神奇的默认值:
$ git checkout feature-xyz
将尝试检查您现有的feature-xyz
分支。如果没有分支,您的feature-xyz
Git 将检查您所有的远程跟踪名称,例如,查看是否存在origin/feature-xyz
. 如果是这样,您的 Git 将创建您自己的feature-xyz
,指向与相同的提交origin/feature-xyz
,并将set作为origin/feature-xyz
其上游。这是为了方便。如果不方便,请不要使用它:-b
改为使用。
该git worktree add
命令与 共享这个特殊技巧git checkout
:两者都有一个-b
创建新分支(不这样做),并且都默认尝试检查一些现有的分支。因此,对于这种特殊情况,两者都会自动创建一个带有上游集的新分支。
分离的 HEAD 和添加的索引,以及 Git 2.5 到(但不包括)2.15 中的错误
在 Git 中,分离的 HEAD仅表示HEAD
未附加到分支名称。请记住,绘制正在发生的事情的通常方法是附加HEAD
到某个名称:
...--F--G--H <-- master (HEAD)
相反,我们可以让 GitHEAD
直接指向一个 commit,而无需通过分支名称:
...--F--G <-- HEAD
\
H <-- master
在这种模式下,如果我们进行新的提交,Git 会将新提交的哈希 ID 写入HEAD
自身,而不是HEAD
未附加到的名称:
...--F--G--I <-- HEAD
\
H <-- master
添加的工作树可以始终处于分离的 HEAD 模式,但在 Git 版本 2.5 中存在一个可怕的错误,该错误git worktree
是首次引入的,直到 Git 版本 2.15 才修复。
具体来说,每个添加的工作树都有自己 HEAD
的私有索引文件。由于 Git 其余部分的工作方式,这是必要的:HEAD
记录有关此工作树的信息,并且索引是此工作树的索引,因此它们都是一个大组项。不幸的是,Git 的垃圾收集器没有git gc
被正确地教导尊重添加的工作树。
垃圾收集器的工作是查找未引用(未使用/不需要)的 Git 对象——blob、树、提交和带注释的标签,它们看起来像存储库中的剩余垃圾。Git 使用它,以便 Git 命令可以在需要时创建这些不同的内部对象,而不必担心它们是否真的需要,也不必采取任何特殊操作来处理被中断(例如,通过CTRL+C或网络会话断开)。其他正常的日常 Git 操作,包括git rebase
,可能会产生这种垃圾。这一切都很好,很正常,因为看门人git gc
会定期清理它。
但是,您使用分离的 HEAD 所做的任何新HEAD
提交都只有其本身引用它们。在主工作树中,这不是问题:gc
管理员检查HEAD
文件,查看引用,并且知道不删除这些提交。但git gc
不检查添加的额外HEAD。因此,如果您添加了带有分离 HEAD 的工作树,则分离的 HEAD 对象可能会消失。类似的规则适用于 blob 对象,如果存储在添加的工作树索引中的 blob 对象仅从该索引中引用,则git gc
可能会删除基础 blob 对象。
有一个二级保护:git gc
默认情况下不会修剪任何小于 14 天的对象。这给了所有 Git 命令 14 天的时间来完成他们的工作,然后看门人过来并将他们正在进行的对象扔进办公室后面的垃圾箱。所以这一切在主工作树中都可以正常工作,并且在 Git 2.15 及更高版本中添加的工作树中工作正常。但是对于中间的 Git 版本,git gc
可能会看到一个 14 天或更长时间的提交、树或 blob,由于添加了工作树,它们不应该被丢弃,但没有意识到这一点并将其丢弃。
如果您没有分离的 HEAD 并且在 14 天内小心添加并提交,则不会出现此错误。如果您禁用垃圾收集,它也不会发生,但这通常不是一个好主意:Git 依赖于gc
清理和保持良好的性能。而且,当然,它在 Git 2.15 中已修复,所以如果你有这个或更高版本,你就可以了。它只影响添加的工作树,所以在 2.5 和 2.15 之间要小心。