即使比较的是同样两个文件,采用不同的 diff 算法,结果也可能不同。它们给出的都是“正确”的 diff,只是在把哪些行视为相同并配对这一点上做法不同,因而可读性不同。git 通过 --diff-algorithm 选项提供四种算法。
| 取值 | git 文档中的说明 | 特点 |
|---|---|---|
myers (default) | 基本的贪心(greedy)算法,目前的默认值 | 速度快,结果通常接近最小 |
minimal | 多花时间以生成尽可能小的 diff | 总是最小;输入很大时可能较慢 |
patience | patience diff 算法 | 以唯一出现的行为锚点 |
histogram | 扩展 patience,支持出现次数少的公共元素 | 以少见的行为锚点 |
也可以使用 git diff --patience、git diff --histogram 这样的简写选项,并可通过 git config diff.algorithm histogram 修改默认值。
Myers:寻找最小编辑的默认算法
Myers 算法寻找使删除与新增数量最少的路径(原理见 diff 的工作原理)。问题出在把哪些行视为相同存在多种选择时。代码里有大量非常常见的行,如 }、{、空行、return,因此算法即使把语义上毫无关系的行配成一对,编辑数也可能同样是最小的。
同一处修改,两种结果
假设像下面这样,在函数 f 和 g 之间新插入了函数 h。下面两个 diff 都是新增 4 行,编辑数相同。
@@ -1,6 +1,10 @@ int f() { return 1; }++int h() {+ return 3;+} int g() { return 2;
@@ -1,5 +1,9 @@ int f() { return 1;+}++int h() {+ return 3; } int g() {
第一个把新函数完整地显示出来;第二个则把原有函数 f 的右括号当成了新函数的括号,新增部分被切得很别扭。这种形态由搜索顺序造成。较新的 git 会根据缩进把变更区块的边界移到更易读的位置(--indent-heuristic,默认启用),从而修正其中很大一部分。
Patience:先对齐唯一的行
patience diff 由 Bram Cohen 提出,它先寻找在两个文件中都各自只出现一次的行。像函数声明 int h() { 这样唯一的行,对应关系是确定的。算法从这些唯一的行中选出顺序一致的最长列表(最长递增子序列)作为锚点,然后只对锚点之间的小区间再做比较。
- 优点:不会被
}、空行这类常见行牵着走,函数、段落级别的修改会成块显示。 - 缺点:对于几乎没有唯一行的输入(同一行反复出现的数据、日志),找不到锚点,结果与普通 diff 没有区别,甚至可能被合并成很大的块。
Histogram:优先考虑少见的行
histogram 算法最初在 JGit(用 Java 编写的 git 实现)中开发,后来也被纳入 git。patience 只看“恰好出现一次的行”,而 histogram 会统计每一行的出现次数,选出出现最少的行作为锚点。因此,即使没有唯一的行,它也能以相对少见的行为基准。git 文档将其描述为“扩展 patience 算法以支持出现次数少的公共元素”。实际使用中,它和 patience 一样,往往能较好地保留代码结构。
该用哪一种
- 日常代码审查:默认值就够了。如果结果切得很怪,就用
--histogram再看一遍。 - 移动函数或一次新增多个函数的大型重构:
--histogram或--patience能更好地保持代码块完整。 - 需要精确地按最小值统计变更行数时:
--minimal。 - 重复内容很多的数据文件:排序和规范化可能比算法更重要。如果是 JSON,请参阅 JSON 对比。
无论使用哪种算法,按结果还原出的文件都相同。改变的只是人阅读时看到的样子。
在本工具中
本站先用 Myers 系列算法按行比较,再根据变更行之间内容的相似度把它们配对为“修改”,并对行内内容再比较一次。因此,即使按行比较的结果切得有点别扭,也能一眼看出哪个词改了。把上面的例子放进 文本对比工具,比较一下合并视图和分栏视图的显示效果吧。结果的阅读方法整理在 unified diff 阅读指南 中。