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

Jen*_*ips 7 git merge

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

Git - 如何在所选文件上强制合并冲突和手动合并(@Dan Moulding)

此页面包含如何设置始终返回失败的合并驱动程序的详细指南,从而使手动合并成为可能。我试图为 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

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

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

tor*_*rek 8

问题有两个部分。相对简单的方法是编写自定义合并驱动程序,就像您在步骤 1 和 2 中所做的那样。困难的是,如果 Git 认为不适合,则 Git实际上不会费心运行自定义驱动程序。必要的。这就是您在步骤 3 中观察到的情况。

\n\n

那么,Git 何时运行合并驱动程序?答案相当复杂,为了达到这个目的,我们必须定义术语“合并基础”,我们稍后会介绍它。您还需要知道 Git 通过哈希 ID来识别文件\xe2\x80\x94(事实上,几乎所有内容:提交、文件、补丁等\xe2\x80\x94)。如果您已经了解所有这些,可以直接跳到最后一节。

\n\n

哈希 ID

\n\n

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

\n\n
$ git rev-parse HEAD\n7f453578c70960158569e63d90374eee06104adc\n$ git log\ncommit 7f453578c70960158569e63d90374eee06104adc\nAuthor: ...\n
Run Code Online (Sandbox Code Playgroud)\n\n

Git 存储的所有内容都有一个唯一的哈希 ID,该 ID 是根据对象的内容(文件或提交或其他内容)计算得出的。

\n\n

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

\n\n
$ git ls-tree HEAD\n100644 blob b22d69ec6378de44eacb9be8b61fdc59c4651453    README\n100644 blob b92abd58c398714eb74cbe66671c7c3d5c030e2e    integer.txt\n100644 blob 27dfc5306fbd27883ca227f08f06ee037cdcb9e2    lorem.txt\n
Run Code Online (Sandbox Code Playgroud)\n\n

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

\n\n

到达合并基础:DAG

\n\n

DAG或向循环图是一种绘制提交之间关系的方法要真正正确地使用 Git,您至少需要对 DAG 是什么有一个模糊的了解。它也称为提交图,在某些方面这是一个更好的术语,因为它避免了专门的信息学术语。

\n\n

在 Git 中,当我们创建分支时,我们可以用多种不同的方式绘制它们。我喜欢在这里使用的方法(在 StackOverflow 上以文本形式)是将较早的提交放在左侧,将较晚的提交放在右侧,并用单个大写字母标记每个提交。理想情况下,我们会按照 Git 保留它们的方式绘制它们,这是相当落后的:

\n\n
A <- B <- C   <-- master\n
Run Code Online (Sandbox Code Playgroud)\n\n

这里我们只有三个提交,全部都在master. 分支名称 master“指向”三个提交中的最后一个。这就是 Git 实际上C通过从分支名称读取其哈希 ID 来查找提交的方式master,而实际上该名称实际上master存储了这一 ID。

\n\n

BGit通过读取 commit 来查找提交C。CommitC内部有 commit 的哈希 ID B。我们说C“指向” B,因此是向后指向的箭头。同样,B“指向” A。由于A是第一次提交,因此它没有先前的提交,因此它没有后向指针。

\n\n

这些内部箭头告诉 Git每个提交的父提交。大多数时候,我们并不关心它们都是向后的,所以我们可以更简单地把它画成:

\n\n
A--B--C   <-- master\n
Run Code Online (Sandbox Code Playgroud)\n\n

C这让我们假装后面的内容是显而易见的B,尽管事实上这在 Git 中很难做到。B(与“在之前”的声明相比C,这在 Git 中非常容易:向后移动很容易,因为内部箭头都是向后的。)

\n\n

现在让我们画一个实际的分支。假设我们创建一个新分支,从 commit 开始B,并进行第四次提交D(不清楚我们何时创建,但最终没关系):

\n\n
A--B--C   <-- master\n    \\\n     D   <-- sidebr\n
Run Code Online (Sandbox Code Playgroud)\n\n

现在名称sidebr指向 commit D,而名称master又指向 commit C

\n\n

Git 的一个关键概念是提交B位于两个分支上。它已打开master sidebr。对于提交来说也是如此A。在 Git 中,任何给定的提交都可以而且经常同时在多个分支上进行。

\n\n

Git 中还隐藏着另一个关键概念,它与大多数其他版本控制系统有很大不同,我只是顺便提一下。这是因为实际的分支实际上是由提交本身形成的,并且分支名称在这里几乎没有任何意义或贡献。这些名称仅用于查找分支提示:在本例中C是提交。D分支本身是我们通过绘制连接线得到的,从较新的(子)提交回到较旧的(父)提交。

\n\n

还值得注意的是,作为一个侧面,这种奇怪的向后链接允许 Git永远不会改变任何提交的任何内容。请注意, 和C都是D的子项,但我们在制作时B不一定知道B我们要制作C D。但是,由于父级并不“了解”其子级,因此 Git 根本不必在其中存储C和的 ID 。它只存储\xe2\x80\x94的 ID ,当它创建每个和时,它在每个和中确实存在DBBCDCD

\n\n

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

\n\n

合并基地

\n\n

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

\n\n

因此,在:

\n\n
A--B--C   <-- master\n    \\\n     D   <-- sidebr\n
Run Code Online (Sandbox Code Playgroud)\n\n

合并基础是 commit B。如果我们进行更多提交:

\n\n
A--B--C--F   <-- master\n    \\\n     D--E--G   <-- sidebr\n
Run Code Online (Sandbox Code Playgroud)\n\n

合并基础仍然是 commit B。如果我们确实成功合并,新的合并提交将有两个父提交,而不是只有一个:

\n\n
A--B--C--F---H   <-- master\n    \\       /\n     D--E--G   <-- sidebr\n
Run Code Online (Sandbox Code Playgroud)\n\n

这里,commit是我们通过运行H进行的合并,它的两个父项是(曾经是 的尖端的提交)和(仍然是 的尖端的提交)。mastergit merge sidebrFmasterGsidebr

\n\n

如果我们现在继续进行提交,然后决定进行另一次合并,G则将成为新的合并基础:

\n\n
A--B--C--F---H--I   <-- master\n    \\       /\n     D--E--G--J   <-- sidebr\n
Run Code Online (Sandbox Code Playgroud)\n\n

H两个父母,当我们向后看时,我们(和 Git)“同时”跟随父母。因此,G如果我们运行另一次合并,提交是两个分支上的第一个提交。

\n\n

旁白:交叉合并

\n\n

请注意F,在本例中, 不是 on sidebr:我们必须在遇到父链接时跟踪它们,因此J会返回到G,从而导致返回到E,等等,这样我们F从 开始时就永远不会到达sidebr。但是,如果我们进行下一次合并from master into sidebr

\n\n
A--B--C--F---H--I   <-- master\n    \\       /    \\\n     D--E--G--J---K   <-- sidebr\n
Run Code Online (Sandbox Code Playgroud)\n\n

现在提交F已在两个分支上进行。但事实上,提交I也在两个分支上,所以即使这使得合并双向进行,我们在这里也没问题。我们可以会遇到所谓的“十字交叉合并”的麻烦,我将画一个只是为了说明问题,但这里不再赘述:

\n\n
A--B--C--E-G--I   <-- br1\n    \\     X\n     D---F-H--J   <-- br2\n
Run Code Online (Sandbox Code Playgroud)\n\n

我们从两个分支开始分别到EF,然后执行git checkout br1; git merge br2; git checkout br2; git merge br1make (和 的G合并,添加到),然后立即也 make (和 的合并,添加到)。我们可以继续致力于两个分支,但最终,当我们再次合并时,我们在选择合并基础时遇到问题,因为 和都是“最佳候选者”。EFbr1HFEbr2E F

\n\n

通常,即使这样“也能工作”,但有时交叉合并会产生问题,Git 会尝试使用其默认的“递归”合并策略以一种奇特的方式来处理。在这些(罕见)情况下,您可能会看到一些看起来很奇怪的合并冲突,特别是如果您设置了merge.conflictstyle = diff3(我通常建议:它会显示冲突合并中的合并基础版本)。

\n\n

你的合并驱动程序什么时候运行?

\n\n

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

\n\n

当你跑步时git merge branch-name,Git:

\n\n
    \n
  1. 标识当前提交,又名HEAD. 这也称为本地--ours提交。
  2. \n
  3. 标识另一个提交,即您通过 给出的提交branch-name。这是另一个分支的提示提交,并且被称为“other”,--theirs有时也称为“remote”提交”(“远程”是一个非常糟糕的名称,因为 Git 也将该术语用于其他目的)。
  4. \n
  5. 标识合并基础。我们将此提交称为“基础”。该信B也不错,但是带有合并驱动程序,%A并分别%B参考--ours--theirs版本,带有%O版本,并参考基础。
  6. \n
  7. 实际上,运行两个单独的git diff命令:git diff base oursgit diff base theirs
  8. \n
\n\n

这两个差异告诉 Git“发生了什么”。请记住, Git 的目标是结合两组更改:“我们在我们的版本中做了什么”和“他们在他们的版本中做了什么”。这就是两人所git diffs展示的:“基地与我们的”是我们所做的,“基地与他们的”是他们所做的。(这也是 Git 发现在我们的和/或他们的\xe2\x80\x94 中是否添加、删除和/或重命名任何文件的方式,但这现在是一个不必要的复杂化,我们会忽略。)

\n\n

这是组合这些更改的实际机制,调用合并驱动程序,或者\xe2\x80\x94,如我们的问题案例中的\xe2\x80\x94 那样。

\n\n

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

\n\n

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

\n\n

同样,如果我们和他们的哈希值相同,那么我们都会进行相同的更改。在这种情况下,组合更改的结果是 file\xe2\x80\x94,它们是相同的,因此 Git 选择哪一个都无关紧要。

\n\n

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

\n\n

只有当所有三个文件都具有三个不同的哈希值时,Git 才必须进行真正的三向合并。此时它将运行您的自定义合并驱动程序(如果您已定义)。

\n\n

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

\n\n

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

\n


max*_*630 1

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

我已经尝试过这个:

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

exit 1
Run Code Online (Sandbox Code Playgroud)

为合并类型创建一个部分

[merge "errorout"]
   name = errorout
   driver = ~/bin/errorout.bat %A %O %B
Run Code Online (Sandbox Code Playgroud)

创建 .gitattributes 文件:

*.txt merge=errorout
Run Code Online (Sandbox Code Playgroud)

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

 $ 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.
Run Code Online (Sandbox Code Playgroud)

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