{"thread":{"id":"44005","subject":"[PATCH v3 1/8] xdl_change_compact(): fix compaction heuristic to adjust ixo","startedAt":"2016-09-05T09:45:37Z","lastAt":"2016-09-20T06:33:52Z","messageCount":16,"participants":["Michael Haggerty","Junio C Hamano","Jacob Keller"],"isPatch":true,"patchVersion":3,"patchTotal":8},"messages":[{"id":"301083","messageId":"119c03e74c022ccdd6ebd8661a98b43917f8e966.1473068229.git.mhagger@alum.mit.edu","threadId":"44005","inReplyTo":"cover.1473068229.git.mhagger@alum.mit.edu","subject":"[PATCH v3 1/8] xdl_change_compact(): fix compaction heuristic to adjust ixo","fromName":"Michael Haggerty","fromEmail":"mhagger@alum.mit.edu","sentAt":"2016-09-05T09:44:46Z","receivedAt":"2016-09-05T09:45:37Z","isPatch":true,"sender":{"key":"mhagger@alum.mit.edu","avatar":"https://avatars.githubusercontent.com/u/119718?v=4"},"body":"The code branch used for the compaction heuristic forgot to keep ixo in\nsync while the group was shifted. This is certainly wrong, as it causes\nthe two counters to get out of sync.\n\nI *think* that this bug could also have caused the function to read past\nthe end of the rchgo array, though I haven't done the work to prove it\nfor sure. Here is my reasoning:\n\nIf ixo is not decremented correctly during one iteration of the outer\nwhile loop, then it will loose sync with the ix counter. In particular,\nixo will be too large.\n\nSuppose that the next iterations of the outer while loop (i.e.,\nprocessing the next block of add/delete lines) don't have any sliders.\nThen the ixo counter would be incremented by the number of non-changed\nlines in xdf, which is the same as the number of non-changed lines in\nxdfo that *should have* followed the group that experienced the\nmalfunction. But since ixo was too large at the end of that iteration,\nit will be incremented past the end of the xdfo->rchg array, and will\ntry to read that memory illegally.\n\nSigned-off-by: Michael Haggerty <mhagger@alum.mit.edu>\n---\n xdiff/xdiffi.c | 1 +\n 1 file changed, 1 insertion(+)\n\ndiff --git a/xdiff/xdiffi.c b/xdiff/xdiffi.c\nindex b3c6848..95b037e 100644\n--- a/xdiff/xdiffi.c\n+++ b/xdiff/xdiffi.c\n@@ -528,6 +528,7 @@ int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) {\n \t\t\t       recs_match(recs, ixs - 1, ix - 1, flags)) {\n \t\t\t\trchg[--ixs] = 1;\n \t\t\t\trchg[--ix] = 0;\n+\t\t\t\twhile (rchgo[--ixo]);\n \t\t\t}\n \t\t}\n \t}\n-- \n2.9.3\n\n"},{"id":"301084","messageId":"cover.1473068229.git.mhagger@alum.mit.edu","threadId":"44005","inReplyTo":null,"subject":"[PATCH v3 0/8] Better heuristics make prettier diffs","fromName":"Michael Haggerty","fromEmail":"mhagger@alum.mit.edu","sentAt":"2016-09-05T09:44:45Z","receivedAt":"2016-09-05T09:45:41Z","isPatch":true,"sender":{"key":"mhagger@alum.mit.edu","avatar":"https://avatars.githubusercontent.com/u/119718?v=4"},"body":"This is v3 of the diff-indent-heuristic patch series. Thanks to René,\nJunio, and Ramsay for their comments about v2 [1,2].\n\nThe heuristic itself hasn't changed since v2. Most of the changes are\nin the tests and in how `git blame` is wired up to support the new\noptions. Changes since v1:\n\n* In \"diff: improve positioning of add/delete blocks in diffs\":\n\n  * Make `score_cmp()` static.\n\n  * Add test t4059 as part of this commit, not as part of its\n    successor.\n\n  * t4059: Create commits to use for the tests (rather than diffing\n    loose files with `--no-cache`), so that the same prep can be used\n    to test `git blame`. Split the tests into multiple smaller tests.\n\n  * Add a test that `--no-indent-heuristic` overrides the config.\n\n* Add a new commit \"parse-options: add parse_opt_unknown_cb()\":\n\n  * Allow \"-h\" help text to be generated for options even if they are\n    being handled by code external to `parse_options()`.\n\n  If there's already a way to do this, I didn't find it.\n\n* In \"blame: honor the diff heuristic options and config\":\n\n  * In v2, I suggested making `blame` honor all diff-related options.\n    Junio explained why this was a bad idea. So this version only\n    makes `blame` honor `--indent-heuristic` and\n    `--compaction-heuristic`.\n\n  * Add some smoke tests.\n\nThis patch series is also available from my GitHub fork [3], branch\n\"diff-indent-heuristic\"\n\n[1] http://public-inbox.org/git/cover.1471864378.git.mhagger@alum.mit.edu/\n[2] http://public-inbox.org/git/a27aa17e-f602-fc49-92b3-2198e4772e47@ramsayjones.plus.com/\n[3] https://github.com/mhagger/git\n\nMichael Haggerty (8):\n  xdl_change_compact(): fix compaction heuristic to adjust ixo\n  xdl_change_compact(): only use heuristic if group can't be matched\n  is_blank_line(): take a single xrecord_t as argument\n  recs_match(): take two xrecord_t pointers as arguments\n  xdl_change_compact(): introduce the concept of a change group\n  diff: improve positioning of add/delete blocks in diffs\n  parse-options: add parse_opt_unknown_cb()\n  blame: honor the diff heuristic options and config\n\n Documentation/diff-config.txt            |   7 +-\n Documentation/diff-heuristic-options.txt |   7 +\n Documentation/diff-options.txt           |   7 +-\n Documentation/git-annotate.txt           |   1 +\n Documentation/git-blame.txt              |   2 +\n builtin/blame.c                          |  12 +\n diff.c                                   |  36 +-\n diff.h                                   |   1 +\n git-add--interactive.perl                |   5 +-\n parse-options-cb.c                       |  12 +\n parse-options.h                          |   1 +\n t/t4059-diff-indent.sh                   | 216 +++++++++++\n xdiff/xdiff.h                            |   1 +\n xdiff/xdiffi.c                           | 635 ++++++++++++++++++++++++++-----\n 14 files changed, 828 insertions(+), 115 deletions(-)\n create mode 100644 Documentation/diff-heuristic-options.txt\n create mode 100755 t/t4059-diff-indent.sh\n\n-- \n2.9.3\n\n"},{"id":"301085","messageId":"963d6e9bce6348af0d5a7ad1284b64e97d28a2a2.1473068229.git.mhagger@alum.mit.edu","threadId":"44005","inReplyTo":"cover.1473068229.git.mhagger@alum.mit.edu","subject":"[PATCH v3 7/8] parse-options: add parse_opt_unknown_cb()","fromName":"Michael Haggerty","fromEmail":"mhagger@alum.mit.edu","sentAt":"2016-09-05T09:44:52Z","receivedAt":"2016-09-05T09:45:44Z","isPatch":true,"sender":{"key":"mhagger@alum.mit.edu","avatar":"https://avatars.githubusercontent.com/u/119718?v=4"},"body":"Add a new callback function, parse_opt_unknown_cb(), which returns -2 to\nindicate that the corresponding option is unknown. This can be used to\nadd \"-h\" documentation for an option that will be handled externally to\nparse_options().\n\nSigned-off-by: Michael Haggerty <mhagger@alum.mit.edu>\n---\nSee the next patch for the reason I implemented this change. Let me\nknow if there is an easier way to do this.\n\n parse-options-cb.c | 12 ++++++++++++\n parse-options.h    |  1 +\n 2 files changed, 13 insertions(+)\n\ndiff --git a/parse-options-cb.c b/parse-options-cb.c\nindex 9667bc7..b5d9209 100644\n--- a/parse-options-cb.c\n+++ b/parse-options-cb.c\n@@ -159,6 +159,18 @@ int parse_opt_noop_cb(const struct option *opt, const char *arg, int unset)\n }\n \n /**\n+ * Report that the option is unknown, so that other code can handle\n+ * it. This can be used as a callback together with\n+ * OPTION_LOWLEVEL_CALLBACK to allow an option to be documented in the\n+ * \"-h\" output even if it's not being handled directly by\n+ * parse_options().\n+ */\n+int parse_opt_unknown_cb(const struct option *opt, const char *arg, int unset)\n+{\n+\treturn -2;\n+}\n+\n+/**\n  * Recreates the command-line option in the strbuf.\n  */\n static int recreate_opt(struct strbuf *sb, const struct option *opt,\ndiff --git a/parse-options.h b/parse-options.h\nindex 78f8384..dcd8a09 100644\n--- a/parse-options.h\n+++ b/parse-options.h\n@@ -228,6 +228,7 @@ extern int parse_opt_commits(const struct option *, const char *, int);\n extern int parse_opt_tertiary(const struct option *, const char *, int);\n extern int parse_opt_string_list(const struct option *, const char *, int);\n extern int parse_opt_noop_cb(const struct option *, const char *, int);\n+extern int parse_opt_unknown_cb(const struct option *, const char *, int);\n extern int parse_opt_passthru(const struct option *, const char *, int);\n extern int parse_opt_passthru_argv(const struct option *, const char *, int);\n \n-- \n2.9.3\n\n"},{"id":"301086","messageId":"453ac11efe49ada493dc8af2041ad6de190c5a77.1473068229.git.mhagger@alum.mit.edu","threadId":"44005","inReplyTo":"cover.1473068229.git.mhagger@alum.mit.edu","subject":"[PATCH v3 3/8] is_blank_line(): take a single xrecord_t as argument","fromName":"Michael Haggerty","fromEmail":"mhagger@alum.mit.edu","sentAt":"2016-09-05T09:44:48Z","receivedAt":"2016-09-05T09:45:46Z","isPatch":true,"sender":{"key":"mhagger@alum.mit.edu","avatar":"https://avatars.githubusercontent.com/u/119718?v=4"},"body":"There is no reason for it to take an array and index as argument, as it\nonly accesses a single element of the array.\n\nSigned-off-by: Michael Haggerty <mhagger@alum.mit.edu>\n---\n xdiff/xdiffi.c | 8 ++++----\n 1 file changed, 4 insertions(+), 4 deletions(-)\n\ndiff --git a/xdiff/xdiffi.c b/xdiff/xdiffi.c\nindex 61deed8..ed2df64 100644\n--- a/xdiff/xdiffi.c\n+++ b/xdiff/xdiffi.c\n@@ -400,9 +400,9 @@ static xdchange_t *xdl_add_change(xdchange_t *xscr, long i1, long i2, long chg1,\n }\n \n \n-static int is_blank_line(xrecord_t **recs, long ix, long flags)\n+static int is_blank_line(xrecord_t *rec, long flags)\n {\n-\treturn xdl_blankline(recs[ix]->ptr, recs[ix]->size, flags);\n+\treturn xdl_blankline(rec->ptr, rec->size, flags);\n }\n \n static int recs_match(xrecord_t **recs, long ixs, long ix, long flags)\n@@ -485,7 +485,7 @@ int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) {\n \t\t\t * the group.\n \t\t\t */\n \t\t\twhile (ix < nrec && recs_match(recs, ixs, ix, flags)) {\n-\t\t\t\tblank_lines += is_blank_line(recs, ix, flags);\n+\t\t\t\tblank_lines += is_blank_line(recs[ix], flags);\n \n \t\t\t\trchg[ixs++] = 0;\n \t\t\t\trchg[ix++] = 1;\n@@ -524,7 +524,7 @@ int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) {\n \t\t\t * back only if at all.\n \t\t\t */\n \t\t\twhile (ixs > 0 &&\n-\t\t\t       !is_blank_line(recs, ix - 1, flags) &&\n+\t\t\t       !is_blank_line(recs[ix - 1], flags) &&\n \t\t\t       recs_match(recs, ixs - 1, ix - 1, flags)) {\n \t\t\t\trchg[--ixs] = 1;\n \t\t\t\trchg[--ix] = 0;\n-- \n2.9.3\n\n"},{"id":"301087","messageId":"d2fd0eefdf6e4f2884df3214b32606e5b43e0555.1473068229.git.mhagger@alum.mit.edu","threadId":"44005","inReplyTo":"cover.1473068229.git.mhagger@alum.mit.edu","subject":"[PATCH v3 2/8] xdl_change_compact(): only use heuristic if group can't be matched","fromName":"Michael Haggerty","fromEmail":"mhagger@alum.mit.edu","sentAt":"2016-09-05T09:44:47Z","receivedAt":"2016-09-05T09:45:49Z","isPatch":true,"sender":{"key":"mhagger@alum.mit.edu","avatar":"https://avatars.githubusercontent.com/u/119718?v=4"},"body":"If the changed group of lines can be matched to a group in the other\nfile, then that positioning should take precedence over the compaction\nheuristic.\n\nThe old code tried the heuristic unconditionally, which cost redundant\neffort and also was broken if the matching code had already shifted the\ngroup higher than the blank line.\n\nSigned-off-by: Michael Haggerty <mhagger@alum.mit.edu>\n---\n xdiff/xdiffi.c | 38 +++++++++++++++++++-------------------\n 1 file changed, 19 insertions(+), 19 deletions(-)\n\ndiff --git a/xdiff/xdiffi.c b/xdiff/xdiffi.c\nindex 95b037e..61deed8 100644\n--- a/xdiff/xdiffi.c\n+++ b/xdiff/xdiffi.c\n@@ -504,25 +504,25 @@ int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) {\n \t\t\t}\n \t\t} while (grpsiz != ix - ixs);\n \n-\t\t/*\n-\t\t * Try to move back the possibly merged group of changes, to match\n-\t\t * the recorded position in the other file.\n-\t\t */\n-\t\twhile (ixref < ix) {\n-\t\t\trchg[--ixs] = 1;\n-\t\t\trchg[--ix] = 0;\n-\t\t\twhile (rchgo[--ixo]);\n-\t\t}\n-\n-\t\t/*\n-\t\t * If a group can be moved back and forth, see if there is a\n-\t\t * blank line in the moving space. If there is a blank line,\n-\t\t * make sure the last blank line is the end of the group.\n-\t\t *\n-\t\t * As we already shifted the group forward as far as possible\n-\t\t * in the earlier loop, we need to shift it back only if at all.\n-\t\t */\n-\t\tif ((flags & XDF_COMPACTION_HEURISTIC) && blank_lines) {\n+\t\tif (ixref < ix) {\n+\t\t\t/*\n+\t\t\t * Try to move back the possibly merged group of changes, to match\n+\t\t\t * the recorded position in the other file.\n+\t\t\t */\n+\t\t\twhile (ixref < ix) {\n+\t\t\t\trchg[--ixs] = 1;\n+\t\t\t\trchg[--ix] = 0;\n+\t\t\t\twhile (rchgo[--ixo]);\n+\t\t\t}\n+\t\t} else if ((flags & XDF_COMPACTION_HEURISTIC) && blank_lines) {\n+\t\t\t/*\n+\t\t\t * The group can be slid up to make its last line a\n+\t\t\t * blank line. Do so.\n+\t\t\t *\n+\t\t\t * As we already shifted the group forward as far as\n+\t\t\t * possible in the earlier loop, we need to shift it\n+\t\t\t * back only if at all.\n+\t\t\t */\n \t\t\twhile (ixs > 0 &&\n \t\t\t       !is_blank_line(recs, ix - 1, flags) &&\n \t\t\t       recs_match(recs, ixs - 1, ix - 1, flags)) {\n-- \n2.9.3\n\n"},{"id":"301088","messageId":"21ade4ab233a868cabbe15598cd7b2ff4d04d286.1473068229.git.mhagger@alum.mit.edu","threadId":"44005","inReplyTo":"cover.1473068229.git.mhagger@alum.mit.edu","subject":"[PATCH v3 5/8] xdl_change_compact(): introduce the concept of a change group","fromName":"Michael Haggerty","fromEmail":"mhagger@alum.mit.edu","sentAt":"2016-09-05T09:44:50Z","receivedAt":"2016-09-05T09:45:53Z","isPatch":true,"sender":{"key":"mhagger@alum.mit.edu","avatar":"https://avatars.githubusercontent.com/u/119718?v=4"},"body":"The idea of xdl_change_compact() is fairly simple:\n\n* Proceed through groups of changed lines in the file to be compacted,\n  keeping track of the corresponding location in the \"other\" file.\n\n* If possible, slide the group up and down to try to give the most\n  aesthetically pleasing diff. Whenever it is slid, the current location\n  in the other file needs to be adjusted.\n\nBut these simple concepts are obfuscated by a lot of index handling that\nis written in terse, subtle, and varied patterns. I found it very hard\nto convince myself that the function was correct.\n\nSo introduce a \"struct group\" that represents a group of changed lines\nin a file. Add some functions that perform elementary operations on\ngroups:\n\n* Initialize a group to the first group in a file\n* Move to the next or previous group in a file\n* Slide a group up or down\n\nEven though the resulting code is longer, I think it is easier to\nunderstand and review. Its performance is not changed\nappreciably (though it would be if `group_next()` and `group_previous()`\nwere not inlined).\n\n...and in fact, the rewriting helped me discover another bug in the\n--compaction-heuristic code: The update of blank_lines was never done\nfor the highest possible position of the group. This means that it could\nfail to slide the group to its highest possible position, even if that\nposition had a blank line as its last line. So for example, it yielded\nthe following diff:\n\n    $ git diff --no-index --compaction-heuristic a.txt b.txt\n    diff --git a/a.txt b/b.txt\n    index e53969f..0d60c5fe 100644\n    --- a/a.txt\n    +++ b/b.txt\n    @@ -1,3 +1,7 @@\n     1\n     A\n    +\n    +B\n    +\n    +A\n     2\n\nwhen in fact the following diff is better (according to the rules of\n--compaction-heuristic):\n\n    $ git diff --no-index --compaction-heuristic a.txt b.txt\n    diff --git a/a.txt b/b.txt\n    index e53969f..0d60c5fe 100644\n    --- a/a.txt\n    +++ b/b.txt\n    @@ -1,3 +1,7 @@\n     1\n    +A\n    +\n    +B\n    +\n     A\n     2\n\nThe new code gives the bottom answer.\n\nSigned-off-by: Michael Haggerty <mhagger@alum.mit.edu>\n---\n xdiff/xdiffi.c | 293 +++++++++++++++++++++++++++++++++++++++------------------\n 1 file changed, 203 insertions(+), 90 deletions(-)\n\ndiff --git a/xdiff/xdiffi.c b/xdiff/xdiffi.c\nindex 8a5832a..44fded6 100644\n--- a/xdiff/xdiffi.c\n+++ b/xdiff/xdiffi.c\n@@ -413,126 +413,239 @@ static int recs_match(xrecord_t *rec1, xrecord_t *rec2, long flags)\n \t\t\t     flags));\n }\n \n-int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) {\n-\tlong ix, ixo, ixs, ixref, grpsiz, nrec = xdf->nrec;\n-\tchar *rchg = xdf->rchg, *rchgo = xdfo->rchg;\n-\tunsigned int blank_lines;\n-\txrecord_t **recs = xdf->recs;\n+/*\n+ * Represent a group of changed lines in an xdfile_t (i.e., a contiguous group\n+ * of lines that was inserted or deleted from the corresponding version of the\n+ * file). We consider there to be such a group at the beginning of the file, at\n+ * the end of the file, and between any two unchanged lines, though most such\n+ * groups will usually be empty.\n+ *\n+ * If the first line in a group is equal to the line following the group, then\n+ * the group can be slid down. Similarly, if the last line in a group is equal\n+ * to the line preceding the group, then the group can be slid up. See\n+ * group_slide_down() and group_slide_up().\n+ *\n+ * Note that loops that are testing for changed lines in xdf->rchg do not need\n+ * index bounding since the array is prepared with a zero at position -1 and N.\n+ */\n+struct group {\n+\t/*\n+\t * The index of the first changed line in the group, or the index of\n+\t * the unchanged line above which the (empty) group is located.\n+\t */\n+\tlong start;\n \n \t/*\n-\t * This is the same of what GNU diff does. Move back and forward\n-\t * change groups for a consistent and pretty diff output. This also\n-\t * helps in finding joinable change groups and reduce the diff size.\n+\t * The index of the first unchanged line after the group. For an empty\n+\t * group, end is equal to start.\n \t */\n-\tfor (ix = ixo = 0;;) {\n-\t\t/*\n-\t\t * Find the first changed line in the to-be-compacted file.\n-\t\t * We need to keep track of both indexes, so if we find a\n-\t\t * changed lines group on the other file, while scanning the\n-\t\t * to-be-compacted file, we need to skip it properly. Note\n-\t\t * that loops that are testing for changed lines on rchg* do\n-\t\t * not need index bounding since the array is prepared with\n-\t\t * a zero at position -1 and N.\n-\t\t */\n-\t\tfor (; ix < nrec && !rchg[ix]; ix++)\n-\t\t\twhile (rchgo[ixo++]);\n-\t\tif (ix == nrec)\n-\t\t\tbreak;\n+\tlong end;\n+};\n+\n+/*\n+ * Initialize g to point at the first group in xdf.\n+ */\n+static void group_init(xdfile_t *xdf, struct group *g)\n+{\n+\tg->start = g->end = 0;\n+\twhile (xdf->rchg[g->end])\n+\t\tg->end++;\n+}\n+\n+/*\n+ * Move g to describe the next (possibly empty) group in xdf and return 0. If g\n+ * is already at the end of the file, do nothing and return -1.\n+ */\n+static inline int group_next(xdfile_t *xdf, struct group *g)\n+{\n+\tif (g->end == xdf->nrec)\n+\t\treturn -1;\n+\n+\tg->start = g->end + 1;\n+\tfor (g->end = g->start; xdf->rchg[g->end]; g->end++)\n+\t\t;\n+\n+\treturn 0;\n+}\n+\n+/*\n+ * Move g to describe the previous (possibly empty) group in xdf and return 0.\n+ * If g is already at the beginning of the file, do nothing and return -1.\n+ */\n+static inline int group_previous(xdfile_t *xdf, struct group *g)\n+{\n+\tif (g->start == 0)\n+\t\treturn -1;\n+\n+\tg->end = g->start - 1;\n+\tfor (g->start = g->end; xdf->rchg[g->start - 1]; g->start--)\n+\t\t;\n+\n+\treturn 0;\n+}\n+\n+/*\n+ * If g can be slid toward the end of the file, do so, and if it bumps into a\n+ * following group, expand this group to include it. Return 0 on success or -1\n+ * if g cannot be slid down.\n+ */\n+static int group_slide_down(xdfile_t *xdf, struct group *g, long flags)\n+{\n+\tif (g->end < xdf->nrec &&\n+\t    recs_match(xdf->recs[g->start], xdf->recs[g->end], flags)) {\n+\t\txdf->rchg[g->start++] = 0;\n+\t\txdf->rchg[g->end++] = 1;\n+\n+\t\twhile (xdf->rchg[g->end])\n+\t\t\tg->end++;\n+\n+\t\treturn 0;\n+\t} else {\n+\t\treturn -1;\n+\t}\n+}\n+\n+/*\n+ * If g can be slid toward the beginning of the file, do so, and if it bumps\n+ * into a previous group, expand this group to include it. Return 0 on success\n+ * or -1 if g cannot be slid up.\n+ */\n+static int group_slide_up(xdfile_t *xdf, struct group *g, long flags)\n+{\n+\tif (g->start > 0 &&\n+\t    recs_match(xdf->recs[g->start - 1], xdf->recs[g->end - 1], flags)) {\n+\t\txdf->rchg[--g->start] = 1;\n+\t\txdf->rchg[--g->end] = 0;\n+\n+\t\twhile (xdf->rchg[g->start - 1])\n+\t\t\tg->start--;\n+\n+\t\treturn 0;\n+\t} else {\n+\t\treturn -1;\n+\t}\n+}\n+\n+static void xdl_bug(const char *msg)\n+{\n+\tfprintf(stderr, \"BUG: %s\\n\", msg);\n+\texit(1);\n+}\n+\n+/*\n+ * Move back and forward change groups for a consistent and pretty diff output.\n+ * This also helps in finding joinable change groups and reducing the diff\n+ * size.\n+ */\n+int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) {\n+\tstruct group g, go;\n+\tlong earliest_end, end_matching_other;\n+\tlong groupsize;\n+\tunsigned int blank_lines;\n+\n+\tgroup_init(xdf, &g);\n+\tgroup_init(xdfo, &go);\n+\n+\twhile (1) {\n+\t\t/* If the group is empty in the to-be-compacted file, skip it: */\n+\t\tif (g.end == g.start)\n+\t\t\tgoto next;\n \n \t\t/*\n-\t\t * Record the start of a changed-group in the to-be-compacted file\n-\t\t * and find the end of it, on both to-be-compacted and other file\n-\t\t * indexes (ix and ixo).\n+\t\t * Now shift the change up and then down as far as possible in\n+\t\t * each direction. If it bumps into any other changes, merge them.\n \t\t */\n-\t\tixs = ix;\n-\t\tfor (ix++; rchg[ix]; ix++);\n-\t\tfor (; rchgo[ixo]; ixo++);\n-\n \t\tdo {\n-\t\t\tgrpsiz = ix - ixs;\n-\t\t\tblank_lines = 0;\n+\t\t\tgroupsize = g.end - g.start;\n \n \t\t\t/*\n-\t\t\t * If the line before the current change group, is equal to\n-\t\t\t * the last line of the current change group, shift backward\n-\t\t\t * the group.\n+\t\t\t * Keep track of the last \"end\" index that causes this\n+\t\t\t * group to align with a group of changed lines in the\n+\t\t\t * other file. -1 indicates that we haven't found such\n+\t\t\t * a match yet:\n \t\t\t */\n-\t\t\twhile (ixs > 0 && recs_match(recs[ixs - 1], recs[ix - 1], flags)) {\n-\t\t\t\trchg[--ixs] = 1;\n-\t\t\t\trchg[--ix] = 0;\n-\n-\t\t\t\t/*\n-\t\t\t\t * This change might have joined two change groups,\n-\t\t\t\t * so we try to take this scenario in account by moving\n-\t\t\t\t * the start index accordingly (and so the other-file\n-\t\t\t\t * end-of-group index).\n-\t\t\t\t */\n-\t\t\t\tfor (; rchg[ixs - 1]; ixs--);\n-\t\t\t\twhile (rchgo[--ixo]);\n-\t\t\t}\n+\t\t\tend_matching_other = -1;\n \n \t\t\t/*\n-\t\t\t * Record the end-of-group position in case we are matched\n-\t\t\t * with a group of changes in the other file (that is, the\n-\t\t\t * change record before the end-of-group index in the other\n-\t\t\t * file is set).\n+\t\t\t * Boolean value that records whether there are any blank\n+\t\t\t * lines that could be made to be the last line of this\n+\t\t\t * group.\n \t\t\t */\n-\t\t\tixref = rchgo[ixo - 1] ? ix: nrec;\n+\t\t\tblank_lines = 0;\n+\n+\t\t\t/* Shift the group backward as much as possible: */\n+\t\t\twhile (!group_slide_up(xdf, &g, flags))\n+\t\t\t\tif (group_previous(xdfo, &go))\n+\t\t\t\t\txdl_bug(\"group sync broken sliding up\");\n \n \t\t\t/*\n-\t\t\t * If the first line of the current change group, is equal to\n-\t\t\t * the line next of the current change group, shift forward\n-\t\t\t * the group.\n+\t\t\t * This is this highest that this group can be shifted.\n+\t\t\t * Record its end index:\n \t\t\t */\n-\t\t\twhile (ix < nrec && recs_match(recs[ixs], recs[ix], flags)) {\n-\t\t\t\tblank_lines += is_blank_line(recs[ix], flags);\n-\n-\t\t\t\trchg[ixs++] = 0;\n-\t\t\t\trchg[ix++] = 1;\n-\n-\t\t\t\t/*\n-\t\t\t\t * This change might have joined two change groups,\n-\t\t\t\t * so we try to take this scenario in account by moving\n-\t\t\t\t * the start index accordingly (and so the other-file\n-\t\t\t\t * end-of-group index). Keep tracking the reference\n-\t\t\t\t * index in case we are shifting together with a\n-\t\t\t\t * corresponding group of changes in the other file.\n-\t\t\t\t */\n-\t\t\t\tfor (; rchg[ix]; ix++);\n-\t\t\t\twhile (rchgo[++ixo])\n-\t\t\t\t\tixref = ix;\n+\t\t\tearliest_end = g.end;\n+\n+\t\t\tif (go.end > go.start)\n+\t\t\t\tend_matching_other = g.end;\n+\n+\t\t\t/* Now shift the group forward as far as possible: */\n+\t\t\twhile (1) {\n+\t\t\t\tif (!blank_lines)\n+\t\t\t\t\tblank_lines = is_blank_line(\n+\t\t\t\t\t\t\txdf->recs[g.end - 1],\n+\t\t\t\t\t\t\tflags);\n+\n+\t\t\t\tif (group_slide_down(xdf, &g, flags))\n+\t\t\t\t\tbreak;\n+\t\t\t\tif (group_next(xdfo, &go))\n+\t\t\t\t\txdl_bug(\"group sync broken sliding down\");\n+\n+\t\t\t\tif (go.end > go.start)\n+\t\t\t\t\tend_matching_other = g.end;\n \t\t\t}\n-\t\t} while (grpsiz != ix - ixs);\n+\t\t} while (groupsize != g.end - g.start);\n \n-\t\tif (ixref < ix) {\n+\t\tif (g.end == earliest_end) {\n+\t\t\t/* no shifting was possible */\n+\t\t} else if (end_matching_other != -1) {\n \t\t\t/*\n-\t\t\t * Try to move back the possibly merged group of changes, to match\n-\t\t\t * the recorded position in the other file.\n+\t\t\t * Move the possibly merged group of changes back to line\n+\t\t\t * up with the last group of changes from the other file\n+\t\t\t * that it can align with.\n \t\t\t */\n-\t\t\twhile (ixref < ix) {\n-\t\t\t\trchg[--ixs] = 1;\n-\t\t\t\trchg[--ix] = 0;\n-\t\t\t\twhile (rchgo[--ixo]);\n+\t\t\twhile (go.end == go.start) {\n+\t\t\t\tif (group_slide_up(xdf, &g, flags))\n+\t\t\t\t\txdl_bug(\"match disappeared\");\n+\t\t\t\tif (group_previous(xdfo, &go))\n+\t\t\t\t\txdl_bug(\"group sync broken sliding to match\");\n \t\t\t}\n \t\t} else if ((flags & XDF_COMPACTION_HEURISTIC) && blank_lines) {\n \t\t\t/*\n-\t\t\t * The group can be slid up to make its last line a\n-\t\t\t * blank line. Do so.\n+\t\t\t * Compaction heuristic: if it is possible to shift the\n+\t\t\t * group to make its bottom line a blank line, do so.\n \t\t\t *\n \t\t\t * As we already shifted the group forward as far as\n-\t\t\t * possible in the earlier loop, we need to shift it\n-\t\t\t * back only if at all.\n+\t\t\t * possible in the earlier loop, we only need to handle\n+\t\t\t * backward shifts, not forward ones.\n \t\t\t */\n-\t\t\twhile (ixs > 0 &&\n-\t\t\t       !is_blank_line(recs[ix - 1], flags) &&\n-\t\t\t       recs_match(recs[ixs - 1], recs[ix - 1], flags)) {\n-\t\t\t\trchg[--ixs] = 1;\n-\t\t\t\trchg[--ix] = 0;\n-\t\t\t\twhile (rchgo[--ixo]);\n+\t\t\twhile (!is_blank_line(xdf->recs[g.end - 1], flags)) {\n+\t\t\t\tif (group_slide_up(xdf, &g, flags))\n+\t\t\t\t\txdl_bug(\"blank line disappeared\");\n+\t\t\t\tif (group_previous(xdfo, &go))\n+\t\t\t\t\txdl_bug(\"group sync broken sliding to blank line\");\n \t\t\t}\n \t\t}\n+\n+\tnext:\n+\t\t/* Move past the just-processed group: */\n+\t\tif (group_next(xdf, &g))\n+\t\t\tbreak;\n+\t\tif (group_next(xdfo, &go))\n+\t\t\txdl_bug(\"group sync broken moving to next group\");\n \t}\n \n+\tif (!group_next(xdfo, &go))\n+\t\txdl_bug(\"group sync broken at end of file\");\n+\n \treturn 0;\n }\n \n-- \n2.9.3\n\n"},{"id":"301089","messageId":"8c78cd7079c925f93488850154fc8352db4e4cb8.1473068229.git.mhagger@alum.mit.edu","threadId":"44005","inReplyTo":"cover.1473068229.git.mhagger@alum.mit.edu","subject":"[PATCH v3 8/8] blame: honor the diff heuristic options and config","fromName":"Michael Haggerty","fromEmail":"mhagger@alum.mit.edu","sentAt":"2016-09-05T09:44:53Z","receivedAt":"2016-09-05T09:45:57Z","isPatch":true,"sender":{"key":"mhagger@alum.mit.edu","avatar":"https://avatars.githubusercontent.com/u/119718?v=4"},"body":"Teach \"git blame\" and \"git annotate\" the --compaction-heuristic and\n--indent-heuristic options that are now supported by \"git diff\".\n\nAlso teach them to honor the `diff.compactionHeuristic` and\n`diff.indentHeuristic` configuration options.\n\nIt would be conceivable to introduce separate configuration options for\n\"blame\" and \"annotate\"; for example `blame.compactionHeuristic` and\n`blame.indentHeuristic`. But it would be confusing to users if blame\noutput is inconsistent with diff output, so it makes more sense for them\nto respect the same configuration.\n\nSigned-off-by: Michael Haggerty <mhagger@alum.mit.edu>\n---\n Documentation/diff-heuristic-options.txt |  7 +++++++\n Documentation/diff-options.txt           |  8 +-------\n Documentation/git-annotate.txt           |  1 +\n Documentation/git-blame.txt              |  2 ++\n builtin/blame.c                          | 12 ++++++++++++\n diff.c                                   | 29 +++++++++++++++++------------\n diff.h                                   |  1 +\n t/t4059-diff-indent.sh                   | 29 +++++++++++++++++++++++++++++\n 8 files changed, 70 insertions(+), 19 deletions(-)\n create mode 100644 Documentation/diff-heuristic-options.txt\n\ndiff --git a/Documentation/diff-heuristic-options.txt b/Documentation/diff-heuristic-options.txt\nnew file mode 100644\nindex 0000000..36cb549\n--- /dev/null\n+++ b/Documentation/diff-heuristic-options.txt\n@@ -0,0 +1,7 @@\n+--indent-heuristic::\n+--no-indent-heuristic::\n+--compaction-heuristic::\n+--no-compaction-heuristic::\n+\tThese are to help debugging and tuning experimental heuristics\n+\t(which are off by default) that shift diff hunk boundaries to\n+\tmake patches easier to read.\ndiff --git a/Documentation/diff-options.txt b/Documentation/diff-options.txt\nindex ffc2b60..d6d41e4 100644\n--- a/Documentation/diff-options.txt\n+++ b/Documentation/diff-options.txt\n@@ -63,13 +63,7 @@ ifndef::git-format-patch[]\n \tSynonym for `-p --raw`.\n endif::git-format-patch[]\n \n---indent-heuristic::\n---no-indent-heuristic::\n---compaction-heuristic::\n---no-compaction-heuristic::\n-\tThese are to help debugging and tuning experimental heuristics\n-\t(which are off by default) that shift diff hunk boundaries to\n-\tmake patches easier to read.\n+include::diff-heuristic-options.txt[]\n \n --minimal::\n \tSpend extra time to make sure the smallest possible\ndiff --git a/Documentation/git-annotate.txt b/Documentation/git-annotate.txt\nindex 05fd482..94be4b8 100644\n--- a/Documentation/git-annotate.txt\n+++ b/Documentation/git-annotate.txt\n@@ -23,6 +23,7 @@ familiar command name for people coming from other SCM systems.\n OPTIONS\n -------\n include::blame-options.txt[]\n+include::diff-heuristic-options.txt[]\n \n SEE ALSO\n --------\ndiff --git a/Documentation/git-blame.txt b/Documentation/git-blame.txt\nindex ba54175..9dccb33 100644\n--- a/Documentation/git-blame.txt\n+++ b/Documentation/git-blame.txt\n@@ -89,6 +89,8 @@ include::blame-options.txt[]\n \tabbreviated object name, use <n>+1 digits. Note that 1 column\n \tis used for a caret to mark the boundary commit.\n \n+include::diff-heuristic-options.txt[]\n+\n \n THE PORCELAIN FORMAT\n --------------------\ndiff --git a/builtin/blame.c b/builtin/blame.c\nindex 7ec7823..f7b67f8 100644\n--- a/builtin/blame.c\n+++ b/builtin/blame.c\n@@ -2220,6 +2220,8 @@ static int git_blame_config(const char *var, const char *value, void *cb)\n \t\treturn 0;\n \t}\n \n+\tif (git_diff_heuristic_config(var, value, cb) < 0)\n+\t\treturn -1;\n \tif (userdiff_config(var, value) < 0)\n \t\treturn -1;\n \n@@ -2549,6 +2551,15 @@ int cmd_blame(int argc, const char **argv, const char *prefix)\n \t\tOPT_BIT('s', NULL, &output_option, N_(\"Suppress author name and timestamp (Default: off)\"), OUTPUT_NO_AUTHOR),\n \t\tOPT_BIT('e', \"show-email\", &output_option, N_(\"Show author email instead of name (Default: off)\"), OUTPUT_SHOW_EMAIL),\n \t\tOPT_BIT('w', NULL, &xdl_opts, N_(\"Ignore whitespace differences\"), XDF_IGNORE_WHITESPACE),\n+\n+\t\t/*\n+\t\t * The following two options are parsed by parse_revision_opt()\n+\t\t * and are only included here to get included in the \"-h\"\n+\t\t * output:\n+\t\t */\n+\t\t{ OPTION_LOWLEVEL_CALLBACK, 0, \"indent-heuristic\", NULL, NULL, N_(\"Use an experimental indent-based heuristic to improve diffs\"), PARSE_OPT_NOARG, parse_opt_unknown_cb },\n+\t\t{ OPTION_LOWLEVEL_CALLBACK, 0, \"compaction-heuristic\", NULL, NULL, N_(\"Use an experimental blank-line-based heuristic to improve diffs\"), PARSE_OPT_NOARG, parse_opt_unknown_cb },\n+\n \t\tOPT_BIT(0, \"minimal\", &xdl_opts, N_(\"Spend extra cycles to find better match\"), XDF_NEED_MINIMAL),\n \t\tOPT_STRING('S', NULL, &revs_file, N_(\"file\"), N_(\"Use revisions from <file> instead of calling git-rev-list\")),\n \t\tOPT_STRING(0, \"contents\", &contents_from, N_(\"file\"), N_(\"Use <file>'s contents as the final image\")),\n@@ -2595,6 +2606,7 @@ int cmd_blame(int argc, const char **argv, const char *prefix)\n \t}\n parse_done:\n \tno_whole_file_rename = !DIFF_OPT_TST(&revs.diffopt, FOLLOW_RENAMES);\n+\txdl_opts |= revs.diffopt.xdl_opts & (XDF_COMPACTION_HEURISTIC | XDF_INDENT_HEURISTIC);\n \tDIFF_OPT_CLR(&revs.diffopt, FOLLOW_RENAMES);\n \targc = parse_options_end(&ctx);\n \ndiff --git a/diff.c b/diff.c\nindex 8b15ed7..8a2003d 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -175,6 +175,21 @@ void init_diff_ui_defaults(void)\n \tdiff_detect_rename_default = 1;\n }\n \n+int git_diff_heuristic_config(const char *var, const char *value, void *cb)\n+{\n+\tif (!strcmp(var, \"diff.indentheuristic\")) {\n+\t\tdiff_indent_heuristic = git_config_bool(var, value);\n+\t\tif (diff_indent_heuristic)\n+\t\t\tdiff_compaction_heuristic = 0;\n+\t}\n+\tif (!strcmp(var, \"diff.compactionheuristic\")) {\n+\t\tdiff_compaction_heuristic = git_config_bool(var, value);\n+\t\tif (diff_compaction_heuristic)\n+\t\t\tdiff_indent_heuristic = 0;\n+\t}\n+\treturn 0;\n+}\n+\n int git_diff_ui_config(const char *var, const char *value, void *cb)\n {\n \tif (!strcmp(var, \"diff.color\") || !strcmp(var, \"color.diff\")) {\n@@ -191,18 +206,6 @@ int git_diff_ui_config(const char *var, const char *value, void *cb)\n \t\tdiff_detect_rename_default = git_config_rename(var, value);\n \t\treturn 0;\n \t}\n-\tif (!strcmp(var, \"diff.indentheuristic\")) {\n-\t\tdiff_indent_heuristic = git_config_bool(var, value);\n-\t\tif (diff_indent_heuristic)\n-\t\t\tdiff_compaction_heuristic = 0;\n-\t\treturn 0;\n-\t}\n-\tif (!strcmp(var, \"diff.compactionheuristic\")) {\n-\t\tdiff_compaction_heuristic = git_config_bool(var, value);\n-\t\tif (diff_compaction_heuristic)\n-\t\t\tdiff_indent_heuristic = 0;\n-\t\treturn 0;\n-\t}\n \tif (!strcmp(var, \"diff.autorefreshindex\")) {\n \t\tdiff_auto_refresh_index = git_config_bool(var, value);\n \t\treturn 0;\n@@ -243,6 +246,8 @@ int git_diff_ui_config(const char *var, const char *value, void *cb)\n \t\treturn 0;\n \t}\n \n+\tif (git_diff_heuristic_config(var, value, cb) < 0)\n+\t\treturn -1;\n \tif (git_color_config(var, value, cb) < 0)\n \t\treturn -1;\n \ndiff --git a/diff.h b/diff.h\nindex 7883729..1ecc9b6 100644\n--- a/diff.h\n+++ b/diff.h\n@@ -266,6 +266,7 @@ extern int parse_long_opt(const char *opt, const char **argv,\n \t\t\t const char **optarg);\n \n extern int git_diff_basic_config(const char *var, const char *value, void *cb);\n+extern int git_diff_heuristic_config(const char *var, const char *value, void *cb);\n extern void init_diff_ui_defaults(void);\n extern int git_diff_ui_config(const char *var, const char *value, void *cb);\n extern void diff_setup(struct diff_options *);\ndiff --git a/t/t4059-diff-indent.sh b/t/t4059-diff-indent.sh\nindex 0a65b58..5564506 100755\n--- a/t/t4059-diff-indent.sh\n+++ b/t/t4059-diff-indent.sh\n@@ -14,6 +14,14 @@ compare_diff () {\n \ttest_cmp .tmp-1 .tmp-2 && rm -f .tmp-1 .tmp-2\n }\n \n+# Compare blame output using the expectation for a diff as reference.\n+# Only look for the lines coming from non-boundary commits.\n+compare_blame () {\n+\tsed -n -e \"1,4d\" -e \"s/^\\+//p\" <\"$1\" >.tmp-1\n+\tsed -ne \"s/^[^^][^)]*) *//p\" <\"$2\" >.tmp-2\n+\ttest_cmp .tmp-1 .tmp-2 && rm -f .tmp-1 .tmp-2\n+}\n+\n test_expect_success 'prepare' '\n \tcat <<-\\EOF >spaces.txt &&\n \t1\n@@ -184,4 +192,25 @@ test_expect_success 'diff: nice functions with --indent-heuristic' '\n \tcompare_diff functions-compacted-expect out-compacted\n '\n \n+test_expect_success 'blame: ugly spaces' '\n+\tgit blame old..new -- spaces.txt >out-blame &&\n+\tcompare_blame spaces-expect out-blame\n+'\n+\n+test_expect_success 'blame: nice spaces with --indent-heuristic' '\n+\tgit blame --indent-heuristic old..new -- spaces.txt >out-blame-compacted &&\n+\tcompare_blame spaces-compacted-expect out-blame-compacted\n+'\n+\n+test_expect_success 'blame: nice spaces with diff.indentHeuristic' '\n+\tgit -c diff.indentHeuristic=true blame old..new -- spaces.txt >out-blame-compacted2 &&\n+\tcompare_blame spaces-compacted-expect out-blame-compacted2\n+'\n+\n+test_expect_success 'blame: --no-indent-heuristic overrides config' '\n+\tgit -c diff.indentHeuristic=true blame --no-indent-heuristic old..new -- spaces.txt >out-blame2 &&\n+\tgit blame old..new -- spaces.txt >out-blame &&\n+\tcompare_blame spaces-expect out-blame2\n+'\n+\n test_done\n-- \n2.9.3\n\n"},{"id":"301090","messageId":"c271923828dfe0b950d8f22803fc509784532fcb.1473068229.git.mhagger@alum.mit.edu","threadId":"44005","inReplyTo":"cover.1473068229.git.mhagger@alum.mit.edu","subject":"[PATCH v3 6/8] diff: improve positioning of add/delete blocks in diffs","fromName":"Michael Haggerty","fromEmail":"mhagger@alum.mit.edu","sentAt":"2016-09-05T09:44:51Z","receivedAt":"2016-09-05T09:46:16Z","isPatch":true,"sender":{"key":"mhagger@alum.mit.edu","avatar":"https://avatars.githubusercontent.com/u/119718?v=4"},"body":"Some groups of added/deleted lines in diffs can be slid up or down,\nbecause lines at the edges of the group are not unique. Picking good\nshifts for such groups is not a matter of correctness but definitely has\na big effect on aesthetics. For example, consider the following two\ndiffs. The first is what standard Git emits:\n\n    --- a/9c572b21dd090a1e5c5bb397053bf8043ffe7fb4:git-send-email.perl\n    +++ b/6dcfa306f2b67b733a7eb2d7ded1bc9987809edb:git-send-email.perl\n    @@ -231,6 +231,9 @@ if (!defined $initial_reply_to && $prompting) {\n     }\n\n     if (!$smtp_server) {\n    +       $smtp_server = $repo->config('sendemail.smtpserver');\n    +}\n    +if (!$smtp_server) {\n            foreach (qw( /usr/sbin/sendmail /usr/lib/sendmail )) {\n                    if (-x $_) {\n                            $smtp_server = $_;\n\nThe following diff is equivalent, but is obviously preferable from an\naesthetic point of view:\n\n    --- a/9c572b21dd090a1e5c5bb397053bf8043ffe7fb4:git-send-email.perl\n    +++ b/6dcfa306f2b67b733a7eb2d7ded1bc9987809edb:git-send-email.perl\n    @@ -230,6 +230,9 @@ if (!defined $initial_reply_to && $prompting) {\n            $initial_reply_to =~ s/(^\\s+|\\s+$)//g;\n     }\n\n    +if (!$smtp_server) {\n    +       $smtp_server = $repo->config('sendemail.smtpserver');\n    +}\n     if (!$smtp_server) {\n            foreach (qw( /usr/sbin/sendmail /usr/lib/sendmail )) {\n                    if (-x $_) {\n\nThis patch teaches Git to pick better positions for such \"diff sliders\"\nusing heuristics that take the positions of nearby blank lines and the\nindentation of nearby lines into account.\n\nThe existing Git code basically always shifts such \"sliders\" as far down\nin the file as possible. The only exception is when the slider can be\naligned with a group of changed lines in the other file, in which case\nGit favors depicting the change as one add+delete block rather than one\nadd and a slightly offset delete block. This naive algorithm often\nyields ugly diffs.\n\nCommit d634d61ed6 improved the situation somewhat by preferring to\nposition add/delete groups to make their last line a blank line, when\nthat is possible. This heuristic does more good than harm, but (1) it\ncan only help if there are blank lines in the right places, and (2)\nalways picks the last blank line, even if there are others that might be\nbetter. The end result is that it makes perhaps 1/3 as many errors as\nthe default Git algorithm, but that still leaves a lot of ugly diffs.\n\nThis commit implements a new and much better heuristic for picking\noptimal \"slider\" positions using the following approach: First observe\nthat each hypothetical positioning of a diff slider introduces two\nsplits: one between the context lines preceding the group and the first\nadded/deleted line, and the other between the last added/deleted line\nand the first line of context following it. It tries to find the\npositioning that creates the least bad splits.\n\nSplits are evaluated based only on the presence and locations of nearby\nblank lines, and the indentation of lines near the split. Basically, it\nprefers to introduce splits adjacent to blank lines, between lines that\nare indented less, and between lines with the same level of indentation.\nIn more detail:\n\n1. It measures the following characteristics of a proposed splitting\n   position in a `struct split_measurement`:\n\n   * the number of blank lines above the proposed split\n   * whether the line directly after the split is blank\n   * the number of blank lines following that line\n   * the indentation of the nearest non-blank line above the split\n   * the indentation of the line directly below the split\n   * the indentation of the nearest non-blank line after that line\n\n2. It combines the measured attributes using a bunch of\n   empirically-optimized weighting factors to derive a `struct\n   split_score` that measures the \"badness\" of splitting the text at\n   that position.\n\n3. It combines the `split_score` for the top and the bottom of the\n   slider at each of its possible positions, and selects the position\n   that has the best `split_score`.\n\nI determined the initial set of weighting factors by collecting a corpus\nof Git histories from 29 open-source software projects in various\nprogramming languages. I generated many diffs from this corpus, and\ndetermined the best positioning \"by eye\" for about 6600 diff sliders. I\nused about half of the repositories in the corpus (corresponding to\nabout 2/3 of the sliders) as a training set, and optimized the weights\nagainst this corpus using a crude automated search of the parameter\nspace to get the best agreement with the manually-determined values.\nThen I tested the resulting heuristic against the full corpus. The\nresults are summarized in the following table, in column `indent-1`:\n\n| repository            | count |      Git 2.9.0 |     compaction | compaction-fixed |       indent-1 |       indent-2 |\n| --------------------- | ----- | -------------- | -------------- | ---------------- | -------------- | -------------- |\n| afnetworking          |   109 |    89  (81.7%) |    37  (33.9%) |      37  (33.9%) |     2   (1.8%) |     2   (1.8%) |\n| alamofire             |    30 |    18  (60.0%) |    14  (46.7%) |      15  (50.0%) |     0   (0.0%) |     0   (0.0%) |\n| angular               |   184 |   127  (69.0%) |    39  (21.2%) |      23  (12.5%) |     5   (2.7%) |     5   (2.7%) |\n| animate               |   313 |     2   (0.6%) |     2   (0.6%) |       2   (0.6%) |     2   (0.6%) |     2   (0.6%) |\n| ant                   |   380 |   356  (93.7%) |   152  (40.0%) |     148  (38.9%) |    15   (3.9%) |    15   (3.9%) | *\n| bugzilla              |   306 |   263  (85.9%) |   109  (35.6%) |      99  (32.4%) |    14   (4.6%) |    15   (4.9%) | *\n| corefx                |   126 |    91  (72.2%) |    22  (17.5%) |      21  (16.7%) |     6   (4.8%) |     6   (4.8%) |\n| couchdb               |    78 |    44  (56.4%) |    26  (33.3%) |      28  (35.9%) |     6   (7.7%) |     6   (7.7%) | *\n| cpython               |   937 |   158  (16.9%) |    50   (5.3%) |      49   (5.2%) |     5   (0.5%) |     5   (0.5%) | *\n| discourse             |   160 |    95  (59.4%) |    42  (26.2%) |      36  (22.5%) |    18  (11.2%) |    13   (8.1%) |\n| docker                |   307 |   194  (63.2%) |   198  (64.5%) |     253  (82.4%) |     8   (2.6%) |     8   (2.6%) | *\n| electron              |   163 |   132  (81.0%) |    38  (23.3%) |      39  (23.9%) |     6   (3.7%) |     6   (3.7%) |\n| git                   |   536 |   470  (87.7%) |    73  (13.6%) |      78  (14.6%) |    16   (3.0%) |    16   (3.0%) | *\n| gitflow               |   127 |     0   (0.0%) |     0   (0.0%) |       0   (0.0%) |     0   (0.0%) |     0   (0.0%) |\n| ionic                 |   133 |    89  (66.9%) |    29  (21.8%) |      38  (28.6%) |     1   (0.8%) |     1   (0.8%) |\n| ipython               |   482 |   362  (75.1%) |   167  (34.6%) |     169  (35.1%) |    11   (2.3%) |    11   (2.3%) | *\n| junit                 |   161 |   147  (91.3%) |    67  (41.6%) |      66  (41.0%) |     1   (0.6%) |     1   (0.6%) | *\n| lighttable            |    15 |     5  (33.3%) |     0   (0.0%) |       2  (13.3%) |     0   (0.0%) |     0   (0.0%) |\n| magit                 |    88 |    75  (85.2%) |    11  (12.5%) |       9  (10.2%) |     1   (1.1%) |     0   (0.0%) |\n| neural-style          |    28 |     0   (0.0%) |     0   (0.0%) |       0   (0.0%) |     0   (0.0%) |     0   (0.0%) |\n| nodejs                |   781 |   649  (83.1%) |   118  (15.1%) |     111  (14.2%) |     4   (0.5%) |     5   (0.6%) | *\n| phpmyadmin            |   491 |   481  (98.0%) |    75  (15.3%) |      48   (9.8%) |     2   (0.4%) |     2   (0.4%) | *\n| react-native          |   168 |   130  (77.4%) |    79  (47.0%) |      81  (48.2%) |     0   (0.0%) |     0   (0.0%) |\n| rust                  |   171 |   128  (74.9%) |    30  (17.5%) |      27  (15.8%) |    16   (9.4%) |    14   (8.2%) |\n| spark                 |   186 |   149  (80.1%) |    52  (28.0%) |      52  (28.0%) |     2   (1.1%) |     2   (1.1%) |\n| tensorflow            |   115 |    66  (57.4%) |    48  (41.7%) |      48  (41.7%) |     5   (4.3%) |     5   (4.3%) |\n| test-more             |    19 |    15  (78.9%) |     2  (10.5%) |       2  (10.5%) |     1   (5.3%) |     1   (5.3%) | *\n| test-unit             |    51 |    34  (66.7%) |    14  (27.5%) |       8  (15.7%) |     2   (3.9%) |     2   (3.9%) | *\n| xmonad                |    23 |    22  (95.7%) |     2   (8.7%) |       2   (8.7%) |     1   (4.3%) |     1   (4.3%) | *\n| --------------------- | ----- | -------------- | -------------- | ---------------- | -------------- | -------------- |\n| totals                |  6668 |  4391  (65.9%) |  1496  (22.4%) |    1491  (22.4%) |   150   (2.2%) |   144   (2.2%) |\n| totals (training set) |  4552 |  3195  (70.2%) |  1053  (23.1%) |    1061  (23.3%) |    86   (1.9%) |    88   (1.9%) |\n| totals (test set)     |  2116 |  1196  (56.5%) |   443  (20.9%) |     430  (20.3%) |    64   (3.0%) |    56   (2.6%) |\n\nIn this table, the numbers are the count and percentage of human-rated\nsliders that the corresponding algorithm got *wrong*. The columns are\n\n* \"repository\" - the name of the repository used. I used the diffs\n  between successive non-merge commits on the HEAD branch of the\n  corresponding repository.\n\n* \"count\" - the number of sliders that were human-rated. I chose most,\n  but not all, sliders to rate from those among which the various\n  algorithms gave different answers.\n\n* \"Git 2.9.0\" - the default algorithm used by `git diff` in Git 2.9.0.\n\n* \"compaction\" - the heuristic used by `git diff --compaction-heuristic`\n  in Git 2.9.0.\n\n* \"compaction-fixed\" - the heuristic used by `git diff\n  --compaction-heuristic` after the fixes from earlier in this patch\n  series. Note that the results are not dramatically different than\n  those for \"compaction\". Both produce non-ideal diffs only about 1/3 as\n  often as the default `git diff`.\n\n* \"indent-1\" - the new `--indent-heuristic` algorithm, using the first\n  set of weighting factors, determined as described above.\n\n* \"indent-2\" - the new `--indent-heuristic` algorithm, using the final\n  set of weighting factors, determined as described below.\n\n* `*` - indicates that repo was part of training set used to determine\n  the first set of weighting factors.\n\nThe fact that the heuristic performed nearly as well on the test set as\non the training set in column \"indent-1\" is a good indication that the\nheuristic was not over-trained. Given that fact, I ran a second round of\noptimization, using the entire corpus as the training set. The resulting\nset of weights gave the results in column \"indent-2\". These are the\nweights included in this patch.\n\nThe final result gives consistently and significantly better results\nacross the whole corpus than either `git diff` or `git diff\n--compaction-heuristic`. It makes only about 1/30 as many errors as the\nformer and about 1/10 as many errors as the latter. (And a good fraction\nof the remaining errors are for diffs that involve weirdly-formatted\ncode, sometimes apparently machine-generated.)\n\nThe tools that were used to do this optimization and analysis, along\nwith the human-generated data values, are recorded in a separate project\n[1].\n\nThis patch adds a new command-line option `--indent-heuristic`, and a\nnew configuration setting `diff.indentHeuristic`, that activate this\nheuristic. This interface is only meant for testing purposes, and should\nbe finalized before including this change in any release.\n\n[1] https://github.com/mhagger/diff-slider-tools\n\nSigned-off-by: Michael Haggerty <mhagger@alum.mit.edu>\n---\n Documentation/diff-config.txt  |   7 +-\n Documentation/diff-options.txt |   9 +-\n diff.c                         |  23 ++-\n git-add--interactive.perl      |   5 +-\n t/t4059-diff-indent.sh         | 187 ++++++++++++++++++++++++\n xdiff/xdiff.h                  |   1 +\n xdiff/xdiffi.c                 | 325 +++++++++++++++++++++++++++++++++++++++++\n 7 files changed, 546 insertions(+), 11 deletions(-)\n create mode 100755 t/t4059-diff-indent.sh\n\ndiff --git a/Documentation/diff-config.txt b/Documentation/diff-config.txt\nindex d5a5b17..c32bd85 100644\n--- a/Documentation/diff-config.txt\n+++ b/Documentation/diff-config.txt\n@@ -170,10 +170,11 @@ diff.tool::\n \n include::mergetools-diff.txt[]\n \n+diff.indentHeuristic::\n diff.compactionHeuristic::\n-\tSet this option to `true` to enable an experimental heuristic that\n-\tshifts the hunk boundary in an attempt to make the resulting\n-\tpatch easier to read.\n+\tSet one of these options to `true` to enable one of two\n+\texperimental heuristics that shift diff hunk boundaries to\n+\tmake patches easier to read.\n \n diff.algorithm::\n \tChoose a diff algorithm.  The variants are as follows:\ndiff --git a/Documentation/diff-options.txt b/Documentation/diff-options.txt\nindex 705a873..ffc2b60 100644\n--- a/Documentation/diff-options.txt\n+++ b/Documentation/diff-options.txt\n@@ -63,12 +63,13 @@ ifndef::git-format-patch[]\n \tSynonym for `-p --raw`.\n endif::git-format-patch[]\n \n+--indent-heuristic::\n+--no-indent-heuristic::\n --compaction-heuristic::\n --no-compaction-heuristic::\n-\tThese are to help debugging and tuning an experimental\n-\theuristic (which is off by default) that shifts the hunk\n-\tboundary in an attempt to make the resulting patch easier\n-\tto read.\n+\tThese are to help debugging and tuning experimental heuristics\n+\t(which are off by default) that shift diff hunk boundaries to\n+\tmake patches easier to read.\n \n --minimal::\n \tSpend extra time to make sure the smallest possible\ndiff --git a/diff.c b/diff.c\nindex 534c12e..8b15ed7 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -26,6 +26,7 @@\n #endif\n \n static int diff_detect_rename_default;\n+static int diff_indent_heuristic; /* experimental */\n static int diff_compaction_heuristic; /* experimental */\n static int diff_rename_limit_default = 400;\n static int diff_suppress_blank_empty;\n@@ -190,8 +191,16 @@ int git_diff_ui_config(const char *var, const char *value, void *cb)\n \t\tdiff_detect_rename_default = git_config_rename(var, value);\n \t\treturn 0;\n \t}\n+\tif (!strcmp(var, \"diff.indentheuristic\")) {\n+\t\tdiff_indent_heuristic = git_config_bool(var, value);\n+\t\tif (diff_indent_heuristic)\n+\t\t\tdiff_compaction_heuristic = 0;\n+\t\treturn 0;\n+\t}\n \tif (!strcmp(var, \"diff.compactionheuristic\")) {\n \t\tdiff_compaction_heuristic = git_config_bool(var, value);\n+\t\tif (diff_compaction_heuristic)\n+\t\t\tdiff_indent_heuristic = 0;\n \t\treturn 0;\n \t}\n \tif (!strcmp(var, \"diff.autorefreshindex\")) {\n@@ -3293,7 +3302,9 @@ void diff_setup(struct diff_options *options)\n \toptions->use_color = diff_use_color_default;\n \toptions->detect_rename = diff_detect_rename_default;\n \toptions->xdl_opts |= diff_algorithm;\n-\tif (diff_compaction_heuristic)\n+\tif (diff_indent_heuristic)\n+\t\tDIFF_XDL_SET(options, INDENT_HEURISTIC);\n+\telse if (diff_compaction_heuristic)\n \t\tDIFF_XDL_SET(options, COMPACTION_HEURISTIC);\n \n \toptions->orderfile = diff_order_file_cfg;\n@@ -3815,9 +3826,15 @@ int diff_opt_parse(struct diff_options *options,\n \t\tDIFF_XDL_SET(options, IGNORE_WHITESPACE_AT_EOL);\n \telse if (!strcmp(arg, \"--ignore-blank-lines\"))\n \t\tDIFF_XDL_SET(options, IGNORE_BLANK_LINES);\n-\telse if (!strcmp(arg, \"--compaction-heuristic\"))\n+\telse if (!strcmp(arg, \"--indent-heuristic\")) {\n+\t\tDIFF_XDL_SET(options, INDENT_HEURISTIC);\n+\t\tDIFF_XDL_CLR(options, COMPACTION_HEURISTIC);\n+\t} else if (!strcmp(arg, \"--no-indent-heuristic\"))\n+\t\tDIFF_XDL_CLR(options, INDENT_HEURISTIC);\n+\telse if (!strcmp(arg, \"--compaction-heuristic\")) {\n \t\tDIFF_XDL_SET(options, COMPACTION_HEURISTIC);\n-\telse if (!strcmp(arg, \"--no-compaction-heuristic\"))\n+\t\tDIFF_XDL_CLR(options, INDENT_HEURISTIC);\n+\t} else if (!strcmp(arg, \"--no-compaction-heuristic\"))\n \t\tDIFF_XDL_CLR(options, COMPACTION_HEURISTIC);\n \telse if (!strcmp(arg, \"--patience\"))\n \t\toptions->xdl_opts = DIFF_WITH_ALG(options, PATIENCE_DIFF);\ndiff --git a/git-add--interactive.perl b/git-add--interactive.perl\nindex 642cce1..ee3d812 100755\n--- a/git-add--interactive.perl\n+++ b/git-add--interactive.perl\n@@ -45,6 +45,7 @@ my ($diff_new_color) =\n my $normal_color = $repo->get_color(\"\", \"reset\");\n \n my $diff_algorithm = $repo->config('diff.algorithm');\n+my $diff_indent_heuristic = $repo->config_bool('diff.indentheuristic');\n my $diff_compaction_heuristic = $repo->config_bool('diff.compactionheuristic');\n my $diff_filter = $repo->config('interactive.difffilter');\n \n@@ -750,7 +751,9 @@ sub parse_diff {\n \tif (defined $diff_algorithm) {\n \t\tsplice @diff_cmd, 1, 0, \"--diff-algorithm=${diff_algorithm}\";\n \t}\n-\tif ($diff_compaction_heuristic) {\n+\tif ($diff_indent_heuristic) {\n+\t\tsplice @diff_cmd, 1, 0, \"--indent-heuristic\";\n+\t} elsif ($diff_compaction_heuristic) {\n \t\tsplice @diff_cmd, 1, 0, \"--compaction-heuristic\";\n \t}\n \tif (defined $patch_mode_revision) {\ndiff --git a/t/t4059-diff-indent.sh b/t/t4059-diff-indent.sh\nnew file mode 100755\nindex 0000000..0a65b58\n--- /dev/null\n+++ b/t/t4059-diff-indent.sh\n@@ -0,0 +1,187 @@\n+#!/bin/sh\n+\n+test_description='Test diff indent heuristic.\n+\n+'\n+. ./test-lib.sh\n+. \"$TEST_DIRECTORY\"/diff-lib.sh\n+\n+# Compare two diff outputs. Ignore \"index\" lines, because we don't\n+# care about SHA-1s or file modes.\n+compare_diff () {\n+\tsed -e \"/^index /d\" <\"$1\" >.tmp-1\n+\tsed -e \"/^index /d\" <\"$2\" >.tmp-2\n+\ttest_cmp .tmp-1 .tmp-2 && rm -f .tmp-1 .tmp-2\n+}\n+\n+test_expect_success 'prepare' '\n+\tcat <<-\\EOF >spaces.txt &&\n+\t1\n+\t2\n+\ta\n+\n+\tb\n+\t3\n+\t4\n+\tEOF\n+\n+\tcat <<-\\EOF >functions.c &&\n+\t1\n+\t2\n+\t/* function */\n+\tfoo() {\n+\t    foo\n+\t}\n+\n+\t3\n+\t4\n+\tEOF\n+\n+\tgit add spaces.txt functions.c &&\n+\ttest_tick &&\n+\tgit commit -m initial &&\n+\tgit branch old &&\n+\n+\tcat <<-\\EOF >spaces.txt &&\n+\t1\n+\t2\n+\ta\n+\n+\tb\n+\ta\n+\n+\tb\n+\t3\n+\t4\n+\tEOF\n+\n+\tcat <<-\\EOF >functions.c &&\n+\t1\n+\t2\n+\t/* function */\n+\tbar() {\n+\t    foo\n+\t}\n+\n+\t/* function */\n+\tfoo() {\n+\t    foo\n+\t}\n+\n+\t3\n+\t4\n+\tEOF\n+\n+\tgit add spaces.txt functions.c &&\n+\ttest_tick &&\n+\tgit commit -m initial &&\n+\tgit branch new &&\n+\n+\ttr \"_\" \" \" <<-\\EOF >spaces-expect &&\n+\tdiff --git a/spaces.txt b/spaces.txt\n+\t--- a/spaces.txt\n+\t+++ b/spaces.txt\n+\t@@ -3,5 +3,8 @@\n+\t a\n+\t_\n+\t b\n+\t+a\n+\t+\n+\t+b\n+\t 3\n+\t 4\n+\tEOF\n+\n+\ttr \"_\" \" \" <<-\\EOF >spaces-compacted-expect &&\n+\tdiff --git a/spaces.txt b/spaces.txt\n+\t--- a/spaces.txt\n+\t+++ b/spaces.txt\n+\t@@ -2,6 +2,9 @@\n+\t 2\n+\t a\n+\t_\n+\t+b\n+\t+a\n+\t+\n+\t b\n+\t 3\n+\t 4\n+\tEOF\n+\n+\ttr \"_\" \" \" <<-\\EOF >functions-expect &&\n+\tdiff --git a/functions.c b/functions.c\n+\t--- a/functions.c\n+\t+++ b/functions.c\n+\t@@ -1,6 +1,11 @@\n+\t 1\n+\t 2\n+\t /* function */\n+\t+bar() {\n+\t+    foo\n+\t+}\n+\t+\n+\t+/* function */\n+\t foo() {\n+\t     foo\n+\t }\n+\tEOF\n+\n+\ttr \"_\" \" \" <<-\\EOF >functions-compacted-expect\n+\tdiff --git a/functions.c b/functions.c\n+\t--- a/functions.c\n+\t+++ b/functions.c\n+\t@@ -1,5 +1,10 @@\n+\t 1\n+\t 2\n+\t+/* function */\n+\t+bar() {\n+\t+    foo\n+\t+}\n+\t+\n+\t /* function */\n+\t foo() {\n+\t     foo\n+\tEOF\n+'\n+\n+test_expect_success 'diff: ugly spaces' '\n+\tgit diff old new -- spaces.txt >out &&\n+\tcompare_diff spaces-expect out\n+'\n+\n+test_expect_success 'diff: nice spaces with --indent-heuristic' '\n+\tgit diff --indent-heuristic old new -- spaces.txt >out-compacted &&\n+\tcompare_diff spaces-compacted-expect out-compacted\n+'\n+\n+test_expect_success 'diff: nice spaces with diff.indentHeuristic' '\n+\tgit -c diff.indentHeuristic=true diff old new -- spaces.txt >out-compacted2 &&\n+\tcompare_diff spaces-compacted-expect out-compacted2\n+'\n+\n+test_expect_success 'diff: --no-indent-heuristic overrides config' '\n+\tgit -c diff.indentHeuristic=true diff --no-indent-heuristic old new -- spaces.txt >out2 &&\n+\tcompare_diff spaces-expect out2\n+'\n+\n+test_expect_success 'diff: --indent-heuristic with --patience' '\n+\tgit diff --indent-heuristic --patience old new -- spaces.txt >out-compacted3 &&\n+\tcompare_diff spaces-compacted-expect out-compacted3\n+'\n+\n+test_expect_success 'diff: --indent-heuristic with --histogram' '\n+\tgit diff --indent-heuristic --histogram old new -- spaces.txt >out-compacted4 &&\n+\tcompare_diff spaces-compacted-expect out-compacted4\n+'\n+\n+test_expect_success 'diff: ugly functions' '\n+\tgit diff old new -- functions.c >out &&\n+\tcompare_diff functions-expect out\n+'\n+\n+test_expect_success 'diff: nice functions with --indent-heuristic' '\n+\tgit diff --indent-heuristic old new -- functions.c >out-compacted &&\n+\tcompare_diff functions-compacted-expect out-compacted\n+'\n+\n+test_done\ndiff --git a/xdiff/xdiff.h b/xdiff/xdiff.h\nindex 7423f77..8db16d4 100644\n--- a/xdiff/xdiff.h\n+++ b/xdiff/xdiff.h\n@@ -42,6 +42,7 @@ extern \"C\" {\n #define XDF_IGNORE_BLANK_LINES (1 << 7)\n \n #define XDF_COMPACTION_HEURISTIC (1 << 8)\n+#define XDF_INDENT_HEURISTIC (1 << 9)\n \n #define XDL_EMIT_FUNCNAMES (1 << 0)\n #define XDL_EMIT_FUNCCONTEXT (1 << 2)\ndiff --git a/xdiff/xdiffi.c b/xdiff/xdiffi.c\nindex 44fded6..67c1ccc 100644\n--- a/xdiff/xdiffi.c\n+++ b/xdiff/xdiffi.c\n@@ -414,6 +414,286 @@ static int recs_match(xrecord_t *rec1, xrecord_t *rec2, long flags)\n }\n \n /*\n+ * If a line is indented more than this, get_indent() just returns this value.\n+ * This avoids having to do absurd amounts of work for data that are not\n+ * human-readable text, and also ensures that the output of get_indent fits within\n+ * an int.\n+ */\n+#define MAX_INDENT 200\n+\n+/*\n+ * Return the amount of indentation of the specified line, treating TAB as 8\n+ * columns. Return -1 if line is empty or contains only whitespace. Clamp the\n+ * output value at MAX_INDENT.\n+ */\n+static int get_indent(xrecord_t *rec)\n+{\n+\tlong i;\n+\tint ret = 0;\n+\n+\tfor (i = 0; i < rec->size; i++) {\n+\t\tchar c = rec->ptr[i];\n+\n+\t\tif (!XDL_ISSPACE(c))\n+\t\t\treturn ret;\n+\t\telse if (c == ' ')\n+\t\t\tret += 1;\n+\t\telse if (c == '\\t')\n+\t\t\tret += 8 - ret % 8;\n+\t\t/* ignore other whitespace characters */\n+\n+\t\tif (ret >= MAX_INDENT)\n+\t\t\treturn MAX_INDENT;\n+\t}\n+\n+\t/* The line contains only whitespace. */\n+\treturn -1;\n+}\n+\n+/*\n+ * If more than this number of consecutive blank rows are found, just return this\n+ * value. This avoids requiring O(N^2) work for pathological cases, and also\n+ * ensures that the output of score_split fits in an int.\n+ */\n+#define MAX_BLANKS 20\n+\n+/* Characteristics measured about a hypothetical split position. */\n+struct split_measurement {\n+\t/*\n+\t * Is the split at the end of the file (aside from any blank lines)?\n+\t */\n+\tint end_of_file;\n+\n+\t/*\n+\t * How much is the line immediately following the split indented (or -1 if\n+\t * the line is blank):\n+\t */\n+\tint indent;\n+\n+\t/*\n+\t * How many consecutive lines above the split are blank?\n+\t */\n+\tint pre_blank;\n+\n+\t/*\n+\t * How much is the nearest non-blank line above the split indented (or -1\n+\t * if there is no such line)?\n+\t */\n+\tint pre_indent;\n+\n+\t/*\n+\t * How many lines after the line following the split are blank?\n+\t */\n+\tint post_blank;\n+\n+\t/*\n+\t * How much is the nearest non-blank line after the line following the\n+\t * split indented (or -1 if there is no such line)?\n+\t */\n+\tint post_indent;\n+};\n+\n+struct split_score {\n+\t/* The effective indent of this split (smaller is preferred). */\n+\tint effective_indent;\n+\n+\t/* Penalty for this split (smaller is preferred). */\n+\tint penalty;\n+};\n+\n+/*\n+ * Fill m with information about a hypothetical split of xdf above line split.\n+ */\n+static void measure_split(const xdfile_t *xdf, long split,\n+\t\t\t  struct split_measurement *m)\n+{\n+\tlong i;\n+\n+\tif (split >= xdf->nrec) {\n+\t\tm->end_of_file = 1;\n+\t\tm->indent = -1;\n+\t} else {\n+\t\tm->end_of_file = 0;\n+\t\tm->indent = get_indent(xdf->recs[split]);\n+\t}\n+\n+\tm->pre_blank = 0;\n+\tm->pre_indent = -1;\n+\tfor (i = split - 1; i >= 0; i--) {\n+\t\tm->pre_indent = get_indent(xdf->recs[i]);\n+\t\tif (m->pre_indent != -1)\n+\t\t\tbreak;\n+\t\tm->pre_blank += 1;\n+\t\tif (m->pre_blank == MAX_BLANKS) {\n+\t\t\tm->pre_indent = 0;\n+\t\t\tbreak;\n+\t\t}\n+\t}\n+\n+\tm->post_blank = 0;\n+\tm->post_indent = -1;\n+\tfor (i = split + 1; i < xdf->nrec; i++) {\n+\t\tm->post_indent = get_indent(xdf->recs[i]);\n+\t\tif (m->post_indent != -1)\n+\t\t\tbreak;\n+\t\tm->post_blank += 1;\n+\t\tif (m->post_blank == MAX_BLANKS) {\n+\t\t\tm->post_indent = 0;\n+\t\t\tbreak;\n+\t\t}\n+\t}\n+}\n+\n+/*\n+ * The empirically-determined weight factors used by score_split() below.\n+ * Larger values means that the position is a less favorable place to split.\n+ *\n+ * Note that scores are only ever compared against each other, so multiplying\n+ * all of these weight/penalty values by the same factor wouldn't change the\n+ * heuristic's behavior. Still, we need to set that arbitrary scale *somehow*.\n+ * In practice, these numbers are chosen to be large enough that they can be\n+ * adjusted relative to each other with sufficient precision despite using\n+ * integer math.\n+ */\n+\n+/* Penalty if there are no non-blank lines before the split */\n+#define START_OF_FILE_PENALTY 1\n+\n+/* Penalty if there are no non-blank lines after the split */\n+#define END_OF_FILE_PENALTY 21\n+\n+/* Multiplier for the number of blank lines around the split */\n+#define TOTAL_BLANK_WEIGHT (-30)\n+\n+/* Multiplier for the number of blank lines after the split */\n+#define POST_BLANK_WEIGHT 6\n+\n+/*\n+ * Penalties applied if the line is indented more than its predecessor\n+ */\n+#define RELATIVE_INDENT_PENALTY (-4)\n+#define RELATIVE_INDENT_WITH_BLANK_PENALTY 10\n+\n+/*\n+ * Penalties applied if the line is indented less than both its predecessor and\n+ * its successor\n+ */\n+#define RELATIVE_OUTDENT_PENALTY 24\n+#define RELATIVE_OUTDENT_WITH_BLANK_PENALTY 17\n+\n+/*\n+ * Penalties applied if the line is indented less than its predecessor but not\n+ * less than its successor\n+ */\n+#define RELATIVE_DEDENT_PENALTY 23\n+#define RELATIVE_DEDENT_WITH_BLANK_PENALTY 17\n+\n+/*\n+ * We only consider whether the sum of the effective indents for splits are\n+ * less than (-1), equal to (0), or greater than (+1) each other. The resulting\n+ * value is multiplied by the following weight and combined with the penalty to\n+ * determine the better of two scores.\n+ */\n+#define INDENT_WEIGHT 60\n+\n+/*\n+ * Compute a badness score for the hypothetical split whose measurements are\n+ * stored in m. The weight factors were determined empirically using the tools and\n+ * corpus described in\n+ *\n+ *     https://github.com/mhagger/diff-slider-tools\n+ *\n+ * Also see that project if you want to improve the weights based on, for example,\n+ * a larger or more diverse corpus.\n+ */\n+static void score_add_split(const struct split_measurement *m, struct split_score *s)\n+{\n+\t/*\n+\t * A place to accumulate penalty factors (positive makes this index more\n+\t * favored):\n+\t */\n+\tint post_blank, total_blank, indent, any_blanks;\n+\n+\tif (m->pre_indent == -1 && m->pre_blank == 0)\n+\t\ts->penalty += START_OF_FILE_PENALTY;\n+\n+\tif (m->end_of_file)\n+\t\ts->penalty += END_OF_FILE_PENALTY;\n+\n+\t/*\n+\t * Set post_blank to the number of blank lines following the split,\n+\t * including the line immediately after the split:\n+\t */\n+\tpost_blank = (m->indent == -1) ? 1 + m->post_blank : 0;\n+\ttotal_blank = m->pre_blank + post_blank;\n+\n+\t/* Penalties based on nearby blank lines: */\n+\ts->penalty += TOTAL_BLANK_WEIGHT * total_blank;\n+\ts->penalty += POST_BLANK_WEIGHT * post_blank;\n+\n+\tif (m->indent != -1)\n+\t\tindent = m->indent;\n+\telse\n+\t\tindent = m->post_indent;\n+\n+\tany_blanks = (total_blank != 0);\n+\n+\t/* Note that the effective indent is -1 at the end of the file: */\n+\ts->effective_indent += indent;\n+\n+\tif (indent == -1) {\n+\t\t/* No additional adjustments needed. */\n+\t} else if (m->pre_indent == -1) {\n+\t\t/* No additional adjustments needed. */\n+\t} else if (indent > m->pre_indent) {\n+\t\t/*\n+\t\t * The line is indented more than its predecessor.\n+\t\t */\n+\t\ts->penalty += any_blanks ?\n+\t\t\tRELATIVE_INDENT_WITH_BLANK_PENALTY :\n+\t\t\tRELATIVE_INDENT_PENALTY;\n+\t} else if (indent == m->pre_indent) {\n+\t\t/*\n+\t\t * The line has the same indentation level as its predecessor.\n+\t\t * No additional adjustments needed.\n+\t\t */\n+\t} else {\n+\t\t/*\n+\t\t * The line is indented less than its predecessor. It could be\n+\t\t * the block terminator of the previous block, but it could\n+\t\t * also be the start of a new block (e.g., an \"else\" block, or\n+\t\t * maybe the previous block didn't have a block terminator).\n+\t\t * Try to distinguish those cases based on what comes next:\n+\t\t */\n+\t\tif (m->post_indent != -1 && m->post_indent > indent) {\n+\t\t\t/*\n+\t\t\t * The following line is indented more. So it is likely\n+\t\t\t * that this line is the start of a block.\n+\t\t\t */\n+\t\t\ts->penalty += any_blanks ?\n+\t\t\t\tRELATIVE_OUTDENT_WITH_BLANK_PENALTY :\n+\t\t\t\tRELATIVE_OUTDENT_PENALTY;\n+\t\t} else {\n+\t\t\t/*\n+\t\t\t * That was probably the end of a block.\n+\t\t\t */\n+\t\t\ts->penalty += any_blanks ?\n+\t\t\t\tRELATIVE_DEDENT_WITH_BLANK_PENALTY :\n+\t\t\t\tRELATIVE_DEDENT_PENALTY;\n+\t\t}\n+\t}\n+}\n+\n+static int score_cmp(struct split_score *s1, struct split_score *s2)\n+{\n+\t/* -1 if s1.effective_indent < s2->effective_indent, etc. */\n+\tint cmp_indents = ((s1->effective_indent > s2->effective_indent) -\n+\t\t\t   (s1->effective_indent < s2->effective_indent));\n+\n+\treturn INDENT_WEIGHT * cmp_indents + (s1->penalty - s2->penalty);\n+}\n+\n+/*\n  * Represent a group of changed lines in an xdfile_t (i.e., a contiguous group\n  * of lines that was inserted or deleted from the corresponding version of the\n  * file). We consider there to be such a group at the beginning of the file, at\n@@ -604,6 +884,14 @@ int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) {\n \t\t\t}\n \t\t} while (groupsize != g.end - g.start);\n \n+\t\t/*\n+\t\t * If the group can be shifted, then we can possibly use this\n+\t\t * freedom to produce a more intuitive diff.\n+\t\t *\n+\t\t * The group is currently shifted as far down as possible, so the\n+\t\t * heuristics below only have to handle upwards shifts.\n+\t\t */\n+\n \t\tif (g.end == earliest_end) {\n \t\t\t/* no shifting was possible */\n \t\t} else if (end_matching_other != -1) {\n@@ -633,6 +921,43 @@ int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) {\n \t\t\t\tif (group_previous(xdfo, &go))\n \t\t\t\t\txdl_bug(\"group sync broken sliding to blank line\");\n \t\t\t}\n+\t\t} else if (flags & XDF_INDENT_HEURISTIC) {\n+\t\t\t/*\n+\t\t\t * Indent heuristic: a group of pure add/delete lines\n+\t\t\t * implies two splits, one between the end of the \"before\"\n+\t\t\t * context and the start of the group, and another between\n+\t\t\t * the end of the group and the beginning of the \"after\"\n+\t\t\t * context. Some splits are aesthetically better and some\n+\t\t\t * are worse. We compute a badness \"score\" for each split,\n+\t\t\t * and add the scores for the two splits to define a\n+\t\t\t * \"score\" for each position that the group can be shifted\n+\t\t\t * to. Then we pick the shift with the lowest score.\n+\t\t\t */\n+\t\t\tlong shift, best_shift = -1;\n+\t\t\tstruct split_score best_score;\n+\n+\t\t\tfor (shift = earliest_end; shift <= g.end; shift++) {\n+\t\t\t\tstruct split_measurement m;\n+\t\t\t\tstruct split_score score = {0, 0};\n+\n+\t\t\t\tmeasure_split(xdf, shift, &m);\n+\t\t\t\tscore_add_split(&m, &score);\n+\t\t\t\tmeasure_split(xdf, shift - groupsize, &m);\n+\t\t\t\tscore_add_split(&m, &score);\n+\t\t\t\tif (best_shift == -1 ||\n+\t\t\t\t    score_cmp(&score, &best_score) <= 0) {\n+\t\t\t\t\tbest_score.effective_indent = score.effective_indent;\n+\t\t\t\t\tbest_score.penalty = score.penalty;\n+\t\t\t\t\tbest_shift = shift;\n+\t\t\t\t}\n+\t\t\t}\n+\n+\t\t\twhile (g.end > best_shift) {\n+\t\t\t\tif (group_slide_up(xdf, &g, flags))\n+\t\t\t\t\txdl_bug(\"best shift unreached\");\n+\t\t\t\tif (group_previous(xdfo, &go))\n+\t\t\t\t\txdl_bug(\"group sync broken sliding to blank line\");\n+\t\t\t}\n \t\t}\n \n \tnext:\n-- \n2.9.3\n\n"},{"id":"301091","messageId":"1f7f17cc1b2434ab5dfc4fea95a09597e61a4886.1473068229.git.mhagger@alum.mit.edu","threadId":"44005","inReplyTo":"cover.1473068229.git.mhagger@alum.mit.edu","subject":"[PATCH v3 4/8] recs_match(): take two xrecord_t pointers as arguments","fromName":"Michael Haggerty","fromEmail":"mhagger@alum.mit.edu","sentAt":"2016-09-05T09:44:49Z","receivedAt":"2016-09-05T09:46:20Z","isPatch":true,"sender":{"key":"mhagger@alum.mit.edu","avatar":"https://avatars.githubusercontent.com/u/119718?v=4"},"body":"There is no reason for it to take an array and two indexes as argument,\nas it only accesses two elements of the array.\n\nSigned-off-by: Michael Haggerty <mhagger@alum.mit.edu>\n---\n xdiff/xdiffi.c | 14 +++++++-------\n 1 file changed, 7 insertions(+), 7 deletions(-)\n\ndiff --git a/xdiff/xdiffi.c b/xdiff/xdiffi.c\nindex ed2df64..8a5832a 100644\n--- a/xdiff/xdiffi.c\n+++ b/xdiff/xdiffi.c\n@@ -405,11 +405,11 @@ static int is_blank_line(xrecord_t *rec, long flags)\n \treturn xdl_blankline(rec->ptr, rec->size, flags);\n }\n \n-static int recs_match(xrecord_t **recs, long ixs, long ix, long flags)\n+static int recs_match(xrecord_t *rec1, xrecord_t *rec2, long flags)\n {\n-\treturn (recs[ixs]->ha == recs[ix]->ha &&\n-\t\txdl_recmatch(recs[ixs]->ptr, recs[ixs]->size,\n-\t\t\t     recs[ix]->ptr, recs[ix]->size,\n+\treturn (rec1->ha == rec2->ha &&\n+\t\txdl_recmatch(rec1->ptr, rec1->size,\n+\t\t\t     rec2->ptr, rec2->size,\n \t\t\t     flags));\n }\n \n@@ -457,7 +457,7 @@ int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) {\n \t\t\t * the last line of the current change group, shift backward\n \t\t\t * the group.\n \t\t\t */\n-\t\t\twhile (ixs > 0 && recs_match(recs, ixs - 1, ix - 1, flags)) {\n+\t\t\twhile (ixs > 0 && recs_match(recs[ixs - 1], recs[ix - 1], flags)) {\n \t\t\t\trchg[--ixs] = 1;\n \t\t\t\trchg[--ix] = 0;\n \n@@ -484,7 +484,7 @@ int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) {\n \t\t\t * the line next of the current change group, shift forward\n \t\t\t * the group.\n \t\t\t */\n-\t\t\twhile (ix < nrec && recs_match(recs, ixs, ix, flags)) {\n+\t\t\twhile (ix < nrec && recs_match(recs[ixs], recs[ix], flags)) {\n \t\t\t\tblank_lines += is_blank_line(recs[ix], flags);\n \n \t\t\t\trchg[ixs++] = 0;\n@@ -525,7 +525,7 @@ int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) {\n \t\t\t */\n \t\t\twhile (ixs > 0 &&\n \t\t\t       !is_blank_line(recs[ix - 1], flags) &&\n-\t\t\t       recs_match(recs, ixs - 1, ix - 1, flags)) {\n+\t\t\t       recs_match(recs[ixs - 1], recs[ix - 1], flags)) {\n \t\t\t\trchg[--ixs] = 1;\n \t\t\t\trchg[--ix] = 0;\n \t\t\t\twhile (rchgo[--ixo]);\n-- \n2.9.3\n\n"},{"id":"301304","messageId":"xmqqy433mvg6.fsf@gitster.mtv.corp.google.com","threadId":"44005","inReplyTo":"cover.1473068229.git.mhagger@alum.mit.edu","subject":"Re: [PATCH v3 0/8] Better heuristics make prettier diffs","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2016-09-07T18:11:21Z","receivedAt":"2016-09-07T18:11:29Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Michael Haggerty <mhagger@alum.mit.edu> writes:\n\n> * In \"blame: honor the diff heuristic options and config\":\n>\n>   * In v2, I suggested making `blame` honor all diff-related options.\n>     Junio explained why this was a bad idea. So this version only\n>     makes `blame` honor `--indent-heuristic` and\n>     `--compaction-heuristic`.\n\nThis makes a lot more sense to me.  I am not sure if it is worth\nadding 7/8 to expose these experimental knobs to the end users, but\nthe amount of the code needed is very small, so let's take it.\n\nI am hoping that we can lose both of the experimental knobs and use\nthe indent heuristic unconditionally in the future, though ;-)\n\nThanks.\n\n\n"},{"id":"301327","messageId":"CA+P7+xo7Q0q+Vg4atb23M90HiMazwgiXGFmgty-+hF-r+3veww@mail.gmail.com","threadId":"44005","inReplyTo":"xmqqy433mvg6.fsf@gitster.mtv.corp.google.com","subject":"Re: [PATCH v3 0/8] Better heuristics make prettier diffs","fromName":"Jacob Keller","fromEmail":"jacob.keller@gmail.com","sentAt":"2016-09-07T21:21:57Z","receivedAt":"2016-09-07T21:22:22Z","isPatch":true,"sender":{"key":"jacob.keller@gmail.com","avatar":"https://avatars.githubusercontent.com/u/874719?v=4"},"body":"On Wed, Sep 7, 2016 at 11:11 AM, Junio C Hamano <gitster@pobox.com> wrote:\n> Michael Haggerty <mhagger@alum.mit.edu> writes:\n>\n>> * In \"blame: honor the diff heuristic options and config\":\n>>\n>>   * In v2, I suggested making `blame` honor all diff-related options.\n>>     Junio explained why this was a bad idea. So this version only\n>>     makes `blame` honor `--indent-heuristic` and\n>>     `--compaction-heuristic`.\n>\n> This makes a lot more sense to me.  I am not sure if it is worth\n> adding 7/8 to expose these experimental knobs to the end users, but\n> the amount of the code needed is very small, so let's take it.\n>\n> I am hoping that we can lose both of the experimental knobs and use\n> the indent heuristic unconditionally in the future, though ;-)\n>\n> Thanks.\n>\n\n\nAgreed, I think we should drop the knobs and use the feature\nunconditionally as well.\n\nThanks,\nJake\n"},{"id":"301340","messageId":"xmqqr38vjns0.fsf@gitster.mtv.corp.google.com","threadId":"44005","inReplyTo":"cover.1473068229.git.mhagger@alum.mit.edu","subject":"Re: [PATCH v3 0/8] Better heuristics make prettier diffs","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2016-09-07T23:25:19Z","receivedAt":"2016-09-07T23:25:29Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Michael Haggerty <mhagger@alum.mit.edu> writes:\n\n>   * Add test t4059 as part of this commit, not as part of its\n>     successor.\n\nWhich needs to be moved to somewhere else, as another topics that\nhas already been in 'next' uses t4059.\n\nI'd move it temporarily to t4061 with a separate SQUASH??? at the\ntip for now, as I am running out of time today.\n\n\n\n"},{"id":"302121","messageId":"c381d458-4c81-f46c-592a-98957b3a177c@alum.mit.edu","threadId":"44005","inReplyTo":"xmqqr38vjns0.fsf@gitster.mtv.corp.google.com","subject":"Re: [PATCH v3 0/8] Better heuristics make prettier diffs","fromName":"Michael Haggerty","fromEmail":"mhagger@alum.mit.edu","sentAt":"2016-09-19T06:35:38Z","receivedAt":"2016-09-19T06:35:50Z","isPatch":true,"sender":{"key":"mhagger@alum.mit.edu","avatar":"https://avatars.githubusercontent.com/u/119718?v=4"},"body":"On 09/08/2016 01:25 AM, Junio C Hamano wrote:\n> Michael Haggerty <mhagger@alum.mit.edu> writes:\n> \n>>   * Add test t4059 as part of this commit, not as part of its\n>>     successor.\n> \n> Which needs to be moved to somewhere else, as another topics that\n> has already been in 'next' uses t4059.\n> \n> I'd move it temporarily to t4061 with a separate SQUASH??? at the\n> tip for now, as I am running out of time today.\n\nI didn't realize you were waiting for an ACK. Yes, it's totally OK to\nrename the test.\n\nMichael\n\n"},{"id":"302145","messageId":"xmqqd1jzho2l.fsf@gitster.mtv.corp.google.com","threadId":"44005","inReplyTo":"c381d458-4c81-f46c-592a-98957b3a177c@alum.mit.edu","subject":"Re: [PATCH v3 0/8] Better heuristics make prettier diffs","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2016-09-19T16:05:54Z","receivedAt":"2016-09-19T16:06:01Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Michael Haggerty <mhagger@alum.mit.edu> writes:\n\n> On 09/08/2016 01:25 AM, Junio C Hamano wrote:\n>> Michael Haggerty <mhagger@alum.mit.edu> writes:\n>> \n>>>   * Add test t4059 as part of this commit, not as part of its\n>>>     successor.\n>> \n>> Which needs to be moved to somewhere else, as another topics that\n>> has already been in 'next' uses t4059.\n>> \n>> I'd move it temporarily to t4061 with a separate SQUASH??? at the\n>> tip for now, as I am running out of time today.\n>\n> I didn't realize you were waiting for an ACK. Yes, it's totally OK to\n> rename the test.\n\nI actually wasn't asking for an Ack.\n\nAs the issue was in the one that is buried a few commits from the\ntip, and there is a later one that adds more tests to it, I didn't\nfind enough energy to rename the new file in a buried commit and\nthen adjust the patch later updates it, I was hoping that you'd\nreroll to save me effort, rather than forcing me to do the rebase\nmyself ;-).\n\n"},{"id":"302155","messageId":"xmqqintrg5pq.fsf@gitster.mtv.corp.google.com","threadId":"44005","inReplyTo":"xmqqd1jzho2l.fsf@gitster.mtv.corp.google.com","subject":"Re: [PATCH v3 0/8] Better heuristics make prettier diffs","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2016-09-19T17:27:45Z","receivedAt":"2016-09-19T17:27:53Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> Michael Haggerty <mhagger@alum.mit.edu> writes:\n>\n>> On 09/08/2016 01:25 AM, Junio C Hamano wrote:\n>>> I'd move it temporarily to t4061 with a separate SQUASH??? at the\n>>> tip for now, as I am running out of time today.\n>>\n>> I didn't realize you were waiting for an ACK. Yes, it's totally OK to\n>> rename the test.\n>\n> I actually wasn't asking for an Ack.\n>\n> As the issue was in the one that is buried a few commits from the\n> tip, and there is a later one that adds more tests to it, I didn't\n> find enough energy to rename the new file in a buried commit and\n> then adjust the patch later updates it, I was hoping that you'd\n> reroll to save me effort, rather than forcing me to do the rebase\n> myself ;-).\n\nNow I did, so no need to resend (unless you have changes other than\nthe renaming of the test script, that is).\n\nLet's move it down to 'next' soonish.\n\nThanks.\n"},{"id":"302216","messageId":"e0ceef8e-1809-1503-eb85-ab0f2c678e6d@alum.mit.edu","threadId":"44005","inReplyTo":"xmqqintrg5pq.fsf@gitster.mtv.corp.google.com","subject":"Re: [PATCH v3 0/8] Better heuristics make prettier diffs","fromName":"Michael Haggerty","fromEmail":"mhagger@alum.mit.edu","sentAt":"2016-09-20T06:33:39Z","receivedAt":"2016-09-20T06:33:52Z","isPatch":true,"sender":{"key":"mhagger@alum.mit.edu","avatar":"https://avatars.githubusercontent.com/u/119718?v=4"},"body":"On 09/19/2016 07:27 PM, Junio C Hamano wrote:\n> Junio C Hamano <gitster@pobox.com> writes:\n> \n>> Michael Haggerty <mhagger@alum.mit.edu> writes:\n>>\n>>> On 09/08/2016 01:25 AM, Junio C Hamano wrote:\n>>>> I'd move it temporarily to t4061 with a separate SQUASH??? at the\n>>>> tip for now, as I am running out of time today.\n>>>\n>>> I didn't realize you were waiting for an ACK. Yes, it's totally OK to\n>>> rename the test.\n>>\n>> I actually wasn't asking for an Ack.\n>>\n>> As the issue was in the one that is buried a few commits from the\n>> tip, and there is a later one that adds more tests to it, I didn't\n>> find enough energy to rename the new file in a buried commit and\n>> then adjust the patch later updates it, I was hoping that you'd\n>> reroll to save me effort, rather than forcing me to do the rebase\n>> myself ;-).\n> \n> Now I did, so no need to resend (unless you have changes other than\n> the renaming of the test script, that is).\n\nThanks for taking care of this.\n\n> Let's move it down to 'next' soonish.\n\nYes, it would be good to get feedback early enough in the cycle that we\ncan make a final decision about which diff heuristics should be used by\ndefault and whether/what UI to implement for switching between them.\n\nMichael\n\n"}]}