Git 中没有“强制合并”之类的东西。幸运的是,这不是你想要的。你想要的是git merge -s theirs。不幸的是,这不存在。幸运的是,有办法伪造它。另请参阅Is there a "theirs" version of "git merge -s ours"? 我可以将其作为副本关闭,但我将写一个长格式的答案。
Git 提交,包括合并提交
merge 这个词有两个或多个含义,具体取决于您是用作动词、合并,还是形容词或名词:a merge commit(形容词)或a merge(形容词变成名词,如adjectives are wont to do in English: this is called nominalization)。名词版本 a merge 只是作为提交存在于您的存储库中,就像任何其他提交一样,只是它不是通常的单亲,而是有两个。1支持>
请记住,在 Git 中,提交由两部分组成:快照——一组已保存的文件——以及一些关于提交的元数据:信息,例如是谁做的,什么时候做的,为什么做的。在元数据中,Git 存储了一些较早提交的哈希 ID。这些是相关提交的父母。大多数普通提交都有一个父哈希 ID;使提交成为 merge 提交的原因在于它有两个父级,而不是通常的一个。但它只有一个快照。该快照就是合并结果。
我发现图片在这里很有帮助。首先,让我们在 Git 中绘制一个简单的提交字符串(如果愿意,可以称为“分支”):
... <-F <-G <-H
这里,H 代表某个提交链中last 提交的实际哈希 ID。由于H 是一个提交,它既有数据——快照——也有元数据。快照以您或任何人进行提交时的形式保存所有文件。 H 的元数据包含先前提交 G 的实际原始哈希 ID。我们说H 指向 G。
G 当然也是一个提交:一个快照(当时的文件)和元数据。此元数据指向更早的提交 F。 F 和 G 一样,有快照和元数据,所以它指向背面。
这一切的意思是,只要我们能找到last提交,我们就能找到所有链中的提交。这就是 Git 分支名称的作用:它让我们在链中找到 last 提交。它只保存该提交的哈希 ID。无论 name 持有什么哈希 ID,这就是 last 提交 在那个链中。所以这可能看起来像:
...--F--G--H <-- main
\
I--J <-- feature
这里,分支名feature指向commit J,它又指向commit I,又指向commit H,又指向G,以此类推。事实上,还有另一个名称 main 指向 H,这并不能阻止 所有我刚刚列出的提交出现在 两个分支上。然而,main 指向H 的事实确实 停止提交I 和J 在该分支上。通过并包括H 的提交在两个分支上,而最后两个仅在feature 上。
我们可以随时添加和删除我们喜欢的任何名称。只有几个限制。例如,名称必须满足某些测试:main 和feature 可以,但hello..world 不是因为双点被禁止。名称还需要指向实际提交。2 我不会涵盖所有内容,但您应该明白:我们可以在特定(合理)限制内创建和销毁任何名称。所以我们可以在上图中添加一个名字dev,像这样:
...--F--G--H <-- dev, main
\
I--J <-- feature
如果我们现在 git checkout dev 或 git switch dev,这将成为 当前名称—我们将提交 H 作为 当前提交 in这个过程——然后进行一些新的提交,我们需要重新绘制图表。在这里,我将feature 放在顶部,dev 放在下面:
I--J <-- feature
/
...--F--G--H <-- main
\
K--L <-- dev (HEAD)
这张图向我们展示了我们有两个提交 (I-J),它们 only 在 feature 和两个 only 在 dev。 (HEAD) 表示法表明我们当前正在使用名称dev,因此使用提交L,这是dev 上的only之一。
1从技术上讲,任何具有两个或更多父级的提交都是合并提交,但两个就足够了,通常有两个。
2这没有技术原因,Git 正在获得一项功能,其中将对某些名称放宽此限制,但目前所有名称都需要它,并且仍然需要分支名称。
merge-as-a-verb 的机制
当您选择进行合并提交时(通常是通过运行 git merge),您可以选择两个(或者,根据脚注 1,更多)提交捆绑在一起。两者之一是当前提交,您在运行git merge 时选择之前,通常使用git checkout <em>branch</em> 或git switch <em>branch</em>。另一个是您在命令行上命名的提交。因此我们可以运行:
git checkout dev
git merge feature
例如。3 Git 然后会自行查找第三次提交。这第三次提交是合并基础,合并基础提供了合并动作的起点:合并过程,或作为动词合并。
你的问题是,如果你的图片是这样的:
I--J <-- feature
/
...--F--G--H
\
K--L <-- dev (HEAD)
——我已经删除了 main 这个名字,以免它弄乱图片;它是否存在并不重要——git merge 动作,作为动词合并过程,将通过:
- 定位 both 分支上的最佳提交,在这种情况下显然是提交
H;
- 将
H 中的快照与L (dev) 中的快照进行比较,看看我们有什么变化;
- 将
H 中的快照与J (feature) 中的快照进行比较,看看它们发生了什么变化;
-
合并这些更改并将合并后的更改应用到
H 中的快照;和
- 通过使用这些组合更改来生成最终的合并结果。
结果将如下所示:
I--J <-- feature
/ \
...--F--G--H M <-- dev (HEAD)
\ /
K--L
注意分支 name dev 现在如何指向新的提交 M,这是执行此合并过程的结果。
3您可以添加git merge --no-ff 以防止Git 进行快进而不是合并。我想有人可以认为这是强制合并——但这仍然不是你想要的。 :-)
有一个ours 策略
Tim Biegeleisen refers to in a comment 的 策略 是 Git 实际实现合并作为动词过程的方式。当您运行git merge 时,它会进行一系列初步测试并处理各种命令行选项。允许的选项之一是 -s,-s 采用 strategy 参数。
Git 内置的开箱即用策略是:
-
-s recursive:这是通常的默认设置。它完成了我们上面谈到的组合。
-
-s resolve:这是-s recursive 的变体;它也这样做了。我们不需要在这里讨论它们之间的技术差异,因为您的问题是您希望避免合并。
-
-s octopus:这是一种特殊用途的算法,用于合并两个以上的提交。这不是您想要的答案。
-
-s ours:这是另一种特殊用途的算法。这几乎是你想要的!
ours 策略的作用很简单:为了制作新快照,它完全忽略您使用 git merge 命令指定的其他提交。然后它说新快照应该与当前提交完全匹配。因此,如果我们在dev 上运行git merge feature,我们会得到:
I--J <-- feature
/ \
...--F--G--H M <-- dev (HEAD)
\ /
K--L
这看起来和以前完全一样,直到我们查看提交 M 中的 snapshot。 之前,@中的 snapshot 987654403@ 是合并两个分支上的工作 并将合并的更改应用到来自H 的合并基础快照的结果。现在,M 中的 快照 与提交 L 中的快照完全相同。
这如此接近但仍然不是你想要的,因为你说你想要的是像往常一样制作M,但使用来自提交J的快照。 如果 Git 有一种开箱即用的方法,它会拼写为 -s theirs。显然 Git 曾经确实 有 -s theirs(这一定是 Git-1.6 之前的版本),但现在没有 -s theirs。
有很多方法可以伪造它。请参阅链接的答案(应该是重复的)以了解这些不同的方法。 确定你真的想伪造它。你最终会得到一个合并提交M,它的快照匹配你选择的“side”文件。
正如那里的几个答案所指出的,请注意 -X ours 和 -X theirs,它们的含义非常不同。这些选项——我喜欢称它们为扩展选项,以明确它们不是策略 (-s) 选项——被传递到 随心所欲的策略。默认的递归/解决代码使用它来自动解决冲突。它不会完全忽略一方的变化。相反,它需要任何一方的更改,但是当这些更改发生冲突时,解决使用一方的更改的冲突,仅针对特定的差异块丢弃另一方的更改。