关注了就能看到更多这么棒的文章哦~
Mergiraf: syntax-aware merging for Git
By Daroc Alden
October 31, 2025
Gemini flash translation
https://lwn.net/Articles/1042355/
版本控制系统中的自动语法感知合并(automatic syntax-aware merging)概念可以追溯到 2005年或更早,但最初的实现通常是针对特定语言且速度缓慢。Mergiraf 是一款合并冲突解决工具,它采用通用算法并结合少量特定语言知识来解决 Git 默认策略无法处理的冲突。该项目的贡献者们投入工具开发的时间尚不足一年,但它已经 支持 33 种语言,包括 C、Python、Rust,甚至 SystemVerilog。
Mergiraf 由 Antonin Delpeuch 启动,但其他几位贡献者也纷纷加入,其中 Ada Alakbarova 是最活跃的。该项目使用 Rust 编写,并采用 GPL 第三版许可证。
Git 默认的合并算法("ort")主要基于行。它确实包含一些用于合并目录的基于树的逻辑,但单个文件内的更改是逐行合并的。这可能导致两个逻辑上独立的更改影响同一行,从而引发合并冲突。
考虑以下基础版本:
void callback(int status);然后假设一个人将函数改为可失败的:
int callback(int status);而另一个人则改变了参数类型:
void callback(long status);默认的合并算法无法处理这种情况,因为它对同一行存在冲突的更改。然而,语法感知合并是基于语言的语法元素,而不是单独的行。因此,例如 Mergiraf 可以像这样解决上述冲突:
int callback(long status);从它的角度来看,这些更改实际上并不重叠,因为返回类型和参数类型被视为独立的、不重叠的区域。这种语法感知合并的概念已被讨论多年,但为语法树编写合并算法的复杂性使其难以在实践中广泛应用。Spork 是针对 Java 实现这一想法的工具,于 2023 年发布,这表明其确实可行。Mergiraf 试图将这种针对 Java 的算法推广到一般的编程(以及配置或标记)语言。
设计
Mergiraf 依赖 tree-sitter 增量解析库 将各种语言转换为通用语法树,其中每个叶子节点(leaf node)对应文件中的一个特定标记(token),每个内部节点(internal node)则表示一个语言结构(language construct)。然而,Mergiraf 本身对每种语言所需的信息相对较少。相反,它使用 一种非语言特定的树匹配算法 来指导冲突解决,并在其之上分层少量语言知识。这种设计是该工具能够适应如此多种不同语言的部分原因。
Mergiraf 算法首先执行常规的基于行的合并;如果成功(通常如此),程序就不需要诉诸更耗费资源的基于树的合并算法。即使基于行的合并失败,它通常也只在少数位置失败。在解析文件不同版本进行合并时,Mergiraf 可以将通过基于行的合并已解决且无冲突的语法树部分标记为无需更改,从而使其能够仅关注冲突部分。这提供了显著的加速,特别是对于大型文件。
对于剩余部分,该工具使用 GumTree 算法在剩余子树之间找到模糊匹配。识别匹配足以生成差异(diff),但它本身不足以解决任何冲突。接下来,Mergiraf 将语法树扁平化(flattens)为一系列关于树中节点如何相互关联的事实。这些事实被标记为是来自合并的基础版本(base revision)、左侧版本(left revision)还是右侧版本(right revision)(即最近的共同祖先、被合并到的提交和正在合并的提交)。然后,从合并后的事实列表中重建一个新的语法树。如果来自基础版本的一个事实与另一个事实冲突,它将被丢弃。如果来自左侧和右侧版本的两个事实不一致,则表明存在 Mergiraf 无法解决的实际冲突。
这种方法的优点在于它消除了困扰 ort 算法的那种移动/编辑冲突:如果一个版本编辑了程序某个部分的内部,而另一个版本重新定位了程序的该部分,这些事实就不会相互矛盾。另一方面,如果两个版本都编辑了程序的完全相同部分,那确实代表了一个人类应该查看的实际冲突。
尽管如此,对于某些语言的编辑,Mergiraf 甚至可以利用特定语言知识来解决此类冲突。例如,考虑对 Rust 结构体的以下更改:

这是一个合并冲突,因为基于行的算法无法判断添加新行的顺序——而行在程序中出现的顺序通常很重要。然而,在 Rust 中,编译器可以根据需要重新排列结构体字段(除非结构体被标记为 `#![repr(C)]` 或其他 `repr` 设置——这 似乎是当前 Mergiraf 版本中的一个已知错误)。因此,通过以任意顺序放置这些行,可以自动解决此合并冲突。无论哪种方式,合并后的程序行为都相同。另一方面,这并不是在 C 语言中解决同等合并冲突的正确方法,因为在 C 语言中,结构体成员的顺序会影响程序的正确性。
当一个语法元素的子节点(children)可以自由重新排序而不改变程序含义时,Mergiraf 称之为“可交换父节点(commutative parent)”。Mergiraf 所需的特定语言信息之一,就是语言中哪些部分是可交换父节点的列表(如果有的话)。然而,可交换父节点并非合并冲突的“免死金牌”:例如,如果两个版本添加了名称相同但类型不同的字段,那仍然会是一个冲突。在这种情况下,Mergiraf 会使用额外的特定语言信息,将冲突行紧密地放置在一起,以便生成的冲突标记(conflict markers)尽可能精确地指出问题。
使用方法
当我接触到 Mergiraf 时,它的方法听起来很有前景,但我很好奇它在 Git 的实际应用中究竟能带来多大影响。截至撰写本文时,Linux 内核仓库包含 7,415 个合并提交(merge commit),当使用默认合并算法重放时,这些提交会导致冲突。这些是原本需要手动修复的合并提交,尽管这可能低估了内核开发者必须处理的合并冲突数量。例如,它不包括在变基(rebasing)过程中可能出现的合并冲突,因为关于变基的信息不包含在 Git 历史中进行分析。
在提取了内核 Git 历史中所有合并冲突的列表后,我尝试使用 Mergiraf 来解决它们。其中 6,987 个仍然导致冲突,但有 428 个成功解决。更大比例的合并冲突仍得到部分解决。如果这些结果具有普遍性(我认为很可能),那么采用 Mergiraf 可能会在一定程度上减少需要手动合并的冲突数量,这仍然可能有助于节省宝贵的维护者时间。
该工具本身有两个接口:一个是可以手动运行在带有冲突标记(例如 ort 生成的)的文件上,以尝试解决冲突;另一个是可以由 Git 自动使用。运行 `mergiraf solve <path>` 将读取给定文件中的冲突标记并尝试解决它们。将以下片段添加到 Git 配置中,并在 `.gitattributes` 中将驱动程序设置为默认值,将从一开始就使用 Mergiraf 作为 Git 合并驱动程序(merge driver):

当被 Git 调用时,用户可以通过运行 `mergiraf review` 来查看 Mergiraf 遇到的冲突以及它是如何解决这些冲突的。对于手头没有合并冲突的人,Mergiraf 提供了一个 示例仓库,其中包含各种冲突,以展示 Mergiraf 如何解决它们。该工具也适用于 Jujutsu,并且很可能适用于其他版本控制系统,只要它们使用与 Git 相同的合并冲突语法。
程序员们在没有 Mergiraf 的情况下也一直工作得很好,所以它不一定是每个人都想添加到其编程工具集中的东西。但很少有人喜欢遇到合并冲突,而那些能够智能地解决它们——尤其是那些对人类来说显而易见,因而处理起来是浪费时间的冲突——的工具,是一个很有吸引力的前景。
全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。
欢迎分享、转载及基于现有协议再创作~
长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~

244

被折叠的 条评论
为什么被折叠?



