public inbox for git@vger.kernel.org
 help / color / mirror / Atom feed
* [PATCH] xdiff: re-diff shifted change groups when using histogram algorithm
@ 2025-12-06 20:51 Yee Cheng Chin via GitGitGadget
  2026-01-21 20:51 ` Junio C Hamano
  2026-03-02 14:54 ` [PATCH v2] " Yee Cheng Chin via GitGitGadget
  0 siblings, 2 replies; 17+ messages in thread
From: Yee Cheng Chin via GitGitGadget @ 2025-12-06 20:51 UTC (permalink / raw)
  To: git; +Cc: Yee Cheng Chin, Yee Cheng Chin

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

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

^ permalink raw reply related	[flat|nested] 17+ messages in thread

* Re: [PATCH] xdiff: re-diff shifted change groups when using histogram algorithm
  2025-12-06 20:51 [PATCH] xdiff: re-diff shifted change groups when using histogram algorithm Yee Cheng Chin via GitGitGadget
@ 2026-01-21 20:51 ` Junio C Hamano
  2026-01-24 10:54   ` Phillip Wood
  2026-03-02 14:54 ` [PATCH v2] " Yee Cheng Chin via GitGitGadget
  1 sibling, 1 reply; 17+ messages in thread
From: Junio C Hamano @ 2026-01-21 20:51 UTC (permalink / raw)
  To: Yee Cheng Chin via GitGitGadget; +Cc: git, Yee Cheng Chin

"Yee Cheng Chin via GitGitGadget" <gitgitgadget@gmail.com> writes:

> 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.

> 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.

> 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.

> 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.

> @@ -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.


> +			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

^ permalink raw reply	[flat|nested] 17+ messages in thread

* Re: [PATCH] xdiff: re-diff shifted change groups when using histogram algorithm
  2026-01-21 20:51 ` Junio C Hamano
@ 2026-01-24 10:54   ` Phillip Wood
  2026-01-25 17:34     ` Junio C Hamano
  0 siblings, 1 reply; 17+ messages in thread
From: Phillip Wood @ 2026-01-24 10:54 UTC (permalink / raw)
  To: Junio C Hamano, Yee Cheng Chin via GitGitGadget; +Cc: git, Yee Cheng Chin

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.
>> +		 *
>> +		 * 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

> 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).

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

>> +
>> +			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
> 


^ permalink raw reply	[flat|nested] 17+ messages in thread

* Re: [PATCH] xdiff: re-diff shifted change groups when using histogram algorithm
  2026-01-24 10:54   ` Phillip Wood
@ 2026-01-25 17:34     ` Junio C Hamano
  2026-01-26  9:37       ` Phillip Wood
  0 siblings, 1 reply; 17+ messages in thread
From: Junio C Hamano @ 2026-01-25 17:34 UTC (permalink / raw)
  To: Phillip Wood; +Cc: Yee Cheng Chin via GitGitGadget, git, Yee Cheng Chin

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?

>>> +		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.

^ permalink raw reply	[flat|nested] 17+ messages in thread

* Re: [PATCH] xdiff: re-diff shifted change groups when using histogram algorithm
  2026-01-25 17:34     ` Junio C Hamano
@ 2026-01-26  9:37       ` Phillip Wood
  2026-01-26 17:32         ` Junio C Hamano
  0 siblings, 1 reply; 17+ messages in thread
From: Phillip Wood @ 2026-01-26  9:37 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: Yee Cheng Chin via GitGitGadget, git, Yee Cheng Chin

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. 
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

>>>> +		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.


^ permalink raw reply	[flat|nested] 17+ messages in thread

* Re: [PATCH] xdiff: re-diff shifted change groups when using histogram algorithm
  2026-01-26  9:37       ` Phillip Wood
@ 2026-01-26 17:32         ` Junio C Hamano
  2026-01-29 16:53           ` Yee Cheng Chin
  0 siblings, 1 reply; 17+ messages in thread
From: Junio C Hamano @ 2026-01-26 17:32 UTC (permalink / raw)
  To: Phillip Wood; +Cc: Yee Cheng Chin via GitGitGadget, git, Yee Cheng Chin

Phillip Wood <phillip.wood123@gmail.com> writes:

> 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.

> 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.

^ permalink raw reply	[flat|nested] 17+ messages in thread

* Re: [PATCH] xdiff: re-diff shifted change groups when using histogram algorithm
  2026-01-26 17:32         ` Junio C Hamano
@ 2026-01-29 16:53           ` Yee Cheng Chin
  2026-01-29 20:58             ` Junio C Hamano
  0 siblings, 1 reply; 17+ messages in thread
From: Yee Cheng Chin @ 2026-01-29 16:53 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: Phillip Wood, Yee Cheng Chin via GitGitGadget, git

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.

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:
> 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.

> >> +                    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:
>
> 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.

> 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.

^ permalink raw reply	[flat|nested] 17+ messages in thread

* Re: [PATCH] xdiff: re-diff shifted change groups when using histogram algorithm
  2026-01-29 16:53           ` Yee Cheng Chin
@ 2026-01-29 20:58             ` Junio C Hamano
  2026-01-30  1:58               ` Yee Cheng Chin
  0 siblings, 1 reply; 17+ messages in thread
From: Junio C Hamano @ 2026-01-29 20:58 UTC (permalink / raw)
  To: Yee Cheng Chin; +Cc: Phillip Wood, Yee Cheng Chin via GitGitGadget, git

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

> 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.

^ permalink raw reply	[flat|nested] 17+ messages in thread

* Re: [PATCH] xdiff: re-diff shifted change groups when using histogram algorithm
  2026-01-29 20:58             ` Junio C Hamano
@ 2026-01-30  1:58               ` Yee Cheng Chin
  2026-01-30  5:43                 ` Junio C Hamano
                                   ` (2 more replies)
  0 siblings, 3 replies; 17+ messages in thread
From: Yee Cheng Chin @ 2026-01-30  1:58 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: Phillip Wood, Yee Cheng Chin via GitGitGadget, git

On Thu, Jan 29, 2026 at 12:58 PM Junio C Hamano <gitster@pobox.com> wrote:
>
> 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.

^ permalink raw reply	[flat|nested] 17+ messages in thread

* Re: [PATCH] xdiff: re-diff shifted change groups when using histogram algorithm
  2026-01-30  1:58               ` Yee Cheng Chin
@ 2026-01-30  5:43                 ` Junio C Hamano
  2026-01-30 16:06                 ` Phillip Wood
  2026-02-20 23:07                 ` Junio C Hamano
  2 siblings, 0 replies; 17+ messages in thread
From: Junio C Hamano @ 2026-01-30  5:43 UTC (permalink / raw)
  To: Yee Cheng Chin; +Cc: Phillip Wood, Yee Cheng Chin via GitGitGadget, git

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

> 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.

^ permalink raw reply	[flat|nested] 17+ messages in thread

* Re: [PATCH] xdiff: re-diff shifted change groups when using histogram algorithm
  2026-01-30  1:58               ` Yee Cheng Chin
  2026-01-30  5:43                 ` Junio C Hamano
@ 2026-01-30 16:06                 ` Phillip Wood
  2026-02-20 23:07                 ` Junio C Hamano
  2 siblings, 0 replies; 17+ messages in thread
From: Phillip Wood @ 2026-01-30 16:06 UTC (permalink / raw)
  To: Yee Cheng Chin, Junio C Hamano; +Cc: Yee Cheng Chin via GitGitGadget, git

On 30/01/2026 01:58, Yee Cheng Chin wrote:
> 
> 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

> 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.
> 


^ permalink raw reply	[flat|nested] 17+ messages in thread

* Re: [PATCH] xdiff: re-diff shifted change groups when using histogram algorithm
  2026-01-30  1:58               ` Yee Cheng Chin
  2026-01-30  5:43                 ` Junio C Hamano
  2026-01-30 16:06                 ` Phillip Wood
@ 2026-02-20 23:07                 ` Junio C Hamano
  2026-02-21  9:56                   ` Yee Cheng Chin
  2 siblings, 1 reply; 17+ messages in thread
From: Junio C Hamano @ 2026-02-20 23:07 UTC (permalink / raw)
  To: Yee Cheng Chin; +Cc: Phillip Wood, Yee Cheng Chin via GitGitGadget, git

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.


^ permalink raw reply	[flat|nested] 17+ messages in thread

* Re: [PATCH] xdiff: re-diff shifted change groups when using histogram algorithm
  2026-02-20 23:07                 ` Junio C Hamano
@ 2026-02-21  9:56                   ` Yee Cheng Chin
  0 siblings, 0 replies; 17+ messages in thread
From: Yee Cheng Chin @ 2026-02-21  9:56 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: Phillip Wood, Yee Cheng Chin via GitGitGadget, git

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:
>
> 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.
>

^ permalink raw reply	[flat|nested] 17+ messages in thread

* [PATCH v2] xdiff: re-diff shifted change groups when using histogram algorithm
  2025-12-06 20:51 [PATCH] xdiff: re-diff shifted change groups when using histogram algorithm Yee Cheng Chin via GitGitGadget
  2026-01-21 20:51 ` Junio C Hamano
@ 2026-03-02 14:54 ` Yee Cheng Chin via GitGitGadget
  2026-03-13  7:07   ` Junio C Hamano
  1 sibling, 1 reply; 17+ messages in thread
From: Yee Cheng Chin via GitGitGadget @ 2026-03-02 14:54 UTC (permalink / raw)
  To: git; +Cc: Phillip Wood, Yee Cheng Chin, Yee Cheng Chin

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

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

^ permalink raw reply related	[flat|nested] 17+ messages in thread

* Re: [PATCH v2] xdiff: re-diff shifted change groups when using histogram algorithm
  2026-03-02 14:54 ` [PATCH v2] " Yee Cheng Chin via GitGitGadget
@ 2026-03-13  7:07   ` Junio C Hamano
  2026-03-13 10:23     ` Phillip Wood
  0 siblings, 1 reply; 17+ messages in thread
From: Junio C Hamano @ 2026-03-13  7:07 UTC (permalink / raw)
  To: Yee Cheng Chin via GitGitGadget; +Cc: git, Phillip Wood, Yee Cheng Chin

"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.

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'.


^ permalink raw reply	[flat|nested] 17+ messages in thread

* Re: [PATCH v2] xdiff: re-diff shifted change groups when using histogram algorithm
  2026-03-13  7:07   ` Junio C Hamano
@ 2026-03-13 10:23     ` Phillip Wood
  2026-03-19 23:30       ` Yee Cheng Chin
  0 siblings, 1 reply; 17+ messages in thread
From: Phillip Wood @ 2026-03-13 10:23 UTC (permalink / raw)
  To: Junio C Hamano, Yee Cheng Chin via GitGitGadget; +Cc: git, Yee Cheng Chin

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'.
> 


^ permalink raw reply	[flat|nested] 17+ messages in thread

* Re: [PATCH v2] xdiff: re-diff shifted change groups when using histogram algorithm
  2026-03-13 10:23     ` Phillip Wood
@ 2026-03-19 23:30       ` Yee Cheng Chin
  0 siblings, 0 replies; 17+ messages in thread
From: Yee Cheng Chin @ 2026-03-19 23:30 UTC (permalink / raw)
  To: phillip.wood; +Cc: Junio C Hamano, Yee Cheng Chin via GitGitGadget, git

Great. Sounds good, thanks!


On Fri, Mar 13, 2026 at 3:23 AM Phillip Wood <phillip.wood123@gmail.com> wrote:
>
> 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'.
> >
>

^ permalink raw reply	[flat|nested] 17+ messages in thread

end of thread, other threads:[~2026-03-19 23:30 UTC | newest]

Thread overview: 17+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2025-12-06 20:51 [PATCH] xdiff: re-diff shifted change groups when using histogram algorithm Yee Cheng Chin via GitGitGadget
2026-01-21 20:51 ` Junio C Hamano
2026-01-24 10:54   ` Phillip Wood
2026-01-25 17:34     ` Junio C Hamano
2026-01-26  9:37       ` Phillip Wood
2026-01-26 17:32         ` Junio C Hamano
2026-01-29 16:53           ` Yee Cheng Chin
2026-01-29 20:58             ` Junio C Hamano
2026-01-30  1:58               ` Yee Cheng Chin
2026-01-30  5:43                 ` Junio C Hamano
2026-01-30 16:06                 ` Phillip Wood
2026-02-20 23:07                 ` Junio C Hamano
2026-02-21  9:56                   ` Yee Cheng Chin
2026-03-02 14:54 ` [PATCH v2] " Yee Cheng Chin via GitGitGadget
2026-03-13  7:07   ` Junio C Hamano
2026-03-13 10:23     ` Phillip Wood
2026-03-19 23:30       ` Yee Cheng Chin

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox