xdiff: re-diff shifted change groups when using histogram algorithm
PATCH 1/1 xdiff: re-diff shifted change groups when using histogram algorithm
From: Yee Cheng Chin <[email protected]>
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
AWhen 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 AAAWhen 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 AAAThis 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 AAAThis 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 |
---
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
t/meson.build +1 -0
@@ -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',
t/t4073-diff-shifted-matched-group.sh new file +137 -0
137 lines not on this page · raw message
xdiff/xdiffi.c +43 -0
61 lines not on this page · raw message
base-commit: f0ef5b6d9bcc258e4cbef93839d1b7465d5212b9
Signature · 1 line
gitgitgadget
Quoted 5 lines · Yee Cheng Chin via GitGitGadget
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.
Quoted 10 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:
Quoted 4 lines
[-A AA-]{+A Ax+}A AAAThis 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.
Quoted 6 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.
Quoted 6 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.
Quoted 17 lines
xdiff/xdiffi.c +13
-0@@ -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
xdiff/xdiffi.c int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) { · line 944
Quoted 10 lines
xdiff/xdiffi.c +10
-0+* 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.
Quoted 21 lines
xdiff/xdiffi.c +16
-0+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
Quoted 27 lines · Junio C Hamano
Quoted 25 lines · Yee Cheng Chin via GitGitGadget
xdiff/xdiffi.c +21
-0@@ -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 ||
Quoted 8 lines
xdiff/xdiffi.c int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) { · line 944
Quoted 2 lines
xdiff/xdiffi.c +2
-0+ 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
Quoted 10 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) }
Quoted 7 lines
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
Quoted 8 lines
xdiff/xdiffi.c int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) { · line 952
Quoted 8 lines
xdiff/xdiffi.c +8
-0+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
Quoted 14 lines
Quoted 13 lines
xdiff/xdiffi.c +8
-0+ +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
Quoted 7 lines · Phillip Wood
Quoted 6 lines · Junio C Hamano
Quoted 4 lines · Yee Cheng Chin via GitGitGadget
(quoted diff) +0
-0@@ -915,6 +919,45 @@ int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) { } }
Quoted 7 lines
Quoted 7 lines
xdiff/xdiffi.c int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) { · line 928
Quoted 7 lines
xdiff/xdiffi.c +7
-0+/* +* 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?
Quoted 17 lines
Quoted 4 lines
Quoted 4 lines
xdiff/xdiffi.c +4
-0+ if (end_matching_other != -1 && + XDF_DIFF_ALG(flags) == XDF_HISTOGRAM_DIFF && + (g.start != g_orig.start || + g.end != g_orig.end ||Quoted 8 lines
xdiff/xdiffi.c int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) { · line 944
Quoted 2 lines
xdiff/xdiffi.c +2
-0+ 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
Quoted 5 lines
g.start == g_orig.start && g.end == g_orig.endwhen
go.start != go.orig.start || go.end != go_orig.end
Interesting.
Quoted 20 lines
Quoted 10 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) }Quoted 7 lines
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
Quoted 12 lines
Quoted 8 lines
xdiff/xdiffi.c int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) { · line 952
Quoted 8 lines
xdiff/xdiffi.c +8
-0+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 formemcpy()here).
Very good readability enhancement suggestion.
Thanks.
Quoted 9 lines · Junio C Hamano
Quoted 7 lines · Phillip Wood
Quoted 6 lines · Junio C Hamano
Quoted 4 lines · Yee Cheng Chin via GitGitGadget
(quoted diff) +0
-0@@ -915,6 +919,45 @@ int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) { } }
Quoted 13 lines
Quoted 7 lines
Quoted 7 lines
xdiff/xdiffi.c int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) { · line 928
Quoted 7 lines
xdiff/xdiffi.c +7
-0+/* +* 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
Quoted 61 lines
Quoted 17 lines
Quoted 4 lines
Quoted 4 lines
xdiff/xdiffi.c +4
-0+ if (end_matching_other != -1 && + XDF_DIFF_ALG(flags) == XDF_HISTOGRAM_DIFF && + (g.start != g_orig.start || + g.end != g_orig.end ||Quoted 8 lines
xdiff/xdiffi.c int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) { · line 944
Quoted 2 lines
xdiff/xdiffi.c +2
-0+ 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
Quoted 5 lines
g.start == g_orig.start && g.end == g_orig.endwhen
go.start != go.orig.start || go.end != go_orig.endInteresting.
Quoted 20 lines
Quoted 10 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) }Quoted 7 lines
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
Quoted 12 lines
Quoted 8 lines
xdiff/xdiffi.c int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) { · line 952
Quoted 8 lines
xdiff/xdiffi.c +8
-0+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 formemcpy()here).Very good readability enhancement suggestion.
Thanks.
Quoted 34 lines · Phillip Wood
Quoted 9 lines · Junio C Hamano
Quoted 7 lines · Phillip Wood
Quoted 6 lines · Junio C Hamano
Quoted 4 lines · Yee Cheng Chin via GitGitGadget
(quoted diff) +0
-0@@ -915,6 +919,45 @@ int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) { } }Quoted 13 lines
Quoted 7 lines
Quoted 7 lines
xdiff/xdiffi.c int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) { · line 928
Quoted 7 lines
xdiff/xdiffi.c +7
-0+/* +* 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
(quoted diff) +0
-2-A A A -> -A -B -B
Quoted 2 lines
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.
Quoted 9 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.
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.
Quoted 16 lines · Junio C Hamano
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) }
Quoted 7 lines
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.
Quoted 10 lines · Phillip Wood
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.endwhen
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.
Quoted 4 lines
Quoted 4 lines
Quoted 4 lines
xdiff/xdiffi.c +4
-0+xpparam_t xpp; +xdfenv_t xe; + +memset (&xpp,0 ,sizeof (xpp));
Quoted 8 lines
Quoted 4 lines
xdiff/xdiffi.c int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) { · line 952
Quoted 4 lines
xdiff/xdiffi.c +4
-0+ 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 formemcpy()here).
Will fix in v2.
Quoted 3 lines
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.
Quoted 15 lines · Phillip Wood
Quoted 5 lines · Junio C Hamano
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
Review
Quoted 3 lines
(quoted diff) +0
-2-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.
Quoted 5 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.
Quoted 28 lines · Yee Cheng Chin
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.
Quoted 16 lines · Junio C Hamano
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) }Quoted 7 lines
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.
Quoted 5 lines
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.
Quoted 28 lines · Junio C Hamano
Quoted 20 lines · Yee Cheng Chin
Quoted 10 lines · Junio C Hamano
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) }Quoted 7 lines
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.
Quoted 5 lines
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.
Quoted 7 lines
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.
Quoted 6 lines · Yee Cheng Chin
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.
Quoted 5 lines · Yee Cheng Chin
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
Quoted 28 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.
Quoted 9 lines · Yee Cheng Chin
...
{-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.
Hi, yes, I'm still on it. Sorry for the delay. I will push an update out this weekend.
Quoted 18 lines · Junio C Hamano
Quoted 9 lines · Yee Cheng Chin
...
{-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.
PATCH v2 1/1 xdiff: re-diff shifted change groups when using histogram algorithm
In reply to Yee Cheng Chin via GitGitGadgetFrom: Yee Cheng Chin <[email protected]>
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
AWhen 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 AAAWhen 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 AAAThis 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 AAAThis 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 |
---
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
t/meson.build +1 -0
@@ -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',
t/t4074-diff-shifted-matched-group.sh new file +164 -0
164 lines not on this page · raw message
xdiff/xdiffi.c +45 -2
74 lines not on this page · raw message
base-commit: 2cc71917514657b93014134350864f4849edfc83
Signature · 1 line
gitgitgadget
Quoted 31 lines · Yee Cheng Chin via GitGitGadget
From: Yee Cheng Chin <[email protected]>
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 |
Quoted 2 lines
Signed-off-by Yee Cheng Chin <[email protected]>---
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'.
Quoted 33 lines · Junio C Hamano
Quoted 31 lines · Yee Cheng Chin via GitGitGadget
From: Yee Cheng Chin <[email protected]>
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 |
Quoted 5 lines
Quoted 2 lines
Signed-off-by Yee Cheng Chin <[email protected]>---
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
Quoted 19 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_origis 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_regexand anchors are not passed to the sub-diff, they aren't currently available toxdl_change_compactanyway. Given that compaction happens before regex filtering in the main pipeline, this is OK, I guess.Let me mark the topic for 'next'.
Great. Sounds good, thanks!
Quoted 68 lines · Phillip Wood
Quoted 33 lines · Junio C Hamano
Quoted 31 lines · Yee Cheng Chin via GitGitGadget
From: Yee Cheng Chin <[email protected]>
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 |Quoted 5 lines
Quoted 2 lines
Signed-off-by Yee Cheng Chin <[email protected]>---
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
Quoted 19 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_origis 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_regexand anchors are not passed to the sub-diff, they aren't currently available toxdl_change_compactanyway. Given that compaction happens before regex filtering in the main pipeline, this is OK, I guess.Let me mark the topic for 'next'.