{"thread":{"id":"43901","subject":"[PATCH v2 2/7] xdl_change_compact(): only use heuristic if group can't be matched","startedAt":"2016-08-22T11:24:19Z","lastAt":"2016-09-07T17:58:18Z","messageCount":13,"participants":["Michael Haggerty","René Scharfe","Junio C Hamano"],"isPatch":true,"patchVersion":2,"patchTotal":7},"messages":[{"id":"299828","messageId":"d2fd0eefdf6e4f2884df3214b32606e5b43e0555.1471864378.git.mhagger@alum.mit.edu","threadId":"43901","inReplyTo":"cover.1471864378.git.mhagger@alum.mit.edu","subject":"[PATCH v2 2/7] xdl_change_compact(): only use heuristic if group can't be matched","fromName":"Michael Haggerty","fromEmail":"mhagger@alum.mit.edu","sentAt":"2016-08-22T11:22:41Z","receivedAt":"2016-08-22T11:24:19Z","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":"299829","messageId":"119c03e74c022ccdd6ebd8661a98b43917f8e966.1471864378.git.mhagger@alum.mit.edu","threadId":"43901","inReplyTo":"cover.1471864378.git.mhagger@alum.mit.edu","subject":"[PATCH v2 1/7] xdl_change_compact(): fix compaction heuristic to adjust ixo","fromName":"Michael Haggerty","fromEmail":"mhagger@alum.mit.edu","sentAt":"2016-08-22T11:22:40Z","receivedAt":"2016-08-22T11:24:23Z","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":"299830","messageId":"830c0001cd2423302ee97be506c3827cd213c879.1471864378.git.mhagger@alum.mit.edu","threadId":"43901","inReplyTo":"cover.1471864378.git.mhagger@alum.mit.edu","subject":"[PATCH v2 6/7] diff: improve positioning of add/delete blocks in diffs","fromName":"Michael Haggerty","fromEmail":"mhagger@alum.mit.edu","sentAt":"2016-08-22T11:22:45Z","receivedAt":"2016-08-22T11:24:26Z","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 xdiff/xdiff.h                  |   1 +\n xdiff/xdiffi.c                 | 325 +++++++++++++++++++++++++++++++++++++++++\n 6 files changed, 359 insertions(+), 11 deletions(-)\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/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..dc79628 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+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":"299831","messageId":"cover.1471864378.git.mhagger@alum.mit.edu","threadId":"43901","inReplyTo":null,"subject":"[PATCH v2 0/7] Better heuristics make prettier diffs","fromName":"Michael Haggerty","fromEmail":"mhagger@alum.mit.edu","sentAt":"2016-08-22T11:22:39Z","receivedAt":"2016-08-22T11:24:31Z","isPatch":true,"sender":{"key":"mhagger@alum.mit.edu","avatar":"https://avatars.githubusercontent.com/u/119718?v=4"},"body":"This is v2 of a patch series to improve the heuristics that `git diff`\nand related commands use to position ambiguous blocks of added/deleted\nlines. Thanks to Stefan, Jacob, Peff, Junio, and Jakub for their\ncomments about v1 [1]. This patch series is also available from my\nGitHub account [2] as branch `diff-indent-heuristic`.\n\nThis version started out as a light touch-up of v1, but as I was\nworking I realized that it needed more changes. Among other things,\nthe final heuristic is now improved relative to the one proposed in\nv1. Summary of changes:\n\n* I changed the `--compaction-heuristic` code such that if a group of\n  added/deleted lines can be aligned with a group of deleted/added\n  lines in the other file, then that should be done rather than try to\n  slide to a blank line. In the existing code, the compaction\n  heuristic was attempted, incorrectly, *after* trying to align\n  groups, which doesn't make sense.\n\n* I changed `recs_match()` similarly to `is_blank_line()`: now it\n  takes two `xrecord_t *` as arguments rather than an array of\n  `xrecord_t` and two indexes.\n\n* I refactored the old `xdl_change_compact()` more thoroughly. All of\n  the index manipulation was pretty confusing, and I was having\n  trouble convincing myself that even the old code was working\n  correctly. So I introduced a `struct group`, which is like a cursor\n  that keeps track of a (possibly empty) group of changed lines in the\n  old or new version of a file. I added functions to initialize a\n  group to the first change in a file, to move to the next or previous\n  groups, and to slide a group forward or backward if possible (i.e.,\n  if the group is a \"slider\").\n\n* In the course of that refactoring, I found (and fixed) another bug\n  in the `--compaction-heuristic` code: it didn't notice if the top\n  possible position of a slider had a blank line as its last line. See\n  the commit message of patch [5/5] for more details.\n\n* I realized that the behavior of the indent heuristic from v1,\n  because it multiplied weighting factors times indent widths, would\n  behave differently if a project changed its convention for how many\n  spaces to use per level of indentation. That doesn't make sense, so\n  I changed the handling of indentation:\n\n  Now, the sum of the indentation width at the top and bottom of the\n  slider are added, *but those sums are only compared* between slider\n  positions, and the weight is multipled by -1, 0, or +1 depending on\n  whether the first indent sum is less than, equal, or greater than\n  the other. This should make the behavior relatively independent of\n  the project's indentation convention, and thus make the heuristic\n  work more consistently across projects.\n\n  The resulting heuristic works significantly better than the one\n  proposed in v1:\n\n  | repository            | count |      Git 2.9.0 |             v1 |             v2 |\n  | --------------------- | ----- | -------------- | -------------- | -------------- |\n  | afnetworking          |   109 |    89  (81.7%) |     3   (2.8%) |     2   (1.8%) |\n  | alamofire             |    30 |    18  (60.0%) |     2   (6.7%) |     0   (0.0%) |\n  | angular               |   184 |   127  (69.0%) |    15   (8.2%) |     5   (2.7%) |\n  | animate               |   313 |     2   (0.6%) |     2   (0.6%) |     2   (0.6%) |\n  | ant                   |   380 |   356  (93.7%) |    22   (5.8%) |    15   (3.9%) |\n  | bugzilla              |   306 |   263  (85.9%) |    24   (7.8%) |    15   (4.9%) |\n  | corefx                |   126 |    91  (72.2%) |    15  (11.9%) |     6   (4.8%) |\n  | couchdb               |    78 |    44  (56.4%) |    17  (21.8%) |     6   (7.7%) |\n  | cpython               |   937 |   158  (16.9%) |    26   (2.8%) |     5   (0.5%) |\n  | discourse             |   160 |    95  (59.4%) |    24  (15.0%) |    13   (8.1%) |\n  | docker                |   307 |   194  (63.2%) |    16   (5.2%) |     8   (2.6%) |\n  | electron              |   163 |   132  (81.0%) |    15   (9.2%) |     6   (3.7%) |\n  | git                   |   536 |   470  (87.7%) |    18   (3.4%) |    16   (3.0%) |\n  | gitflow               |   127 |     0   (0.0%) |     0   (0.0%) |     0   (0.0%) |\n  | ionic                 |   133 |    89  (66.9%) |     6   (4.5%) |     1   (0.8%) |\n  | ipython               |   482 |   362  (75.1%) |    45   (9.3%) |    11   (2.3%) |\n  | junit                 |   161 |   147  (91.3%) |     4   (2.5%) |     1   (0.6%) |\n  | lighttable            |    15 |     5  (33.3%) |     2  (13.3%) |     0   (0.0%) |\n  | magit                 |    88 |    75  (85.2%) |     5   (5.7%) |     0   (0.0%) |\n  | neural-style          |    28 |     0   (0.0%) |     0   (0.0%) |     0   (0.0%) |\n  | nodejs                |   781 |   649  (83.1%) |    98  (12.5%) |     5   (0.6%) |\n  | phpmyadmin            |   491 |   481  (98.0%) |     7   (1.4%) |     2   (0.4%) |\n  | react-native          |   168 |   130  (77.4%) |     5   (3.0%) |     0   (0.0%) |\n  | rust                  |   171 |   128  (74.9%) |    17   (9.9%) |    14   (8.2%) |\n  | spark                 |   186 |   149  (80.1%) |    17   (9.1%) |     2   (1.1%) |\n  | tensorflow            |   115 |    66  (57.4%) |     7   (6.1%) |     5   (4.3%) |\n  | test-more             |    19 |    15  (78.9%) |     1   (5.3%) |     1   (5.3%) |\n  | test-unit             |    51 |    34  (66.7%) |     8  (15.7%) |     2   (3.9%) |\n  | xmonad                |    23 |    22  (95.7%) |     1   (4.3%) |     1   (4.3%) |\n  | --------------------- | ----- | -------------- | -------------- | -------------- |\n  | totals                |  6668 |  4391  (65.9%) |   422   (6.3%) |   144   (2.2%) |\n\n* I noticed that most of the \"bonus\" weights were actually negative,\n  so calling them \"bonuses\" was misleading. Therefore, I negated the\n  coefficients and now call them \"penalties\"\n\n* I noticed that `git blame` was parsing diff options like\n  `--indent-heuristic` from the command line, but wasn't using the\n  values. That is fixed in a new patch [07/07].\n\n* I redid all of the analysis and training with a bigger corpus. To\n  check whether I was overtraining the heuristic, I also did the\n  following experiment: I optimized the weights against a training set\n  consisting of only some of the repositories, then tested it against\n  the rest of the corpus. See the full results in the commit message\n  for patch [06/06].\n\n* I added a couple of smoke tests.\n\nThe companion project [3] now provides an easy way to replicate and\nextend these results, if anybody is interested.\n\nThe new heuristic still has to be enabled via the `--indent-heuristic`\ncommand-line parameter or the `diff.indentHeuristic` configuration\nsetting. Before we release this, we should decide what the final UI\nshould look like and make it so. If we decide to replace the existing\ncompaction heuristic with this one and/or turn this heuristic on for\nall users by default, those steps might look like branch\n`indent-heuristic-default` in my GitHub repository [2].\n\nMichael\n\n[1] http://public-inbox.org/git/cover.1470259583.git.mhagger@alum.mit.edu/t/#u\n[2] https://github.com/mhagger/git\n[3] https://github.com/mhagger/diff-slider-tools\n\nMichael Haggerty (7):\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  blame: actually use the diff opts parsed from the command line\n\n Documentation/diff-config.txt  |   7 +-\n Documentation/diff-options.txt |   9 +-\n builtin/blame.c                |  11 +-\n diff.c                         |  23 +-\n git-add--interactive.perl      |   5 +-\n t/t4059-diff-indent.sh         | 160 +++++++++++\n xdiff/xdiff.h                  |   1 +\n xdiff/xdiffi.c                 | 635 ++++++++++++++++++++++++++++++++++-------\n 8 files changed, 736 insertions(+), 115 deletions(-)\n create mode 100755 t/t4059-diff-indent.sh\n\n-- \n2.9.3\n\n"},{"id":"299832","messageId":"453ac11efe49ada493dc8af2041ad6de190c5a77.1471864378.git.mhagger@alum.mit.edu","threadId":"43901","inReplyTo":"cover.1471864378.git.mhagger@alum.mit.edu","subject":"[PATCH v2 3/7] is_blank_line(): take a single xrecord_t as argument","fromName":"Michael Haggerty","fromEmail":"mhagger@alum.mit.edu","sentAt":"2016-08-22T11:22:42Z","receivedAt":"2016-08-22T11:25:05Z","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":"299833","messageId":"21ade4ab233a868cabbe15598cd7b2ff4d04d286.1471864378.git.mhagger@alum.mit.edu","threadId":"43901","inReplyTo":"cover.1471864378.git.mhagger@alum.mit.edu","subject":"[PATCH v2 5/7] xdl_change_compact(): introduce the concept of a change group","fromName":"Michael Haggerty","fromEmail":"mhagger@alum.mit.edu","sentAt":"2016-08-22T11:22:44Z","receivedAt":"2016-08-22T11:25:08Z","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":"299834","messageId":"1f7f17cc1b2434ab5dfc4fea95a09597e61a4886.1471864378.git.mhagger@alum.mit.edu","threadId":"43901","inReplyTo":"cover.1471864378.git.mhagger@alum.mit.edu","subject":"[PATCH v2 4/7] recs_match(): take two xrecord_t pointers as arguments","fromName":"Michael Haggerty","fromEmail":"mhagger@alum.mit.edu","sentAt":"2016-08-22T11:22:43Z","receivedAt":"2016-08-22T11:25:09Z","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":"299835","messageId":"8192012a6bf725e0460522f9e67bab83b613127a.1471864378.git.mhagger@alum.mit.edu","threadId":"43901","inReplyTo":"cover.1471864378.git.mhagger@alum.mit.edu","subject":"[PATCH v2 7/7] blame: actually use the diff opts parsed from the command line","fromName":"Michael Haggerty","fromEmail":"mhagger@alum.mit.edu","sentAt":"2016-08-22T11:22:46Z","receivedAt":"2016-08-22T11:25:10Z","isPatch":true,"sender":{"key":"mhagger@alum.mit.edu","avatar":"https://avatars.githubusercontent.com/u/119718?v=4"},"body":"\"git blame\" already parsed generic diff options from the command line\nvia diff_opt_parse(), but instead of passing the resulting xdl_opts to\nxdi_diff(), it sent its own xdl_opts, which only reflected the values of\nthe self-parsed options \"-w\" and \"--minimal\". Instead, rely on\ndiff_opt_parse() to parse all of the diff options, including \"-w\" and\n\"--minimal\", and pass the resulting xdl_opts to xdi_diff().\n\nSigned-off-by: Michael Haggerty <mhagger@alum.mit.edu>\n---\nSomebody who knows more about how diff operations are configured\nshould please review this. I'm not certain that the change as\nimplemented won't have other unwanted side-effects, though of course\nI checked that the test suite runs correctly.\n\n builtin/blame.c        |  11 ++--\n t/t4059-diff-indent.sh | 160 +++++++++++++++++++++++++++++++++++++++++++++++++\n 2 files changed, 165 insertions(+), 6 deletions(-)\n create mode 100755 t/t4059-diff-indent.sh\n\ndiff --git a/builtin/blame.c b/builtin/blame.c\nindex 7ec7823..cde2d15 100644\n--- a/builtin/blame.c\n+++ b/builtin/blame.c\n@@ -48,11 +48,12 @@ static int show_root;\n static int reverse;\n static int blank_boundary;\n static int incremental;\n-static int xdl_opts;\n static int abbrev = -1;\n static int no_whole_file_rename;\n static int show_progress;\n \n+static struct rev_info revs;\n+\n static struct date_mode blame_date_mode = { DATE_ISO8601 };\n static size_t blame_date_width;\n \n@@ -137,11 +138,12 @@ struct progress_info {\n static int diff_hunks(mmfile_t *file_a, mmfile_t *file_b,\n \t\t      xdl_emit_hunk_consume_func_t hunk_func, void *cb_data)\n {\n-\txpparam_t xpp = {0};\n+\txpparam_t xpp;\n \txdemitconf_t xecfg = {0};\n \txdemitcb_t ecb = {NULL};\n \n-\txpp.flags = xdl_opts;\n+\tmemset(&xpp, 0, sizeof(xpp));\n+\txpp.flags = revs.diffopt.xdl_opts;\n \txecfg.hunk_func = hunk_func;\n \tecb.priv = cb_data;\n \treturn xdi_diff(file_a, file_b, &xpp, &xecfg, &ecb);\n@@ -2517,7 +2519,6 @@ static int blame_move_callback(const struct option *option, const char *arg, int\n \n int cmd_blame(int argc, const char **argv, const char *prefix)\n {\n-\tstruct rev_info revs;\n \tconst char *path;\n \tstruct scoreboard sb;\n \tstruct origin *o;\n@@ -2548,8 +2549,6 @@ int cmd_blame(int argc, const char **argv, const char *prefix)\n \t\tOPT_BIT('l', NULL, &output_option, N_(\"Show long commit SHA1 (Default: off)\"), OUTPUT_LONG_OBJECT_NAME),\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-\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 \t\t{ OPTION_CALLBACK, 'C', NULL, &opt, N_(\"score\"), N_(\"Find line copies within and across files\"), PARSE_OPT_OPTARG, blame_copy_callback },\ndiff --git a/t/t4059-diff-indent.sh b/t/t4059-diff-indent.sh\nnew file mode 100755\nindex 0000000..8b6c7ef\n--- /dev/null\n+++ b/t/t4059-diff-indent.sh\n@@ -0,0 +1,160 @@\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 'diff: favor trailing blank lines' '\n+\tcat <<-\\EOF >old &&\n+\t1\n+\t2\n+\ta\n+\n+\tb\n+\t3\n+\t4\n+\tEOF\n+\n+\tcat <<-\\EOF >new &&\n+\t1\n+\t2\n+\ta\n+\n+\tb\n+\ta\n+\n+\tb\n+\t3\n+\t4\n+\tEOF\n+\n+\ttr \"_\" \" \" <<-\\EOF >expect &&\n+\tdiff --git a/old b/new\n+\t--- a/old\n+\t+++ b/new\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 >expect-compacted &&\n+\tdiff --git a/old b/new\n+\t--- a/old\n+\t+++ b/new\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+\ttest_must_fail git diff --no-index old new >out &&\n+\tcompare_diff expect out &&\n+\n+\ttest_must_fail git diff --no-index --indent-heuristic old new >out-compacted &&\n+\tcompare_diff expect-compacted out-compacted &&\n+\n+\ttest_must_fail git -c diff.indentHeuristic=true diff --no-index old new >out-compacted2 &&\n+\tcompare_diff expect-compacted out-compacted2 &&\n+\n+\ttest_must_fail git diff --indent-heuristic --patience --no-index old new >out-compacted3 &&\n+\tcompare_diff expect-compacted out-compacted3 &&\n+\n+\ttest_must_fail git diff --indent-heuristic --histogram --no-index old new >out-compacted4 &&\n+\tcompare_diff expect-compacted out-compacted4\n+'\n+\n+test_expect_success 'diff: keep functions together' '\n+\tcat <<-\\EOF >old &&\n+\t1\n+\t2\n+\t/* function */\n+\tfoo() {\n+\t    foo\n+\t}\n+\n+\t3\n+\t4\n+\tEOF\n+\n+\tcat <<-\\EOF >new &&\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+\ttr \"_\" \" \" <<-\\EOF >expect &&\n+\tdiff --git a/old b/new\n+\t--- a/old\n+\t+++ b/new\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 >expect-compacted &&\n+\tdiff --git a/old b/new\n+\t--- a/old\n+\t+++ b/new\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+\ttest_must_fail git diff --no-index old new >out &&\n+\tcompare_diff expect out &&\n+\n+\ttest_must_fail git diff --no-index --indent-heuristic old new >out-compacted &&\n+\tcompare_diff expect-compacted out-compacted\n+'\n+\n+test_done\n-- \n2.9.3\n\n"},{"id":"299899","messageId":"fe96d9e6-b306-0b57-8f24-6106b7e285cb@web.de","threadId":"43901","inReplyTo":"8192012a6bf725e0460522f9e67bab83b613127a.1471864378.git.mhagger@alum.mit.edu","subject":"Re: [PATCH v2 7/7] blame: actually use the diff opts parsed from the command line","fromName":"René Scharfe","fromEmail":"l.s.r@web.de","sentAt":"2016-08-23T09:56:54Z","receivedAt":"2016-08-23T10:06:42Z","isPatch":true,"sender":{"key":"l.s.r@web.de","avatar":"https://avatars.githubusercontent.com/u/26122331?v=4"},"body":"Am 22.08.2016 um 13:22 schrieb Michael Haggerty:\n> \"git blame\" already parsed generic diff options from the command line\n> via diff_opt_parse(), but instead of passing the resulting xdl_opts to\n> xdi_diff(), it sent its own xdl_opts, which only reflected the values of\n> the self-parsed options \"-w\" and \"--minimal\". Instead, rely on\n> diff_opt_parse() to parse all of the diff options, including \"-w\" and\n> \"--minimal\", and pass the resulting xdl_opts to xdi_diff().\n\nSounds useful: It allows more fine-grained control over which whitespace \nchanges to ignore and which diff algorithm to use.  There is a bit of \noverlap (e.g. with -b meaning show blank boundaries vs. ignore \nwhitespace changes), but with your patch blame's own options still take \nprecedence, so there should be no unpleasant surprises.\n\n> Signed-off-by: Michael Haggerty <mhagger@alum.mit.edu>\n> ---\n> Somebody who knows more about how diff operations are configured\n> should please review this. I'm not certain that the change as\n> implemented won't have other unwanted side-effects, though of course\n> I checked that the test suite runs correctly.\n\nI don't qualify, but I'll comment anyway..\n\n>  builtin/blame.c        |  11 ++--\n>  t/t4059-diff-indent.sh | 160 +++++++++++++++++++++++++++++++++++++++++++++++++\n>  2 files changed, 165 insertions(+), 6 deletions(-)\n>  create mode 100755 t/t4059-diff-indent.sh\n\nThis new test doesn't call git blame.  Does it belong to a different \ncommit?  And shouldn't the change to blame.c stand on its own, outside \nof this series?\n\n> diff --git a/builtin/blame.c b/builtin/blame.c\n> index 7ec7823..cde2d15 100644\n> --- a/builtin/blame.c\n> +++ b/builtin/blame.c\n> @@ -48,11 +48,12 @@ static int show_root;\n>  static int reverse;\n>  static int blank_boundary;\n>  static int incremental;\n> -static int xdl_opts;\n>  static int abbrev = -1;\n>  static int no_whole_file_rename;\n>  static int show_progress;\n>\n> +static struct rev_info revs;\n> +\n>  static struct date_mode blame_date_mode = { DATE_ISO8601 };\n>  static size_t blame_date_width;\n>\n> @@ -137,11 +138,12 @@ struct progress_info {\n>  static int diff_hunks(mmfile_t *file_a, mmfile_t *file_b,\n>  \t\t      xdl_emit_hunk_consume_func_t hunk_func, void *cb_data)\n>  {\n> -\txpparam_t xpp = {0};\n> +\txpparam_t xpp;\n>  \txdemitconf_t xecfg = {0};\n>  \txdemitcb_t ecb = {NULL};\n>\n> -\txpp.flags = xdl_opts;\n> +\tmemset(&xpp, 0, sizeof(xpp));\n> +\txpp.flags = revs.diffopt.xdl_opts;\n\nWhy call memset instead of using a static initializer?  The intent of \nthis patch is just to change the .flags assignment, isn't it?\n\n>  \txecfg.hunk_func = hunk_func;\n>  \tecb.priv = cb_data;\n>  \treturn xdi_diff(file_a, file_b, &xpp, &xecfg, &ecb);\n> @@ -2517,7 +2519,6 @@ static int blame_move_callback(const struct option *option, const char *arg, int\n>\n>  int cmd_blame(int argc, const char **argv, const char *prefix)\n>  {\n> -\tstruct rev_info revs;\n>  \tconst char *path;\n>  \tstruct scoreboard sb;\n>  \tstruct origin *o;\n> @@ -2548,8 +2549,6 @@ int cmd_blame(int argc, const char **argv, const char *prefix)\n>  \t\tOPT_BIT('l', NULL, &output_option, N_(\"Show long commit SHA1 (Default: off)\"), OUTPUT_LONG_OBJECT_NAME),\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> -\t\tOPT_BIT(0, \"minimal\", &xdl_opts, N_(\"Spend extra cycles to find better match\"), XDF_NEED_MINIMAL),\n\nThis removes -w and --minimal from blame's short help; diff options \nshould be mentioned somehow in exchange for that loss.  Or perhaps they \nshould be mentioned in git-rev-list(1)?  (git blame -h points to \ngit-rev-list(1) already.)\n\nDocumentation/git-blame.txt needs an update as well.\n\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>  \t\t{ OPTION_CALLBACK, 'C', NULL, &opt, N_(\"score\"), N_(\"Find line copies within and across files\"), PARSE_OPT_OPTARG, blame_copy_callback },\n\n"},{"id":"299963","messageId":"xmqqtwebwhbg.fsf@gitster.mtv.corp.google.com","threadId":"43901","inReplyTo":"8192012a6bf725e0460522f9e67bab83b613127a.1471864378.git.mhagger@alum.mit.edu","subject":"Re: [PATCH v2 7/7] blame: actually use the diff opts parsed from the command line","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2016-08-23T17:01:07Z","receivedAt":"2016-08-23T17:01:16Z","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> Somebody who knows more about how diff operations are configured\n> should please review this. I'm not certain that the change as\n> implemented won't have other unwanted side-effects, though of course\n> I checked that the test suite runs correctly.\n\nGenerally, I think this is a bad idea.  \n\nWe run \"diff-tree\" internally for multiple purposes:\n\n - When the same path we are drilling down appears, we use diff-tree\n   to compare that path alone, to obtain textual diff, so that we\n   can say \"what did not appear in the textual diff output are\n   attributable to the parent commit, we can exonerate this child\".\n\n   Even if the command line to \"git blame\" had \"-Sfoo\", you do not\n   want to pass it down to this invocation.  If the child did not\n   change the number of occurrences of string \"foo\" in the path\n   being drilled down, it will pass down the blame for all lines to\n   the parent.  And copying -R from the command line to this\n   invocation does not make any sense.  Copying -C -C from the\n   command line will defeat the whole purpose of having find_origin\n   vs find_rename distinction, I would think, for this step.\n\n - When the path we have been drilling down for no longer appears in\n   the parent, we use diff-tree with rename detection to find where\n   the file _could_ have come from.\n\n   It is a good idea to allow the similarity threshold to be tweaked\n   from the command line.  Copying -R from the command line to this\n   invocation may make sense, but I think you would break this step\n   if you copied -C/-C -C\n\n - When -C/-C -C/-C -C -C is given from the command line, after\n   running the \"pass the blame to our primary suspect\", we run\n   tree-level diff-tree with find-copy option, only to enumerate\n   paths that appear in the parent.  We pick pieces from these paths\n   by doing blob-level diffs in diff_hunks() ourselves.  Copying -C\n   from the command line would be useful if you are only doing a\n   single '-C' (because it would allow you to tweak the similarity\n   threshold), but otherwise it would probably break the command.\n\nThe design principle taken in \"git blame\" is to leave the decision\non what diff options do or do not make sense for these particular\ninvocations of the internal \"diff-tree\", and have \"blame\" give the\nselected options to the internal \"diff-tree\" invocations.  \"-w\"\nhappens to use xdl_opts that will eventually be passed down to diff\nmachinery but you should consider it an accidental optimization\n(i.e. \"blame\" designer considered that it makes sense to give\nwhitespace-ignoring comparison at the leaf-level \"diff between\nblobs\", and found that the most efficient way to do so is to just\ntake \"-w\" from the command line and set the ignore-whitespace bit\nused to drive the internal \"diff-tree\" invocation).  If we _were_\nadding -R<num> to allow the users to affect similarity threshold,\nwe would parse it in \"git blame\" command line option processing,\nand would give that _ONLY_ to the second invocation above, i.e. the\none done in find_rename() but not in others like in find_origin.\n\n\n\n\n\n"},{"id":"299989","messageId":"xmqqeg5fuq1y.fsf@gitster.mtv.corp.google.com","threadId":"43901","inReplyTo":"21ade4ab233a868cabbe15598cd7b2ff4d04d286.1471864378.git.mhagger@alum.mit.edu","subject":"Re: [PATCH v2 5/7] xdl_change_compact(): introduce the concept of a change group","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2016-08-23T21:35:21Z","receivedAt":"2016-08-23T21:36:03Z","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> 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>\n> But these simple concepts are obfuscated by a lot of index handling that\n> is written in terse, subtle, and varied patterns. I found it very hard\n> to convince myself that the function was correct.\n>\n> So introduce a \"struct group\" that represents a group of changed lines\n> in a file. Add some functions that perform elementary operations on\n> groups:\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>\n> Even though the resulting code is longer, I think it is easier to\n> understand and review.\n\nYup.  The important thing is that the length of the core logic of\nsliding up and down becomes easier to read, because it shrinks; the\nmechanics of sliding up and down may need more lines with boilderplate,\nbut they are isolated \"do one thing and do it well\" helpers.\n\nNice.\n\n\n"},{"id":"301079","messageId":"a6d9e9a7-76c4-5b26-0b49-55698369180d@alum.mit.edu","threadId":"43901","inReplyTo":"fe96d9e6-b306-0b57-8f24-6106b7e285cb@web.de","subject":"Re: [PATCH v2 7/7] blame: actually use the diff opts parsed from the command line","fromName":"Michael Haggerty","fromEmail":"mhagger@alum.mit.edu","sentAt":"2016-09-05T04:12:41Z","receivedAt":"2016-09-05T04:12:59Z","isPatch":true,"sender":{"key":"mhagger@alum.mit.edu","avatar":"https://avatars.githubusercontent.com/u/119718?v=4"},"body":"On 08/23/2016 11:56 AM, René Scharfe wrote:\n> Am 22.08.2016 um 13:22 schrieb Michael Haggerty:\n>> \"git blame\" already parsed generic diff options from the command line\n>> via diff_opt_parse(), but instead of passing the resulting xdl_opts to\n>> xdi_diff(), it sent its own xdl_opts, which only reflected the values of\n>> the self-parsed options \"-w\" and \"--minimal\". Instead, rely on\n>> diff_opt_parse() to parse all of the diff options, including \"-w\" and\n>> \"--minimal\", and pass the resulting xdl_opts to xdi_diff().\n> \n> Sounds useful: It allows more fine-grained control over which whitespace\n> changes to ignore and which diff algorithm to use.  There is a bit of\n> overlap (e.g. with -b meaning show blank boundaries vs. ignore\n> whitespace changes), but with your patch blame's own options still take\n> precedence, so there should be no unpleasant surprises.\n> \n>> Signed-off-by: Michael Haggerty <mhagger@alum.mit.edu>\n>> ---\n>> Somebody who knows more about how diff operations are configured\n>> should please review this. I'm not certain that the change as\n>> implemented won't have other unwanted side-effects, though of course\n>> I checked that the test suite runs correctly.\n> \n> I don't qualify, but I'll comment anyway..\n> \n>>  builtin/blame.c        |  11 ++--\n>>  t/t4059-diff-indent.sh | 160\n>> +++++++++++++++++++++++++++++++++++++++++++++++++\n>>  2 files changed, 165 insertions(+), 6 deletions(-)\n>>  create mode 100755 t/t4059-diff-indent.sh\n> \n> This new test doesn't call git blame.  Does it belong to a different\n> commit?  And shouldn't the change to blame.c stand on its own, outside\n> of this series?\n\nThanks for catching this. I squashed the test incorrectly; it was meant\nto be part of the previous commit. I'll fix it in v3.\n\nThe reason that I would prefer to change `blame` as part of this patch\nseries is that I think it would be disconcerting for `git diff` and `git\nblame` to use different heuristics when computing diffs. It would make\ntheir output inconsistent.\n\nBut probably that means passing just the new option to `git blame`\nrather than passing all possible `diff` options through.\n\n> [...]\n\nYour other comments may be moot given that changing `git blame` in this\nway seems to have been a bad idea in the first place [1]. But I'll keep\nthem in mind if a new version contains similar code.\n\nThanks,\nMichael\n\n[1]\nhttp://public-inbox.org/git/xmqqtwebwhbg.fsf@gitster.mtv.corp.google.com/\n\n"},{"id":"301301","messageId":"xmqq37lboaml.fsf@gitster.mtv.corp.google.com","threadId":"43901","inReplyTo":"a6d9e9a7-76c4-5b26-0b49-55698369180d@alum.mit.edu","subject":"Re: [PATCH v2 7/7] blame: actually use the diff opts parsed from the command line","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2016-09-07T17:58:10Z","receivedAt":"2016-09-07T17:58:18Z","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> The reason that I would prefer to change `blame` as part of this patch\n> series is that I think it would be disconcerting for `git diff` and `git\n> blame` to use different heuristics when computing diffs. It would make\n> their output inconsistent.\n\nI do think it is the right thing to do.  With your shifting heuristics,\n\"git diff\" would attribute an addition of a whole block more\ncorrectly, e.g.\n\n\t }\n\n        +foo {\n        +\tbar\n        +       baz\n\t+}\n\ninstead of attributing the tail of the new thing to the old author,\nand the \"blame\" should take advantage of the better heuristics as\nwell.\n"}]}