【发布时间】:2020-08-22 12:03:44
【问题描述】:
假设有一个包含子树存储库的 MainRepo:subRepoA、subRepoB、SubRepoC。 如果我在所有存储库中进行了更改,但只想合并和推送在 subRepoB 中完成的更改。可能吗? 似乎 MainRepo 的行为就像一个大型存储库,无法区分其子存储库。
【问题讨论】:
标签: git git-subtree
假设有一个包含子树存储库的 MainRepo:subRepoA、subRepoB、SubRepoC。 如果我在所有存储库中进行了更改,但只想合并和推送在 subRepoB 中完成的更改。可能吗? 似乎 MainRepo 的行为就像一个大型存储库,无法区分其子存储库。
【问题讨论】:
标签: git git-subtree
这里的答案是“不”和“是”。也就是说,您可以实现您的要求,但是:
git merge 命令(它需要额外的命令);和不过,要做到这一点,请使用:
git merge --no-commit
然后使用git checkout 或(自Git 2.23 起)git restore 来“撤消”某些合并。然后使用git merge --continue 或git commit 完成合并。有关详细信息,请参阅下面的详细信息。
要了解这一切是如何运作的(以及为什么这是一个坏主意),请记住关于 Git 的这一点:Git 是关于提交的。 Git 与文件无关,甚至与分支无关。确实,提交包含文件——这就是为什么我们有提交,来保存文件——和分支名称find提交,这就是我们有分支名称的原因。但归根结底,Git 是关于 commits 的。
提交已编号。这些不是简单的计数:我们不是从提交 #1 开始,然后是 #2、#3 等等。相反,每个人都有一个看起来随机(但实际上根本不是随机)的唯一 hash ID,它显示为一大串难看的字母和数字,通常缩写,因为人类通常只是善良在它们上面闪过(dca3c76df9bb99b0... 和 dca3c76dfb9b99b0... 一样吗?)。
一旦提交,任何提交的任何部分都不能更改。这样做的原因是哈希 ID 实际上是提交的每一位的加密校验和。如果您确实取出一个,进行一些更改,然后将其放回原处,您将得到一个带有新的不同哈希 ID 的 new 提交。具有唯一编号的旧提交仍然存在,任何查找该编号的人都会获得旧提交。
每个提交存储两件事:
Git 知道的每个文件都有完整的快照。这些文件以特殊的、只读的、仅限 Git 的、压缩的和去重复的格式存储。 (重复数据删除会立即处理大多数提交中的大多数文件与之前提交中相同文件的版本完全相同的事实。)
同时,每个提交都存储一些元数据,即有关提交本身的信息。这包括制作人(姓名和电子邮件地址)以及制作时间,以及他们的日志消息,以解释他们制作它的原因。为什么。在这个元数据中,Git 存储了 Git 本身需要的一些东西:提交的提交号在我们在这里查看的提交之前。 Git 将此称为父提交。
每个提交都存储其父级的编号(哈希 ID)这一事实意味着,如果我们可以在提交字符串中找到 last 提交,Git 就可以使用它向后工作。也就是说,假设我们使用单个大写字母来代表实际的哈希 ID,并绘制以下内容:
... <-F <-G <-H
使用哈希 ID H,Git 可以检索您所做的实际提交(包括快照),无论何时进行。这样就可以得到文件。它还获取 Git 元数据,包括早期提交的哈希 ID G。这意味着 Git 可以提取 两个 提交并将 G 中的文件与 H 中的文件进行比较,以向您显示您在 H 中更改的内容。 Git 还可以打印出快照G 的人的姓名和电子邮件地址,并使用G 的元数据查找提交F。将F 中的快照与G 中的快照进行比较,Git 可以向您显示G 中的更改,Git 可以回到更早的提交F,等等。
当然,我们必须以某种方式找到提交 H 的哈希 ID。
像master 或develop 这样的分支名称 只包含一 (1) 个哈希 ID。但只要我们——或 Git——确保这是链中 last 提交的哈希 ID,我们都很好:
...--F--G--H <-- master
new 提交要求 Git 将新提交的哈希 ID 存储到分支名称中:
...--F--G--H--I <-- master
一旦我们提交I(我们使用master 作为名称),Git 将自动更新master,使其指向最后 em>提交。新提交 I 的父级将是现有提交 H。
由于每个提交中指向其父级的“箭头”是提交的一部分,因此无法更改它们。就像提交中的所有内容一样,这些都是纯只读的。请注意,分支名称 确实的箭头会发生变化。所以这就是为什么我一直画那个箭头,同时把提交到提交的箭头变成更简单的线:我们只需要记住提交点向后,Git 工作向后 .
一次提交可以在多个分支上。例如:
...--F--G--H <-- master, develop
这里,两个 names 都将提交 H 标识为他们的最后一次提交。所以所有提交都在两个分支上。
对此的技术术语是可达性。我们将在下面轻轻地使用它,在合并中,但考虑从提交 H 开始并向后工作,一次提交一个。没有移动,我们已经到达提交H。我们后退一步,我们已经达到了提交G。后退两步,我们在提交F,依此类推。
请注意,Git 可以比较任何两个 提交,而不仅仅是父子对。我们将较早的提交放在左侧(好吧,通常无论如何),稍后的提交放在右侧。 Git 然后比较两个提交的快照。对于相同的文件,Git 什么也没说。对于不同的文件,Git 计算出我们可以做的一些更改:在第 42 行之后添加这些行,并删除第 86 行这是一个 diff: 它显示了如何将左侧文件更改为右侧文件。
如果我们比较父母和孩子,这个差异列表通常就是我们所做的。但请注意,Git 只会找到 a 组更改。在某些情况下,我们改变它的方式并不完全。 Git 找到的差异会起作用,即使我们做的事情有点不同——但有时(见下面的合并),这可能会导致轻微但烦人的合并冲突,如果 Git 在这方面做得更好,就不会发生这种情况。
当我们使用git push(或git fetch,因此也使用git pull)时,Git 使用提交。推送操作发送整个提交。这包括快照和元数据。两个 Git 通过比较这些哈希 ID 就知道彼此有哪些提交:这就是为什么哈希 ID 是提交的加密校验和的原因。每个 Git 要么有提交,要么没有。无论哪个 Git 发送提交都会向接收 Git 提供哈希 ID,它要么说“是的,我需要那个,发送它”或“不,谢谢,我已经有了那个”。
git merge 将合并 commits 并进行 merge commit
git merge 命令本身会合并提交。我们喜欢将它与分支名称一起使用。也就是说,我们从这样的事情开始:
I--J <-- branch1 (HEAD)
/
...--G--H
\
K--L <-- branch2
因为我们在这个图中有两个名字,所以我们需要记住我们使用的是哪个name。这就是特殊名称 HEAD 的来源:我们将它附加到我们告诉 Git 与 git checkout 或(自 Git 2.23 起)git switch 一起使用的任何分支。这是我们进行新提交时将更新的名称。
所以,现在我们运行git merge branch2。 Git 使用 name branch2 来查找 一个特定的提交: name 指向的那个。在这种情况下,这是提交L。因此,其中两个有趣的提交是 commit J,我们现在正在使用的提交,以及提交 L,我们在命令行中命名的提交。
然而,合并操作实际上需要 三个 提交。第三个——或者在某种程度上,第一个——是其他两个提交中最好的共同祖先。你可以把它想象成 Git 查看我们已经命名的两个提交——J 和 L——并向后工作。我们将从两个提交中尽可能向后移动,直到找到一些提交,我们可以从两个提交中找到。
在这种情况下,最好的共享提交是显而易见的:它是提交H。提交H 在两个分支上。提交G 也是如此,但它更靠后,所以H 是最好的一个。
为了真正完成合并,Git 现在将比较合并基础——提交H——与我们当前的提交J,看看我们改变了什么:
git diff --find-renames <hash-of-H> <hash-of-J> # what we changed
然后 Git 会将 相同的合并基础与我们命名的另一个提交进行比较:
git diff --find-renames <hash-of-H> <hash-of-L> # what they changed
git merge 的核心——我喜欢称之为动词形式,或合并——现在是结合这两个差异的过程。 Git 找到了共同的起点,并找到了两组更改:“我们的”(来自 HEAD / current-branch 提交)和“他们的”(来自我们在命令行中命名的提交)。只要我们和他们更改了不同的文件或同一文件中的不同行,1Git 本身就可以在它的拥有。
Git 将对所有文件重复此操作。 Git 会将合并后的更改应用到来自合并基础的快照(此处为提交 H),如果没有冲突,Git 将自行进行新的合并提交。这就是我所说的merge as an noun,因为commit这个词前面的形容词merge经常被用作名词,“a合并”。
为了防止 Git 自行提交,我们将使用 --no-commit。如果我们不这样做,Git 仍然会在合并冲突的情况下停止(然后您必须在提交之前解决冲突)。
在我们继续展示如何撤消部分合并之前,让我们假设我们正常完成了合并,或者省略了--no-commit,以便我们得到最终的合并提交。让我们把它画进去:
I--J
/ \
...--G--H M <-- branch1 (HEAD)
\ /
K--L <-- branch2
请注意,名称 branch1 已照常更新。它现在指向新的 merge commit M。使M 合并的原因很简单:它有两个 的父提交,而不是通常的单父提交J。 Git 添加提交 L 作为新提交的第二个父节点。
这个新的第二个父节点的真正意义稍后会变得更清楚,但请注意,我们现在可以通过名称 branch1 和提交 M 访问提交 K 和 L,通过向下和向左走。 所以现在所有提交都在名称 branch1 上(可访问),而 I 和 J 上的提交不是在 branch2 上: 从branch2 无法访问它们,因为branch2 上的最后一次提交是提交L,其(单一)父级是K,其(单一)父级是H。从H我们只能向后到G,然后到F等等。
1如果我们都以不同的方式更改(例如)第 42 行,Git 将不知道是使用我们的更改,还是使用他们的更改,或者不同的东西。在这里,Git 将声明一个合并冲突,并在合并中间停止,合并未完成。你的工作变成了告诉 Git 最终结果应该是什么。
即使我们的更改和它们的更改只是邻接(触摸),Git 也会停止:如果我们用新的第 42 行替换旧的第 42 行,并且他们用新的第 43 行替换旧的第 43 行,Git 将声明一个在这里也合并冲突。这对文件顶部或末尾的更改特别有用,但也特别烦人,因为 Git 不知道将这些更改放入哪个 order 中。例如,如果有是一个 10 行的文件,我们添加了第 11 行,他们添加了第 11 行,哪一行先行?哪一行变成第 12 行? Git 本身不知道,所以它让执行git merge 的人提供正确的答案。
--no-commit
当 Git 为合并提交 M 创建快照时,Git 会以与任何提交相同的方式这样做。我们还没有在这里讨论 Git 的 index 或 暂存区 的作用——由于篇幅原因,我们不会——但重点是新提交 M将有一个快照,就像任何其他提交一样。我们可以使用git checkout 或git restore,或者仅通过编辑文件的工作树副本并使用git add,更改提交M 的内容。
所以,如果我们运行:
git checkout branch1
git merge --no-commit branch2
Git 认为这一切都完成了,但还没有进行合并,我们现在可以使特定文件(例如某个目录中的每个文件)与 @987654419 中这些文件的副本匹配@(即当前,即J)提交:
git checkout HEAD -- subdir2 subdir3
这将在 Git 的索引和您的工作树中,将 subdir2/ 和 subdir3/ 中文件的所有副本替换为来自 HEAD 快照的副本。或者:
git restore -iw --source HEAD subdir2 subdir3
做同样的事情。
如果您现在运行git merge --continue 或git commit,Git 现在将从合并文件中为M 制作快照此步骤已更新。你会得到和以前一样的提交图:
I--J
/ \
...--G--H M <-- branch1 (HEAD)
\ /
K--L <-- branch2
不同的是提交M中的快照现在匹配提交J中的快照,除了你没有的文件 restore,现在包含 Git 自动进行的合并,使用 H、J 和 L 作为三个输入提交。
请注意,三个现有的输入提交没有任何变化。没有任何变化可以改变,所以没有任何变化。这意味着,如果您愿意,您可以在以后重新进行相同的合并,无论是否使用--no-commit。因为在计算加密校验和时所有提交哈希 ID 都包含时间戳,所以如果您进行新合并,则新合并将具有与现有合并提交 M 不同的哈希 ID。您可能希望稍后利用这个事实。
现在提交 M 存在:
I--J
/ \
...--G--H M <-- branch1 (HEAD)
\ /
K--L <-- branch2
本质上,Git 会相信提交 M 是合并的正确结果。让我们以通常的方式(git checkout 或git switch,加上通常的工作)向branch1 和branch2 添加更多提交,然后准备再次将branch2 合并到branch1:
I--J
/ \
...--G--H M--N <-- branch1 (HEAD)
\ /
K--L--O--P <-- branch2
如果我们运行git log,我们将看到提交N,然后是M,然后——按某种顺序——J和I、L和K——然后是H ,然后是G,以此类推。如果我们运行git log branch2,我们将看到提交P,然后是O,然后是L,然后是K,然后是H,然后是G,等等。这是因为这些是来自每个分支提示提交的可访问的提交。当向后遍历M 时,Git 将访问分支的两条腿:2 请注意,向后查看时,合并实际上是一个分支(和一个分支拆分,其中H 拆分为流I 和K,是合并)。
无论如何,如果我们现在运行:
git merge branch2
再次(有或没有--no-commit),Git 将通过通常的过程来定位两个分支提示提交 N 和 P,然后向后工作以找到最佳共享 提交作为合并基础。在这种情况下,最好的共享提交是提交L:只要我们在分叉处向下,就从N 后退两步,并且从P 后退两步。3
Git 现在将执行通常的比较,从 L 到 N 以查看我们更改了什么,从 L 到 P 以查看它们更改了什么。如果我们使用git checkout 或git restore 使合并M 中的文件匹配J 中的文件,“我们改变的”是将我们的东西从J 放回去,“他们改变的”是通常什么都没有,因为O 和P 中的快照,在branch2 上,不需要进行任何更改 来保留他们的代码。
这意味着通过告诉 Git 合并J 和L 的正确方法是保留来自J 的文件,Git 将继续相信这是进行合并的正确方法。
请注意,如果您重新执行 J 和 L 的合并(通过将其中一个提交检出为历史提交,或者创建一个新的分支名称,然后合并另一个提交),Git 仍然会重新- 做与我们第一次合并J 和L 时相同的工作。也就是说,这一次,Git 将再次合并你手动放回的文件。当我们合并 N 和 P(它们的历史记录中都有 M 提交)时,Git 将“看到”我们之前所做的合并。
2这有助于说明为什么单词 branch 在 Git 中存在问题。如果我们想要准确,我们应该在谈论 names 时使用短语 branch name,例如 master、branch1 和 branch2。结构分支——H forks 如果你正在向前阅读,或者M forks 当 Git 向后阅读时——没有很好的名字。我喜欢叫他们DAGlets:看我对What exactly do we mean by "branch"?的回答
3事实上,它每次都向后退了 2 步,在两条“腿”上,这是我试图绘制漂亮图表的一种巧合。通常每条腿上的步数不同,在某些情况下,它是 no 从一个或两个提交后退步。然而,当不退一步时,合并要么是微不足道的(并且 Git 会做其他事情——而不是实际合并——默认情况下)或者已经完成(并且 Git 只会说你是最新的并且什么都不做)。
git merge 的合并部分——合并提交。也就是说,它会查看每次提交中的快照。git merge --continue 或 git commit 将合并作为名词时,生成的快照将是您所做的任何内容,而 Git 已暂停。这就是你实现你想要的东西的方法。由于您在其他地方处理git-subtree(我假设),这使得以后的合并在某种意义上“更难”这一事实可能无关紧要:如果您需要更新subdir2 文件,您可以只使用git checkout 或@ 987654508@他们来自适当的提交。
【讨论】: