4

我有一个主分支,并且正在研究涉及相同文件的 2 个功能。我想让 2 个本地分支指向同一个上游主节点,但有不同的变化。我不想在本地提交更改,以便 IDE 保留格式,如
https://d3nmt5vlzunoa1.cloudfront.net/idea/files/2018/10/k8sCompletion.png中的边框阴影

我一直无法成功使用 git checkout ,因为当我在一个分支中进行更改并切换到另一个分支时,未暂存的更改也可见。我想出的解决方案是在 2 个 repos 中检查我的代码,因为 git worktree 似乎需要 2 个不同的远程分支。但是,这意味着硬盘效率低下。有没有办法实现我想要的?

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

4

2 回答 2

4

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 --bareor创建git init --bare)有一个索引并且没有工作树,但假设您没有使用裸存储库似乎是安全的。


... git worktree 似乎需要 2 个不同的远程分支

不是这种情况。

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

强加的约束git worktree add是每个工作树必须使用不同的分支名称,或者根本没有分支名称HEAD。为了正确定义它是如何工作的,我们需要离题一下HEAD和分支名称。我稍后会谈到这个,但首先,让我们

注意:没有远程分支之类的东西。Git 确实有一个术语,它称为远程跟踪分支名称。我现在更喜欢称这些远程跟踪名称,因为它们缺少分支名称所具有的一个关键属性。远程跟踪名称通常看起来像origin/masteror 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

所以现在我们有两个名称masterdevelop,我们可以使用这两个不同的分支名称创建两个不同的工作树:

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 addgit 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 -bgit 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-xyzGit 将检查您所有的远程跟踪名称,例如,查看是否存在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 之间要小心。

于 2018-12-24T12:31:11.283 回答
0

我有这个完全相同的问题。以防万一这里有人分享我的困惑,我没有意识到在功能分支上提交可以解决这个问题!

在其中一个功能分支上进行提交后,分阶段的更改将从另一个功能分支中删除。例如,您在分支 1 中添加三行代码。如果您签出分支 2,您将看到这三行代码。但是,如果您首先在分支 1 上 git add 和 git commit,那么当您签出分支 2 时,您将看不到这些更改。

希望这可以帮助某人。

于 2021-03-30T09:33:05.043 回答