Git - 即使没有冲突,如何强制手动合并

时间:2017-01-24 08:04:30

标签: git merge

这是多年来多次被问过的问题。我找到了很多答案,特别是这个答案:

Git - how to force merge conflict and manual merge on selected file(@Dan Molding)

此页面包含详细指南,介绍如何设置始终返回失败的合并驱动程序,从而实现手动合并。我试图为Windows修改该解决方案:

  1. 我在%homepath%\.gitconfig添加了以下内容:

    [merge "verify"] name = merge and verify driver driver = %homepath%\\merge-and-verify-driver.bat %A %O %B

  2. 我将驱动程序更改为

    cmd /K "echo Working > merge.log & git merge-file %1% %2% %3% & exit 1"

    (添加echo Working > merge.log以检查是否已调用驱动程序。)

  3. 并在repo的根目录下创建了一个文件.gitattributes,其中包含以下行:

    *.txt merge=verify

  4. 不幸的是,它不起作用。我试图合并一个文件feature.txt,唉,合并成功完成。似乎根本没有调用驱动程序,因为没有创建merge.log文件。

    我做错了吗?任何强制手动合并问题的解决方案都是最受欢迎的。

2 个答案:

答案 0 :(得分:1)

问题分为两部分。相对容易的是编写自定义合并驱动程序,正如您在步骤1和步骤2中所做的那样。困难的是,如果在Git&#39中,Git实际上并不打扰运行自定义驱动程序的意见,没有必要。这是您在步骤3中观察到的内容。

那么,当 时,Git运行你的合并驱动程序?答案相当复杂,为了实现这一目标,我们必须定义术语 merge base ,我们将在稍后介绍。您还需要知道Git识别文件 - 事实上,几乎所有内容:提交,文件,补丁等等 - 通过哈希ID 。如果你已经知道所有这些,你可以直接跳到最后一节。

哈希ID

哈希ID(有时是对象ID 或OID)是您在提交时看到的那些丑陋的名字:

$ git rev-parse HEAD
7f453578c70960158569e63d90374eee06104adc
$ git log
commit 7f453578c70960158569e63d90374eee06104adc
Author: ...

所有Git商店都有一个唯一的哈希ID,根据对象的内容(文件或提交或其他)计算。

如果将相同的文件存储两次(或更多),则会获得两次(或更多)相同的哈希ID。由于每次提交最终都会存储截至提交时每个文件的快照,因此每个提交都有一个每个文件的副本,由其哈希ID列出。你实际上可以看到这些:

$ git ls-tree HEAD
100644 blob b22d69ec6378de44eacb9be8b61fdc59c4651453    README
100644 blob b92abd58c398714eb74cbe66671c7c3d5c030e2e    integer.txt
100644 blob 27dfc5306fbd27883ca227f08f06ee037cdcb9e2    lorem.txt

中间的三个丑陋的ID是三个哈希ID。这三个文件位于这些ID下的HEAD提交中。我在几个提交中有相同的三个文件,通常内容略有不同。

进入合并基地:DAG

DAG D 竖立 A 循环 G raph,是一种绘制关系的方法提交。要真正正确地使用Git,您至少需要对DAG的含义有一个模糊的概念。它也被称为提交图,这在某些方面是一个更好的术语,因为它避免了专门的信息学术语。

在Git中,当我们制作分支时,我们可以用各种方式绘制它们。我喜欢在这里使用的方法(在文本中,在StackOverflow上)是在左边提交早期提交,在右边提交稍后提交,并用单个大写字母标记每个提交。理想情况下,我们按照Git保留它们的方式绘制这些,这是相反的:

A <- B <- C   <-- master

这里我们只有三次提交,都在master上。分支名称 master&#34;指向&#34;三个提交中的最后一个。这就是Git实际上通过从分支名称C读取其哈希ID来实现提交master的方式,实际上名称master有效地存储只是这个ID。

Git通过读取提交B找到提交C。提交C在其中包含提交B的哈希ID。我们说C&#34;指向&#34; B,因此是向后箭头。同样,B&#34;指向&#34; A。由于A是第一次提交,因此它没有先前的提交,因此它没有后向指针。

这些内部箭头告诉Git每个提交的父提交。大多数情况下,我们并不关心它们都是倒退的,所以我们可以更简单地将其绘制为:

A--B--C   <-- master

这让我们假装明显C来自B,尽管其实在Git中相当困难。 (与{34} B C之前的B进行比较,这在Git中很容易:它很容易倒退,因为内部箭头都是向后的。)

现在让我们画一个实际的分支。假设我们从提交D开始创建一个新分支,然后进行第四次提交A--B--C <-- master \ D <-- sidebr (当我们我们制作它时,它并不完全清楚但最终无论如何都不重要):

sidebr

名称D现在指向提交master,而名称C指向提交B

这里的一个关键Git概念是提交master两个分支上。它位于sidebr A上。对于提交C也是如此。在Git中,任何给定的提交可以同时在许多分支上进行。

这里隐藏在Git中的另一个关键概念与大多数其他版本控制系统完全不同,我将在其中提及。这是实际的分支实际由提交本身形成,而分支名称在这里几乎没有任何意义或贡献。这些名称仅用于查找分支提示:在这种情况下提交DC。分支本身就是我们通过绘制连接线获得的,从较新的(子)提交回到较旧的(父)提交。

作为一个侧面点,值得注意的是,这种奇怪的向后链接允许Git永远不会改变任何有关任何提交的内容。请注意,DB都是B的子项,但我们并非必须知道,当我们创建C时,我们要同时制作D C。但是,因为父母不知道&#34;对于它的孩子,Git根本不必在D内存储BB的ID。它只会存储C的ID - 当DC创建每个D时,其中 确实存在<和A--B--C <-- master \ D <-- sidebr

我们制作的这些图纸显示(部分)提交图

合并基础

合并基础的正确定义太长了,不能在这里讨论,但是现在我们已经绘制了图形,非正式定义非常简单,并且视觉上很明显。当我们像Git那样向后工作时,两个分支的合并基础是他们第一次聚集在一起的点。也就是说,它是两个分支上的第一个这样的提交

因此,在:

B

合并基础是提交A--B--C--F <-- master \ D--E--G <-- sidebr 。如果我们提交更多提交:

B

合并基础仍为提交A--B--C--F---H <-- master \ / D--E--G <-- sidebr 。如果我们实际上成功合并,则新的合并提交有两个父提交而不是一个:

H

此处,提交master是合并,我们在git merge sidebr上通过运行F进行合并,其两个父项为master(曾经是G)和sidebr提示的提交(仍然的提交 G的提示)。

如果我们现在继续提交,后来决定进行另一个合并,A--B--C--F---H--I <-- master \ / D--E--G--J <-- sidebr 将成为新的合并基础:

H

G两个父母,我们(和Git)同时跟随父母和#34;同时&#34;当我们向后看时因此,如果我们运行另一个合并,则提交F是两个分支上的第一个。

除此之外:交叉合并

请注意,在这种情况下,sidebr不在J上:我们在遇到父链接时必须遵循这些链接,因此G会返回E返回到F等,以便从sidebr开始时我们永远不会到达master。但是,如果我们将下一个合并 sidebr 合并到 A--B--C--F---H--I <-- master \ / \ D--E--G--J---K <-- sidebr

F

现在提交I在两个分支上。但事实上,提交A--B--C--E-G--I <-- br1 \ X D---F-H--J <-- br2 也在两个分支上,所以即使这使得合并双向进行,我们也可以在这里。我们可以在所谓的&#34; criss cross merges&#34;中遇到麻烦,我会画一个只是为了说明问题,但不在这里讨论:

E

我们从分别前往Fgit checkout br1; git merge br2; git checkout br2; git merge br1的两个分支开始,然后执行GEF的合并}和br1,已添加到H),然后立即生成FEbr2合并,添加到E) 。我们可以继续提交两个分支,但最终,当我们再次合并时,我们在选择合并基础时遇到问题,因为F merge.conflictstyle = diff3都是&# 34;最佳候选人&#34;。

通常,即使这个&#34;只是工作&#34;,但有时纵横交错合并会产生Git试图以一种奇特的方式处理的问题,使用其默认的&#34;递归&#34;合并策略。在这些(罕见的)情况下,您可以看到一些奇怪的合并冲突,特别是如果您设置git merge branch-name(我通常建议:它显示冲突合并中的合并基础版本)。

您的合并驱动程序何时运行?

既然我们已经定义了合并基础并且看到了哈希识别对象(包括文件)的方式,我们现在可以回答原始问题了。

当你运行HEAD时,Git:

  1. 标识当前提交,a.k.a。--ours。这也称为本地branch-name提交。
  2. 标识另一个提交,即您通过 --theirs 提交的提交。这是另一个分支的提示提交,并且被不同地称为另一个,B,或者有时远程提交(&#34;远程&#34;是非常差的名称,因为Git也将该术语用于其他目的。)
  3. 标识合并基础。让我们称这个提交&#34; base&#34;。字母%A也不错,但合并驱动程序%B--ours分别引用--theirs%O版本git diff指基地。
  4. 实际上,运行两个单独的git diff base ours命令:git diff base theirsgit diffs
  5. 这两个差异告诉Git&#34;发生了什么&#34;。 Git的目标,请记住,是要结合两组更改:&#34;我们在我们的工作中做了什么&#34;和他们在他们的作品中做了什么&#34;。这就是两个-s显示的内容:&#34; base vs our&#34;是我们做了什么,&#34; base vs theirs&#34;是他们做了什么。 (这也是Git如果发现任何文件被添加,删除和/或重命名的方式,从基础到我们和/或基础到他们的 - 但这是一个不必要的复杂现在,我们将忽略。)

    组合这些调用合并驱动程序的更改的实际机制,或者 - 在我们的问题案例中 - 并没有。

    请记住,Git的每个对象都由其哈希ID编目。根据对象的内容,每个ID都是唯一的。这意味着它可以立即判断任何两个文件是否100%相同:当且仅当它们具有相同的散列时,它们才完全相同。

    这意味着如果在base-vs-ours或base-vs-theirs中,这两个文件具有相同的哈希值,那么我们没有做任何更改,或他们 em>没有做任何改变。如果我们没有进行任何更改并且他们进行了更改,那么为什么显然组合这些更改的结果是他们的文件。或者,如果他们没有做任何更改并且我们进行了更改,则结果是我们的文件。

    同样,如果我们和他们的相同的哈希,那么我们都进行了相同的更改。在这种情况下,组合更改的结果是文件 - 它们相同,因此即使Git选择了哪一个也不会。

    因此,对于所有这些情况,Git只会从基本版本中选择 new 文件具有不同的哈希值(如果有)。这是合并结果,没有合并冲突,Git完成合并该文件。 它永远不会运行您的合并驱动程序,因为显然没有必要。

    只有当所有三个文件都有三个不同的哈希值时,Git才能进行真正的三向合并。这是它运行自定义合并驱动程序的时候,如果你已经定义了一个。

    有一种解决方法,,但它不适合胆小的人。 Git不仅提供自定义合并驱动程序,还提供custom merge strategies。有四种内置合并策略,全部通过-s ours选项选择:-s recursive-s resolve-s octopus和{{1} }。但是,您可以使用-s custom-strategy来调用自己的。

    问题是要编写合并策略,必须识别合并基础,在模糊的情况下做任何你想要的递归合并(la -s recursive)合并基础,运行两个git diff,找出文件添加/删除/重命名操作,然后运行各种驱动程序。因为这会占用whole megillah,所以可以做任何你想做的事情 - 但你必须做很多事情。据我所知,没有使用这种技术的固定解决方案。

答案 1 :(得分:1)

tl;博士:我试图重复你描述的内容,似乎有效。与你的版本相比有2个变化,但没有它们我合并也失败了(因为驱动程序基本上无法运行)

我试过这个:

创建合并驱动程序$HOME/bin/errorout.bat

exit 1

为合并类型

创建一个部分
[merge "errorout"]
   name = errorout
   driver = ~/bin/errorout.bat %A %O %B

创建.gitattributes文件:

*.txt merge=errorout

之后,报告错误,因为我认为您希望报告错误:

 $ git merge a

 C:\...>exit 1
 Auto-merging f.txt
 CONFLICT (content): Merge conflict in f.txt
 Automatic merge failed; fix conflicts and then commit the result.

我有git版本2.11.0.rc1.windows.1。我无法按照您指定的运行成功执行复杂的命令,它报告了一些语法错误。