重复合并 GIT。它如何计算差异?

Repetitive merges in GIT. How does it calculate differences?

我一直在做一项研究,试图了解 GIT 合并是如何工作的。我知道有几种合并类型,如递归、章鱼等。我发现最常用的是解析/递归。而且递归合并只有在有几个共同的祖先/基础时才有用。

但是,我找不到从分支到 master 的重复合并使用了哪种算法(或者应该如何计算祖先)。

一个简单的例子。让我们创建一个包含 1 个文件的空项目 "A":

A

然后创建另一个文件"B"并提交给master

A
B

然后我从 第一个版本 创建了一个分支,它只有一个文件 "A" 并创建了另一个文件 "C"。所以我的分支看起来像这样:

A
C

然后我决定将我的分支更改合并到 master,我得到:

A
B
C

然后我决定回到我的分支机构继续我的工作。我创建另一个文件 "D"

A
C
D

现在我想将我的更改从分支合并回主干。祖先是怎么计算的?

一个直观的例子:

如果我取祖先"AC",应该说"B"也是新增的,因为它没有分支和祖先两个版本。

如果我取祖先 "ABC",应该说 "B" 被删除,因为 B 存在两个版本:master 和 ancestor。

这两个选项看起来都不正确。我试图通过使用具有合并解释功能的 "Plastic SCM" 来弄清楚它。如图所示,ancestor/base 被用作版本 "AC",但是它仍然正确地计算出添加了多少文件(只有 1 个而不是 2 个)。

既要总结评论,又要解决问题...

寻找合并基地

  1. Git 使用用于查找有向无环图的最低公共祖先的算法计算一对提交的合并基础。精确的算法没有在任何地方描述并且可能会改变,只要新的算法产生正确的结果。另见 Algorithm to find lowest common ancestor in directed acyclic graph?

    可能有多个 LCA。在这种情况下,-s resolve 合并策略会选择其中之一。你无法控制它选择哪一个。 -s recursive 合并策略 运行s git merge 在他们身上,一次两个,就好像通过下面的:

    commits=$(git merge-base --all $left $right)
    if len($commits) > 1
        a=$commits[0]
        for i in range(1, len(commits))
            b=$commits[i]
            a=$(git-merge-recursively-inner $a $b)
        rof
        commits=($a)
    fi
    

    (伪代码)。请注意,内部递归合并本身可能会找到多个合并基础;如果是,则使用此算法合并它们。

    最终结果是一次提交,$commits[0]。这是合并基地

  2. 在任何情况下,既然我们有一个单一的合并基础提交——来自只找到一个 LCA 的 LCA 发现算法,或者通过合并递归合并来自的多个合并基础LCA 查找算法,或者通过 merge-resolve 仅从列表中选择一个提交——我们可以看看 git merge-(recursive|resolve) 实际上是如何合并文件的。它必须 运行 两个内部 git diff 操作,每个操作都打开了重命名检测器。

差异和文件身份/重命名检测

文件差异引擎比较两个文件。我们把一个文件放在左边,另一个文件放在右边。在两个文件匹配的地方,差异什么也没说。在两个文件不同的地方,差异引擎——取决于它有多好——提出了一些我们可以应用的更改,使左侧的内容与右侧文件的内容相匹配。

要区分一对 提交 ,Git 将一个放在左边,一个放在右边。然后它必须在这两个提交中配对 files。 Git 可以在启用或不启用重命名检测器的情况下执行此操作。

在没有重命名检测器的情况下,图片很清晰。左边和右边的文件是 "the same file" 当且仅当它们具有相同的 名称 。添加重命名检测器 识别 (标记为 "the same")差异左侧和右侧的一些文件,即使 names已经改变了。

Git 现有的重命名检测器正在进行一些更改以使其更好。这里不需要确切的细节:我们只需要知道它会说某些文件已重命名,"the same" 文件也是如此,即使它们具有不同的名称。其他文件自动成为 "the same" 文件,因为它们具有相同的名称。

对于每个配对文件,差异引擎都会产生一组更改,使左侧文件成为右侧文件。重命名检测器生成需要首先执行的重命名操作。右边new的文件叫做added,左边commit存在,右边commit不存在的文件, 被删除。

因此,提交对的差异导致:

  • 要重命名的文件(从旧名称到新名称)
  • 要添加的文件
  • 要删除的文件

根据需要对两个提交中存在的文件进行一些更改。

合并,给定合并基础

给定一个合并基础提交,解析和递归都以相同的方式进行:

  • 根据 HEAD 区分合并基础,并启用重命名检测。这些是 我们的 更改。
  • 在启用重命名检测的情况下,将合并基础与其他提交进行区分。这些是 他们的 更改。
  • 合并更改。

"Combining" 需要在单个文件中解决高级更改(例如重命名、添加和删除)和低级更改。将应用合并更改的文件是来自 合并库 的文件。这保证了结果在所有情况下都有效。

例如,假设我们重命名了一个文件并且他们修改了我们重命名的文件。合并后的更改实际上说 最后,将文件 base.ext 重命名为 head.ext;同时,更改 base.ext 的第 17 行。 因此我们将更改第 17 行,并重命名文件,捕获这两个操作。

高级操作可能会发生冲突!例如,如果我们重命名一个文件而他们将其删除,那就是高级冲突。如果我们和他们都重命名一个文件,那就是冲突,除非我们都选择了相同的最终名称。如果我们和他们都删除了一个文件,这与明显的结果结合得很好。

低级别的更改也可能会发生冲突。如果我们和他们都以不同的方式修改相同的行,或者如果我们的更改和他们的更改 "touch" 在任一边缘发生冲突。例如,如果我们替换第 9 行和第 10 行(在第 8 行之后删除 2 行并在第 8 行之后插入 2 行)并且它们替换第 11 行和第 12 行,我们的更改就会邻接。出于一般的谨慎,将此称为冲突。

当然,如果我们和他们对same原来的线路做same的改动,那就不冲突了。 Git 只需复制一份这些更改。

扩展选项 -Xours-Xtheirs 通过选择一方(我们的或他们的)忽略另一方来解决低级冲突。这仅适用于低级别冲突。从逻辑上讲,它也适用于高层冲突,但事实并非如此。

合并了我们和他们的所有更改后,Git 会将 组合的 更改应用到在 merge base[=108] 中找到的快照=] 提交。如果没有冲突,生成的文件可以自动提交。这是这些合并的默认操作;使用 --no-commit 来抑制这个默认提交。

当合并递归使用内部合并进行合并基础提交时,它会强制提交结果即使存在合并冲突。你看不到它对这些冲突做了什么,除了当你的(外部)合并也有冲突时合并基础中出现的任何内容。 (在这种情况下,文件的合并基础副本在索引槽 1 中可用。此外,如果将 merge.conflictStyle 设置为 diff3,冲突文件的每个工作树副本将显示文本来自合并基础,带有冲突标记。)