bdiff: deal better with duplicate lines stable
authorMatt Mackall <mpm@selenic.com>
Thu, 21 Apr 2016 21:05:26 -0500
branchstable
changeset 29013 9a8363d23419
parent 29012 4bd67ae7d75a
child 29014 f1ca249696ed
bdiff: deal better with duplicate lines The longest_match code compares all the possible positions in two files to find the best match. Given a pair of sequences, it effectively searches a grid like this: a b b b c . d e . f 0 1 2 3 4 5 6 7 8 9 a 1 - - - - - - - - - b - 2 1 1 - - - - - - b - 1 3 2 - - - - - - b - 1 2 4 - - - - - - . - - - - - 1 - - 1 - Here, the 4 in the middle says "the first four lines of the file match", which it can compute be comparing the fourth lines and then adding one to the result found when comparing the third lines in the entry to the upper left. We generally avoid the quadratic worst case by only looking at lines that match, which is precomputed. We also avoid quadratic storage by only keeping a single column vector and then keeping track of the best match. Unfortunately, this can get us into trouble with the sequences above. Because we want to reuse the '3' value when calculating the '4', we need to be careful not to overwrite it with the '2' we calculate immediately before. If we scan left to right, top to bottom, we're going to have a problem: we'll overwrite our 3 before we use it and calculate a suboptimal best match. To address this, we can either keep two column vectors and swap between them (which significantly complicates bookkeeping), or change our scanning order. If we instead scan from left to right, bottom to top, we'll avoid ever overwriting values we'll need in the future. This unfortunately needs several changes to be made simultaneously: - change the order we build the initial hash chains for the b sequence - change the sentinel values from INT_MAX to -1 - change the visit order in the longest_match inner loop - add a tie-breaker preference for earlier matches This last is needed because we previously had an implicit tie-breaker from our visitation order that our test suite relies on. Later matches can also trigger a bug in the normalization code in diff().
mercurial/bdiff.c
tests/test-bdiff.py
tests/test-bdiff.py.out
--- a/mercurial/bdiff.c	Thu Apr 21 21:53:18 2016 -0500
+++ b/mercurial/bdiff.c	Thu Apr 21 21:05:26 2016 -0500
@@ -103,14 +103,14 @@
 
 	/* clear the hash table */
 	for (i = 0; i <= buckets; i++) {
-		h[i].pos = INT_MAX;
+		h[i].pos = -1;
 		h[i].len = 0;
 	}
 
 	/* add lines to the hash table chains */
-	for (i = bn - 1; i >= 0; i--) {
+	for (i = 0; i < bn; i++) {
 		/* find the equivalence class */
-		for (j = b[i].hash & buckets; h[j].pos != INT_MAX;
+		for (j = b[i].hash & buckets; h[j].pos != -1;
 		     j = (j + 1) & buckets)
 			if (!cmp(b + i, b + h[j].pos))
 				break;
@@ -128,7 +128,7 @@
 	/* match items in a to their equivalence class in b */
 	for (i = 0; i < an; i++) {
 		/* find the equivalence class */
-		for (j = a[i].hash & buckets; h[j].pos != INT_MAX;
+		for (j = a[i].hash & buckets; h[j].pos != -1;
 		     j = (j + 1) & buckets)
 			if (!cmp(a + i, b + h[j].pos))
 				break;
@@ -137,7 +137,7 @@
 		if (h[j].len <= t)
 			a[i].n = h[j].pos; /* point to head of match list */
 		else
-			a[i].n = INT_MAX; /* too popular */
+			a[i].n = -1; /* too popular */
 	}
 
 	/* discard hash tables */
@@ -151,12 +151,12 @@
 	int mi = a1, mj = b1, mk = 0, mb = 0, i, j, k;
 
 	for (i = a1; i < a2; i++) {
-		/* skip things before the current block */
-		for (j = a[i].n; j < b1; j = b[j].n)
+		/* skip all lines in b after the current block */
+		for (j = a[i].n; j >= b2; j = b[j].n)
 			;
 
 		/* loop through all lines match a[i] in b */
-		for (; j < b2; j = b[j].n) {
+		for (; j >= b1; j = b[j].n) {
 			/* does this extend an earlier match? */
 			if (i > a1 && j > b1 && pos[j - 1].pos == i - 1)
 				k = pos[j - 1].len + 1;
@@ -166,7 +166,7 @@
 			pos[j].len = k;
 
 			/* best match so far? */
-			if (k > mk) {
+			if (k > mk || (k == mk && i <= mi)) {
 				mi = i;
 				mj = j;
 				mk = k;
--- a/tests/test-bdiff.py	Thu Apr 21 21:53:18 2016 -0500
+++ b/tests/test-bdiff.py	Thu Apr 21 21:05:26 2016 -0500
@@ -52,6 +52,9 @@
         pos += l
 showdiff("x\n\nx\n\nx\n\nx\n\nz\n", "x\n\nx\n\ny\n\nx\n\nx\n\nz\n")
 showdiff("x\n\nx\n\nx\n\nx\n\nz\n", "x\n\nx\n\ny\n\nx\n\ny\n\nx\n\nz\n")
+# we should pick up abbbc. rather than bc.de as the longest match
+showdiff("a\nb\nb\nb\nc\n.\nd\ne\n.\nf\n",
+         "a\nb\nb\na\nb\nb\nb\nc\n.\nb\nc\n.\nd\ne\nf\n")
 
 print("done")
 
--- a/tests/test-bdiff.py.out	Thu Apr 21 21:53:18 2016 -0500
+++ b/tests/test-bdiff.py.out	Thu Apr 21 21:05:26 2016 -0500
@@ -20,5 +20,8 @@
 6 6 'y\n\n'
 6 6 'y\n\n'
 9 9 'y\n\n'
+0 0 'a\nb\nb\n'
+12 12 'b\nc\n.\n'
+16 18 ''
 done
 done