如何在同一个远程分支上有2个独立的本地Git分支

How can I have 2 independent local Git branches off same remote branch

我有一个主分支,正在处理两个涉及相同文件的功能。我希望有两个本地分支指向相同的上游主节点,但有不同的更改。我不想在本地提交更改,以便IDE保留像边框阴影一样的格式https://d3nmt5vlzunoa1.cloudfront.net/idea/files/2018/10/k8scompletion.png

我无法成功地使用git checkout,因为当我在其中一个分支中进行更改并切换到另一个分支时,未标记的更改也会显示在该分支上。我提出的解决方案是在2repos中签出我的代码,因为GitWorkTree似乎需要两个不同的远程分支。然而,这意味着硬盘效率低下。有没有办法达到我想要的?

我希望当我在本地分支之间切换时,即使一个分支的未分页更改在另一个分支中也不可见。


tl;dr:如果您的Git至少是2.15版,那么您的问题实际上是非常小的:只需正确使用git worktree add,创建两个与上游使用相同远程跟踪名称的分支。好的。

如果没有,那么使用两个存储库的方法可能是最好的。只要您避免了一个主要问题(我将在下面讨论),您仍然可以在2.5和2.15之间的版本中使用git worktree add。好的。长

I expect that when I switch between local branches, even the unstaged changes of one branch should not be visible in the other.

Ok.

Git不支持此期望。好的。

这里真正的问题是,没有所谓的"未经调整的变化",也没有所谓的"阶段性变化"。你所看到的两者都是一个动态的错觉,因为错觉对人类程序员更有用。通过比较三项中的两项(当前提交、索引和工作树),Git向您显示的更改是按需计算的。但实际上,只有存储在工作树和索引中的文件是不可更改的;另外,存储在存储库中的提交文件是永久性的,大部分是永久性的,并且一直处于冻结状态。请看我最近的答案,为什么在git diff和gitdiff下的输出是不同的——分阶段的?关于这个的更多信息。好的。

存储库中有(可能)许多提交,但每个存储库只有一(1)个工作树+索引对。1您可以使用git worktree add添加更多的索引和工作树对,您已经尝试过了。只要您的Git至少是2.15版(从Git2.5到Git2.15,但不包括Git2.15,git worktree add有一个潜在的严重错误,具体取决于您使用它的方式),这对您的情况应该很好。好的。

1裸存储库(使用git clone --baregit init --bare创建)有一个索引,没有工作树,但假设您没有使用裸存储库似乎是安全的。好的。

... git worktree seems to require 2 different remote branches

Ok.

事实并非如此。好的。

git worktree add所做的是添加一个索引和工作树对。添加的工作树位于单独的工作树目录中(主工作树目录正好位于主存储库的.git目录旁边;.git目录包含所有索引以及git需要的所有其他辅助信息)。添加的工作树也有自己的HEAD,但共享所有分支名称和远程跟踪名称。好的。

git worktree add所施加的约束是,每个工作树必须为其HEAD使用不同的分支名称,或者根本没有分支名称。为了正确地定义这是如何工作的,我们需要一个关于HEAD和分支名称的偏离。我一会儿就到,但首先,让我们好的。

注意:没有远程分支这样的东西。Git有一个术语,它调用远程跟踪分支名称。我现在更喜欢称这些远程跟踪名称,因为它们缺少分支名称所拥有的一个关键属性。远程跟踪名称通常类似于origin/masterorigin/develop:即以origin/开头的名称。好的。

2您可以定义多个远程,或者将一个远程的默认名称更改为除origin之外的其他名称。例如,您可以添加第二个名为upstream的远程程序。在这种情况下,您可能还拥有upstream/master和/或upstream/develop。这些都是远程跟踪名称的有效缩写形式。好的。提交、分支名称和负责人

任何Git存储库中的永久存储单元都是commit。正如您现在看到的,提交是由一个大的、丑陋的、显然是随机的(完全不是随机的)标识的,对于每个提交哈希ID(如5d826e972970a784bd7a7bdf587512510097b8c7)都是唯一的。这些东西对人类没有用处,我们通常只通过剪切粘贴或间接使用它们,但是散列ID是实名。如果您有5d826e972970a784bd7a7bdf587512510097b8c7(git存储库中的commit for git),那么它总是特定的commit。如果你没有,你可以得到一份Git存储库的副本(或者更新你现有的副本),现在你有了它,它是提交的,它是Git版本2.20。(名称v2.20.0是这个承诺的更人性化的名称,也是我们通常使用的名称。Git将标记名的转换表存储到哈希ID,这就是v2.20.0成为此提交的可读名称的方式。)好的。

提交包含索引中所有文件的完整快照,这些文件在有人指示Git进行提交时就在索引中。但是,它还包含一些关于提交的额外元数据数据,例如提交者、提交时间和提交原因(用户名、电子邮件地址、时间戳和日志消息)。在同一个元数据部分中,Git存储上一次提交的确切哈希ID。Git调用前一个commit作为commit的父级。好的。

这样,存储库中进行的每一次提交都会连接回同一存储库中以前的提交。这是存储库中的历史记录:提交字符串,从末尾开始,然后向后工作。在非常简单的情况下,例如在一个非常新的存储库中,我们可能只需要在一个非常简单的行中进行一些提交,如下所示:好的。

1
A <-B <-C

在这里,大写字母代表实际的散列ID(记住,散列ID很大、很难看,而且显然是随机的)。我们和/或Git要做的是从最后开始,在提交C时,然后向后工作。commit C存储commit其父B的实际哈希ID,以便从C中找到B。同时,B存储父A的散列ID。由于A是第一次提交,它没有父级,因此git告诉我们,我们已经到达了历史的开始:没有地方可去了。好的。

不过,诀窍是我们需要找到commit C,它的散列ID显然是随机的。这就是分支名称的来源。我们选择一个像master这样的名称,并使用它来存储C的实际哈希ID:好的。

1
A <-B <-C   <--master

我们之前提到,承诺一旦做出,就永远不会改变。这意味着我们实际上不需要绘制所有的内部箭头:我们知道一个提交不能记住它的子级,因为在我们提交时它们不存在,但是提交可以记住它的父级,因为父级在那时确实存在。Git将永远冻结父哈希到新提交中。因此,如果我们想在三个字符串中添加一个新的commit,A-B-C,我们只需这样做:好的。

1
A--B--C--D

为了记住D的散列ID,git立即将新提交的散列ID写入master的名称中:好的。

1
A--B--C--D   <-- master

所以提交一直都是固定的,但是分支名称总是移动的!好的。

现在,假设我们添加一个新的分支名称,develop。在Git中,分支名称必须指向一个提交。我们要指出的一个承诺可能是最新的,D:好的。

1
A--B--C--D   <-- develop, master

注意,两个名称都指向同一个提交。这是完全正常的!所有四个提交都在两个分支上。好的。

现在我们添加一个新的commit,并将其命名为E:好的。

1
2
3
A--B--C--D
          \
           E

我们应该更新哪两个分支名称?这就是HEAD进来的地方。好的。

在制作E之前,我们告诉Git要将HEAD附加到哪个名称上。我们用git checkout做这个。如果我们是git checkout master,git会把HEAD附在master的名字上。如果我们是git checkout develop号,git将把HEAD号附在develop号上。在制作E之前,让我们先做后一个,这样我们就可以从:好的。

1
A--B--C--D   <-- develop (HEAD), master

现在我们制作E,git会更新HEAD附加到的名称,即develop:好的。

1
2
3
A--B--C--D   <-- master
          \
           E   <-- develop (HEAD)

简而言之,这就是树枝的生长方式。git创建一个新的commit,其父级是当前commit,可以通过名称HEAD找到,该名称附加到某个分支名称上。在创建了新的提交之后,这个提交给它一个新的、唯一的、丑陋的大哈希ID git将新提交的新哈希ID写入到相同的分支名称中,这样分支名称现在就指向新提交。新提交继续指向旧提交。好的。添加的工作树要求将其头连接到不同的分支

由于暂时有意义的原因,git worktree add要求新添加的工作树对该工作树的HEAD使用不同的分支名称。也就是说,当我们绘制提交和分支名称,并将HEAD附加到某个分支名称时,我们实际上附加了这个工作树的HEAD,因为现在有多个HEAD。好的。

现在我们有了两个名称,masterdevelop,我们可以用这两个不同的分支名称制作两个不同的工作树:好的。

1
2
3
A--B--C--D   <-- master (HEAD)    # in work-tree M
          \
           E   <-- develop

VS:好的。

1
2
3
A--B--C--D   <-- master
          \
           E   <-- develop (HEAD)  # in work-tree D

通常,工作树的内容及其索引将开始匹配其HEAD提交的内容。我们将修改工作树中的一些文件,git add它们到该工作树的索引,git commit在那里,并更新该工作树的HEAD。这就是为什么这两者需要使用不同的分支名称。当我们在工作树m(对于master)中工作时,观察发生了什么。我们从以下开始:好的。

1
2
3
A--B--C--D   <-- master (HEAD)    # in work-tree M
          \
           E   <-- develop

索引和工作树匹配提交D。我们做了一些工作,git addgit commit来作出新的承诺。新提交的散列ID是新的和唯一的;我们在这里称它为F,并将其引入,更新名称master:好的。

1
2
3
A--B--C--D--F   <-- master (HEAD)    # in work-tree M
          \
           E   <-- develop

现在让我们导航到另一个工作树(D表示开发,但这听起来很像commit D),所以我们不要这样命名它。它有自己的HEAD,所以图片是:好的。

1
2
3
A--B--C--D--F   <-- master
          \
           E   <-- develop (HEAD)

注意,master已经更改了所有工作树之间共享的分支名称,并且出现了新的commit F,因为commit也是共享的。但是develop仍然指向commit E,这里的索引树和工作树,在这个工作树中,与E的索引树和工作树相匹配。现在我们修改了一些文件,git add将它们复制回索引中,git commit作出一个新的承诺,我们可以调用G:好的。

1
2
3
A--B--C--D--F   <-- master
          \
           E--G   <-- develop (HEAD)

commit G将出现在另一个工作树中,而另一个工作树的develop将标识commit G,但由于另一个工作树已签出master/commit F,另一个工作树的索引和工作树仍将与commit F匹配。好的。任何分支名称的上游设置都是由您控制的。

使用git checkout -bgit branch创建新的分支名称时,控制:好的。

  • 新分行是否有上游设置,以及
  • 如果是这样,那么什么名称-origin/whatever是典型的,但它可以是存储在该设置中的任何名称。

你的master使用origin/master作为其上游名称是很正常的,你的develop使用origin/develop作为其上游名称是很正常的,但是这里没有任何限制。例如,您可以让所有分支机构共享origin/master作为它们的上游。或者,您可以拥有没有上游集的分支。明白为什么我必须"git push--set upstream origin"?关于上游设置的讨论。好的。

有一个神奇的默认值:好的。

1
$ git checkout feature-xyz

将尝试检查您现有的feature-xyz分支。如果没有feature-xyz分支,您的Git将检查所有远程跟踪名称,以查看是否有origin/feature-xyz分支。如果是这样,您的Git将创建自己的feature-xyz,指向与origin/feature-xyz相同的提交,并将origin/feature-xyz设置为其上游。这是为了方便。如果不方便的话,不要用:用-b代替。好的。

git worktree add命令与git checkout共享这个特殊的技巧:两个命令都有一个-b,创建一个新的分支(不这样做),并且都默认尝试检查一些现有的分支。因此,对于这种特殊情况,这两个分支都将自动创建一个具有上游集的新分支。好的。分离的头和添加的索引,以及Git 2.5到(但不包括)2.15中的bug

在git中,分离的头仅仅意味着HEAD没有附加到分支名称上。记住,通常的绘制方法是将HEAD附加到某个名称上:好的。

1
...--F--G--H   <-- master (HEAD)

相反,我们可以让git直接指向commit,而不必经过分支名称:好的。

1
2
3
...--F--G   <-- HEAD
         \
          H   <-- master

在这种模式下,如果我们进行新的提交,git会将新提交的哈希ID写入HEAD本身,而不是HEAD未附加到的名称:好的。

1
2
3
...--F--G--I   <-- HEAD
         \
          H   <-- master

添加的工作树可以一直处于分离头模式,但在Git版本2.5中有一个可怕的bug,其中git worktree是第一次引入的,直到Git版本2.15才被修复。好的。

具体来说,每个添加的工作树都有自己的HEAD和自己的私有索引文件。这是因为Git的其余工作方式:HEAD记录了关于这个工作树的信息,索引是这个工作树的索引,所以它们都是一个大的组项。不幸的是,Git的垃圾收集器git gc没有被正确地教导要尊重添加的工作树。好的。

垃圾收集器的工作是查找未被引用(未使用/不需要)的Git对象blob、trees、commits和带注释的标记,这些标记看起来像存储库中的剩余垃圾。Git使用它,这样Git命令可以在任何时候创建这些不同的内部对象,而不必担心它们是否真的是必要的,也不必采取任何特殊的操作来处理被中断的情况(例如,CtrL+C,或者网络会话断开)。其他日常的Git行为,包括git rebase,都会产生这种垃圾。这一切都非常好和正常,因为看门人git gc会定期清理。好的。

但是,你用一个独立的头所做的任何新的承诺都只涉及到它们本身。在主工作树中,这不是问题:gc看门人检查HEAD文件,查看引用,并知道不删除这些提交。但是git gc没有检查额外的头。因此,如果您添加了一个带有分离头的工作树,那么分离头的对象可能会消失。类似的规则适用于BLOB对象,如果存储在添加的工作树索引中的BLOB对象仅从该索引中引用,则git gc可以删除基础BLOB对象。好的。

有一个二级保护:默认情况下,git gc不会修剪任何14天以下的对象。这使所有的git命令14天内完成他们的工作,在门卫来之前,把他们正在进行中的物品扔到办公室后面的垃圾桶。因此,这一切在主工作树中都可以正常工作,在Git2.15及更高版本中的附加工作树中也可以正常工作。但是对于中级的Git版本,git gc可能会遇到14天或更长时间的提交、树或blob,这些内容不应该因为添加了工作树而被抛出,但却没有意识到并将其抛出。好的。

如果你没有一个分离的头部,并且小心地在14天内添加和提交,这个bug不会攻击你。如果禁用垃圾收集,它也不会罢工,但这通常不是一个好主意:Git依赖于gc来清理和保持良好的性能。当然,它是固定的2.15吉特,所以如果你有这个或更晚,你很好。它只影响添加的工作树,因此在2.5和2.15之间要小心。好的。好啊。