xdiff: implement empty line chunk heuristic

In order to produce the smallest possible diff and combine several diff hunks together, we implement a heuristic from GNU Diff which moves diff hunks forward as far as possible when we find common context above and below a diff hunk. This sometimes produces less readable diffs when writing C, Shell, or other programming languages, ie: ... /* + * + * + */ + +/* ... instead of the more readable equivalent of ... +/* + * + * + */ + /* ... Implement the following heuristic to (optionally) produce the desired output. If there are diff chunks which can be shifted around, shift each hunk such that the last common empty line is below the chunk with the rest of the context above. This heuristic appears to resolve the above example and several other common issues without producing significantly weird results. However, as with any heuristic it is not really known whether this will always be more optimal. Thus, it can be disabled via diff.compactionHeuristic. Signed-off-by: Stefan Beller <sbeller@google.com> Signed-off-by: Jacob Keller <jacob.e.keller@intel.com> Signed-off-by: Stefan Beller <sbeller@google.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Stefan Beller committed Apr 19, 2016 at 08:21 UTC d634d61ed6c5f19948937012b5e3a0ed2d631d3f
5 files changed +50
Documentation/diff-config.txt
+5
@@ -166,6 +166,11 @@ diff.tool::
166
167 include::mergetools-diff.txt[]
168
169 +diff.compactionHeuristic::
170 + Set this option to enable an experimental heuristic that
171 + shifts the hunk boundary in an attempt to make the resulting
172 + patch easier to read.
173 +
174 diff.algorithm::
175 Choose a diff algorithm. The variants are as follows:
176 +
Documentation/diff-options.txt
+6
@@ -63,6 +63,12 @@ ifndef::git-format-patch[]
63 Synonym for `-p --raw`.
64 endif::git-format-patch[]
65
66 +--compaction-heuristic::
67 +--no-compaction-heuristic::
68 + These are to help debugging and tuning an experimental
69 + heuristic that shifts the hunk boundary in an attempt to
70 + make the resulting patch easier to read.
71 +
72 --minimal::
73 Spend extra time to make sure the smallest possible
74 diff is produced.
diff.c
+11
@@ -25,6 +25,7 @@
25 #endif
26
27 static int diff_detect_rename_default;
28 +static int diff_compaction_heuristic = 1;
29 static int diff_rename_limit_default = 400;
30 static int diff_suppress_blank_empty;
31 static int diff_use_color_default = -1;
@@ -183,6 +184,10 @@ int git_diff_ui_config(const char *var, const char *value, void *cb)
184 diff_detect_rename_default = git_config_rename(var, value);
185 return 0;
186 }
187 + if (!strcmp(var, "diff.compactionheuristic")) {
188 + diff_compaction_heuristic = git_config_bool(var, value);
189 + return 0;
190 + }
191 if (!strcmp(var, "diff.autorefreshindex")) {
192 diff_auto_refresh_index = git_config_bool(var, value);
193 return 0;
@@ -3235,6 +3240,8 @@ void diff_setup(struct diff_options *options)
3240 options->use_color = diff_use_color_default;
3241 options->detect_rename = diff_detect_rename_default;
3242 options->xdl_opts |= diff_algorithm;
3243 + if (diff_compaction_heuristic)
3244 + DIFF_XDL_SET(options, COMPACTION_HEURISTIC);
3245
3246 options->orderfile = diff_order_file_cfg;
3247
@@ -3712,6 +3719,10 @@ int diff_opt_parse(struct diff_options *options, const char **av, int ac)
3719 DIFF_XDL_SET(options, IGNORE_WHITESPACE_AT_EOL);
3720 else if (!strcmp(arg, "--ignore-blank-lines"))
3721 DIFF_XDL_SET(options, IGNORE_BLANK_LINES);
3722 + else if (!strcmp(arg, "--compaction-heuristic"))
3723 + DIFF_XDL_SET(options, COMPACTION_HEURISTIC);
3724 + else if (!strcmp(arg, "--no-compaction-heuristic"))
3725 + DIFF_XDL_CLR(options, COMPACTION_HEURISTIC);
3726 else if (!strcmp(arg, "--patience"))
3727 options->xdl_opts = DIFF_WITH_ALG(options, PATIENCE_DIFF);
3728 else if (!strcmp(arg, "--histogram"))
xdiff/xdiff.h
+2
@@ -41,6 +41,8 @@ extern "C" {
41
42 #define XDF_IGNORE_BLANK_LINES (1 << 7)
43
44 +#define XDF_COMPACTION_HEURISTIC (1 << 8)
45 +
46 #define XDL_EMIT_FUNCNAMES (1 << 0)
47 #define XDL_EMIT_COMMON (1 << 1)
48 #define XDL_EMIT_FUNCCONTEXT (1 << 2)
xdiff/xdiffi.c
+26
@@ -400,6 +400,11 @@ static xdchange_t *xdl_add_change(xdchange_t *xscr, long i1, long i2, long chg1,
400 }
401
402
403 +static int is_blank_line(xrecord_t **recs, long ix, long flags)
404 +{
405 + return xdl_blankline(recs[ix]->ptr, recs[ix]->size, flags);
406 +}
407 +
408 static int recs_match(xrecord_t **recs, long ixs, long ix, long flags)
409 {
410 return (recs[ixs]->ha == recs[ix]->ha &&
@@ -411,6 +416,7 @@ static int recs_match(xrecord_t **recs, long ixs, long ix, long flags)
416 int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) {
417 long ix, ixo, ixs, ixref, grpsiz, nrec = xdf->nrec;
418 char *rchg = xdf->rchg, *rchgo = xdfo->rchg;
419 + unsigned int blank_lines;
420 xrecord_t **recs = xdf->recs;
421
422 /*
@@ -444,6 +450,7 @@ int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) {
450
451 do {
452 grpsiz = ix - ixs;
453 + blank_lines = 0;
454
455 /*
456 * If the line before the current change group, is equal to
@@ -478,6 +485,8 @@ int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) {
485 * the group.
486 */
487 while (ix < nrec && recs_match(recs, ixs, ix, flags)) {
488 + blank_lines += is_blank_line(recs, ix, flags);
489 +
490 rchg[ixs++] = 0;
491 rchg[ix++] = 1;
492
@@ -504,6 +513,23 @@ int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) {
513 rchg[--ix] = 0;
514 while (rchgo[--ixo]);
515 }
516 +
517 + /*
518 + * If a group can be moved back and forth, see if there is a
519 + * blank line in the moving space. If there is a blank line,
520 + * make sure the last blank line is the end of the group.
521 + *
522 + * As we already shifted the group forward as far as possible
523 + * in the earlier loop, we need to shift it back only if at all.
524 + */
525 + if ((flags & XDF_COMPACTION_HEURISTIC) && blank_lines) {
526 + while (ixs > 0 &&
527 + !is_blank_line(recs, ix - 1, flags) &&
528 + recs_match(recs, ixs - 1, ix - 1, flags)) {
529 + rchg[--ixs] = 1;
530 + rchg[--ix] = 0;
531 + }
532 + }
533 }
534
535 return 0;