git的耐心差异算法的实现是否正确?

时间:2016-10-19 13:59:59

标签: git git-diff

This question on Stackoverflow似乎是应用耐心差异算法的一个很好的候选者。然而,在测试我的潜在答案时,我发现git diff --patience不符合我的期望(在这种情况下,与默认的diff算法没有区别):

$ cat a
/**
 * Function foo description.
 */
function foo() {}

/**
 * Function bar description.
 */
function bar() {}

$ cat b
/**
 * Function bar description.
 */
function bar() {}

$ git diff --no-index --patience a b
diff --git a/a b/b
index 3064e15..a93bad0 100644
--- a/a
+++ b/b
@@ -1,9 +1,4 @@
 /**
- * Function foo description.
- */
-function foo() {}
-
-/**
  * Function bar description.
  */
 function bar() {}

我希望差异是:

diff --git a/a b/b
index 3064e15..a93bad0 100644
--- a/a
+++ b/b
@@ -1,8 +1,3 @@
-/**
- * Function foo description.
- */
-function foo() {}
-
 /**
  * Function bar description.
  */

根据我的理解,在这种情况下,唯一的共同行是提及 bar 的两行,并且围绕这些行的最长公共上下文应该是函数bar()及其docblock,其中表示差异应该归结为已移除的函数foo()以及它自己的docblock和以下空白行。

2 个答案:

答案 0 :(得分:11)

其他人暂时没有解决这个问题,所以我会采取措施。这完全是纯粹的高级理论,因为我还没有阅读关于原始耐心算法的论文。

LCS(最长公共子序列)算法都是为了减少寻找最小编辑距离解决方案所花费的时间。标准(动态编程)解决方案是O( MN ),其中 M 是原始字符串中的符号数, N 是数字目标字符串中的符号。在我们的例子中,"符号"是行,"字符串"是行的集合,而不是带有字符的字符串(符号可以是,例如,ASCII代码)。我们只需填写 M x N 矩阵"编辑费用&#34 ;;当我们完成时,我们通过在结果矩阵中向后追踪最小路径来产生实际编辑。有关示例,请参阅https://jlordiales.me/2014/03/01/dynamic-programming-edit-distance/。 (通过谷歌搜索找到的网页:它不是我有什么关系,除了现在高速扫描它是正确的。看来是正确的。:-))

实际上计算这个矩阵对于大文件来说相当昂贵,因为 M N 是源行数(通常大致相等):a~4k行文件结果在矩阵中的~16M条目中,必须在我们追溯最小路径之前完全填充。此外,比较"符号"不再像比较字符那样微不足道,因为每个"符号"是一条完整的路线。 (通常的技巧是在矩阵生成期间散列每一行并比较哈希,然后在追溯期间重新检查,替换"保持未更改的符号"用"删除原始并插入新的"如果哈希误导了我们。即使在存在哈希冲突的情况下也能正常工作:我们可能得到一个非常次优的编辑序列,但实际上它永远不会糟糕。)

LCS通过观察保持长公共子序列("保留所有这些线")几乎总是导致大赢,来修改矩阵计算。找到一些好的LCS-es之后,我们将问题分解为"编辑非公共前缀,保持公共序列,并编辑非公共后缀":现在我们计算 2 动态编程矩阵,但对于较小的问题,它会更快。 (当然,我们可以对前缀和后缀进行说明。如果我们有一个~4k行文件,我们发现~2k完全没有变化,中间靠近共线,在顶部留下~0.5k行在底部〜1.5k左右,我们可以检查〜0.5k&#34中的长公共子序列;顶部有差异"线,然后再次在~1.5k"底部有差异& #34;行。)

LCS表现不佳,因此当共同的子序列"时,会产生可怕的差异。像     }这样的平凡线条,有很多匹配,但并不真正相关。 耐心差异变体只是从最初的LCS计算中丢弃这些行,因此它们不属于"公共子序列"。这使剩余的矩阵更大,这就是你必须耐心的原因。 : - )

结果是耐心差异在这里没有帮助,因为我们的问题与常见的子序列无关。事实上,即使我们完全抛弃LCS并只做了一个大矩阵,我们仍然会得到不良结果。我们的问题是删除费用:

- * Function foo description.
- */
-function foo() {}
-
-/**

(并且不插入任何内容)与相同与删除费用相同:

-/**
- * Function foo description.
- */
-function foo() {}
-

任何一个的成本只是"删除5个符号"。即使我们对每个符号进行加权 - 制作非空行"更昂贵"要删除除空行 - 费用保持不变:我们最后会删除相同的五行。

相反,我们需要的是基于"视觉聚类"的重量线的某种方式:边缘的短线比短线更便宜中间。添加到Git 2.9的压缩启发式尝试在事后执行此操作。它显然至少有轻微的缺陷(只有空行计数,它们必须实际存在,而不仅仅是通过达到边缘暗示)。在矩阵填充期间进行加权可能更好(假设在执行LCS消除之后剩下的是什么,实际上是通过完整的动态编程矩阵)。不过,这非常重要。

答案 1 :(得分:1)

I found a newer post by Bram Cohen, with his description of the patience diff algorithm that supports the observed output of git diff:

... how Patience Diff works -

  1. Match the first lines of both if they're identical, then match the second, third, etc. until a pair doesn't match.
  2. Match the last lines of both if they're identical, then match the next to last, second to last, etc. until a pair doesn't match.
  3. Find all lines which occur exactly once on both sides, then do longest common subsequence on those lines, matching them up.
  4. Do steps 1-2 on each section between matched lines

So the algorithm's emphasis on unique lines is undermined by the steps 1. and 2. which detect the common prefix and suffix even if they are made of noisy lines.

Such a formulation is a little different from what I've seen before that, and Bram admits that he has slightly changed it:

I've previously described it with the ordering a bit different ...

My question actually repeated the concern expressed in this comment to Bram's post.