Volume XXII, number 280Wednesday, October 7, 2026Latest message 2 hours ago

The Git List

News and archive of git@vger.kernel.org, since April 2005

patchxdiff: re-diff shifted change groups when using histogram algorithm

17 messages between Dec 6, 2025 and Mar 19, 2026, from Yee Cheng Chin via GitGitGadget, Junio C Hamano, Phillip Wood, Yee Cheng Chin.

Plain Markdown or JSON for tools and agents. Diffs are folded; open one to read it.

Yee Cheng Chin via GitGitGadgetDec 6, 2025, 20:51 UTC on lore
From: Yee Cheng Chin <ychin.git@gmail.com>

After a diff algorithm has been run, the compaction phase (xdl_change_compact()) shifts and merges change groups to produce a cleaner output. However, this shifting could create a new matched group where both sides now have matching lines. This results in a wrong-looking diff output which contains redundant lines that are the same on both files.

Fix this by detecting this situation, and re-diff the texts on each side to find similar lines, using the fall-back Myer's diff. Only do this for histogram diff as it's the only algorithm where this is relevant. Below contains an example, and more details.

For an example, consider two files below:
    file1:
        A
        A
        A
        A
        A
        A
        A
    file2:
        A
        A
        x
        A
        A
        A
        A

When using Myer's diff, the algorithm finds that only the "x" has been changed, and produces a final diff result (these are line diffs, but using word-diff syntax for ease of presentation):

        A A[-A-]{+x+}A AAA

When using histogram diff, the algorithm first discovers the LCS "A AAA", which it uses as anchor, then produces an intermediate diff:

        {+A Ax+}A AAA[- AAA-].

This is a longer diff than Myer's, but it's still self-consistent. However, the compaction phase attempts to shift the first file's diff group upwards (note that this shift crosses the anchor that histogram had used), leading to the final results for histogram diff:

        [-A AA-]{+A Ax+}A AAA

This is a technically correct patch but looks clearly redundant to a human as the first 3 lines should not be in the diff.

The fix would detect that a shift has caused matching to a new group, and re-diff the "A AA" and "A Ax" parts, which results in "A A" correctly re-marked as unchanged. This creates the now correct histogram diff:

        A A[-A-]{+x+}A AAA

This issue is not applicable to Myer's diff algorithm as it already generates a minimal diff, which means a shift cannot result in a smaller diff output (the default Myer's diff in xdiff is not guaranteed to be minimal for performance reasons, but it typically does a good enough job).

It's also not applicable to patience diff, because it uses only unique lines as anchor for its splits, and falls back to Myer's diff within each split. Shifting requires both ends having the same lines, and therefore cannot cross the unique line boundaries established by the patience algorithm. In contrast histogram diff uses non-unique lines as anchors, and therefore shifting can cross over them.

This issue is rare in a normal repository. Below is a table of repositories (`git log --no-merges -p --histogram -1000`), showing how many times a re-diff was done and how many times it resulted in finding matching lines (therefore addressing this issue) with the fix. In general it is fewer than 1% of diff's that exhibit this offending behavior:

| Repo (1k commits)  | Re-diff | Found matching lines |
|--------------------|---------|----------------------|
| llvm-project       |  45     | 11                   |
| vim                | 110     |  9                   |
| git                |  18     |  2                   |
| WebKit             | 168     |  1                   |
| ripgrep            |  22     |  1                   |
| cpython            |  32     |  0                   |
| vscode             |  13     |  0                   |
Signed-off-by: Yee Cheng Chin <ychin.git@gmail.com>
---
    xdiff: re-diff shifted change groups when using histogram algorithm
    
    This is a somewhat rare issue when using histogram to diff files, as the
    algorithm will generate a diff output that looks redundant and wrong to
    a human. I provided a synthetic example in the commit message, but for
    one from the real world, do the following command in the Git repo:
    
    git show -U0 --diff-algorithm=histogram 2c8999027c -- po/ga.po
    
    
    Scroll to the line "@@ -7239,3 +5831,5 @@", and we can see the following
    diff hunk:
    
    -#: builtin/diff.c
    -msgid "Not a git repository"
    -msgstr "Ní stór git"
    +msgid "cannot come back to cwd"
    +msgstr "ní féidir teacht ar ais chuig cwd"
    +
    +msgid "Not a git repository"
    +msgstr "Ní stór git é"
    
    
    We can see that the "Not a git repository" line is identical on both
    sides, which means it should not have been in the diff results to begin
    with. Under other diff algorithms (or histogram diff with this fix),
    said line is not considered to be part of the diff.
    
    Also, when I was implementing this, an alternative I was considering was
    to add a bespoke linear-time algorithm to remove matching lines on both
    sides. Just calling the fall-back diff seems the easiest and cleanest
    and so I went with that.
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2120%2Fychin%2Fxdiff-fix-compact-remove-redundant-lines-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2120/ychin/xdiff-fix-compact-remove-redundant-lines-v1
Pull-Request: https://github.com/git/git/pull/2120
 t/meson.build                         |   1 +
 t/t4073-diff-shifted-matched-group.sh | 137 ++++++++++++++++++++++++++
 xdiff/xdiffi.c                        |  43 ++++++++
 3 files changed, 181 insertions(+)
 create mode 100755 t/t4073-diff-shifted-matched-group.sh
Show changes to 3 files +181 −0

t/meson.build, t/t4073-diff-shifted-matched-group.sh, xdiff/xdiffi.c

diff --git a/t/meson.build b/t/meson.build
index 7c994d4643..ee233e80da 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -497,6 +497,7 @@ integration_tests = [
   't4070-diff-pairs.sh',
   't4071-diff-minimal.sh',
   't4072-diff-max-depth.sh',
+  't4073-diff-shifted-matched-group.sh',
   't4100-apply-stat.sh',
   't4101-apply-nonl.sh',
   't4102-apply-rename.sh',
diff --git a/t/t4073-diff-shifted-matched-group.sh b/t/t4073-diff-shifted-matched-group.sh
new file mode 100755
index 0000000000..0e915b78a6
--- /dev/null
+++ b/t/t4073-diff-shifted-matched-group.sh
@@ -0,0 +1,137 @@
+#!/bin/sh
+
+test_description='shifted diff groups re-diffing during histogram diff'
+
+. ./test-lib.sh
+
+test_expect_success 'shifted diff group should re-diff to minimize patch' '
+	test_write_lines A x A A A x A A A >file1 &&
+	test_write_lines A x A Z A x A A A >file2 &&
+
+	file1_h=$(git rev-parse --short $(git hash-object file1)) &&
+	file2_h=$(git rev-parse --short $(git hash-object file2)) &&
+
+	cat >expect <<-EOF &&
+	diff --git a/file1 b/file2
+	index $file1_h..$file2_h 100644
+	--- a/file1
+	+++ b/file2
+	@@ -1,7 +1,7 @@
+	 A
+	 x
+	 A
+	-A
+	+Z
+	 A
+	 x
+	 A
+	EOF
+
+	test_expect_code 1 git diff --no-index --histogram file1 file2 >output &&
+	test_cmp expect output
+'
+
+test_expect_success 're-diff should preserve diff flags' '
+	test_write_lines a b c a b c >file1 &&
+	test_write_lines x " b" z a b c >file2 &&
+
+	file1_h=$(git rev-parse --short $(git hash-object file1)) &&
+	file2_h=$(git rev-parse --short $(git hash-object file2)) &&
+
+	cat >expect <<-EOF &&
+	diff --git a/file1 b/file2
+	index $file1_h..$file2_h 100644
+	--- a/file1
+	+++ b/file2
+	@@ -1,6 +1,6 @@
+	-a
+	-b
+	-c
+	+x
+	+ b
+	+z
+	 a
+	 b
+	 c
+	EOF
+
+	test_expect_code 1 git diff --no-index --histogram file1 file2 >output &&
+	test_cmp expect output &&
+
+	cat >expect_iwhite <<-EOF &&
+	diff --git a/file1 b/file2
+	index $file1_h..$file2_h 100644
+	--- a/file1
+	+++ b/file2
+	@@ -1,6 +1,6 @@
+	-a
+	+x
+	  b
+	-c
+	+z
+	 a
+	 b
+	 c
+	EOF
+
+	test_expect_code 1 git diff --no-index --histogram --ignore-all-space file1 file2 >output_iwhite &&
+	test_cmp expect_iwhite output_iwhite
+'
+
+test_expect_success 'shifting on either side should trigger re-diff properly' '
+	test_write_lines a b c a b c a b c >file1 &&
+	test_write_lines a b c a1 a2 a3 b c1 a b c >file2 &&
+
+	file1_h=$(git rev-parse --short $(git hash-object file1)) &&
+	file2_h=$(git rev-parse --short $(git hash-object file2)) &&
+
+	cat >expect1 <<-EOF &&
+	diff --git a/file1 b/file2
+	index $file1_h..$file2_h 100644
+	--- a/file1
+	+++ b/file2
+	@@ -1,9 +1,11 @@
+	 a
+	 b
+	 c
+	-a
+	+a1
+	+a2
+	+a3
+	 b
+	-c
+	+c1
+	 a
+	 b
+	 c
+	EOF
+
+	test_expect_code 1 git diff --no-index --histogram file1 file2 >output1 &&
+	test_cmp expect1 output1 &&
+
+	cat >expect2 <<-EOF &&
+	diff --git a/file2 b/file1
+	index $file2_h..$file1_h 100644
+	--- a/file2
+	+++ b/file1
+	@@ -1,11 +1,9 @@
+	 a
+	 b
+	 c
+	-a1
+	-a2
+	-a3
+	+a
+	 b
+	-c1
+	+c
+	 a
+	 b
+	 c
+	EOF
+
+	test_expect_code 1 git diff --no-index --histogram file2 file1 >output2 &&
+	test_cmp expect2 output2
+'
+
+test_done
diff --git a/xdiff/xdiffi.c b/xdiff/xdiffi.c
index 6f3998ee54..5d9c7b5434 100644
--- a/xdiff/xdiffi.c
+++ b/xdiff/xdiffi.c
@@ -793,6 +793,7 @@ static int group_slide_up(xdfile_t *xdf, struct xdlgroup *g)
  */
 int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) {
 	struct xdlgroup g, go;
+	struct xdlgroup g_orig, go_orig;
 	long earliest_end, end_matching_other;
 	long groupsize;
 
@@ -806,6 +807,9 @@ int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) {
 		if (g.end == g.start)
 			goto next;
 
+		g_orig = g;
+		go_orig = go;
+
 		/*
 		 * Now shift the change up and then down as far as possible in
 		 * each direction. If it bumps into any other changes, merge
@@ -915,6 +919,45 @@ int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) {
 			}
 		}
 
+		/*
+		 * If this has a matching group from the other file, it could
+		 * either be the original match from the diff algorithm, or
+		 * arrived at by shifting and joining groups. When it's the
+		 * latter, it's possible for the two newly joined sides to have
+		 * matching lines. Re-diff the group to mark these matching
+		 * lines as unchanged and remove from the diff output.
+		 *
+		 * Only do this for histogram diff as its LCS algorithm makes
+		 * this scenario possible. In contrast, patience diff finds LCS
+		 * of unique lines that groups cannot be shifted across.
+		 * Myer's diff (standalone or used as fall-back in patience
+		 * diff) already finds minimal edits so it is not possible for
+		 * shifted groups to result in a smaller diff. (Without
+		 * XDF_NEED_MINIMAL, Myer's isn't technically guaranteed to be
+		 * minimal, but it should be so most of the time)
+		 */
+		if (end_matching_other != -1 &&
+				XDF_DIFF_ALG(flags) == XDF_HISTOGRAM_DIFF &&
+				(g.start != g_orig.start ||
+				 g.end != g_orig.end ||
+				 go.start != go_orig.start ||
+				 go.end != go_orig.end)) {
+			xpparam_t xpp;
+			xdfenv_t xe;
+
+			memset(&xpp, 0, sizeof(xpp));
+			xpp.flags = flags & ~XDF_DIFF_ALGORITHM_MASK;
+
+			memcpy(&xe.xdf1, xdf, sizeof(xdfile_t));
+			memcpy(&xe.xdf2, xdfo, sizeof(xdfile_t));
+
+			if (xdl_fall_back_diff(&xe, &xpp,
+					       g.start + 1, g.end - g.start,
+					       go.start + 1, go.end - go.start)) {
+				return -1;
+			}
+		}
+
 	next:
 		/* Move past the just-processed group: */
 		if (group_next(xdf, &g))

base-commit: f0ef5b6d9bcc258e4cbef93839d1b7465d5212b9
-- 
gitgitgadget
Junio C HamanoJan 21, 2026, 20:51 UTC in reply to Yee Cheng Chin via GitGitGadget on lore

Re: [PATCH] xdiff: re-diff shifted change groups when using histogram algorithm

"Yee Cheng Chin via GitGitGadget" <gitgitgadget@gmail.com> writes:
Show 5 quoted lines
> When using Myer's diff, the algorithm finds that only the "x" has been
> changed, and produces a final diff result (these are line diffs, but
> using word-diff syntax for ease of presentation):
>
>         A A[-A-]{+x+}A AAA

And patience gives the same result; as you noted, it uses a unique line as the anchoring point.

Show 14 quoted lines
> When using histogram diff, the algorithm first discovers the LCS "A
> AAA", which it uses as anchor, then produces an intermediate diff:
>
>         {+A Ax+}A AAA[- AAA-].
>
> This is a longer diff than Myer's, but it's still self-consistent.
> However, the compaction phase attempts to shift the first file's diff
> group upwards (note that this shift crosses the anchor that histogram
> had used), leading to the final results for histogram diff:
>
>         [-A AA-]{+A Ax+}A AAA
>
> This is a technically correct patch but looks clearly redundant to a
> human as the first 3 lines should not be in the diff.
So true.
Show 6 quoted lines
> The fix would detect that a shift has caused matching to a new group,
> and re-diff the "A AA" and "A Ax" parts, which results in "A A"
> correctly re-marked as unchanged. This creates the now correct histogram
> diff:
>
>         A A[-A-]{+x+}A AAA
OK.
Show 6 quoted lines
> This issue is rare in a normal repository. Below is a table of
> repositories (`git log --no-merges -p --histogram -1000`), showing how
> many times a re-diff was done and how many times it resulted in finding
> matching lines (therefore addressing this issue) with the fix. In
> general it is fewer than 1% of diff's that exhibit this offending
> behavior:

In other words, without the fix, we'd see 1% or so commits with suboptimal (or "funny looking") diff that will trigger bug reports, which sounds like an unacceptably high failure rate.

Show 27 quoted lines
> @@ -915,6 +919,45 @@ int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) {
>  			}
>  		}
>  
> +		/*
> +		 * If this has a matching group from the other file, it could
> +		 * either be the original match from the diff algorithm, or
> +		 * arrived at by shifting and joining groups. When it's the
> +		 * latter, it's possible for the two newly joined sides to have
> +		 * matching lines. Re-diff the group to mark these matching
> +		 * lines as unchanged and remove from the diff output.
> +		 *
> +		 * Only do this for histogram diff as its LCS algorithm makes
> +		 * this scenario possible. In contrast, patience diff finds LCS
> +		 * of unique lines that groups cannot be shifted across.
> +		 * Myer's diff (standalone or used as fall-back in patience
> +		 * diff) already finds minimal edits so it is not possible for
> +		 * shifted groups to result in a smaller diff. (Without
> +		 * XDF_NEED_MINIMAL, Myer's isn't technically guaranteed to be
> +		 * minimal, but it should be so most of the time)
> +		 */
> +		if (end_matching_other != -1 &&
> +				XDF_DIFF_ALG(flags) == XDF_HISTOGRAM_DIFF &&
> +				(g.start != g_orig.start ||
> +				 g.end != g_orig.end ||
> +				 go.start != go_orig.start ||
> +				 go.end != go_orig.end)) {

So the idea is to remember the original values in g and go (the location of the group in the file and the other file) and if shifting up and down changed any one of the four ends from the original locations, we always take the fall-back route (if we are doing histogram)?

By the way, this appears after the if/else if/ cascade that has:
	if (g.end == earliest_end) {
		... do nothing case (case #1)
	} else if (end_matching_other != -1) {
		... do the slide-up thing (case #2)
	} else if (flags & XDF_INDENT_HEIRISTIC) {
		... do the indent heuristic thing (case #3)
	}

Am I reading the code correctly that, even though this new block appears as if it is a post-clean-up phase that is independent from which one of the three choices are taken in the previous if/elseif cascade, it only is relevant to the second case? I am wondering if it would make it easier to follow if the new code were made into a small helper function that is called from the (case #2) arm of the existing if/else if cascade.

Thanks.
Show 21 quoted lines
> +			xpparam_t xpp;
> +			xdfenv_t xe;
> +
> +			memset(&xpp, 0, sizeof(xpp));
> +			xpp.flags = flags & ~XDF_DIFF_ALGORITHM_MASK;
> +
> +			memcpy(&xe.xdf1, xdf, sizeof(xdfile_t));
> +			memcpy(&xe.xdf2, xdfo, sizeof(xdfile_t));
> +
> +			if (xdl_fall_back_diff(&xe, &xpp,
> +					       g.start + 1, g.end - g.start,
> +					       go.start + 1, go.end - go.start)) {
> +				return -1;
> +			}
> +		}
> +
>  	next:
>  		/* Move past the just-processed group: */
>  		if (group_next(xdf, &g))
>
> base-commit: f0ef5b6d9bcc258e4cbef93839d1b7465d5212b9
Phillip WoodJan 24, 2026, 10:54 UTC in reply to Junio C Hamano on lore

Re: [PATCH] xdiff: re-diff shifted change groups when using histogram algorithm

On 21/01/2026 20:51, Junio C Hamano wrote:
Show 35 quoted lines
> "Yee Cheng Chin via GitGitGadget" <gitgitgadget@gmail.com> writes:
> 
>> @@ -915,6 +919,45 @@ int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) {
>>   			}
>>   		}
>>   
>> +		/*
>> +		 * If this has a matching group from the other file, it could
>> +		 * either be the original match from the diff algorithm, or
>> +		 * arrived at by shifting and joining groups. When it's the
>> +		 * latter, it's possible for the two newly joined sides to have
>> +		 * matching lines. Re-diff the group to mark these matching
>> +		 * lines as unchanged and remove from the diff output.
>> +		 *
>> +		 * Only do this for histogram diff as its LCS algorithm makes
>> +		 * this scenario possible. In contrast, patience diff finds LCS
>> +		 * of unique lines that groups cannot be shifted across.
>> +		 * Myer's diff (standalone or used as fall-back in patience
>> +		 * diff) already finds minimal edits so it is not possible for
>> +		 * shifted groups to result in a smaller diff. (Without
>> +		 * XDF_NEED_MINIMAL, Myer's isn't technically guaranteed to be
>> +		 * minimal, but it should be so most of the time)
>> +		 */
>> +		if (end_matching_other != -1 &&
>> +				XDF_DIFF_ALG(flags) == XDF_HISTOGRAM_DIFF &&
>> +				(g.start != g_orig.start ||
>> +				 g.end != g_orig.end ||
>> +				 go.start != go_orig.start ||
>> +				 go.end != go_orig.end)) {
> 
> So the idea is to remember the original values in g and go (the
> location of the group in the file and the other file) and if
> shifting up and down changed any one of the four ends from the
> original locations, we always take the fall-back route (if we are
> doing histogram)?

I'm a bit confused why we need to check both groups. I think they're supposed to move together (if we move "g" by n context lines we also move "go" by n context lines) so I can't see how we can have

	g.start == g_orig.start && g.end == g_orig.end
when
	go.start != go.orig.start || go.end != go_orig.end
Show 17 quoted lines
> By the way, this appears after the if/else if/ cascade that has:
> 
> 	if (g.end == earliest_end) {
> 		... do nothing case (case #1)
> 	} else if (end_matching_other != -1) {
> 		... do the slide-up thing (case #2)
> 	} else if (flags & XDF_INDENT_HEIRISTIC) {
> 		... do the indent heuristic thing (case #3)
> 	}
> 
> Am I reading the code correctly that, even though this new block
> appears as if it is a post-clean-up phase that is independent from
> which one of the three choices are taken in the previous if/elseif
> cascade, it only is relevant to the second case?  I am wondering if
> it would make it easier to follow if the new code were made into a
> small helper function that is called from the (case #2) arm of the
> existing if/else if cascade.
That's a good point
Show 8 quoted lines
>> +			xpparam_t xpp;
>> +			xdfenv_t xe;
>> +
>> +			memset(&xpp, 0, sizeof(xpp));
>> +			xpp.flags = flags & ~XDF_DIFF_ALGORITHM_MASK;
>> +
>> +			memcpy(&xe.xdf1, xdf, sizeof(xdfile_t));
>> +			memcpy(&xe.xdf2, xdfo, sizeof(xdfile_t));

These would be safer as "xe.xdf1 = *xdf" so we don't have to worry about getting the size correct (sizeof(*xdf) would also be safer but there is no need for memcpy() here).

I also wondered if we need to do a diff or if we can just mark the common prefix and suffix as unchanged but I suspect that wont will work for more complicated examples.

Thanks
Phillip
Show 14 quoted lines
>> +
>> +			if (xdl_fall_back_diff(&xe, &xpp,
>> +					       g.start + 1, g.end - g.start,
>> +					       go.start + 1, go.end - go.start)) {
>> +				return -1;
>> +			}
>> +		}
>> +
>>   	next:
>>   		/* Move past the just-processed group: */
>>   		if (group_next(xdf, &g))
>>
>> base-commit: f0ef5b6d9bcc258e4cbef93839d1b7465d5212b9
> 
Junio C HamanoJan 25, 2026, 17:34 UTC in reply to Phillip Wood on lore

Re: [PATCH] xdiff: re-diff shifted change groups when using histogram algorithm

Phillip Wood <phillip.wood123@gmail.com> writes:
Show 14 quoted lines
> On 21/01/2026 20:51, Junio C Hamano wrote:
>> "Yee Cheng Chin via GitGitGadget" <gitgitgadget@gmail.com> writes:
>> 
>>> @@ -915,6 +919,45 @@ int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) {
>>>   			}
>>>   		}
>>>   
>>> +		/*
>>> +		 * If this has a matching group from the other file, it could
>>> +		 * either be the original match from the diff algorithm, or
>>> +		 * arrived at by shifting and joining groups. When it's the
>>> +		 * latter, it's possible for the two newly joined sides to have
>>> +		 * matching lines. Re-diff the group to mark these matching
>>> +		 * lines as unchanged and remove from the diff output.

Also, after reading the first paragraph of the big comment again, it makes me wonder if it is saying the same thing as "When histogram is being used, we shouldn't bother shifting up and down to join groups, as the result will always worse than the fallback", but is it that bad?

Show 22 quoted lines
>>> +		if (end_matching_other != -1 &&
>>> +				XDF_DIFF_ALG(flags) == XDF_HISTOGRAM_DIFF &&
>>> +				(g.start != g_orig.start ||
>>> +				 g.end != g_orig.end ||
>>> +				 go.start != go_orig.start ||
>>> +				 go.end != go_orig.end)) {
>> 
>> So the idea is to remember the original values in g and go (the
>> location of the group in the file and the other file) and if
>> shifting up and down changed any one of the four ends from the
>> original locations, we always take the fall-back route (if we are
>> doing histogram)?
>
> I'm a bit confused why we need to check both groups. I think they're 
> supposed to move together (if we move "g" by n context lines we also 
> move "go" by n context lines) so I can't see how we can have
>
> 	g.start == g_orig.start && g.end == g_orig.end
>
> when
>
> 	go.start != go.orig.start || go.end != go_orig.end
Interesting.
Show 32 quoted lines
>> By the way, this appears after the if/else if/ cascade that has:
>> 
>> 	if (g.end == earliest_end) {
>> 		... do nothing case (case #1)
>> 	} else if (end_matching_other != -1) {
>> 		... do the slide-up thing (case #2)
>> 	} else if (flags & XDF_INDENT_HEIRISTIC) {
>> 		... do the indent heuristic thing (case #3)
>> 	}
>> 
>> Am I reading the code correctly that, even though this new block
>> appears as if it is a post-clean-up phase that is independent from
>> which one of the three choices are taken in the previous if/elseif
>> cascade, it only is relevant to the second case?  I am wondering if
>> it would make it easier to follow if the new code were made into a
>> small helper function that is called from the (case #2) arm of the
>> existing if/else if cascade.
>
> That's a good point
>
>>> +			xpparam_t xpp;
>>> +			xdfenv_t xe;
>>> +
>>> +			memset(&xpp, 0, sizeof(xpp));
>>> +			xpp.flags = flags & ~XDF_DIFF_ALGORITHM_MASK;
>>> +
>>> +			memcpy(&xe.xdf1, xdf, sizeof(xdfile_t));
>>> +			memcpy(&xe.xdf2, xdfo, sizeof(xdfile_t));
>
> These would be safer as "xe.xdf1 = *xdf" so we don't have to worry about 
> getting the size correct (sizeof(*xdf) would also be safer but there is 
> no need for memcpy() here).
Very good readability enhancement suggestion.
Thanks.
Phillip WoodJan 26, 2026, 09:37 UTC in reply to Junio C Hamano on lore

Re: [PATCH] xdiff: re-diff shifted change groups when using histogram algorithm

On 25/01/2026 17:34, Junio C Hamano wrote:
Show 22 quoted lines
> Phillip Wood <phillip.wood123@gmail.com> writes:
> 
>> On 21/01/2026 20:51, Junio C Hamano wrote:
>>> "Yee Cheng Chin via GitGitGadget" <gitgitgadget@gmail.com> writes:
>>>
>>>> @@ -915,6 +919,45 @@ int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) {
>>>>    			}
>>>>    		}
>>>>    
>>>> +		/*
>>>> +		 * If this has a matching group from the other file, it could
>>>> +		 * either be the original match from the diff algorithm, or
>>>> +		 * arrived at by shifting and joining groups. When it's the
>>>> +		 * latter, it's possible for the two newly joined sides to have
>>>> +		 * matching lines. Re-diff the group to mark these matching
>>>> +		 * lines as unchanged and remove from the diff output.
> 
> Also, after reading the first paragraph of the big comment again, it
> makes me wonder if it is saying the same thing as "When histogram is
> being used, we shouldn't bother shifting up and down to join groups,
> as the result will always worse than the fallback", but is it that
> bad?

Looking at the example in the commit message the result of shifting up and down and then calling the fallback is better than either the unshifted diff or shifting without the fallback, so I don't think just disabling shifting improves things. It would also stop us coalescing changed lines, for example

-A             A
  A     ->     -A
-B            -B

The indent heuristic seems to assume that we've shifted down as far as possible before trying it so that would probably get messed up as well. To me the problem is that the histogram diff does not always generate particularly good diffs (maybe I'm biased - whenever I've tried switching the default to "histogram" I've always switched back "patience" fairly quickly after being presented with a diff that I found hard to comprehend)

Thanks
Phillip
Show 61 quoted lines
>>>> +		if (end_matching_other != -1 &&
>>>> +				XDF_DIFF_ALG(flags) == XDF_HISTOGRAM_DIFF &&
>>>> +				(g.start != g_orig.start ||
>>>> +				 g.end != g_orig.end ||
>>>> +				 go.start != go_orig.start ||
>>>> +				 go.end != go_orig.end)) {
>>>
>>> So the idea is to remember the original values in g and go (the
>>> location of the group in the file and the other file) and if
>>> shifting up and down changed any one of the four ends from the
>>> original locations, we always take the fall-back route (if we are
>>> doing histogram)?
>>
>> I'm a bit confused why we need to check both groups. I think they're
>> supposed to move together (if we move "g" by n context lines we also
>> move "go" by n context lines) so I can't see how we can have
>>
>> 	g.start == g_orig.start && g.end == g_orig.end
>>
>> when
>>
>> 	go.start != go.orig.start || go.end != go_orig.end
> 
> Interesting.
> 
>>> By the way, this appears after the if/else if/ cascade that has:
>>>
>>> 	if (g.end == earliest_end) {
>>> 		... do nothing case (case #1)
>>> 	} else if (end_matching_other != -1) {
>>> 		... do the slide-up thing (case #2)
>>> 	} else if (flags & XDF_INDENT_HEIRISTIC) {
>>> 		... do the indent heuristic thing (case #3)
>>> 	}
>>>
>>> Am I reading the code correctly that, even though this new block
>>> appears as if it is a post-clean-up phase that is independent from
>>> which one of the three choices are taken in the previous if/elseif
>>> cascade, it only is relevant to the second case?  I am wondering if
>>> it would make it easier to follow if the new code were made into a
>>> small helper function that is called from the (case #2) arm of the
>>> existing if/else if cascade.
>>
>> That's a good point
>>
>>>> +			xpparam_t xpp;
>>>> +			xdfenv_t xe;
>>>> +
>>>> +			memset(&xpp, 0, sizeof(xpp));
>>>> +			xpp.flags = flags & ~XDF_DIFF_ALGORITHM_MASK;
>>>> +
>>>> +			memcpy(&xe.xdf1, xdf, sizeof(xdfile_t));
>>>> +			memcpy(&xe.xdf2, xdfo, sizeof(xdfile_t));
>>
>> These would be safer as "xe.xdf1 = *xdf" so we don't have to worry about
>> getting the size correct (sizeof(*xdf) would also be safer but there is
>> no need for memcpy() here).
> 
> Very good readability enhancement suggestion.
> 
> Thanks.
Junio C HamanoJan 26, 2026, 17:32 UTC in reply to Phillip Wood on lore

Re: [PATCH] xdiff: re-diff shifted change groups when using histogram algorithm

Phillip Wood <phillip.wood123@gmail.com> writes:
Show 36 quoted lines
> On 25/01/2026 17:34, Junio C Hamano wrote:
>> Phillip Wood <phillip.wood123@gmail.com> writes:
>> 
>>> On 21/01/2026 20:51, Junio C Hamano wrote:
>>>> "Yee Cheng Chin via GitGitGadget" <gitgitgadget@gmail.com> writes:
>>>>
>>>>> @@ -915,6 +919,45 @@ int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) {
>>>>>    			}
>>>>>    		}
>>>>>    
>>>>> +		/*
>>>>> +		 * If this has a matching group from the other file, it could
>>>>> +		 * either be the original match from the diff algorithm, or
>>>>> +		 * arrived at by shifting and joining groups. When it's the
>>>>> +		 * latter, it's possible for the two newly joined sides to have
>>>>> +		 * matching lines. Re-diff the group to mark these matching
>>>>> +		 * lines as unchanged and remove from the diff output.
>> 
>> Also, after reading the first paragraph of the big comment again, it
>> makes me wonder if it is saying the same thing as "When histogram is
>> being used, we shouldn't bother shifting up and down to join groups,
>> as the result will always worse than the fallback", but is it that
>> bad?
>
> Looking at the example in the commit message the result of shifting up 
> and down and then calling the fallback is better than either the 
> unshifted diff or shifting without the fallback, so I don't think just 
> disabling shifting improves things. It would also stop us coalescing 
> changed lines, for example
>
> -A             A
>   A     ->     -A
> -B            -B
>
> The indent heuristic seems to assume that we've shifted down as far as 
> possible before trying it so that would probably get messed up as well. 
I see.  Thanks for a good explanation.
Show 9 quoted lines
> To me the problem is that the histogram diff does not always generate 
> particularly good diffs (maybe I'm biased - whenever I've tried 
> switching the default to "histogram" I've always switched back 
> "patience" fairly quickly after being presented with a diff that I found 
> hard to comprehend)
>
> Thanks
>
> Phillip
Thanks.
Yee Cheng ChinJan 29, 2026, 16:53 UTC in reply to Junio C Hamano on lore

Re: [PATCH] xdiff: re-diff shifted change groups when using histogram algorithm

Thanks for the review and sorry for being a little late in replying. Aggregating all my inline replies in one email if that's ok.

On Wed, Jan 21, 2026 at 12:51 PM Junio C Hamano <gitster@pobox.com> wrote:
Show 23 quoted lines
> So the idea is to remember the original values in g and go (the
> location of the group in the file and the other file) and if
> shifting up and down changed any one of the four ends from the
> original locations, we always take the fall-back route (if we are
> doing histogram)?
>
> By the way, this appears after the if/else if/ cascade that has:
>
>         if (g.end == earliest_end) {
>                 ... do nothing case (case #1)
>         } else if (end_matching_other != -1) {
>                 ... do the slide-up thing (case #2)
>         } else if (flags & XDF_INDENT_HEIRISTIC) {
>                 ... do the indent heuristic thing (case #3)
>         }
>
> Am I reading the code correctly that, even though this new block
> appears as if it is a post-clean-up phase that is independent from
> which one of the three choices are taken in the previous if/elseif
> cascade, it only is relevant to the second case?  I am wondering if
> it would make it easier to follow if the new code were made into a
> small helper function that is called from the (case #2) arm of the
> existing if/else if cascade.

That's correct. This condition happens only in the 2nd case. The problematic scenario here only happens when the opposite side is non-empty. If the opposite is empty (case #3, where we run the indent heuristic algorithm), there's simply no need to re-diff anything because diff'ing against an empty hunk is pointless.

You made a good point about placing it in the if block itself. The existing code was a little confusing and took me re-reading the code before I remember the condition. I'll fix it in v2.

On Sat, Jan 24, 2026 at 2:54 AM Phillip Wood <phillip.wood123@gmail.com> wrote:
Show 10 quoted lines
> I'm a bit confused why we need to check both groups. I think they're
> supposed to move together (if we move "g" by n context lines we also
> move "go" by n context lines) so I can't see how we can have
>
>         g.start == g_orig.start && g.end == g_orig.end
>
> when
>
>         go.start != go.orig.start || go.end != go_orig.end
>

You are right. It was an over-specification. Looking through the code we should be able to just use "g" and there is no need to test for "g_orig". Will fix in v2.

Show 12 quoted lines
> >> +                    xpparam_t xpp;
> >> +                    xdfenv_t xe;
> >> +
> >> +                    memset(&xpp, 0, sizeof(xpp));
> >> +                    xpp.flags = flags & ~XDF_DIFF_ALGORITHM_MASK;
> >> +
> >> +                    memcpy(&xe.xdf1, xdf, sizeof(xdfile_t));
> >> +                    memcpy(&xe.xdf2, xdfo, sizeof(xdfile_t));
>
> These would be safer as "xe.xdf1 = *xdf" so we don't have to worry about
> getting the size correct (sizeof(*xdf) would also be safer but there is
> no need for memcpy() here).
Will fix in v2.
> I also wondered if we need to do a diff or if we can just mark the
> common prefix and suffix as unchanged but I suspect that wont will work
> for more complicated examples.

Common prefix/suffix would not work for more complicated examples. Here's an example (imagine each character to be its own line):

File 1: A AAyz AAA File 2: A xAA AAA

The current Git histogram diff generates the following: A [-AAyz -]{+xAA +}AAA

After the fix, we have: A {+x+}AA[-yz-] AA

Note that there is no common prefix here, and we need a real diff algorithm if we want to solve this issue in a generic fashion. As I mentioned in the cover letter, I thought about implementing a "bespoke linear-time algorithm" but decided against it. What I meant was we could implement a simple diff algorithm that finds the common lines in both hunks that would run faster than Myer's, but isn't guaranteed to be a optimal minimal diff. I decided that it is unnecessary to overcomplicate things given that we can just call the fallback diff.

On Mon, Jan 26, 2026 at 1:37 AM Phillip Wood <phillip.wood123@gmail.com> wrote:
Show 18 quoted lines
>
> On 25/01/2026 17:34, Junio C Hamano wrote:
> > Also, after reading the first paragraph of the big comment again, it
> > makes me wonder if it is saying the same thing as "When histogram is
> > being used, we shouldn't bother shifting up and down to join groups,
> > as the result will always worse than the fallback", but is it that
> > bad?
>
> Looking at the example in the commit message the result of shifting up
> and down and then calling the fallback is better than either the
> unshifted diff or shifting without the fallback, so I don't think just
> disabling shifting improves things. It would also stop us coalescing
> changed lines, for example
>
> -A             A
>   A     ->     -A
> -B            -B
>

I agree with you, but I think it is actually a nuanced decision. The histogram diff algorithm explicitly chose the specific alignment/anchor points to align both files due to the frequency of the lines. When we do the sliding / compaction step, we are essentially ignoring and overriding the algorithmic decision made by histogram, for the sake of other metrics that we value (compaction values fewer diff hunks, and indent heuristics values aligning by semantics approximated by indentation). I think those metrics do help which is why we added them, but there's a bit of design tension between the underlying algorithm and the cleanup step.

Show 5 quoted lines
> To me the problem is that the histogram diff does not always generate
> particularly good diffs (maybe I'm biased - whenever I've tried
> switching the default to "histogram" I've always switched back
> "patience" fairly quickly after being presented with a diff that I found
> hard to comprehend)

FWIW I personally feel that way as well. I think the documentation and narrative that histogram diff is a "more advanced/extended version" of patience diff is sometimes problematic, as both algorithms are fairly different and have their own weaknesses. The Longest Common Subsequence (LCS) used for alignment in patience diff is global for the file and allows gaps, whereas the LCS in histogram diff requires consecutive lines. This means even if the diff has unique lines across both files the diff results could be quite different between histogram and patience. This consecutive requirement for a subsequence is why histogram diff runs faster than patience diff most of the time, but it does mean the patience algorithm is better at discovering a global "spine" across a file.

Junio C HamanoJan 29, 2026, 20:58 UTC in reply to Yee Cheng Chin on lore

Re: [PATCH] xdiff: re-diff shifted change groups when using histogram algorithm

Yee Cheng Chin <ychin.git@gmail.com> writes:
Show 33 quoted lines
> Thanks for the review and sorry for being a little late in replying.
> Aggregating all my inline replies in one email if that's ok.
>
> On Wed, Jan 21, 2026 at 12:51 PM Junio C Hamano <gitster@pobox.com> wrote:
>> So the idea is to remember the original values in g and go (the
>> location of the group in the file and the other file) and if
>> shifting up and down changed any one of the four ends from the
>> original locations, we always take the fall-back route (if we are
>> doing histogram)?
>>
>> By the way, this appears after the if/else if/ cascade that has:
>>
>>         if (g.end == earliest_end) {
>>                 ... do nothing case (case #1)
>>         } else if (end_matching_other != -1) {
>>                 ... do the slide-up thing (case #2)
>>         } else if (flags & XDF_INDENT_HEIRISTIC) {
>>                 ... do the indent heuristic thing (case #3)
>>         }
>>
>> Am I reading the code correctly that, even though this new block
>> appears as if it is a post-clean-up phase that is independent from
>> which one of the three choices are taken in the previous if/elseif
>> cascade, it only is relevant to the second case?  I am wondering if
>> it would make it easier to follow if the new code were made into a
>> small helper function that is called from the (case #2) arm of the
>> existing if/else if cascade.
>
> That's correct. This condition happens only in the 2nd case. The
> problematic scenario here only happens when the opposite side is
> non-empty. If the opposite is empty (case #3, where we run the indent
> heuristic algorithm), there's simply no need to re-diff anything
> because diff'ing against an empty hunk is pointless.

OK. In the version posted, it appeard that it is possible, after not doing the slide-up thing but using indent heuristic thing, to fall into this compensation codepath because the new code was placed after the above if-else-if cascade as if it is an independent clean-up phase. Encapsulating that new code in a helper function and calling it at the end of "do the slide-up thing" block will make the intent clearer.

Thanks.
Yee Cheng ChinJan 30, 2026, 01:58 UTC in reply to Junio C Hamano on lore

Re: [PATCH] xdiff: re-diff shifted change groups when using histogram algorithm

On Thu, Jan 29, 2026 at 12:58 PM Junio C Hamano <gitster@pobox.com> wrote:
Show 35 quoted lines
>
> Yee Cheng Chin <ychin.git@gmail.com> writes:
> >
> > On Wed, Jan 21, 2026 at 12:51 PM Junio C Hamano <gitster@pobox.com> wrote:
> >> By the way, this appears after the if/else if/ cascade that has:
> >>
> >>         if (g.end == earliest_end) {
> >>                 ... do nothing case (case #1)
> >>         } else if (end_matching_other != -1) {
> >>                 ... do the slide-up thing (case #2)
> >>         } else if (flags & XDF_INDENT_HEIRISTIC) {
> >>                 ... do the indent heuristic thing (case #3)
> >>         }
> >>
> >> Am I reading the code correctly that, even though this new block
> >> appears as if it is a post-clean-up phase that is independent from
> >> which one of the three choices are taken in the previous if/elseif
> >> cascade, it only is relevant to the second case?  I am wondering if
> >> it would make it easier to follow if the new code were made into a
> >> small helper function that is called from the (case #2) arm of the
> >> existing if/else if cascade.
> >
> > That's correct. This condition happens only in the 2nd case. The
> > problematic scenario here only happens when the opposite side is
> > non-empty. If the opposite is empty (case #3, where we run the indent
> > heuristic algorithm), there's simply no need to re-diff anything
> > because diff'ing against an empty hunk is pointless.
>
> OK.  In the version posted, it appeard that it is possible, after
> not doing the slide-up thing but using indent heuristic thing, to
> fall into this compensation codepath because the new code was placed
> after the above if-else-if cascade as if it is an independent
> clean-up phase.  Encapsulating that new code in a helper function
> and calling it at the end of "do the slide-up thing" block will make
> the intent clearer.

Sorry, I actually misspoke. I forgot that re-diff is actually needed in both case #1 and #2. Note that even in #1, it's possible for `end_matching_other != -1` to be true. In case #3, it only cannot happen because `end_matching_other` has to be -1 by then (meaning that this diff hunk only has content on this side and is empty on the other).

Case #1 happens when no *remaining* shifting was necessary, but note that this happens after the do/while loop above, where previous loops could have shifted and compacted the diff blocks already. Case #2 just means there's some remaining clean up work to be done.

Just for a concrete test case that will illustrate this in case someone is running the code and want a demonstration:

File 1: AXB*

File 2: CD*XE*

The first "*" is used as the histogram alignment anchor, which will be shifted resulting in a compaction, and therefore needs to trigger a re-diff. The correct output is as follows (which will only happen if we also run the re-diff in case #1):

{-A-}[+CD*+]X{-B-}[+E+]*

Otherwise we will get the wrong output (note how the "X" is erroneuously included on both sides):

{-AXB-}[+CD*XE+]*

Because of that, I'm leaning on keeping the current code structure, because it *is* indeed a cleanup step to be run after the previous one. I could still refactor it into a separate function and put it into the the case #1/#2 if blocks if you think that's cleaner.

I will also add the above to the test case in v2.
Junio C HamanoJan 30, 2026, 05:43 UTC in reply to Yee Cheng Chin on lore

Re: [PATCH] xdiff: re-diff shifted change groups when using histogram algorithm

Yee Cheng Chin <ychin.git@gmail.com> writes:
Show 6 quoted lines
> Because of that, I'm leaning on keeping the current code structure,
> because it *is* indeed a cleanup step to be run after the previous
> one. I could still refactor it into a separate function and put it
> into the the case #1/#2 if blocks if you think that's cleaner.
>
> I will also add the above to the test case in v2.

As long as the resulting code is explained (perhaps in the comment and/or with the code structure) well enough so that when read by somebody else in two months, it won't have to invite the same question as I asked in this thread, it would be OK. I do not know offhand if a comment with the current code structure is good enough, or calling the same helper function from two out of three arms of if/else-if cascade would make it even clearer.

I agree that the case you gave is tricky enough that it would be a good idea to add it as a test.

Thanks.
Phillip WoodJan 30, 2026, 16:06 UTC in reply to Yee Cheng Chin on lore

Re: [PATCH] xdiff: re-diff shifted change groups when using histogram algorithm

On 30/01/2026 01:58, Yee Cheng Chin wrote:
Show 5 quoted lines
> 
> Case #1 happens when no *remaining* shifting was necessary, but note
> that this happens after the do/while loop above, where previous loops
> could have shifted and compacted the diff blocks already. Case #2 just
> means there's some remaining clean up work to be done.

That's a good point - as well as commenting the new code, it would be helpful to update the comment in case #1 to make it clear that we don't need to shift back up to align with a matching block, not there there was no shift possible. I agree with Junio that it would be useful to add the example below as a test

Thanks
Phillip
Show 28 quoted lines
> Just for a concrete test case that will illustrate this in case
> someone is running the code and want a demonstration:
> 
> File 1:
> AXB*
> 
> File 2:
> CD*XE*
> 
> The first "*" is used as the histogram alignment anchor, which will be
> shifted resulting in a compaction, and therefore needs to trigger a
> re-diff. The correct output is as follows (which will only happen if
> we also run the re-diff in case #1):
> 
> {-A-}[+CD*+]X{-B-}[+E+]*
> 
> Otherwise we will get the wrong output (note how the "X" is
> erroneuously included on both sides):
> 
> {-AXB-}[+CD*XE+]*
> 
> Because of that, I'm leaning on keeping the current code structure,
> because it *is* indeed a cleanup step to be run after the previous
> one. I could still refactor it into a separate function and put it
> into the the case #1/#2 if blocks if you think that's cleaner.
> 
> I will also add the above to the test case in v2.
> 
Junio C HamanoFeb 20, 2026, 23:07 UTC in reply to Yee Cheng Chin on lore

Re: [PATCH] xdiff: re-diff shifted change groups when using histogram algorithm

Yee Cheng Chin <ychin.git@gmail.com> writes:
Show 9 quoted lines
> ...
> {-AXB-}[+CD*XE+]*
>
> Because of that, I'm leaning on keeping the current code structure,
> because it *is* indeed a cleanup step to be run after the previous
> one. I could still refactor it into a separate function and put it
> into the the case #1/#2 if blocks if you think that's cleaner.
>
> I will also add the above to the test case in v2.

OK, it has been a few weeks since we had this message. Will we see an update sometime soon? No rush, but just pinging.

Thanks.
Yee Cheng ChinFeb 21, 2026, 09:56 UTC in reply to Junio C Hamano on lore

Re: [PATCH] xdiff: re-diff shifted change groups when using histogram algorithm

Hi, yes, I'm still on it. Sorry for the delay. I will push an update out this weekend.

On Fri, Feb 20, 2026 at 3:07 PM Junio C Hamano <gitster@pobox.com> wrote:
Show 18 quoted lines
>
> Yee Cheng Chin <ychin.git@gmail.com> writes:
>
> > ...
> > {-AXB-}[+CD*XE+]*
> >
> > Because of that, I'm leaning on keeping the current code structure,
> > because it *is* indeed a cleanup step to be run after the previous
> > one. I could still refactor it into a separate function and put it
> > into the the case #1/#2 if blocks if you think that's cleaner.
> >
> > I will also add the above to the test case in v2.
>
> OK, it has been a few weeks since we had this message.  Will we see
> an update sometime soon?  No rush, but just pinging.
>
> Thanks.
>
Yee Cheng Chin via GitGitGadgetMar 2, 2026, 14:54 UTC in reply to Yee Cheng Chin via GitGitGadget on lore

[PATCH v2] xdiff: re-diff shifted change groups when using histogram algorithm

From: Yee Cheng Chin <ychin.git@gmail.com>

After a diff algorithm has been run, the compaction phase (xdl_change_compact()) shifts and merges change groups to produce a cleaner output. However, this shifting could create a new matched group where both sides now have matching lines. This results in a wrong-looking diff output which contains redundant lines that are the same on both files.

Fix this by detecting this situation, and re-diff the texts on each side to find similar lines, using the fall-back Myer's diff. Only do this for histogram diff as it's the only algorithm where this is relevant. Below contains an example, and more details.

For an example, consider two files below:
    file1:
        A
        A
        A
        A
        A
        A
        A
    file2:
        A
        A
        x
        A
        A
        A
        A

When using Myer's diff, the algorithm finds that only the "x" has been changed, and produces a final diff result (these are line diffs, but using word-diff syntax for ease of presentation):

        A A[-A-]{+x+}A AAA

When using histogram diff, the algorithm first discovers the LCS "A AAA", which it uses as anchor, then produces an intermediate diff:

        {+A Ax+}A AAA[- AAA-].

This is a longer diff than Myer's, but it's still self-consistent. However, the compaction phase attempts to shift the first file's diff group upwards (note that this shift crosses the anchor that histogram had used), leading to the final results for histogram diff:

        [-A AA-]{+A Ax+}A AAA

This is a technically correct patch but looks clearly redundant to a human as the first 3 lines should not be in the diff.

The fix would detect that a shift has caused matching to a new group, and re-diff the "A AA" and "A Ax" parts, which results in "A A" correctly re-marked as unchanged. This creates the now correct histogram diff:

        A A[-A-]{+x+}A AAA

This issue is not applicable to Myer's diff algorithm as it already generates a minimal diff, which means a shift cannot result in a smaller diff output (the default Myer's diff in xdiff is not guaranteed to be minimal for performance reasons, but it typically does a good enough job).

It's also not applicable to patience diff, because it uses only unique lines as anchor for its splits, and falls back to Myer's diff within each split. Shifting requires both ends having the same lines, and therefore cannot cross the unique line boundaries established by the patience algorithm. In contrast histogram diff uses non-unique lines as anchors, and therefore shifting can cross over them.

This issue is rare in a normal repository. Below is a table of repositories (`git log --no-merges -p --histogram -1000`), showing how many times a re-diff was done and how many times it resulted in finding matching lines (therefore addressing this issue) with the fix. In general it is fewer than 1% of diff's that exhibit this offending behavior:

| Repo (1k commits)  | Re-diff | Found matching lines |
|--------------------|---------|----------------------|
| llvm-project       |  45     | 11                   |
| vim                | 110     |  9                   |
| git                |  18     |  2                   |
| WebKit             | 168     |  1                   |
| ripgrep            |  22     |  1                   |
| cpython            |  32     |  0                   |
| vscode             |  13     |  0                   |
Signed-off-by: Yee Cheng Chin <ychin.git@gmail.com>
---
    xdiff: re-diff shifted change groups when using histogram algorithm
    
    Changes since v1:
    
     * Fix the entry condition to be easier to understand by checking for
       go.end!=go.start, which makes it clear that this is just a triviality
       test (if one side is empty there is no point in diff'ing anything)
     * Remove go_orig, which was redundant as it was tracking the same thing
       as g_orig.
     * Use assignment instead of memcpy()
     * Clean up comments
     * Per discussed, add test to show that we need to re-diff even if we
       entere the first condition "no shifting was possible".
    
    This is a somewhat rare issue when using histogram to diff files, as the
    algorithm will generate a diff output that looks redundant and wrong to
    a human. I provided a synthetic example in the commit message, but for
    one from the real world, do the following command in the Git repo:
    
    git show -U0 --diff-algorithm=histogram 2c8999027c -- po/ga.po
    
    
    Scroll to the line "@@ -7239,3 +5831,5 @@", and we can see the following
    diff hunk:
    
    -#: builtin/diff.c
    -msgid "Not a git repository"
    -msgstr "Ní stór git"
    +msgid "cannot come back to cwd"
    +msgstr "ní féidir teacht ar ais chuig cwd"
    +
    +msgid "Not a git repository"
    +msgstr "Ní stór git é"
    
    
    We can see that the "Not a git repository" line is identical on both
    sides, which means it should not have been in the diff results to begin
    with. Under other diff algorithms (or histogram diff with this fix),
    said line is not considered to be part of the diff.
    
    Also, when I was implementing this, an alternative I was considering was
    to add a bespoke linear-time algorithm to remove matching lines on both
    sides. Just calling the fall-back diff seems the easiest and cleanest
    and so I went with that.
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2120%2Fychin%2Fxdiff-fix-compact-remove-redundant-lines-v2
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2120/ychin/xdiff-fix-compact-remove-redundant-lines-v2
Pull-Request: https://github.com/git/git/pull/2120
Range-diff vs v1:
 1:  34a370e59e ! 1:  bd63c6866b xdiff: re-diff shifted change groups when using histogram algorithm
     @@ Commit message
      
       ## t/meson.build ##
      @@ t/meson.build: integration_tests = [
     -   't4070-diff-pairs.sh',
         't4071-diff-minimal.sh',
         't4072-diff-max-depth.sh',
     -+  't4073-diff-shifted-matched-group.sh',
     +   't4073-diff-stat-name-width.sh',
     ++  't4074-diff-shifted-matched-group.sh',
         't4100-apply-stat.sh',
         't4101-apply-nonl.sh',
         't4102-apply-rename.sh',
      
     - ## t/t4073-diff-shifted-matched-group.sh (new) ##
     + ## t/t4074-diff-shifted-matched-group.sh (new) ##
      @@
      +#!/bin/sh
      +
     @@ t/t4073-diff-shifted-matched-group.sh (new)
      +
      +. ./test-lib.sh
      +
     -+test_expect_success 'shifted diff group should re-diff to minimize patch' '
     ++test_expect_success 'shifted/merged diff group should re-diff to minimize patch' '
      +	test_write_lines A x A A A x A A A >file1 &&
      +	test_write_lines A x A Z A x A A A >file2 &&
      +
     @@ t/t4073-diff-shifted-matched-group.sh (new)
      +	test_cmp expect output
      +'
      +
     ++test_expect_success 'merged diff group with no shift' '
     ++	test_write_lines A Z B x >file1 &&
     ++	test_write_lines C D x Z E x >file2 &&
     ++
     ++	file1_h=$(git rev-parse --short $(git hash-object file1)) &&
     ++	file2_h=$(git rev-parse --short $(git hash-object file2)) &&
     ++
     ++	cat >expect <<-EOF &&
     ++	diff --git a/file1 b/file2
     ++	index $file1_h..$file2_h 100644
     ++	--- a/file1
     ++	+++ b/file2
     ++	@@ -1,4 +1,6 @@
     ++	-A
     ++	+C
     ++	+D
     ++	+x
     ++	 Z
     ++	-B
     ++	+E
     ++	 x
     ++	EOF
     ++
     ++	test_expect_code 1 git diff --no-index --histogram file1 file2 >output &&
     ++	test_cmp expect output
     ++'
     ++
      +test_expect_success 're-diff should preserve diff flags' '
      +	test_write_lines a b c a b c >file1 &&
      +	test_write_lines x " b" z a b c >file2 &&
     @@ xdiff/xdiffi.c: static int group_slide_up(xdfile_t *xdf, struct xdlgroup *g)
        */
       int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) {
       	struct xdlgroup g, go;
     -+	struct xdlgroup g_orig, go_orig;
     ++	struct xdlgroup g_orig;
       	long earliest_end, end_matching_other;
       	long groupsize;
       
     @@ xdiff/xdiffi.c: int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags
       			goto next;
       
      +		g_orig = g;
     -+		go_orig = go;
      +
       		/*
       		 * Now shift the change up and then down as far as possible in
       		 * each direction. If it bumps into any other changes, merge
     +-		 * them.
     ++		 * them and restart the process.
     + 		 */
     + 		do {
     + 			groupsize = g.end - g.start;
     +@@ xdiff/xdiffi.c: int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) {
     + 			/*
     + 			 * Move the possibly merged group of changes back to
     + 			 * line up with the last group of changes from the
     +-			 * other file that it can align with.
     ++			 * other file that it can align with. This avoids breaking
     ++			 * a single change into a separate addition/deletion.
     + 			 */
     + 			while (go.end == go.start) {
     + 				if (group_slide_up(xdf, &g))
      @@ xdiff/xdiffi.c: int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) {
       			}
       		}
       
      +		/*
     -+		 * If this has a matching group from the other file, it could
     -+		 * either be the original match from the diff algorithm, or
     -+		 * arrived at by shifting and joining groups. When it's the
     -+		 * latter, it's possible for the two newly joined sides to have
     -+		 * matching lines. Re-diff the group to mark these matching
     -+		 * lines as unchanged and remove from the diff output.
     ++		 * If we merged change groups during shifting, the new
     ++		 * combined group could now have matching lines in both files,
     ++		 * even if the original separate groups did not. Re-diff the
     ++		 * new group to find these matching lines to mark them as
     ++		 * unchanged.
     ++		 *
     ++		 * Only do this if the corresponding group in the other file is
     ++		 * non-empty, as it's trivial otherwise.
      +		 *
     -+		 * Only do this for histogram diff as its LCS algorithm makes
     -+		 * this scenario possible. In contrast, patience diff finds LCS
     ++		 * Only do this for histogram diff as its LCS algorithm allows
     ++		 * for this scenario. In contrast, patience diff finds LCS
      +		 * of unique lines that groups cannot be shifted across.
      +		 * Myer's diff (standalone or used as fall-back in patience
      +		 * diff) already finds minimal edits so it is not possible for
     @@ xdiff/xdiffi.c: int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags
      +		 * XDF_NEED_MINIMAL, Myer's isn't technically guaranteed to be
      +		 * minimal, but it should be so most of the time)
      +		 */
     -+		if (end_matching_other != -1 &&
     ++		if (go.end != go.start &&
      +				XDF_DIFF_ALG(flags) == XDF_HISTOGRAM_DIFF &&
      +				(g.start != g_orig.start ||
     -+				 g.end != g_orig.end ||
     -+				 go.start != go_orig.start ||
     -+				 go.end != go_orig.end)) {
     ++				 g.end != g_orig.end)) {
      +			xpparam_t xpp;
      +			xdfenv_t xe;
      +
      +			memset(&xpp, 0, sizeof(xpp));
      +			xpp.flags = flags & ~XDF_DIFF_ALGORITHM_MASK;
      +
     -+			memcpy(&xe.xdf1, xdf, sizeof(xdfile_t));
     -+			memcpy(&xe.xdf2, xdfo, sizeof(xdfile_t));
     ++			xe.xdf1 = *xdf;
     ++			xe.xdf2 = *xdfo;
      +
      +			if (xdl_fall_back_diff(&xe, &xpp,
      +					       g.start + 1, g.end - g.start,
 t/meson.build                         |   1 +
 t/t4074-diff-shifted-matched-group.sh | 164 ++++++++++++++++++++++++++
 xdiff/xdiffi.c                        |  47 +++++++-
 3 files changed, 210 insertions(+), 2 deletions(-)
 create mode 100755 t/t4074-diff-shifted-matched-group.sh
Show changes to 3 files +210 −2

t/meson.build, t/t4074-diff-shifted-matched-group.sh, xdiff/xdiffi.c

diff --git a/t/meson.build b/t/meson.build
index 6d91470ebc..dfd0a5a7d9 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -504,6 +504,7 @@ integration_tests = [
   't4071-diff-minimal.sh',
   't4072-diff-max-depth.sh',
   't4073-diff-stat-name-width.sh',
+  't4074-diff-shifted-matched-group.sh',
   't4100-apply-stat.sh',
   't4101-apply-nonl.sh',
   't4102-apply-rename.sh',
diff --git a/t/t4074-diff-shifted-matched-group.sh b/t/t4074-diff-shifted-matched-group.sh
new file mode 100755
index 0000000000..d77fa3b79d
--- /dev/null
+++ b/t/t4074-diff-shifted-matched-group.sh
@@ -0,0 +1,164 @@
+#!/bin/sh
+
+test_description='shifted diff groups re-diffing during histogram diff'
+
+. ./test-lib.sh
+
+test_expect_success 'shifted/merged diff group should re-diff to minimize patch' '
+	test_write_lines A x A A A x A A A >file1 &&
+	test_write_lines A x A Z A x A A A >file2 &&
+
+	file1_h=$(git rev-parse --short $(git hash-object file1)) &&
+	file2_h=$(git rev-parse --short $(git hash-object file2)) &&
+
+	cat >expect <<-EOF &&
+	diff --git a/file1 b/file2
+	index $file1_h..$file2_h 100644
+	--- a/file1
+	+++ b/file2
+	@@ -1,7 +1,7 @@
+	 A
+	 x
+	 A
+	-A
+	+Z
+	 A
+	 x
+	 A
+	EOF
+
+	test_expect_code 1 git diff --no-index --histogram file1 file2 >output &&
+	test_cmp expect output
+'
+
+test_expect_success 'merged diff group with no shift' '
+	test_write_lines A Z B x >file1 &&
+	test_write_lines C D x Z E x >file2 &&
+
+	file1_h=$(git rev-parse --short $(git hash-object file1)) &&
+	file2_h=$(git rev-parse --short $(git hash-object file2)) &&
+
+	cat >expect <<-EOF &&
+	diff --git a/file1 b/file2
+	index $file1_h..$file2_h 100644
+	--- a/file1
+	+++ b/file2
+	@@ -1,4 +1,6 @@
+	-A
+	+C
+	+D
+	+x
+	 Z
+	-B
+	+E
+	 x
+	EOF
+
+	test_expect_code 1 git diff --no-index --histogram file1 file2 >output &&
+	test_cmp expect output
+'
+
+test_expect_success 're-diff should preserve diff flags' '
+	test_write_lines a b c a b c >file1 &&
+	test_write_lines x " b" z a b c >file2 &&
+
+	file1_h=$(git rev-parse --short $(git hash-object file1)) &&
+	file2_h=$(git rev-parse --short $(git hash-object file2)) &&
+
+	cat >expect <<-EOF &&
+	diff --git a/file1 b/file2
+	index $file1_h..$file2_h 100644
+	--- a/file1
+	+++ b/file2
+	@@ -1,6 +1,6 @@
+	-a
+	-b
+	-c
+	+x
+	+ b
+	+z
+	 a
+	 b
+	 c
+	EOF
+
+	test_expect_code 1 git diff --no-index --histogram file1 file2 >output &&
+	test_cmp expect output &&
+
+	cat >expect_iwhite <<-EOF &&
+	diff --git a/file1 b/file2
+	index $file1_h..$file2_h 100644
+	--- a/file1
+	+++ b/file2
+	@@ -1,6 +1,6 @@
+	-a
+	+x
+	  b
+	-c
+	+z
+	 a
+	 b
+	 c
+	EOF
+
+	test_expect_code 1 git diff --no-index --histogram --ignore-all-space file1 file2 >output_iwhite &&
+	test_cmp expect_iwhite output_iwhite
+'
+
+test_expect_success 'shifting on either side should trigger re-diff properly' '
+	test_write_lines a b c a b c a b c >file1 &&
+	test_write_lines a b c a1 a2 a3 b c1 a b c >file2 &&
+
+	file1_h=$(git rev-parse --short $(git hash-object file1)) &&
+	file2_h=$(git rev-parse --short $(git hash-object file2)) &&
+
+	cat >expect1 <<-EOF &&
+	diff --git a/file1 b/file2
+	index $file1_h..$file2_h 100644
+	--- a/file1
+	+++ b/file2
+	@@ -1,9 +1,11 @@
+	 a
+	 b
+	 c
+	-a
+	+a1
+	+a2
+	+a3
+	 b
+	-c
+	+c1
+	 a
+	 b
+	 c
+	EOF
+
+	test_expect_code 1 git diff --no-index --histogram file1 file2 >output1 &&
+	test_cmp expect1 output1 &&
+
+	cat >expect2 <<-EOF &&
+	diff --git a/file2 b/file1
+	index $file2_h..$file1_h 100644
+	--- a/file2
+	+++ b/file1
+	@@ -1,11 +1,9 @@
+	 a
+	 b
+	 c
+	-a1
+	-a2
+	-a3
+	+a
+	 b
+	-c1
+	+c
+	 a
+	 b
+	 c
+	EOF
+
+	test_expect_code 1 git diff --no-index --histogram file2 file1 >output2 &&
+	test_cmp expect2 output2
+'
+
+test_done
diff --git a/xdiff/xdiffi.c b/xdiff/xdiffi.c
index 4376f943db..5455b4690d 100644
--- a/xdiff/xdiffi.c
+++ b/xdiff/xdiffi.c
@@ -792,6 +792,7 @@ static int group_slide_up(xdfile_t *xdf, struct xdlgroup *g)
  */
 int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) {
 	struct xdlgroup g, go;
+	struct xdlgroup g_orig;
 	long earliest_end, end_matching_other;
 	long groupsize;
 
@@ -805,10 +806,12 @@ int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) {
 		if (g.end == g.start)
 			goto next;
 
+		g_orig = g;
+
 		/*
 		 * Now shift the change up and then down as far as possible in
 		 * each direction. If it bumps into any other changes, merge
-		 * them.
+		 * them and restart the process.
 		 */
 		do {
 			groupsize = g.end - g.start;
@@ -861,7 +864,8 @@ int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) {
 			/*
 			 * Move the possibly merged group of changes back to
 			 * line up with the last group of changes from the
-			 * other file that it can align with.
+			 * other file that it can align with. This avoids breaking
+			 * a single change into a separate addition/deletion.
 			 */
 			while (go.end == go.start) {
 				if (group_slide_up(xdf, &g))
@@ -914,6 +918,45 @@ int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) {
 			}
 		}
 
+		/*
+		 * If we merged change groups during shifting, the new
+		 * combined group could now have matching lines in both files,
+		 * even if the original separate groups did not. Re-diff the
+		 * new group to find these matching lines to mark them as
+		 * unchanged.
+		 *
+		 * Only do this if the corresponding group in the other file is
+		 * non-empty, as it's trivial otherwise.
+		 *
+		 * Only do this for histogram diff as its LCS algorithm allows
+		 * for this scenario. In contrast, patience diff finds LCS
+		 * of unique lines that groups cannot be shifted across.
+		 * Myer's diff (standalone or used as fall-back in patience
+		 * diff) already finds minimal edits so it is not possible for
+		 * shifted groups to result in a smaller diff. (Without
+		 * XDF_NEED_MINIMAL, Myer's isn't technically guaranteed to be
+		 * minimal, but it should be so most of the time)
+		 */
+		if (go.end != go.start &&
+				XDF_DIFF_ALG(flags) == XDF_HISTOGRAM_DIFF &&
+				(g.start != g_orig.start ||
+				 g.end != g_orig.end)) {
+			xpparam_t xpp;
+			xdfenv_t xe;
+
+			memset(&xpp, 0, sizeof(xpp));
+			xpp.flags = flags & ~XDF_DIFF_ALGORITHM_MASK;
+
+			xe.xdf1 = *xdf;
+			xe.xdf2 = *xdfo;
+
+			if (xdl_fall_back_diff(&xe, &xpp,
+					       g.start + 1, g.end - g.start,
+					       go.start + 1, go.end - go.start)) {
+				return -1;
+			}
+		}
+
 	next:
 		/* Move past the just-processed group: */
 		if (group_next(xdf, &g))

base-commit: 2cc71917514657b93014134350864f4849edfc83
-- 
gitgitgadget
Junio C HamanoMar 13, 2026, 07:07 UTC in reply to Yee Cheng Chin via GitGitGadget on lore

Re: [PATCH v2] xdiff: re-diff shifted change groups when using histogram algorithm

"Yee Cheng Chin via GitGitGadget" <gitgitgadget@gmail.com> writes:
Show 33 quoted lines
> From: Yee Cheng Chin <ychin.git@gmail.com>
>
> After a diff algorithm has been run, the compaction phase
> (xdl_change_compact()) shifts and merges change groups to produce a
> cleaner output. However, this shifting could create a new matched group
> where both sides now have matching lines. This results in a
> wrong-looking diff output which contains redundant lines that are the
> same on both files.
>
> Fix this by detecting this situation, and re-diff the texts on each side
> to find similar lines, using the fall-back Myer's diff. Only do this for
> histogram diff as it's the only algorithm where this is relevant. Below
> contains an example, and more details.
> ...
> This issue is rare in a normal repository. Below is a table of
> repositories (`git log --no-merges -p --histogram -1000`), showing how
> many times a re-diff was done and how many times it resulted in finding
> matching lines (therefore addressing this issue) with the fix. In
> general it is fewer than 1% of diff's that exhibit this offending
> behavior:
>
> | Repo (1k commits)  | Re-diff | Found matching lines |
> |--------------------|---------|----------------------|
> | llvm-project       |  45     | 11                   |
> | vim                | 110     |  9                   |
> | git                |  18     |  2                   |
> | WebKit             | 168     |  1                   |
> | ripgrep            |  22     |  1                   |
> | cpython            |  32     |  0                   |
> | vscode             |  13     |  0                   |
>
> Signed-off-by: Yee Cheng Chin <ychin.git@gmail.com>
> ---

Thanks for the updated patch, and sorry for nobody responding to the patch for over a week.

The detailed explanation of the issue and the inclusion of the repository analysis results are very helpful; they clearly show that while this is a rare edge case, it significantly improves the quality of histogram diffs when it does occur.

 - The removal of go_orig is correct since g and go are kept in sync 
   throughout the slide loops.
 - Clearing the algorithm mask while preserving other flags ensures that 
   user-provided options like --ignore-all-space are correctly applied 
   during the re-diff.
 - While ignore_regex and anchors are not passed to the sub-diff, they 
   aren't currently available to xdl_change_compact anyway. Given that 
   compaction happens before regex filtering in the main pipeline, this
   is OK, I guess.
Let me mark the topic for 'next'.
Phillip WoodMar 13, 2026, 10:23 UTC in reply to Junio C Hamano on lore

Re: [PATCH v2] xdiff: re-diff shifted change groups when using histogram algorithm

On 13/03/2026 07:07, Junio C Hamano wrote:
Show 38 quoted lines
> "Yee Cheng Chin via GitGitGadget" <gitgitgadget@gmail.com> writes:
> 
>> From: Yee Cheng Chin <ychin.git@gmail.com>
>>
>> After a diff algorithm has been run, the compaction phase
>> (xdl_change_compact()) shifts and merges change groups to produce a
>> cleaner output. However, this shifting could create a new matched group
>> where both sides now have matching lines. This results in a
>> wrong-looking diff output which contains redundant lines that are the
>> same on both files.
>>
>> Fix this by detecting this situation, and re-diff the texts on each side
>> to find similar lines, using the fall-back Myer's diff. Only do this for
>> histogram diff as it's the only algorithm where this is relevant. Below
>> contains an example, and more details.
>> ...
>> This issue is rare in a normal repository. Below is a table of
>> repositories (`git log --no-merges -p --histogram -1000`), showing how
>> many times a re-diff was done and how many times it resulted in finding
>> matching lines (therefore addressing this issue) with the fix. In
>> general it is fewer than 1% of diff's that exhibit this offending
>> behavior:
>>
>> | Repo (1k commits)  | Re-diff | Found matching lines |
>> |--------------------|---------|----------------------|
>> | llvm-project       |  45     | 11                   |
>> | vim                | 110     |  9                   |
>> | git                |  18     |  2                   |
>> | WebKit             | 168     |  1                   |
>> | ripgrep            |  22     |  1                   |
>> | cpython            |  32     |  0                   |
>> | vscode             |  13     |  0                   |
>>
>> Signed-off-by: Yee Cheng Chin <ychin.git@gmail.com>
>> ---
> 
> Thanks for the updated patch, and sorry for nobody responding to the
> patch for over a week.

Yes, sorry for the slow response. I agree with Junio that this is explained well and looks good

Thanks
Phillip
Show 19 quoted lines
> The detailed explanation of the issue and the inclusion of the
> repository analysis results are very helpful; they clearly show that
> while this is a rare edge case, it significantly improves the
> quality of histogram diffs when it does occur.
> 
>   - The removal of go_orig is correct since g and go are kept in sync
>     throughout the slide loops.
> 
>   - Clearing the algorithm mask while preserving other flags ensures that
>     user-provided options like --ignore-all-space are correctly applied
>     during the re-diff.
> 
>   - While ignore_regex and anchors are not passed to the sub-diff, they
>     aren't currently available to xdl_change_compact anyway. Given that
>     compaction happens before regex filtering in the main pipeline, this
>     is OK, I guess.
> 
> Let me mark the topic for 'next'.
> 
Yee Cheng ChinMar 19, 2026, 23:30 UTC in reply to Phillip Wood on lore

Re: [PATCH v2] xdiff: re-diff shifted change groups when using histogram algorithm

Great. Sounds good, thanks!
On Fri, Mar 13, 2026 at 3:23 AM Phillip Wood <phillip.wood123@gmail.com> wrote:
Show 68 quoted lines
>
> On 13/03/2026 07:07, Junio C Hamano wrote:
> > "Yee Cheng Chin via GitGitGadget" <gitgitgadget@gmail.com> writes:
> >
> >> From: Yee Cheng Chin <ychin.git@gmail.com>
> >>
> >> After a diff algorithm has been run, the compaction phase
> >> (xdl_change_compact()) shifts and merges change groups to produce a
> >> cleaner output. However, this shifting could create a new matched group
> >> where both sides now have matching lines. This results in a
> >> wrong-looking diff output which contains redundant lines that are the
> >> same on both files.
> >>
> >> Fix this by detecting this situation, and re-diff the texts on each side
> >> to find similar lines, using the fall-back Myer's diff. Only do this for
> >> histogram diff as it's the only algorithm where this is relevant. Below
> >> contains an example, and more details.
> >> ...
> >> This issue is rare in a normal repository. Below is a table of
> >> repositories (`git log --no-merges -p --histogram -1000`), showing how
> >> many times a re-diff was done and how many times it resulted in finding
> >> matching lines (therefore addressing this issue) with the fix. In
> >> general it is fewer than 1% of diff's that exhibit this offending
> >> behavior:
> >>
> >> | Repo (1k commits)  | Re-diff | Found matching lines |
> >> |--------------------|---------|----------------------|
> >> | llvm-project       |  45     | 11                   |
> >> | vim                | 110     |  9                   |
> >> | git                |  18     |  2                   |
> >> | WebKit             | 168     |  1                   |
> >> | ripgrep            |  22     |  1                   |
> >> | cpython            |  32     |  0                   |
> >> | vscode             |  13     |  0                   |
> >>
> >> Signed-off-by: Yee Cheng Chin <ychin.git@gmail.com>
> >> ---
> >
> > Thanks for the updated patch, and sorry for nobody responding to the
> > patch for over a week.
>
> Yes, sorry for the slow response. I agree with Junio that this is
> explained well and looks good
>
> Thanks
>
> Phillip
>
> > The detailed explanation of the issue and the inclusion of the
> > repository analysis results are very helpful; they clearly show that
> > while this is a rare edge case, it significantly improves the
> > quality of histogram diffs when it does occur.
> >
> >   - The removal of go_orig is correct since g and go are kept in sync
> >     throughout the slide loops.
> >
> >   - Clearing the algorithm mask while preserving other flags ensures that
> >     user-provided options like --ignore-all-space are correctly applied
> >     during the re-diff.
> >
> >   - While ignore_regex and anchors are not passed to the sub-diff, they
> >     aren't currently available to xdl_change_compact anyway. Given that
> >     compaction happens before regex filtering in the main pipeline, this
> >     is OK, I guess.
> >
> > Let me mark the topic for 'next'.
> >
>

Back to recent threads