Better heuristics make prettier diffs
v2 cover Better heuristics make prettier diffs
This is v2 of a patch series to improve the heuristics that git diff and related commands use to position ambiguous blocks of added/deleted lines. Thanks to Stefan, Jacob, Peff, Junio, and Jakub for their comments about v1 [1]. This patch series is also available from my GitHub account [2] as branch diff-indent-heuristic.
This version started out as a light touch-up of v1, but as I was working I realized that it needed more changes. Among other things, the final heuristic is now improved relative to the one proposed in v1. Summary of changes:
* I changed the --compaction-heuristic code such that if a group of added/deleted lines can be aligned with a group of deleted/added lines in the other file, then that should be done rather than try to slide to a blank line. In the existing code, the compaction heuristic was attempted, incorrectly, *after* trying to align groups, which doesn't make sense.
* I changed recs_match() similarly to is_blank_line(): now it takes two xrecord_t * as arguments rather than an array of xrecord_t and two indexes.
* I refactored the old xdl_change_compact() more thoroughly. All of the index manipulation was pretty confusing, and I was having trouble convincing myself that even the old code was working correctly. So I introduced a struct group, which is like a cursor that keeps track of a (possibly empty) group of changed lines in the old or new version of a file. I added functions to initialize a group to the first change in a file, to move to the next or previous groups, and to slide a group forward or backward if possible (i.e., if the group is a "slider").
* In the course of that refactoring, I found (and fixed) another bug in the --compaction-heuristic code: it didn't notice if the top possible position of a slider had a blank line as its last line. See the commit message of patch [5/5] for more details.
* I realized that the behavior of the indent heuristic from v1, because it multiplied weighting factors times indent widths, would behave differently if a project changed its convention for how many spaces to use per level of indentation. That doesn't make sense, so I changed the handling of indentation:
Now, the sum of the indentation width at the top and bottom of the slider are added, *but those sums are only compared* between slider positions, and the weight is multipled by -1, 0, or +1 depending on whether the first indent sum is less than, equal, or greater than the other. This should make the behavior relatively independent of the project's indentation convention, and thus make the heuristic work more consistently across projects.
The resulting heuristic works significantly better than the one proposed in v1:
| repository | count | Git 2.9.0 | v1 | v2 | | --------------------- | ----- | -------------- | -------------- | -------------- | | afnetworking | 109 | 89 (81.7%) | 3 (2.8%) | 2 (1.8%) | | alamofire | 30 | 18 (60.0%) | 2 (6.7%) | 0 (0.0%) | | angular | 184 | 127 (69.0%) | 15 (8.2%) | 5 (2.7%) | | animate | 313 | 2 (0.6%) | 2 (0.6%) | 2 (0.6%) | | ant | 380 | 356 (93.7%) | 22 (5.8%) | 15 (3.9%) | | bugzilla | 306 | 263 (85.9%) | 24 (7.8%) | 15 (4.9%) | | corefx | 126 | 91 (72.2%) | 15 (11.9%) | 6 (4.8%) | | couchdb | 78 | 44 (56.4%) | 17 (21.8%) | 6 (7.7%) | | cpython | 937 | 158 (16.9%) | 26 (2.8%) | 5 (0.5%) | | discourse | 160 | 95 (59.4%) | 24 (15.0%) | 13 (8.1%) | | docker | 307 | 194 (63.2%) | 16 (5.2%) | 8 (2.6%) | | electron | 163 | 132 (81.0%) | 15 (9.2%) | 6 (3.7%) | | git | 536 | 470 (87.7%) | 18 (3.4%) | 16 (3.0%) | | gitflow | 127 | 0 (0.0%) | 0 (0.0%) | 0 (0.0%) | | ionic | 133 | 89 (66.9%) | 6 (4.5%) | 1 (0.8%) | | ipython | 482 | 362 (75.1%) | 45 (9.3%) | 11 (2.3%) | | junit | 161 | 147 (91.3%) | 4 (2.5%) | 1 (0.6%) | | lighttable | 15 | 5 (33.3%) | 2 (13.3%) | 0 (0.0%) | | magit | 88 | 75 (85.2%) | 5 (5.7%) | 0 (0.0%) | | neural-style | 28 | 0 (0.0%) | 0 (0.0%) | 0 (0.0%) | | nodejs | 781 | 649 (83.1%) | 98 (12.5%) | 5 (0.6%) | | phpmyadmin | 491 | 481 (98.0%) | 7 (1.4%) | 2 (0.4%) | | react-native | 168 | 130 (77.4%) | 5 (3.0%) | 0 (0.0%) | | rust | 171 | 128 (74.9%) | 17 (9.9%) | 14 (8.2%) | | spark | 186 | 149 (80.1%) | 17 (9.1%) | 2 (1.1%) | | tensorflow | 115 | 66 (57.4%) | 7 (6.1%) | 5 (4.3%) | | test-more | 19 | 15 (78.9%) | 1 (5.3%) | 1 (5.3%) | | test-unit | 51 | 34 (66.7%) | 8 (15.7%) | 2 (3.9%) | | xmonad | 23 | 22 (95.7%) | 1 (4.3%) | 1 (4.3%) | | --------------------- | ----- | -------------- | -------------- | -------------- | | totals | 6668 | 4391 (65.9%) | 422 (6.3%) | 144 (2.2%) |
* I noticed that most of the "bonus" weights were actually negative, so calling them "bonuses" was misleading. Therefore, I negated the coefficients and now call them "penalties"
* I noticed that git blame was parsing diff options like --indent-heuristic from the command line, but wasn't using the values. That is fixed in a new patch [07/07].
* I redid all of the analysis and training with a bigger corpus. To check whether I was overtraining the heuristic, I also did the following experiment: I optimized the weights against a training set consisting of only some of the repositories, then tested it against the rest of the corpus. See the full results in the commit message for patch [06/06].
* I added a couple of smoke tests.
The companion project [3] now provides an easy way to replicate and extend these results, if anybody is interested.
The new heuristic still has to be enabled via the --indent-heuristic command-line parameter or the diff.indentHeuristic configuration setting. Before we release this, we should decide what the final UI should look like and make it so. If we decide to replace the existing compaction heuristic with this one and/or turn this heuristic on for all users by default, those steps might look like branch indent-heuristic-default in my GitHub repository [2].
Michael
[1] http://public-inbox.org/git/[email protected]/t/#u
[2] https://github.com/mhagger/git
[3] https://github.com/mhagger/diff-slider-tools
Michael Haggerty (7):
xdl_change_compact(): fix compaction heuristic to adjust ixo xdl_change_compact(): only use heuristic if group can't be matched is_blank_line(): take a single xrecord_t as argument recs_match(): take two xrecord_t pointers as arguments xdl_change_compact(): introduce the concept of a change group
diff: improve positioning of add/delete blocks in diffs
blame: actually use the diff opts parsed from the command line
Documentation/diff-config.txt | 7 +- Documentation/diff-options.txt | 9 +- builtin/blame.c | 11 +- diff.c | 23 +- git-add--interactive.perl | 5 +- t/t4059-diff-indent.sh | 160 +++++++++++ xdiff/xdiff.h | 1 + xdiff/xdiffi.c | 635 ++++++++++++++++++++++++++++++++++------- 8 files changed, 736 insertions(+), 115 deletions(-) create mode 100755 t/t4059-diff-indent.sh
Signature · 1 line
2.9.3
PATCH v2 1/7 xdl_change_compact(): fix compaction heuristic to adjust ixo
The code branch used for the compaction heuristic forgot to keep ixo in sync while the group was shifted. This is certainly wrong, as it causes the two counters to get out of sync.
I *think* that this bug could also have caused the function to read past the end of the rchgo array, though I haven't done the work to prove it for sure. Here is my reasoning:
If ixo is not decremented correctly during one iteration of the outer while loop, then it will loose sync with the ix counter. In particular, ixo will be too large.
Suppose that the next iterations of the outer while loop (i.e., processing the next block of add/delete lines) don't have any sliders. Then the ixo counter would be incremented by the number of non-changed lines in xdf, which is the same as the number of non-changed lines in xdfo that *should have* followed the group that experienced the malfunction. But since ixo was too large at the end of that iteration, it will be incremented past the end of the xdfo->rchg array, and will try to read that memory illegally.
--- xdiff/xdiffi.c | 1 + 1 file changed, 1 insertion(+)
xdiff/xdiffi.c +1 -0
@@ -528,6 +528,7 @@ int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) { recs_match(recs, ixs - 1, ix - 1, flags)) { rchg[--ixs] =1 ; rchg[--ix] =0 ; +while (rchgo[--ixo]); } } }
Signature · 1 line
2.9.3
PATCH v2 2/7 xdl_change_compact(): only use heuristic if group can't be matched
In reply to Michael HaggertyIf the changed group of lines can be matched to a group in the other file, then that positioning should take precedence over the compaction heuristic.
The old code tried the heuristic unconditionally, which cost redundant effort and also was broken if the matching code had already shifted the group higher than the blank line.
--- xdiff/xdiffi.c | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-)
xdiff/xdiffi.c +19 -19
@@ -504,25 +504,25 @@ int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) { } }while (grpsiz != ix - ixs); -/* -* Try to move back the possibly merged group of changes, to match -* the recorded position in the other file. -*/ -while (ixref < ix) { - rchg[--ixs] =1 ; - rchg[--ix] =0 ; -while (rchgo[--ixo]); - } - -/* -* If a group can be moved back and forth, see if there is a -* blank line in the moving space. If there is a blank line, -* make sure the last blank line is the end of the group. -* -* As we already shifted the group forward as far as possible -* in the earlier loop, we need to shift it back only if at all. -*/ -if ((flags &XDF_COMPACTION_HEURISTIC ) && blank_lines) { +if (ixref < ix) { +/* +* Try to move back the possibly merged group of changes, to match +* the recorded position in the other file. +*/ +while (ixref < ix) { + rchg[--ixs] =1 ; + rchg[--ix] =0 ; +while (rchgo[--ixo]); + } + }else if ((flags &XDF_COMPACTION_HEURISTIC ) && blank_lines) { +/* +* The group can be slid up to make its last line a +* blank line. Do so. +* +* As we already shifted the group forward as far as +* possible in the earlier loop, we need to shift it +* back only if at all. +*/ while (ixs >0 && !is_blank_line (recs, ix -1 , flags) &&recs_match (recs, ixs -1 , ix -1 , flags)) {
Signature · 1 line
2.9.3
PATCH v2 3/7 is_blank_line(): take a single xrecord_t as argument
In reply to Michael HaggertyThere is no reason for it to take an array and index as argument, as it only accesses a single element of the array.
--- xdiff/xdiffi.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-)
xdiff/xdiffi.c +4 -4
@@ -400,9 +400,9 @@ static xdchange_t *xdl_add_change(xdchange_t *xscr, long i1, long i2, long chg1, } -static int is_blank_line (xrecord_t **recs,long ix,long flags) +static int is_blank_line (xrecord_t *rec,long flags) { -return xdl_blankline (recs[ix]->ptr, recs[ix]->size, flags); +return xdl_blankline (rec->ptr, rec->size, flags); }static int recs_match (xrecord_t **recs,long ixs,long ix,long flags)
@@ -485,7 +485,7 @@ int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) {* the group. */ while (ix < nrec &&recs_match (recs, ixs, ix, flags)) { - blank_lines +=is_blank_line (recs, ix, flags); + blank_lines +=is_blank_line (recs[ix], flags); rchg[ixs++] =0 ; rchg[ix++] =1 ;
@@ -524,7 +524,7 @@ int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) {* back only if at all. */ while (ixs >0 && - !is_blank_line (recs, ix -1 , flags) && + !is_blank_line (recs[ix -1 ], flags) &&recs_match (recs, ixs -1 , ix -1 , flags)) { rchg[--ixs] =1 ; rchg[--ix] =0 ;
Signature · 1 line
2.9.3
PATCH v2 4/7 recs_match(): take two xrecord_t pointers as arguments
In reply to Michael HaggertyThere is no reason for it to take an array and two indexes as argument, as it only accesses two elements of the array.
--- xdiff/xdiffi.c | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-)
xdiff/xdiffi.c +7 -7
@@ -405,11 +405,11 @@ static int is_blank_line(xrecord_t *rec, long flags)return xdl_blankline (rec->ptr, rec->size, flags); } -static int recs_match (xrecord_t **recs,long ixs,long ix,long flags) +static int recs_match (xrecord_t *rec1,xrecord_t *rec2,long flags) { -return (recs[ixs]->ha == recs[ix]->ha && -xdl_recmatch (recs[ixs]->ptr, recs[ixs]->size, - recs[ix]->ptr, recs[ix]->size, +return (rec1->ha == rec2->ha && +xdl_recmatch (rec1->ptr, rec1->size, + rec2->ptr, rec2->size, flags)); }
@@ -457,7 +457,7 @@ int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) {* the last line of the current change group, shift backward * the group. */ -while (ixs >0 &&recs_match (recs, ixs -1 , ix -1 , flags)) { +while (ixs >0 &&recs_match (recs[ixs -1 ], recs[ix -1 ], flags)) { rchg[--ixs] =1 ; rchg[--ix] =0 ;
@@ -484,7 +484,7 @@ int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) {* the line next of the current change group, shift forward * the group. */ -while (ix < nrec &&recs_match (recs, ixs, ix, flags)) { +while (ix < nrec &&recs_match (recs[ixs], recs[ix], flags)) { blank_lines +=is_blank_line (recs[ix], flags); rchg[ixs++] =0 ;
@@ -525,7 +525,7 @@ int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) {*/ while (ixs >0 && !is_blank_line (recs[ix -1 ], flags) && -recs_match (recs, ixs -1 , ix -1 , flags)) { +recs_match (recs[ixs -1 ], recs[ix -1 ], flags)) { rchg[--ixs] =1 ; rchg[--ix] =0 ;while (rchgo[--ixo]);
Signature · 1 line
2.9.3
PATCH v2 5/7 xdl_change_compact(): introduce the concept of a change group
In reply to Michael HaggertyThe idea of xdl_change_compact() is fairly simple:
* Proceed through groups of changed lines in the file to be compacted, keeping track of the corresponding location in the "other" file.
* If possible, slide the group up and down to try to give the most aesthetically pleasing diff. Whenever it is slid, the current location in the other file needs to be adjusted.
But these simple concepts are obfuscated by a lot of index handling that is written in terse, subtle, and varied patterns. I found it very hard to convince myself that the function was correct.
So introduce a "struct group" that represents a group of changed lines in a file. Add some functions that perform elementary operations on groups:
* Initialize a group to the first group in a file
* Move to the next or previous group in a file
* Slide a group up or down
Even though the resulting code is longer, I think it is easier to understand and review. Its performance is not changed appreciably (though it would be if group_next() and group_previous() were not inlined).
...and in fact, the rewriting helped me discover another bug in the --compaction-heuristic code: The update of blank_lines was never done for the highest possible position of the group. This means that it could fail to slide the group to its highest possible position, even if that position had a blank line as its last line. So for example, it yielded the following diff:
$ git diff --no-index --compaction-heuristic a.txt b.txt
diff --git a/a.txt b/b.txt
index e53969f..0d60c5fe 100644
--- a/a.txt
+++ b/b.txt
@@ -1,3 +1,7 @@
1
A
+
+B
+
+A
2when in fact the following diff is better (according to the rules of --compaction-heuristic):
$ git diff --no-index --compaction-heuristic a.txt b.txt
diff --git a/a.txt b/b.txt
index e53969f..0d60c5fe 100644
--- a/a.txt
+++ b/b.txt
@@ -1,3 +1,7 @@
1
+A
+
+B
+
A
2The new code gives the bottom answer.
--- xdiff/xdiffi.c | 293 +++++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 203 insertions(+), 90 deletions(-)
xdiff/xdiffi.c +203 -90
@@ -413,126 +413,239 @@ static int recs_match(xrecord_t *rec1, xrecord_t *rec2, long flags) flags)); } -int xdl_change_compact (xdfile_t *xdf,xdfile_t *xdfo,long flags) { -long ix, ixo, ixs, ixref, grpsiz, nrec = xdf->nrec; -char *rchg = xdf->rchg, *rchgo = xdfo->rchg; -unsigned int blank_lines; -xrecord_t **recs = xdf->recs; +/* +* Represent a group of changed lines in an xdfile_t (i.e., a contiguous group +* of lines that was inserted or deleted from the corresponding version of the +* file). We consider there to be such a group at the beginning of the file, at +* the end of the file, and between any two unchanged lines, though most such +* groups will usually be empty. +* +* If the first line in a group is equal to the line following the group, then +* the group can be slid down. Similarly, if the last line in a group is equal +* to the line preceding the group, then the group can be slid up. See +* group_slide_down() and group_slide_up(). +* +* Note that loops that are testing for changed lines in xdf->rchg do not need +* index bounding since the array is prepared with a zero at position -1 and N. +*/ +struct group { +/* +* The index of the first changed line in the group, or the index of +* the unchanged line above which the (empty) group is located. +*/ +long start;/* -* This is the same of what GNU diff does. Move back and forward -* change groups for a consistent and pretty diff output. This also -* helps in finding joinable change groups and reduce the diff size. +* The index of the first unchanged line after the group. For an empty +* group, end is equal to start. */ -for (ix = ixo =0 ;;) { -/* -* Find the first changed line in the to-be-compacted file. -* We need to keep track of both indexes, so if we find a -* changed lines group on the other file, while scanning the -* to-be-compacted file, we need to skip it properly. Note -* that loops that are testing for changed lines on rchg* do -* not need index bounding since the array is prepared with -* a zero at position -1 and N. -*/ -for (; ix < nrec && !rchg[ix]; ix++) -while (rchgo[ixo++]); -if (ix == nrec) -break ; +long end; +}; + +/* +* Initialize g to point at the first group in xdf. +*/ +static void group_init (xdfile_t *xdf,struct group *g) +{ + g->start = g->end =0 ; +while (xdf->rchg[g->end]) + g->end++; +} + +/* +* Move g to describe the next (possibly empty) group in xdf and return 0. If g +* is already at the end of the file, do nothing and return -1. +*/ +static inline int group_next (xdfile_t *xdf,struct group *g) +{ +if (g->end == xdf->nrec) +return -1 ; + + g->start = g->end +1 ; +for (g->end = g->start; xdf->rchg[g->end]; g->end++) + ; + +return 0 ; +} + +/* +* Move g to describe the previous (possibly empty) group in xdf and return 0. +* If g is already at the beginning of the file, do nothing and return -1. +*/ +static inline int group_previous (xdfile_t *xdf,struct group *g) +{ +if (g->start ==0 ) +return -1 ; + + g->end = g->start -1 ; +for (g->start = g->end; xdf->rchg[g->start -1 ]; g->start--) + ; + +return 0 ; +} + +/* +* If g can be slid toward the end of the file, do so, and if it bumps into a +* following group, expand this group to include it. Return 0 on success or -1 +* if g cannot be slid down. +*/ +static int group_slide_down (xdfile_t *xdf,struct group *g,long flags) +{ +if (g->end < xdf->nrec && +recs_match (xdf->recs[g->start], xdf->recs[g->end], flags)) { + xdf->rchg[g->start++] =0 ; + xdf->rchg[g->end++] =1 ; + +while (xdf->rchg[g->end]) + g->end++; + +return 0 ; + }else { +return -1 ; + } +} + +/* +* If g can be slid toward the beginning of the file, do so, and if it bumps +* into a previous group, expand this group to include it. Return 0 on success +* or -1 if g cannot be slid up. +*/ +static int group_slide_up (xdfile_t *xdf,struct group *g,long flags) +{ +if (g->start >0 && +recs_match (xdf->recs[g->start -1 ], xdf->recs[g->end -1 ], flags)) { + xdf->rchg[--g->start] =1 ; + xdf->rchg[--g->end] =0 ; + +while (xdf->rchg[g->start -1 ]) + g->start--; + +return 0 ; + }else { +return -1 ; + } +} + +static void xdl_bug (const char *msg) +{ +fprintf (stderr,"BUG: %s\n" , msg); +exit (1 ); +} + +/* +* Move back and forward change groups for a consistent and pretty diff output. +* This also helps in finding joinable change groups and reducing the diff +* size. +*/ +int xdl_change_compact (xdfile_t *xdf,xdfile_t *xdfo,long flags) { +struct group g, go; +long earliest_end, end_matching_other; +long groupsize; +unsigned int blank_lines; + +group_init (xdf, &g); +group_init (xdfo, &go); + +while (1 ) { +/* If the group is empty in the to-be-compacted file, skip it: */ +if (g.end == g.start) + goto next;/* -* Record the start of a changed-group in the to-be-compacted file -* and find the end of it, on both to-be-compacted and other file -* indexes (ix and ixo). +* Now shift the change up and then down as far as possible in +* each direction. If it bumps into any other changes, merge them. */ - ixs = ix; -for (ix++; rchg[ix]; ix++); -for (; rchgo[ixo]; ixo++); -do { - grpsiz = ix - ixs; - blank_lines =0 ; + groupsize = g.end - g.start;/* -* If the line before the current change group, is equal to -* the last line of the current change group, shift backward -* the group. +* Keep track of the last "end" index that causes this +* group to align with a group of changed lines in the +* other file. -1 indicates that we haven't found such +* a match yet: */ -while (ixs >0 &&recs_match (recs[ixs -1 ], recs[ix -1 ], flags)) { - rchg[--ixs] =1 ; - rchg[--ix] =0 ; - -/* -* This change might have joined two change groups, -* so we try to take this scenario in account by moving -* the start index accordingly (and so the other-file -* end-of-group index). -*/ -for (; rchg[ixs -1 ]; ixs--); -while (rchgo[--ixo]); - } + end_matching_other =-1 ;/* -* Record the end-of-group position in case we are matched -* with a group of changes in the other file (that is, the -* change record before the end-of-group index in the other -* file is set). +* Boolean value that records whether there are any blank +* lines that could be made to be the last line of this +* group. */ - ixref = rchgo[ixo -1 ] ? ix: nrec; + blank_lines =0 ; + +/* Shift the group backward as much as possible: */ +while (!group_slide_up (xdf, &g, flags)) +if (group_previous (xdfo, &go)) +xdl_bug ("group sync broken sliding up" );/* -* If the first line of the current change group, is equal to -* the line next of the current change group, shift forward -* the group. +* This is this highest that this group can be shifted. +* Record its end index: */ -while (ix < nrec &&recs_match (recs[ixs], recs[ix], flags)) { - blank_lines +=is_blank_line (recs[ix], flags); - - rchg[ixs++] =0 ; - rchg[ix++] =1 ; - -/* -* This change might have joined two change groups, -* so we try to take this scenario in account by moving -* the start index accordingly (and so the other-file -* end-of-group index). Keep tracking the reference -* index in case we are shifting together with a -* corresponding group of changes in the other file. -*/ -for (; rchg[ix]; ix++); -while (rchgo[++ixo]) - ixref = ix; + earliest_end = g.end; + +if (go.end > go.start) + end_matching_other = g.end; + +/* Now shift the group forward as far as possible: */ +while (1 ) { +if (!blank_lines) + blank_lines =is_blank_line ( + xdf->recs[g.end -1 ], + flags); + +if (group_slide_down (xdf, &g, flags)) +break ; +if (group_next (xdfo, &go)) +xdl_bug ("group sync broken sliding down" ); + +if (go.end > go.start) + end_matching_other = g.end; } - }while (grpsiz != ix - ixs); + }while (groupsize != g.end - g.start); -if (ixref < ix) { +if (g.end == earliest_end) { +/* no shifting was possible */ + }else if (end_matching_other !=-1 ) {/* -* Try to move back the possibly merged group of changes, to match -* the recorded position in the other file. +* 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. */ -while (ixref < ix) { - rchg[--ixs] =1 ; - rchg[--ix] =0 ; -while (rchgo[--ixo]); +while (go.end == go.start) { +if (group_slide_up (xdf, &g, flags)) +xdl_bug ("match disappeared" ); +if (group_previous (xdfo, &go)) +xdl_bug ("group sync broken sliding to match" ); } }else if ((flags &XDF_COMPACTION_HEURISTIC ) && blank_lines) {/* -* The group can be slid up to make its last line a -* blank line. Do so. +* Compaction heuristic: if it is possible to shift the +* group to make its bottom line a blank line, do so. * * As we already shifted the group forward as far as -* possible in the earlier loop, we need to shift it -* back only if at all. +* possible in the earlier loop, we only need to handle +* backward shifts, not forward ones. */ -while (ixs >0 && - !is_blank_line (recs[ix -1 ], flags) && -recs_match (recs[ixs -1 ], recs[ix -1 ], flags)) { - rchg[--ixs] =1 ; - rchg[--ix] =0 ; -while (rchgo[--ixo]); +while (!is_blank_line (xdf->recs[g.end -1 ], flags)) { +if (group_slide_up (xdf, &g, flags)) +xdl_bug ("blank line disappeared" ); +if (group_previous (xdfo, &go)) +xdl_bug ("group sync broken sliding to blank line" ); } } + + next: +/* Move past the just-processed group: */ +if (group_next (xdf, &g)) +break ; +if (group_next (xdfo, &go)) +xdl_bug ("group sync broken moving to next group" ); } +if (!group_next (xdfo, &go)) +xdl_bug ("group sync broken at end of file" ); +return 0 ; }
Signature · 1 line
2.9.3
Re: [PATCH v2 5/7] xdl_change_compact(): introduce the concept of a change group
Quoted 17 lines · Michael Haggerty
The idea of
xdl_change_compact()is fairly simple:* Proceed through groups of changed lines in the file to be compacted, keeping track of the corresponding location in the "other" file.
* If possible, slide the group up and down to try to give the most aesthetically pleasing diff. Whenever it is slid, the current location in the other file needs to be adjusted.
But these simple concepts are obfuscated by a lot of index handling that is written in terse, subtle, and varied patterns. I found it very hard to convince myself that the function was correct.
So introduce a "struct group" that represents a group of changed lines in a file. Add some functions that perform elementary operations on groups:
Quoted 6 lines
* Initialize a group to the first group in a file
* Move to the next or previous group in a file
* Slide a group up or downEven though the resulting code is longer, I think it is easier to understand and review.
Yup. The important thing is that the length of the core logic of sliding up and down becomes easier to read, because it shrinks; the mechanics of sliding up and down may need more lines with boilderplate, but they are isolated "do one thing and do it well" helpers.
Nice.
PATCH v2 6/7 diff: improve positioning of add/delete blocks in diffs
In reply to Michael HaggertySome groups of added/deleted lines in diffs can be slid up or down, because lines at the edges of the group are not unique. Picking good shifts for such groups is not a matter of correctness but definitely has a big effect on aesthetics. For example, consider the following two diffs. The first is what standard Git emits:
--- a/9c572b21dd090a1e5c5bb397053bf8043ffe7fb4:git-send-email.perl
+++ b/6dcfa306f2b67b733a7eb2d7ded1bc9987809edb:git-send-email.perl
@@ -231,6 +231,9 @@ if (!defined $initial_reply_to && $prompting) {
}
if (!$smtp_server) {
+ $smtp_server = $repo->config('sendemail.smtpserver');
+}
+if (!$smtp_server) {
foreach (qw( /usr/sbin/sendmail /usr/lib/sendmail )) {
if (-x $_) {
$smtp_server = $_;The following diff is equivalent, but is obviously preferable from an aesthetic point of view:
--- a/9c572b21dd090a1e5c5bb397053bf8043ffe7fb4:git-send-email.perl
+++ b/6dcfa306f2b67b733a7eb2d7ded1bc9987809edb:git-send-email.perl
@@ -230,6 +230,9 @@ if (!defined $initial_reply_to && $prompting) {
$initial_reply_to =~ s/(^\s+|\s+$)//g;
}
+if (!$smtp_server) {
+ $smtp_server = $repo->config('sendemail.smtpserver');
+}
if (!$smtp_server) {
foreach (qw( /usr/sbin/sendmail /usr/lib/sendmail )) {
if (-x $_) {This patch teaches Git to pick better positions for such "diff sliders" using heuristics that take the positions of nearby blank lines and the indentation of nearby lines into account.
The existing Git code basically always shifts such "sliders" as far down in the file as possible. The only exception is when the slider can be aligned with a group of changed lines in the other file, in which case Git favors depicting the change as one add+delete block rather than one add and a slightly offset delete block. This naive algorithm often yields ugly diffs.
Commit d634d61ed6 improved the situation somewhat by preferring to position add/delete groups to make their last line a blank line, when that is possible. This heuristic does more good than harm, but (1) it can only help if there are blank lines in the right places, and (2) always picks the last blank line, even if there are others that might be better. The end result is that it makes perhaps 1/3 as many errors as the default Git algorithm, but that still leaves a lot of ugly diffs.
This commit implements a new and much better heuristic for picking optimal "slider" positions using the following approach: First observe that each hypothetical positioning of a diff slider introduces two
splits: one between the context lines preceding the group and the first added/deleted line, and the other between the last added/deleted line and the first line of context following it. It tries to find the positioning that creates the least bad splits.
Splits are evaluated based only on the presence and locations of nearby blank lines, and the indentation of lines near the split. Basically, it prefers to introduce splits adjacent to blank lines, between lines that are indented less, and between lines with the same level of indentation. In more detail:
1. It measures the following characteristics of a proposed splitting position in a struct split_measurement:
* the number of blank lines above the proposed split
* whether the line directly after the split is blank
* the number of blank lines following that line
* the indentation of the nearest non-blank line above the split
* the indentation of the line directly below the split
* the indentation of the nearest non-blank line after that line
2. It combines the measured attributes using a bunch of empirically-optimized weighting factors to derive a struct split_score that measures the "badness" of splitting the text at that position.
3. It combines the split_score for the top and the bottom of the slider at each of its possible positions, and selects the position that has the best split_score.
I determined the initial set of weighting factors by collecting a corpus of Git histories from 29 open-source software projects in various programming languages. I generated many diffs from this corpus, and determined the best positioning "by eye" for about 6600 diff sliders. I used about half of the repositories in the corpus (corresponding to about 2/3 of the sliders) as a training set, and optimized the weights against this corpus using a crude automated search of the parameter space to get the best agreement with the manually-determined values. Then I tested the resulting heuristic against the full corpus. The results are summarized in the following table, in column indent-1:
| repository | count | Git 2.9.0 | compaction | compaction-fixed | indent-1 | indent-2 | | --------------------- | ----- | -------------- | -------------- | ---------------- | -------------- | -------------- | | afnetworking | 109 | 89 (81.7%) | 37 (33.9%) | 37 (33.9%) | 2 (1.8%) | 2 (1.8%) | | alamofire | 30 | 18 (60.0%) | 14 (46.7%) | 15 (50.0%) | 0 (0.0%) | 0 (0.0%) | | angular | 184 | 127 (69.0%) | 39 (21.2%) | 23 (12.5%) | 5 (2.7%) | 5 (2.7%) | | animate | 313 | 2 (0.6%) | 2 (0.6%) | 2 (0.6%) | 2 (0.6%) | 2 (0.6%) | | ant | 380 | 356 (93.7%) | 152 (40.0%) | 148 (38.9%) | 15 (3.9%) | 15 (3.9%) | * | bugzilla | 306 | 263 (85.9%) | 109 (35.6%) | 99 (32.4%) | 14 (4.6%) | 15 (4.9%) | * | corefx | 126 | 91 (72.2%) | 22 (17.5%) | 21 (16.7%) | 6 (4.8%) | 6 (4.8%) | | couchdb | 78 | 44 (56.4%) | 26 (33.3%) | 28 (35.9%) | 6 (7.7%) | 6 (7.7%) | * | cpython | 937 | 158 (16.9%) | 50 (5.3%) | 49 (5.2%) | 5 (0.5%) | 5 (0.5%) | * | discourse | 160 | 95 (59.4%) | 42 (26.2%) | 36 (22.5%) | 18 (11.2%) | 13 (8.1%) | | docker | 307 | 194 (63.2%) | 198 (64.5%) | 253 (82.4%) | 8 (2.6%) | 8 (2.6%) | * | electron | 163 | 132 (81.0%) | 38 (23.3%) | 39 (23.9%) | 6 (3.7%) | 6 (3.7%) | | git | 536 | 470 (87.7%) | 73 (13.6%) | 78 (14.6%) | 16 (3.0%) | 16 (3.0%) | * | gitflow | 127 | 0 (0.0%) | 0 (0.0%) | 0 (0.0%) | 0 (0.0%) | 0 (0.0%) | | ionic | 133 | 89 (66.9%) | 29 (21.8%) | 38 (28.6%) | 1 (0.8%) | 1 (0.8%) | | ipython | 482 | 362 (75.1%) | 167 (34.6%) | 169 (35.1%) | 11 (2.3%) | 11 (2.3%) | * | junit | 161 | 147 (91.3%) | 67 (41.6%) | 66 (41.0%) | 1 (0.6%) | 1 (0.6%) | * | lighttable | 15 | 5 (33.3%) | 0 (0.0%) | 2 (13.3%) | 0 (0.0%) | 0 (0.0%) | | magit | 88 | 75 (85.2%) | 11 (12.5%) | 9 (10.2%) | 1 (1.1%) | 0 (0.0%) | | neural-style | 28 | 0 (0.0%) | 0 (0.0%) | 0 (0.0%) | 0 (0.0%) | 0 (0.0%) | | nodejs | 781 | 649 (83.1%) | 118 (15.1%) | 111 (14.2%) | 4 (0.5%) | 5 (0.6%) | * | phpmyadmin | 491 | 481 (98.0%) | 75 (15.3%) | 48 (9.8%) | 2 (0.4%) | 2 (0.4%) | * | react-native | 168 | 130 (77.4%) | 79 (47.0%) | 81 (48.2%) | 0 (0.0%) | 0 (0.0%) | | rust | 171 | 128 (74.9%) | 30 (17.5%) | 27 (15.8%) | 16 (9.4%) | 14 (8.2%) | | spark | 186 | 149 (80.1%) | 52 (28.0%) | 52 (28.0%) | 2 (1.1%) | 2 (1.1%) | | tensorflow | 115 | 66 (57.4%) | 48 (41.7%) | 48 (41.7%) | 5 (4.3%) | 5 (4.3%) | | test-more | 19 | 15 (78.9%) | 2 (10.5%) | 2 (10.5%) | 1 (5.3%) | 1 (5.3%) | * | test-unit | 51 | 34 (66.7%) | 14 (27.5%) | 8 (15.7%) | 2 (3.9%) | 2 (3.9%) | * | xmonad | 23 | 22 (95.7%) | 2 (8.7%) | 2 (8.7%) | 1 (4.3%) | 1 (4.3%) | * | --------------------- | ----- | -------------- | -------------- | ---------------- | -------------- | -------------- | | totals | 6668 | 4391 (65.9%) | 1496 (22.4%) | 1491 (22.4%) | 150 (2.2%) | 144 (2.2%) | | totals (training set) | 4552 | 3195 (70.2%) | 1053 (23.1%) | 1061 (23.3%) | 86 (1.9%) | 88 (1.9%) | | totals (test set) | 2116 | 1196 (56.5%) | 443 (20.9%) | 430 (20.3%) | 64 (3.0%) | 56 (2.6%) |
In this table, the numbers are the count and percentage of human-rated sliders that the corresponding algorithm got *wrong*. The columns are
* "repository" - the name of the repository used. I used the diffs between successive non-merge commits on the HEAD branch of the corresponding repository.
* "count" - the number of sliders that were human-rated. I chose most, but not all, sliders to rate from those among which the various algorithms gave different answers.
* "Git 2.9.0" - the default algorithm used by git diff in Git 2.9.0.
* "compaction" - the heuristic used by git diff --compaction-heuristic in Git 2.9.0.
* "compaction-fixed" - the heuristic used by git diff --compaction-heuristic after the fixes from earlier in this patch series. Note that the results are not dramatically different than those for "compaction". Both produce non-ideal diffs only about 1/3 as often as the default git diff.
* "indent-1" - the new --indent-heuristic algorithm, using the first set of weighting factors, determined as described above.
* "indent-2" - the new --indent-heuristic algorithm, using the final set of weighting factors, determined as described below.
* * - indicates that repo was part of training set used to determine the first set of weighting factors.
The fact that the heuristic performed nearly as well on the test set as on the training set in column "indent-1" is a good indication that the heuristic was not over-trained. Given that fact, I ran a second round of optimization, using the entire corpus as the training set. The resulting set of weights gave the results in column "indent-2". These are the weights included in this patch.
The final result gives consistently and significantly better results across the whole corpus than either git diff or git diff --compaction-heuristic. It makes only about 1/30 as many errors as the former and about 1/10 as many errors as the latter. (And a good fraction of the remaining errors are for diffs that involve weirdly-formatted code, sometimes apparently machine-generated.)
The tools that were used to do this optimization and analysis, along with the human-generated data values, are recorded in a separate project [1].
This patch adds a new command-line option --indent-heuristic, and a new configuration setting diff.indentHeuristic, that activate this heuristic. This interface is only meant for testing purposes, and should be finalized before including this change in any release.
[1] https://github.com/mhagger/diff-slider-tools
--- Documentation/diff-config.txt | 7 +- Documentation/diff-options.txt | 9 +- diff.c | 23 ++- git-add--interactive.perl | 5 +- xdiff/xdiff.h | 1 + xdiff/xdiffi.c | 325 +++++++++++++++++++++++++++++++++++++++++ 6 files changed, 359 insertions(+), 11 deletions(-)
Documentation/diff-config.txt +4 -3
@@ -170,10 +170,11 @@ diff.tool:: include::mergetools-diff.txt[] +diff.indentHeuristic:: diff.compactionHeuristic:: - Set this option to `true` to enable an experimental heuristic that - shifts the hunk boundary in an attempt to make the resulting - patch easier to read. + Set one of these options to `true` to enable one of two + experimental heuristics that shift diff hunk boundaries to + make patches easier to read. diff.algorithm:: Choose a diff algorithm. The variants are as follows:
Documentation/diff-options.txt +5 -4
@@ -63,12 +63,13 @@ ifndef::git-format-patch[] Synonym for `-p --raw`. endif::git-format-patch[] +--indent-heuristic:: +--no-indent-heuristic:: --compaction-heuristic:: --no-compaction-heuristic:: - These are to help debugging and tuning an experimental - heuristic (which is off by default) that shifts the hunk - boundary in an attempt to make the resulting patch easier - to read. + These are to help debugging and tuning experimental heuristics + (which are off by default) that shift diff hunk boundaries to + make patches easier to read. --minimal:: Spend extra time to make sure the smallest possible
diff.c +20 -3
@@ -26,6 +26,7 @@#endif static int diff_detect_rename_default; +static int diff_indent_heuristic;/* experimental */ static int diff_compaction_heuristic;/* experimental */ static int diff_rename_limit_default =400 ;static int diff_suppress_blank_empty;
@@ -190,8 +191,16 @@ int git_diff_ui_config(const char *var, const char *value, void *cb) diff_detect_rename_default =git_config_rename (var, value);return 0 ; } +if (!strcmp (var,"diff.indentheuristic" )) { + diff_indent_heuristic =git_config_bool (var, value); +if (diff_indent_heuristic) + diff_compaction_heuristic =0 ; +return 0 ; + }if (!strcmp (var,"diff.compactionheuristic" )) { diff_compaction_heuristic =git_config_bool (var, value); +if (diff_compaction_heuristic) + diff_indent_heuristic =0 ;return 0 ; }if (!strcmp (var,"diff.autorefreshindex" )) {
@@ -3293,7 +3302,9 @@ void diff_setup(struct diff_options *options) options->use_color = diff_use_color_default; options->detect_rename = diff_detect_rename_default; options->xdl_opts |= diff_algorithm; -if (diff_compaction_heuristic) +if (diff_indent_heuristic) +DIFF_XDL_SET (options,INDENT_HEURISTIC ); +else if (diff_compaction_heuristic)DIFF_XDL_SET (options,COMPACTION_HEURISTIC ); options->orderfile = diff_order_file_cfg;
@@ -3815,9 +3826,15 @@ int diff_opt_parse(struct diff_options *options,DIFF_XDL_SET (options,IGNORE_WHITESPACE_AT_EOL ); else if (!strcmp(arg, "--ignore-blank-lines"))DIFF_XDL_SET (options,IGNORE_BLANK_LINES ); - else if (!strcmp(arg, "--compaction-heuristic")) + else if (!strcmp(arg, "--indent-heuristic")) { +DIFF_XDL_SET (options,INDENT_HEURISTIC ); +DIFF_XDL_CLR (options,COMPACTION_HEURISTIC ); + } else if (!strcmp(arg, "--no-indent-heuristic")) +DIFF_XDL_CLR (options,INDENT_HEURISTIC ); + else if (!strcmp(arg, "--compaction-heuristic")) {DIFF_XDL_SET (options,COMPACTION_HEURISTIC ); - else if (!strcmp(arg, "--no-compaction-heuristic")) +DIFF_XDL_CLR (options,INDENT_HEURISTIC ); + } else if (!strcmp(arg, "--no-compaction-heuristic"))DIFF_XDL_CLR (options,COMPACTION_HEURISTIC ); else if (!strcmp(arg, "--patience")) options->xdl_opts =DIFF_WITH_ALG (options,PATIENCE_DIFF );
git-add--interactive.perl +4 -1
@@ -45,6 +45,7 @@ my ($diff_new_color) = my $normal_color = $repo->get_color("", "reset"); my $diff_algorithm = $repo->config('diff.algorithm'); +my $diff_indent_heuristic = $repo->config_bool('diff.indentheuristic'); my $diff_compaction_heuristic = $repo->config_bool('diff.compactionheuristic'); my $diff_filter = $repo->config('interactive.difffilter');
@@ -750,7 +751,9 @@ sub parse_diff { if (defined $diff_algorithm) { splice @diff_cmd, 1, 0, "--diff-algorithm=${diff_algorithm}"; } - if ($diff_compaction_heuristic) { + if ($diff_indent_heuristic) { + splice @diff_cmd, 1, 0, "--indent-heuristic"; + } elsif ($diff_compaction_heuristic) { splice @diff_cmd, 1, 0, "--compaction-heuristic"; } if (defined $patch_mode_revision) {
xdiff/xdiff.h +1 -0
@@ -42,6 +42,7 @@ extern "C" {#define XDF_IGNORE_BLANK_LINES (1 << 7)#define XDF_COMPACTION_HEURISTIC (1 << 8) +#define XDF_INDENT_HEURISTIC (1 << 9)#define XDL_EMIT_FUNCNAMES (1 << 0)#define XDL_EMIT_FUNCCONTEXT (1 << 2)
xdiff/xdiffi.c +325 -0
343 lines not on this page · raw message
Signature · 1 line
2.9.3
PATCH v2 7/7 blame: actually use the diff opts parsed from the command line
In reply to Michael Haggerty"git blame" already parsed generic diff options from the command line via diff_opt_parse(), but instead of passing the resulting xdl_opts to xdi_diff(), it sent its own xdl_opts, which only reflected the values of the self-parsed options "-w" and "--minimal". Instead, rely on diff_opt_parse() to parse all of the diff options, including "-w" and "--minimal", and pass the resulting xdl_opts to xdi_diff().
---
Somebody who knows more about how diff operations are configured should please review this. I'm not certain that the change as implemented won't have other unwanted side-effects, though of course I checked that the test suite runs correctly.
builtin/blame.c | 11 ++-- t/t4059-diff-indent.sh | 160 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 165 insertions(+), 6 deletions(-) create mode 100755 t/t4059-diff-indent.sh
builtin/blame.c +5 -6
@@ -48,11 +48,12 @@ static int show_root;static int reverse;static int blank_boundary;static int incremental; -static int xdl_opts;static int abbrev =-1 ;static int no_whole_file_rename;static int show_progress; +static struct rev_info revs; +static struct date_mode blame_date_mode = {DATE_ISO8601 };static size_t blame_date_width;
@@ -137,11 +138,12 @@ struct progress_info {static int diff_hunks (mmfile_t *file_a,mmfile_t *file_b,xdl_emit_hunk_consume_func_t hunk_func,void *cb_data) { -xpparam_t xpp = {0 }; +xpparam_t xpp;xdemitconf_t xecfg = {0 };xdemitcb_t ecb = {NULL }; - xpp.flags = xdl_opts; +memset (&xpp,0 ,sizeof (xpp)); + xpp.flags = revs.diffopt.xdl_opts; xecfg.hunk_func = hunk_func; ecb.priv = cb_data;return xdi_diff (file_a, file_b, &xpp, &xecfg, &ecb);
@@ -2517,7 +2519,6 @@ static int blame_move_callback(const struct option *option, const char *arg, intint cmd_blame (int argc,const char **argv,const char *prefix) { -struct rev_info revs;const char *path;struct scoreboard sb;struct origin *o;
@@ -2548,8 +2549,6 @@ int cmd_blame(int argc, const char **argv, const char *prefix)OPT_BIT ('l' ,NULL , &output_option,N_ ("Show long commit SHA1 (Default: off)" ),OUTPUT_LONG_OBJECT_NAME ),OPT_BIT ('s' ,NULL , &output_option,N_ ("Suppress author name and timestamp (Default: off)" ),OUTPUT_NO_AUTHOR ),OPT_BIT ('e' ,"show-email" , &output_option,N_ ("Show author email instead of name (Default: off)" ),OUTPUT_SHOW_EMAIL ), -OPT_BIT ('w' ,NULL , &xdl_opts,N_ ("Ignore whitespace differences" ),XDF_IGNORE_WHITESPACE ), -OPT_BIT (0 ,"minimal" , &xdl_opts,N_ ("Spend extra cycles to find better match" ),XDF_NEED_MINIMAL ),OPT_STRING ('S' ,NULL , &revs_file,N_ ("file" ),N_ ("Use revisions from <file> instead of calling git-rev-list" )),OPT_STRING (0 ,"contents" , &contents_from,N_ ("file" ),N_ ("Use <file>'s contents as the final image" )), { OPTION_CALLBACK, 'C', NULL, &opt, N_("score"), N_("Find line copies within and across files"), PARSE_OPT_OPTARG, blame_copy_callback },
t/t4059-diff-indent.sh new file +160 -0
160 lines not on this page · raw message
Signature · 1 line
2.9.3
Re: [PATCH v2 7/7] blame: actually use the diff opts parsed from the command line
Am 22.08.2016 um 13:22 schrieb Michael Haggerty:
Quoted 6 lines
"git blame" already parsed generic diff options from the command line via
diff_opt_parse(), but instead of passing the resultingxdl_optstoxdi_diff(), it sent its ownxdl_opts, which only reflected the values of the self-parsed options "-w" and "--minimal". Instead, rely ondiff_opt_parse()to parse all of the diff options, including "-w" and "--minimal", and pass the resultingxdl_optstoxdi_diff().
Sounds useful: It allows more fine-grained control over which whitespace changes to ignore and which diff algorithm to use. There is a bit of overlap (e.g. with -b meaning show blank boundaries vs. ignore whitespace changes), but with your patch blame's own options still take precedence, so there should be no unpleasant surprises.
Quoted 6 lines
Signed-off-by Michael Haggerty <[email protected]>---
Somebody who knows more about how diff operations are configured should please review this. I'm not certain that the change as implemented won't have other unwanted side-effects, though of course I checked that the test suite runs correctly.
I don't qualify, but I'll comment anyway..
Quoted 4 lines
builtin/blame.c | 11 ++-- t/t4059-diff-indent.sh | 160 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 165 insertions(+), 6 deletions(-) create mode 100755 t/t4059-diff-indent.sh
This new test doesn't call git blame. Does it belong to a different commit? And shouldn't the change to blame.c stand on its own, outside of this series?
Quoted 20 lines
builtin/blame.c +2
-1@@ -48,11 +48,12 @@ static int show_root;static int reverse;static int blank_boundary;static int incremental; -static int xdl_opts;static int abbrev =-1 ;static int no_whole_file_rename;static int show_progress; +static struct rev_info revs; +static struct date_mode blame_date_mode = {DATE_ISO8601 };static size_t blame_date_width;@@ -137,11 +138,12 @@ struct progress_info { static int diff_hunks(mmfile_t *file_a, mmfile_t *file_b,
builtin/blame.c struct progress_info { · line 146
Quoted 10 lines
builtin/blame.c +3
-2xdl_emit_hunk_consume_func_t hunk_func, void *cb_data) { - xpparam_t xpp = {0}; + xpparam_t xpp;xdemitconf_t xecfg = {0 };xdemitcb_t ecb = {NULL }; - xpp.flags = xdl_opts; +memset (&xpp,0 ,sizeof (xpp)); + xpp.flags = revs.diffopt.xdl_opts;
Why call memset instead of using a static initializer? The intent of this patch is just to change the .flags assignment, isn't it?
Quoted 12 lines
builtin/blame.c +0
-1xecfg.hunk_func = hunk_func; ecb.priv = cb_data;return xdi_diff (file_a, file_b, &xpp, &xecfg, &ecb);@@ -2517,7 +2519,6 @@ static int blame_move_callback(const struct option *option, const char *arg, intint cmd_blame (int argc,const char **argv,const char *prefix) { -struct rev_info revs;const char *path;struct scoreboard sb;struct origin *o;
builtin/blame.c int cmd_blame(int argc, const char **argv, const char *prefix) · line 2552
Quoted 5 lines
builtin/blame.c +0
-2@@ -2548,8 +2549,6 @@ int cmd_blame(int argc, const char **argv, const char *prefix)OPT_BIT ('l' ,NULL , &output_option,N_ ("Show long commit SHA1 (Default: off)" ),OUTPUT_LONG_OBJECT_NAME ),OPT_BIT ('s' ,NULL , &output_option,N_ ("Suppress author name and timestamp (Default: off)" ),OUTPUT_NO_AUTHOR ), OPT_BIT('e', "show-email", &output_option, N_("Show author email instead of name (Default: off)"), OUTPUT_SHOW_EMAIL), -OPT_BIT ('w' ,NULL , &xdl_opts,N_ ("Ignore whitespace differences" ),XDF_IGNORE_WHITESPACE ), - OPT_BIT(0, "minimal", &xdl_opts, N_("Spend extra cycles to find better match"), XDF_NEED_MINIMAL),
This removes -w and --minimal from blame's short help; diff options should be mentioned somehow in exchange for that loss. Or perhaps they should be mentioned in git-rev-list(1)? (git blame -h points to git-rev-list(1) already.)
Documentation/git-blame.txt needs an update as well.
Quoted 3 lines
builtin/blame.c +0
-0OPT_STRING ('S' ,NULL , &revs_file,N_ ("file" ),N_ ("Use revisions from <file> instead of calling git-rev-list" )),OPT_STRING (0 ,"contents" , &contents_from,N_ ("file" ),N_ ("Use <file>'s contents as the final image" )), { OPTION_CALLBACK, 'C', NULL, &opt, N_("score"), N_("Find line copies within and across files"), PARSE_OPT_OPTARG, blame_copy_callback },
Re: [PATCH v2 7/7] blame: actually use the diff opts parsed from the command line
Quoted 29 lines · René Scharfe
Am 22.08.2016 um 13:22 schrieb Michael Haggerty:
Quoted 6 lines
"git blame" already parsed generic diff options from the command line via
diff_opt_parse(), but instead of passing the resultingxdl_optstoxdi_diff(), it sent its ownxdl_opts, which only reflected the values of the self-parsed options "-w" and "--minimal". Instead, rely ondiff_opt_parse()to parse all of the diff options, including "-w" and "--minimal", and pass the resultingxdl_optstoxdi_diff().Sounds useful: It allows more fine-grained control over which whitespace changes to ignore and which diff algorithm to use. There is a bit of overlap (e.g. with -b meaning show blank boundaries vs. ignore whitespace changes), but with your patch blame's own options still take precedence, so there should be no unpleasant surprises.
Quoted 6 lines
Signed-off-by Michael Haggerty <[email protected]>---
Somebody who knows more about how diff operations are configured should please review this. I'm not certain that the change as implemented won't have other unwanted side-effects, though of course I checked that the test suite runs correctly.I don't qualify, but I'll comment anyway..
Quoted 5 lines
builtin/blame.c | 11 ++-- t/t4059-diff-indent.sh | 160 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 165 insertions(+), 6 deletions(-) create mode 100755 t/t4059-diff-indent.sh
Quoted 3 lines
This new test doesn't call git blame. Does it belong to a different commit? And shouldn't the change to
blame.cstand on its own, outside of this series?
Thanks for catching this. I squashed the test incorrectly; it was meant to be part of the previous commit. I'll fix it in v3.
The reason that I would prefer to change blame as part of this patch series is that I think it would be disconcerting for git diff and git blame to use different heuristics when computing diffs. It would make their output inconsistent.
But probably that means passing just the new option to git blame rather than passing all possible diff options through.
Quoted 1 line
[...]
Your other comments may be moot given that changing git blame in this way seems to have been a bad idea in the first place [1]. But I'll keep them in mind if a new version contains similar code.
Thanks,
Michael
Re: [PATCH v2 7/7] blame: actually use the diff opts parsed from the command line
Quoted 4 lines · Michael Haggerty
The reason that I would prefer to change
blameas part of this patch series is that I think it would be disconcerting forgit diffandgit blameto use different heuristics when computing diffs. It would make their output inconsistent.
I do think it is the right thing to do. With your shifting heuristics, "git diff" would attribute an addition of a whole block more correctly, e.g.
}
+foo {
+ bar
+ baz
+}instead of attributing the tail of the new thing to the old author, and the "blame" should take advantage of the better heuristics as well.
Re: [PATCH v2 7/7] blame: actually use the diff opts parsed from the command line
In reply to Michael HaggertyQuoted 4 lines · Michael Haggerty
Somebody who knows more about how diff operations are configured should please review this. I'm not certain that the change as implemented won't have other unwanted side-effects, though of course I checked that the test suite runs correctly.
Generally, I think this is a bad idea.
We run "diff-tree" internally for multiple purposes:
- When the same path we are drilling down appears, we use diff-tree to compare that path alone, to obtain textual diff, so that we can say "what did not appear in the textual diff output are attributable to the parent commit, we can exonerate this child".
Even if the command line to "git blame" had "-Sfoo", you do not want to pass it down to this invocation. If the child did not change the number of occurrences of string "foo" in the path being drilled down, it will pass down the blame for all lines to the parent. And copying -R from the command line to this invocation does not make any sense. Copying -C -C from the command line will defeat the whole purpose of having find_origin vs find_rename distinction, I would think, for this step.
- When the path we have been drilling down for no longer appears in the parent, we use diff-tree with rename detection to find where the file _could_ have come from.
It is a good idea to allow the similarity threshold to be tweaked from the command line. Copying -R from the command line to this invocation may make sense, but I think you would break this step if you copied -C/-C -C
- When -C/-C -C/-C -C -C is given from the command line, after running the "pass the blame to our primary suspect", we run tree-level diff-tree with find-copy option, only to enumerate paths that appear in the parent. We pick pieces from these paths by doing blob-level diffs in diff_hunks() ourselves. Copying -C from the command line would be useful if you are only doing a single '-C' (because it would allow you to tweak the similarity threshold), but otherwise it would probably break the command.
The design principle taken in "git blame" is to leave the decision on what diff options do or do not make sense for these particular invocations of the internal "diff-tree", and have "blame" give the selected options to the internal "diff-tree" invocations. "-w" happens to use xdl_opts that will eventually be passed down to diff machinery but you should consider it an accidental optimization (i.e. "blame" designer considered that it makes sense to give whitespace-ignoring comparison at the leaf-level "diff between blobs", and found that the most efficient way to do so is to just take "-w" from the command line and set the ignore-whitespace bit used to drive the internal "diff-tree" invocation). If we _were_ adding -R<num> to allow the users to affect similarity threshold, we would parse it in "git blame" command line option processing, and would give that _ONLY_ to the second invocation above, i.e. the one done in find_rename() but not in others like in find_origin.