功能分支合并到 Master 后发生变化

Posted

技术标签:

【中文标题】功能分支合并到 Master 后发生变化【英文标题】:Feature branch got change after merging it to Master 【发布时间】:2021-12-29 02:10:20 【问题描述】:

我有一个 feature 分支,我需要合并到 master,所以我通过 github 创建了一个拉取请求并将其合并。

我期待 feature 分支中与 master 不同的所有更改将覆盖 master 中的特定文件,但在合并后我注意到相反的功能分支已用代码更新(那个主文件不同)并且主文件保持不变。

如果我错了,请纠正我的理解。当我们将分支 A 合并到分支 B 时,分支 A 不应该被更改。不是吗?

谢谢,

【问题讨论】:

不,源分支不应该改变。您确定您的 pr 中的源分支和目标分支是正确的吗? @fbede 如果我看到关闭的 PR,它会说“将 35 个提交从 featurebranch 合并到 master”,尽管在 intelliJ gitlog 中它说“将分支 'master' 合并到 featurebranch”。创建 pullrequest 在第一个下拉列表中我选择了主(基础)和第二个下拉功能分支(比较),它不正确吗? 它是公共存储库吗?如果是的话,可以链接pr吗? @fbede 很遗憾没有,否则我什至会在之前分享它。 【参考方案1】:

我猜你跑了git merge master。这将 master 合并到当前分支中。

要将feature分支合并到master,需要切换到master分支,然后运行git merge feature。话虽如此,您可以在不将功能合并到 master 的情况下创建 PR。

【讨论】:

我没有运行任何命令,我使用 github 进行合并。创建 pullrequest 在第一个下拉列表中我选择了主(基础)和第二个下拉功能分支(比较),它不正确吗? 嗯。不,没错【参考方案2】:

我期待 feature 分支中与 master 不同的所有更改将覆盖 master 中的特定文件...

这种期望是错误的。

合并并不意味着相同。合并意味着合并工作

这有点复杂:Git 不会存储您所做的工作。相反,Git 存储每个文件完整快照。那就是:

每个提交都有编号,带有一个又大又丑的唯一hash ID。这个数字是 Git 实际从 Git 的主数据库中检索提交的方式。 (这个数据库包含提交和其他支持对象,所有这些对象都有这些又大又丑的哈希 ID。)

每个提交存储两件事:

提交具有每个文件的完整快照。每个提交中的文件都以一种特殊的、只读的、仅限 Git 的、压缩的和去重复的格式存储。如果您有一个包含大量提交的存储库,并且您检查了一个包含一些非常大的文件和一些非常小的文件的提交,并且您只更改了一个文件并添加并提交, Git 必须重新存储所有文件,但它可以重新使用除一个更改的文件之外的所有文件。因此,只有更改后的文件实际上占用了任何空间。但是每次提交仍然有每个文件!

提交还存储一些元数据,或有关提交本身的信息。这包括您在git log 输出中看到的内容:例如,谁进行了提交、何时以及为什么(他们的日志消息)。对于 Git 自己的目的,每个提交也会在此元数据中存储一个先前提交哈希 ID 的列表

这个先前提交的哈希ID列表通常只有一个条目。我们称该条目为提交的父项。这意味着每个子提交“指向”其父:

... <-F <-G <-H

这里H 代表最新提交的哈希ID。这个提交是一个提交,所以它有一个快照——所有文件的格式都是你创建H时的格式——和元数据。元数据说你是去年八月制作的,或者其他什么。它还说提交 H 是较早的提交 G(无论真正的、随机的哈希 ID 是什么:git log 都会显示它)。

不过,提交G 是一个提交,因此它具有每个文件的完整快照和一些元数据。 Git 可以提取两个提交(到内存中的临时区域)并比较文件。当文件完全匹配时——因此被删除重复——这很无聊,而且 Git 通常什么都不说。当文件匹配时,Git 可以提出一个配方——一组应用于旧文件的更改——使其与新文件匹配。这就是您在git log -p 中看到的内容。

将提交 H 显示为较早提交 G更改git log 现在后退一跳。现在它的工作是显示提交G。为此,它需要提交FG 的父级,以获取快照以找出发生了什么变化,如果你想看到的话,但这很容易,因为G 持有F 的哈希ID。

所以 Git 可以向您显示 G 的作者和消息等,然后从 FG 的差异查看更改了,现在 Git 可以返回再跳一跳,告诉你F。这是一个提交,所以它有一个快照和一个父级,而且……嗯,这个想法现在应该很明显了。

对于简单的情况来说这一切都很好,但是我们有一个问题:我们必须以某种方式将提交H 的哈希 ID 提供给 Git。我们可以把它写在白板上,或者记在纸上。但是,当我们有一台 计算机时,为什么要这样做呢?Git 为我们存储这个哈希 ID,例如,一个 分支名称。这就是我们所做的。

现在,如果我们有一个像这样的简单提交链:

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

我们创建了一个新的分支名称feature,我们得到:

...--F--G--H   <-- feature, master (HEAD)

我们目前使用提交H,通过名称master。如果我们切换到feature,使用git checkout featuregit switch feature,我们通过名称feature 切换到使用提交H

...--F--G--H   <-- feature (HEAD), master

没有什么需要改变的,所以 Git 选择了一个捷径并且没有改变其他任何东西:我们仍然使用提交 H,只是通过另一个名称。

如果我们现在进行新的提交,这些新的提交会得到:

一个新的快照,来自我们告诉 Git 使用的内容; 新元数据:您的姓名为作者/提交者,“现在”为日期和时间,父提交当前提交

通过写出新的提交,Git 获得了一个新的哈希 ID——这里有一些相当深奥的魔法,使用加密哈希,这也解释了为什么任何提交都无法更改成功了——但我们只需将我们的新提交称为I。新提交 I 将指向现有提交 H

             I
            /
...--F--G--H

现在 Git 使用了它的巧妙小技巧:它将I 的哈希 ID 写入 当前分支名称,即feature

             I   <-- feature (HEAD)
            /
...--F--G--H   <-- master

如果我们在做任何其他事情之前再次进行新的提交,我们会得到:

             I--J   <-- feature (HEAD)
            /
...--F--G--H   <-- master

如果我们现在切换回名称 master,会发生以下情况:

             I--J   <-- feature
            /
...--F--G--H   <-- master (HEAD)

我们现在让 Git 将特殊名称 HEAD 附加到名称 master,它指向提交 H,而不是提交 J。由于我们正在更改 commits,Git 现在将从我们的工作树中删除来自 J 的所有文件,并改为放入来自 H 的所有文件(使用快照)。1

如果我们现在运行git merge feature,你会得到你所期望的。但在此之前,让我们进行更多提交。我们将修改一些在I 和/或J 中也被修改过的文件,因为我们会进行两个新的提交KL

             I--J   <-- feature
            /
...--F--G--H
            \
             K--L   <-- master (HEAD)

如果我们比较J 中的文件和L 中的文件(以任意顺序),我们将得到一个配方,它可以更改这两个提交中的一个以匹配另一个提交中的内容。但是如果我们运行git merge feature,我们不想失去我们在提交K-L 中所做的好东西。所以git merge 不是这样做的。

相反,git merge:

定位当前提交:这很容易,它是任何HEAD 所附加的,我们也已经检查了该提交,如--ours; 根据我们提供的名称定位另一个提交,在本例中为提交J:我们实际上可以提供原始哈希 ID,因为 Git 只需要提交;这成为--theirs 提交; 最后,使用我们一直绘制的图表来找到最佳共享起点提交

从这里的图中可以明显看出最后一次提交:它是提交H。提交H 位于两个分支I-J 的提交仅在 feature 上,K-L 的提交仅在 master 上,但直到并包括 H 的所有提交都在 both 分支上。所以所有这些提交都是共享的,H 显然是最好的。2

现在,为了进行合并,Git 将:

H 中的快照与L 中的快照进行比较:这显示了 在分支master 上为到达--ours 提交所做的工作; 将H 中的快照与J 中的快照进行比较:这表明他们 在分支feature 上确实到达了--theirs 提交; 结合两组变化

组合的 更改随后会应用到来自H 的快照。这会保留我们的更改并添加他们的更改,或者等效地保留他们的更改并添加我们的更改。无论哪种方式,文件中的最终结果不一定与 either commit J commit L 匹配,因为我们进行了两组更改。

如果一切顺利,Git 会进行一个新的合并提交。合并提交与任何其他提交完全相同:它有一个快照和一个父哈希 ID 列表。 合并提交的原因在于父哈希 ID 列表不仅有一个哈希 ID,还有两个:3

             I--J   <-- feature
            /    \₂
...--F--G--H      M
            \    /¹
             K--L   <-- master (HEAD)

仍然只有一个快照第一个父节点,在这里用一个小的1 标记,会返回到我们在启动git merge 时所做的提交。第二个父节点返回到我们告诉 Git 合并的提交。

请注意,在更简单的情况下:

             I--J   <-- feature
            /
...--F--G--H   <-- master (HEAD)

Git 仍然进行所有花哨的基于合并的计算。然而,这一次,我们的提交是H,他们的提交是J,合并基础又是...H。如果 Git 将H 中的快照与H 中的快照进行比较,它会发现的更改列表将为空。

无论如何,我们都可以使用git merge --no-ff 强制 Git 这样做。结果是一个新的合并提交,像往常一样有两个父母:

             I--J   <-- feature
            /    \
...--F--G--H------M   <-- master (HEAD)

但现在 in M 的快照确实 确实J 中的快照匹配。这是因为 Git 比较了 HJ 以查看它们的变化,然后将这些变化应用到 H,并使用我们这边的 no changes 来制作快照。在代数上,4H + (J - H) 又是 J

如果我们不强制进行真正的合并,Git 会进行一种虚假的非实际合并,称为快进。但是 GitHub 不会让你快进,如果你使用他们的点击按钮:他们有 MERGEREBASE AND MERGESQUASH AND MERGE 并且这些都不是 Git 的内部快进。 (REBASE AND MERGE 选项很接近,但不一样。)


1这个解释故意跳过了 Git 允许的很多奇怪的极端情况。

2Proof by vigorous hand waving,名单上的第 8 位。更严重的是,请参阅Lowest Common Ancestor of a DAG。

3从技术上讲,Git 合并可以有两个以上的父级,但这种情况很少见(主要用于炫耀?)。

4一些版本控制系统确实试图定义变化的代数,因此会进行这种计算。它……变得一团糟。

【讨论】:

以上是关于功能分支合并到 Master 后发生变化的主要内容,如果未能解决你的问题,请参考以下文章

Git 分支合并

合并分支,从当地分支机构挑选樱桃,没有任何冲突

合并后的 Git 分支和提交历史记录

Git:无法将功能分支合并到当前分支

git 把分支代码合并给master主分支

合并到 master 后,如何检查来自哪个分支的哪些提交以及从何处删除的合并?