threads / patch / 43901

v2, 7 partsxdl_change_compact(): only use heuristic if group can't be matched

Subject: [PATCH v2 2/7] xdl_change_compact(): only use heuristic if group can't be matched

## tl;dr

13 messages between Aug 22, 2016 and Sep 7, 2016. Diffs are folded; open one to read it.

replies: 12people: 3as markdown or json

Michael Haggerty· Aug 22, 2016, 11:22 UTC · lore

[PATCH v2 0/7] Better heuristics make prettier diffs

This is v2 of a patch series to improve the heuristics that `git diff` and related commands use to position ambiguous blocks of added/deleted lines. Thanks to Stefan, Jacob, Peff, Junio, and Jakub for their comments about v1 [1]. This patch series is also available from my GitHub account [2] as branch `diff-indent-heuristic`.

This version started out as a light touch-up of v1, but as I was working I realized that it needed more changes. Among other things, the final heuristic is now improved relative to the one proposed in v1. Summary of changes:

* I changed the `--compaction-heuristic` code such that if a group of
  added/deleted lines can be aligned with a group of deleted/added
  lines in the other file, then that should be done rather than try to
  slide to a blank line. In the existing code, the compaction
  heuristic was attempted, incorrectly, *after* trying to align
  groups, which doesn't make sense.
* I changed `recs_match()` similarly to `is_blank_line()`: now it
  takes two `xrecord_t *` as arguments rather than an array of
  `xrecord_t` and two indexes.
* I refactored the old `xdl_change_compact()` more thoroughly. All of
  the index manipulation was pretty confusing, and I was having
  trouble convincing myself that even the old code was working
  correctly. So I introduced a `struct group`, which is like a cursor
  that keeps track of a (possibly empty) group of changed lines in the
  old or new version of a file. I added functions to initialize a
  group to the first change in a file, to move to the next or previous
  groups, and to slide a group forward or backward if possible (i.e.,
  if the group is a "slider").
* In the course of that refactoring, I found (and fixed) another bug
  in the `--compaction-heuristic` code: it didn't notice if the top
  possible position of a slider had a blank line as its last line. See
  the commit message of patch [5/5] for more details.
* I realized that the behavior of the indent heuristic from v1,
  because it multiplied weighting factors times indent widths, would
  behave differently if a project changed its convention for how many
  spaces to use per level of indentation. That doesn't make sense, so
  I changed the handling of indentation:
  Now, the sum of the indentation width at the top and bottom of the
  slider are added, *but those sums are only compared* between slider
  positions, and the weight is multipled by -1, 0, or +1 depending on
  whether the first indent sum is less than, equal, or greater than
  the other. This should make the behavior relatively independent of
  the project's indentation convention, and thus make the heuristic
  work more consistently across projects.
  The resulting heuristic works significantly better than the one
  proposed in v1:
  | repository            | count |      Git 2.9.0 |             v1 |             v2 |
  | --------------------- | ----- | -------------- | -------------- | -------------- |
  | afnetworking          |   109 |    89  (81.7%) |     3   (2.8%) |     2   (1.8%) |
  | alamofire             |    30 |    18  (60.0%) |     2   (6.7%) |     0   (0.0%) |
  | angular               |   184 |   127  (69.0%) |    15   (8.2%) |     5   (2.7%) |
  | animate               |   313 |     2   (0.6%) |     2   (0.6%) |     2   (0.6%) |
  | ant                   |   380 |   356  (93.7%) |    22   (5.8%) |    15   (3.9%) |
  | bugzilla              |   306 |   263  (85.9%) |    24   (7.8%) |    15   (4.9%) |
  | corefx                |   126 |    91  (72.2%) |    15  (11.9%) |     6   (4.8%) |
  | couchdb               |    78 |    44  (56.4%) |    17  (21.8%) |     6   (7.7%) |
  | cpython               |   937 |   158  (16.9%) |    26   (2.8%) |     5   (0.5%) |
  | discourse             |   160 |    95  (59.4%) |    24  (15.0%) |    13   (8.1%) |
  | docker                |   307 |   194  (63.2%) |    16   (5.2%) |     8   (2.6%) |
  | electron              |   163 |   132  (81.0%) |    15   (9.2%) |     6   (3.7%) |
  | git                   |   536 |   470  (87.7%) |    18   (3.4%) |    16   (3.0%) |
  | gitflow               |   127 |     0   (0.0%) |     0   (0.0%) |     0   (0.0%) |
  | ionic                 |   133 |    89  (66.9%) |     6   (4.5%) |     1   (0.8%) |
  | ipython               |   482 |   362  (75.1%) |    45   (9.3%) |    11   (2.3%) |
  | junit                 |   161 |   147  (91.3%) |     4   (2.5%) |     1   (0.6%) |
  | lighttable            |    15 |     5  (33.3%) |     2  (13.3%) |     0   (0.0%) |
  | magit                 |    88 |    75  (85.2%) |     5   (5.7%) |     0   (0.0%) |
  | neural-style          |    28 |     0   (0.0%) |     0   (0.0%) |     0   (0.0%) |
  | nodejs                |   781 |   649  (83.1%) |    98  (12.5%) |     5   (0.6%) |
  | phpmyadmin            |   491 |   481  (98.0%) |     7   (1.4%) |     2   (0.4%) |
  | react-native          |   168 |   130  (77.4%) |     5   (3.0%) |     0   (0.0%) |
  | rust                  |   171 |   128  (74.9%) |    17   (9.9%) |    14   (8.2%) |
  | spark                 |   186 |   149  (80.1%) |    17   (9.1%) |     2   (1.1%) |
  | tensorflow            |   115 |    66  (57.4%) |     7   (6.1%) |     5   (4.3%) |
  | test-more             |    19 |    15  (78.9%) |     1   (5.3%) |     1   (5.3%) |
  | test-unit             |    51 |    34  (66.7%) |     8  (15.7%) |     2   (3.9%) |
  | xmonad                |    23 |    22  (95.7%) |     1   (4.3%) |     1   (4.3%) |
  | --------------------- | ----- | -------------- | -------------- | -------------- |
  | totals                |  6668 |  4391  (65.9%) |   422   (6.3%) |   144   (2.2%) |
* I noticed that most of the "bonus" weights were actually negative,
  so calling them "bonuses" was misleading. Therefore, I negated the
  coefficients and now call them "penalties"
* I noticed that `git blame` was parsing diff options like
  `--indent-heuristic` from the command line, but wasn't using the
  values. That is fixed in a new patch [07/07].
* I redid all of the analysis and training with a bigger corpus. To
  check whether I was overtraining the heuristic, I also did the
  following experiment: I optimized the weights against a training set
  consisting of only some of the repositories, then tested it against
  the rest of the corpus. See the full results in the commit message
  for patch [06/06].
* I added a couple of smoke tests.

The companion project [3] now provides an easy way to replicate and extend these results, if anybody is interested.

The new heuristic still has to be enabled via the `--indent-heuristic` command-line parameter or the `diff.indentHeuristic` configuration setting. Before we release this, we should decide what the final UI should look like and make it so. If we decide to replace the existing compaction heuristic with this one and/or turn this heuristic on for all users by default, those steps might look like branch `indent-heuristic-default` in my GitHub repository [2].

Michael

[1] http://public-inbox.org/git/cover.1470259583.git.mhagger@alum.mit.edu/t/#u [2] https://github.com/mhagger/git [3] https://github.com/mhagger/diff-slider-tools

Michael Haggerty (7):
  xdl_change_compact(): fix compaction heuristic to adjust ixo
  xdl_change_compact(): only use heuristic if group can't be matched
  is_blank_line(): take a single xrecord_t as argument
  recs_match(): take two xrecord_t pointers as arguments
  xdl_change_compact(): introduce the concept of a change group
  diff: improve positioning of add/delete blocks in diffs
  blame: actually use the diff opts parsed from the command line
 Documentation/diff-config.txt  |   7 +-
 Documentation/diff-options.txt |   9 +-
 builtin/blame.c                |  11 +-
 diff.c                         |  23 +-
 git-add--interactive.perl      |   5 +-
 t/t4059-diff-indent.sh         | 160 +++++++++++
 xdiff/xdiff.h                  |   1 +
 xdiff/xdiffi.c                 | 635 ++++++++++++++++++++++++++++++++++-------
 8 files changed, 736 insertions(+), 115 deletions(-)
 create mode 100755 t/t4059-diff-indent.sh
-- 
2.9.3
Michael Haggerty· Aug 22, 2016, 11:22 UTC · re: Michael Haggerty · lore

If the changed group of lines can be matched to a group in the other file, then that positioning should take precedence over the compaction heuristic.

The old code tried the heuristic unconditionally, which cost redundant effort and also was broken if the matching code had already shifted the group higher than the blank line.

Signed-off-by: Michael Haggerty <mhagger@alum.mit.edu>
---
 xdiff/xdiffi.c | 38 +++++++++++++++++++-------------------
 1 file changed, 19 insertions(+), 19 deletions(-)
Show changes to xdiff/xdiffi.c +19 −19
diff --git a/xdiff/xdiffi.c b/xdiff/xdiffi.c
index 95b037e..61deed8 100644
--- a/xdiff/xdiffi.c
+++ b/xdiff/xdiffi.c
@@ -504,25 +504,25 @@ int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) {
 			}
 		} while (grpsiz != ix - ixs);
 
-		/*
-		 * Try to move back the possibly merged group of changes, to match
-		 * the recorded position in the other file.
-		 */
-		while (ixref < ix) {
-			rchg[--ixs] = 1;
-			rchg[--ix] = 0;
-			while (rchgo[--ixo]);
-		}
-
-		/*
-		 * If a group can be moved back and forth, see if there is a
-		 * blank line in the moving space. If there is a blank line,
-		 * make sure the last blank line is the end of the group.
-		 *
-		 * As we already shifted the group forward as far as possible
-		 * in the earlier loop, we need to shift it back only if at all.
-		 */
-		if ((flags & XDF_COMPACTION_HEURISTIC) && blank_lines) {
+		if (ixref < ix) {
+			/*
+			 * Try to move back the possibly merged group of changes, to match
+			 * the recorded position in the other file.
+			 */
+			while (ixref < ix) {
+				rchg[--ixs] = 1;
+				rchg[--ix] = 0;
+				while (rchgo[--ixo]);
+			}
+		} else if ((flags & XDF_COMPACTION_HEURISTIC) && blank_lines) {
+			/*
+			 * The group can be slid up to make its last line a
+			 * blank line. Do so.
+			 *
+			 * As we already shifted the group forward as far as
+			 * possible in the earlier loop, we need to shift it
+			 * back only if at all.
+			 */
 			while (ixs > 0 &&
 			       !is_blank_line(recs, ix - 1, flags) &&
 			       recs_match(recs, ixs - 1, ix - 1, flags)) {
-- 
2.9.3
Michael Haggerty· Aug 22, 2016, 11:22 UTC · re: Michael Haggerty · lore

[PATCH v2 1/7] xdl_change_compact(): fix compaction heuristic to adjust ixo

The code branch used for the compaction heuristic forgot to keep ixo in sync while the group was shifted. This is certainly wrong, as it causes the two counters to get out of sync.

I *think* that this bug could also have caused the function to read past the end of the rchgo array, though I haven't done the work to prove it for sure. Here is my reasoning:

If ixo is not decremented correctly during one iteration of the outer while loop, then it will loose sync with the ix counter. In particular, ixo will be too large.

Suppose that the next iterations of the outer while loop (i.e., processing the next block of add/delete lines) don't have any sliders. Then the ixo counter would be incremented by the number of non-changed lines in xdf, which is the same as the number of non-changed lines in xdfo that *should have* followed the group that experienced the malfunction. But since ixo was too large at the end of that iteration, it will be incremented past the end of the xdfo->rchg array, and will try to read that memory illegally.

Signed-off-by: Michael Haggerty <mhagger@alum.mit.edu>
---
 xdiff/xdiffi.c | 1 +
 1 file changed, 1 insertion(+)
Show changes to xdiff/xdiffi.c +1 −0
diff --git a/xdiff/xdiffi.c b/xdiff/xdiffi.c
index b3c6848..95b037e 100644
--- a/xdiff/xdiffi.c
+++ b/xdiff/xdiffi.c
@@ -528,6 +528,7 @@ int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) {
 			       recs_match(recs, ixs - 1, ix - 1, flags)) {
 				rchg[--ixs] = 1;
 				rchg[--ix] = 0;
+				while (rchgo[--ixo]);
 			}
 		}
 	}
-- 
2.9.3
Michael Haggerty· Aug 22, 2016, 11:22 UTC · re: Michael Haggerty · lore

[PATCH v2 6/7] diff: improve positioning of add/delete blocks in diffs

Some groups of added/deleted lines in diffs can be slid up or down, because lines at the edges of the group are not unique. Picking good shifts for such groups is not a matter of correctness but definitely has a big effect on aesthetics. For example, consider the following two diffs. The first is what standard Git emits:

    --- a/9c572b21dd090a1e5c5bb397053bf8043ffe7fb4:git-send-email.perl
    +++ b/6dcfa306f2b67b733a7eb2d7ded1bc9987809edb:git-send-email.perl
    @@ -231,6 +231,9 @@ if (!defined $initial_reply_to && $prompting) {
     }
     if (!$smtp_server) {
    +       $smtp_server = $repo->config('sendemail.smtpserver');
    +}
    +if (!$smtp_server) {
            foreach (qw( /usr/sbin/sendmail /usr/lib/sendmail )) {
                    if (-x $_) {
                            $smtp_server = $_;

The following diff is equivalent, but is obviously preferable from an aesthetic point of view:

    --- a/9c572b21dd090a1e5c5bb397053bf8043ffe7fb4:git-send-email.perl
    +++ b/6dcfa306f2b67b733a7eb2d7ded1bc9987809edb:git-send-email.perl
    @@ -230,6 +230,9 @@ if (!defined $initial_reply_to && $prompting) {
            $initial_reply_to =~ s/(^\s+|\s+$)//g;
     }
    +if (!$smtp_server) {
    +       $smtp_server = $repo->config('sendemail.smtpserver');
    +}
     if (!$smtp_server) {
            foreach (qw( /usr/sbin/sendmail /usr/lib/sendmail )) {
                    if (-x $_) {

This patch teaches Git to pick better positions for such "diff sliders" using heuristics that take the positions of nearby blank lines and the indentation of nearby lines into account.

The existing Git code basically always shifts such "sliders" as far down in the file as possible. The only exception is when the slider can be aligned with a group of changed lines in the other file, in which case Git favors depicting the change as one add+delete block rather than one add and a slightly offset delete block. This naive algorithm often yields ugly diffs.

Commit d634d61ed6 improved the situation somewhat by preferring to position add/delete groups to make their last line a blank line, when that is possible. This heuristic does more good than harm, but (1) it can only help if there are blank lines in the right places, and (2) always picks the last blank line, even if there are others that might be better. The end result is that it makes perhaps 1/3 as many errors as the default Git algorithm, but that still leaves a lot of ugly diffs.

This commit implements a new and much better heuristic for picking optimal "slider" positions using the following approach: First observe that each hypothetical positioning of a diff slider introduces two splits: one between the context lines preceding the group and the first added/deleted line, and the other between the last added/deleted line and the first line of context following it. It tries to find the positioning that creates the least bad splits.

Splits are evaluated based only on the presence and locations of nearby blank lines, and the indentation of lines near the split. Basically, it prefers to introduce splits adjacent to blank lines, between lines that are indented less, and between lines with the same level of indentation. In more detail:

1. It measures the following characteristics of a proposed splitting
   position in a `struct split_measurement`:
   * the number of blank lines above the proposed split
   * whether the line directly after the split is blank
   * the number of blank lines following that line
   * the indentation of the nearest non-blank line above the split
   * the indentation of the line directly below the split
   * the indentation of the nearest non-blank line after that line
2. It combines the measured attributes using a bunch of
   empirically-optimized weighting factors to derive a `struct
   split_score` that measures the "badness" of splitting the text at
   that position.
3. It combines the `split_score` for the top and the bottom of the
   slider at each of its possible positions, and selects the position
   that has the best `split_score`.

I determined the initial set of weighting factors by collecting a corpus of Git histories from 29 open-source software projects in various programming languages. I generated many diffs from this corpus, and determined the best positioning "by eye" for about 6600 diff sliders. I used about half of the repositories in the corpus (corresponding to about 2/3 of the sliders) as a training set, and optimized the weights against this corpus using a crude automated search of the parameter space to get the best agreement with the manually-determined values. Then I tested the resulting heuristic against the full corpus. The results are summarized in the following table, in column `indent-1`:

| repository            | count |      Git 2.9.0 |     compaction | compaction-fixed |       indent-1 |       indent-2 |
| --------------------- | ----- | -------------- | -------------- | ---------------- | -------------- | -------------- |
| afnetworking          |   109 |    89  (81.7%) |    37  (33.9%) |      37  (33.9%) |     2   (1.8%) |     2   (1.8%) |
| alamofire             |    30 |    18  (60.0%) |    14  (46.7%) |      15  (50.0%) |     0   (0.0%) |     0   (0.0%) |
| angular               |   184 |   127  (69.0%) |    39  (21.2%) |      23  (12.5%) |     5   (2.7%) |     5   (2.7%) |
| animate               |   313 |     2   (0.6%) |     2   (0.6%) |       2   (0.6%) |     2   (0.6%) |     2   (0.6%) |
| ant                   |   380 |   356  (93.7%) |   152  (40.0%) |     148  (38.9%) |    15   (3.9%) |    15   (3.9%) | *
| bugzilla              |   306 |   263  (85.9%) |   109  (35.6%) |      99  (32.4%) |    14   (4.6%) |    15   (4.9%) | *
| corefx                |   126 |    91  (72.2%) |    22  (17.5%) |      21  (16.7%) |     6   (4.8%) |     6   (4.8%) |
| couchdb               |    78 |    44  (56.4%) |    26  (33.3%) |      28  (35.9%) |     6   (7.7%) |     6   (7.7%) | *
| cpython               |   937 |   158  (16.9%) |    50   (5.3%) |      49   (5.2%) |     5   (0.5%) |     5   (0.5%) | *
| discourse             |   160 |    95  (59.4%) |    42  (26.2%) |      36  (22.5%) |    18  (11.2%) |    13   (8.1%) |
| docker                |   307 |   194  (63.2%) |   198  (64.5%) |     253  (82.4%) |     8   (2.6%) |     8   (2.6%) | *
| electron              |   163 |   132  (81.0%) |    38  (23.3%) |      39  (23.9%) |     6   (3.7%) |     6   (3.7%) |
| git                   |   536 |   470  (87.7%) |    73  (13.6%) |      78  (14.6%) |    16   (3.0%) |    16   (3.0%) | *
| gitflow               |   127 |     0   (0.0%) |     0   (0.0%) |       0   (0.0%) |     0   (0.0%) |     0   (0.0%) |
| ionic                 |   133 |    89  (66.9%) |    29  (21.8%) |      38  (28.6%) |     1   (0.8%) |     1   (0.8%) |
| ipython               |   482 |   362  (75.1%) |   167  (34.6%) |     169  (35.1%) |    11   (2.3%) |    11   (2.3%) | *
| junit                 |   161 |   147  (91.3%) |    67  (41.6%) |      66  (41.0%) |     1   (0.6%) |     1   (0.6%) | *
| lighttable            |    15 |     5  (33.3%) |     0   (0.0%) |       2  (13.3%) |     0   (0.0%) |     0   (0.0%) |
| magit                 |    88 |    75  (85.2%) |    11  (12.5%) |       9  (10.2%) |     1   (1.1%) |     0   (0.0%) |
| neural-style          |    28 |     0   (0.0%) |     0   (0.0%) |       0   (0.0%) |     0   (0.0%) |     0   (0.0%) |
| nodejs                |   781 |   649  (83.1%) |   118  (15.1%) |     111  (14.2%) |     4   (0.5%) |     5   (0.6%) | *
| phpmyadmin            |   491 |   481  (98.0%) |    75  (15.3%) |      48   (9.8%) |     2   (0.4%) |     2   (0.4%) | *
| react-native          |   168 |   130  (77.4%) |    79  (47.0%) |      81  (48.2%) |     0   (0.0%) |     0   (0.0%) |
| rust                  |   171 |   128  (74.9%) |    30  (17.5%) |      27  (15.8%) |    16   (9.4%) |    14   (8.2%) |
| spark                 |   186 |   149  (80.1%) |    52  (28.0%) |      52  (28.0%) |     2   (1.1%) |     2   (1.1%) |
| tensorflow            |   115 |    66  (57.4%) |    48  (41.7%) |      48  (41.7%) |     5   (4.3%) |     5   (4.3%) |
| test-more             |    19 |    15  (78.9%) |     2  (10.5%) |       2  (10.5%) |     1   (5.3%) |     1   (5.3%) | *
| test-unit             |    51 |    34  (66.7%) |    14  (27.5%) |       8  (15.7%) |     2   (3.9%) |     2   (3.9%) | *
| xmonad                |    23 |    22  (95.7%) |     2   (8.7%) |       2   (8.7%) |     1   (4.3%) |     1   (4.3%) | *
| --------------------- | ----- | -------------- | -------------- | ---------------- | -------------- | -------------- |
| totals                |  6668 |  4391  (65.9%) |  1496  (22.4%) |    1491  (22.4%) |   150   (2.2%) |   144   (2.2%) |
| totals (training set) |  4552 |  3195  (70.2%) |  1053  (23.1%) |    1061  (23.3%) |    86   (1.9%) |    88   (1.9%) |
| totals (test set)     |  2116 |  1196  (56.5%) |   443  (20.9%) |     430  (20.3%) |    64   (3.0%) |    56   (2.6%) |

In this table, the numbers are the count and percentage of human-rated sliders that the corresponding algorithm got *wrong*. The columns are

* "repository" - the name of the repository used. I used the diffs
  between successive non-merge commits on the HEAD branch of the
  corresponding repository.
* "count" - the number of sliders that were human-rated. I chose most,
  but not all, sliders to rate from those among which the various
  algorithms gave different answers.
* "Git 2.9.0" - the default algorithm used by `git diff` in Git 2.9.0.
* "compaction" - the heuristic used by `git diff --compaction-heuristic`
  in Git 2.9.0.
* "compaction-fixed" - the heuristic used by `git diff
  --compaction-heuristic` after the fixes from earlier in this patch
  series. Note that the results are not dramatically different than
  those for "compaction". Both produce non-ideal diffs only about 1/3 as
  often as the default `git diff`.
* "indent-1" - the new `--indent-heuristic` algorithm, using the first
  set of weighting factors, determined as described above.
* "indent-2" - the new `--indent-heuristic` algorithm, using the final
  set of weighting factors, determined as described below.
* `*` - indicates that repo was part of training set used to determine
  the first set of weighting factors.

The fact that the heuristic performed nearly as well on the test set as on the training set in column "indent-1" is a good indication that the heuristic was not over-trained. Given that fact, I ran a second round of optimization, using the entire corpus as the training set. The resulting set of weights gave the results in column "indent-2". These are the weights included in this patch.

The final result gives consistently and significantly better results across the whole corpus than either `git diff` or `git diff --compaction-heuristic`. It makes only about 1/30 as many errors as the former and about 1/10 as many errors as the latter. (And a good fraction of the remaining errors are for diffs that involve weirdly-formatted code, sometimes apparently machine-generated.)

The tools that were used to do this optimization and analysis, along with the human-generated data values, are recorded in a separate project [1].

This patch adds a new command-line option `--indent-heuristic`, and a new configuration setting `diff.indentHeuristic`, that activate this heuristic. This interface is only meant for testing purposes, and should be finalized before including this change in any release.

[1] https://github.com/mhagger/diff-slider-tools
Signed-off-by: Michael Haggerty <mhagger@alum.mit.edu>
---
 Documentation/diff-config.txt  |   7 +-
 Documentation/diff-options.txt |   9 +-
 diff.c                         |  23 ++-
 git-add--interactive.perl      |   5 +-
 xdiff/xdiff.h                  |   1 +
 xdiff/xdiffi.c                 | 325 +++++++++++++++++++++++++++++++++++++++++
 6 files changed, 359 insertions(+), 11 deletions(-)
Show changes to 6 files +359 −11

Documentation/diff-config.txt, Documentation/diff-options.txt, diff.c, git-add--interactive.perl, xdiff/xdiff.h, xdiff/xdiffi.c

diff --git a/Documentation/diff-config.txt b/Documentation/diff-config.txt
index d5a5b17..c32bd85 100644
--- a/Documentation/diff-config.txt
+++ b/Documentation/diff-config.txt
@@ -170,10 +170,11 @@ diff.tool::
 
 include::mergetools-diff.txt[]
 
+diff.indentHeuristic::
 diff.compactionHeuristic::
-	Set this option to `true` to enable an experimental heuristic that
-	shifts the hunk boundary in an attempt to make the resulting
-	patch easier to read.
+	Set one of these options to `true` to enable one of two
+	experimental heuristics that shift diff hunk boundaries to
+	make patches easier to read.
 
 diff.algorithm::
 	Choose a diff algorithm.  The variants are as follows:
diff --git a/Documentation/diff-options.txt b/Documentation/diff-options.txt
index 705a873..ffc2b60 100644
--- a/Documentation/diff-options.txt
+++ b/Documentation/diff-options.txt
@@ -63,12 +63,13 @@ ifndef::git-format-patch[]
 	Synonym for `-p --raw`.
 endif::git-format-patch[]
 
+--indent-heuristic::
+--no-indent-heuristic::
 --compaction-heuristic::
 --no-compaction-heuristic::
-	These are to help debugging and tuning an experimental
-	heuristic (which is off by default) that shifts the hunk
-	boundary in an attempt to make the resulting patch easier
-	to read.
+	These are to help debugging and tuning experimental heuristics
+	(which are off by default) that shift diff hunk boundaries to
+	make patches easier to read.
 
 --minimal::
 	Spend extra time to make sure the smallest possible
diff --git a/diff.c b/diff.c
index 534c12e..8b15ed7 100644
--- a/diff.c
+++ b/diff.c
@@ -26,6 +26,7 @@
 #endif
 
 static int diff_detect_rename_default;
+static int diff_indent_heuristic; /* experimental */
 static int diff_compaction_heuristic; /* experimental */
 static int diff_rename_limit_default = 400;
 static int diff_suppress_blank_empty;
@@ -190,8 +191,16 @@ int git_diff_ui_config(const char *var, const char *value, void *cb)
 		diff_detect_rename_default = git_config_rename(var, value);
 		return 0;
 	}
+	if (!strcmp(var, "diff.indentheuristic")) {
+		diff_indent_heuristic = git_config_bool(var, value);
+		if (diff_indent_heuristic)
+			diff_compaction_heuristic = 0;
+		return 0;
+	}
 	if (!strcmp(var, "diff.compactionheuristic")) {
 		diff_compaction_heuristic = git_config_bool(var, value);
+		if (diff_compaction_heuristic)
+			diff_indent_heuristic = 0;
 		return 0;
 	}
 	if (!strcmp(var, "diff.autorefreshindex")) {
@@ -3293,7 +3302,9 @@ void diff_setup(struct diff_options *options)
 	options->use_color = diff_use_color_default;
 	options->detect_rename = diff_detect_rename_default;
 	options->xdl_opts |= diff_algorithm;
-	if (diff_compaction_heuristic)
+	if (diff_indent_heuristic)
+		DIFF_XDL_SET(options, INDENT_HEURISTIC);
+	else if (diff_compaction_heuristic)
 		DIFF_XDL_SET(options, COMPACTION_HEURISTIC);
 
 	options->orderfile = diff_order_file_cfg;
@@ -3815,9 +3826,15 @@ int diff_opt_parse(struct diff_options *options,
 		DIFF_XDL_SET(options, IGNORE_WHITESPACE_AT_EOL);
 	else if (!strcmp(arg, "--ignore-blank-lines"))
 		DIFF_XDL_SET(options, IGNORE_BLANK_LINES);
-	else if (!strcmp(arg, "--compaction-heuristic"))
+	else if (!strcmp(arg, "--indent-heuristic")) {
+		DIFF_XDL_SET(options, INDENT_HEURISTIC);
+		DIFF_XDL_CLR(options, COMPACTION_HEURISTIC);
+	} else if (!strcmp(arg, "--no-indent-heuristic"))
+		DIFF_XDL_CLR(options, INDENT_HEURISTIC);
+	else if (!strcmp(arg, "--compaction-heuristic")) {
 		DIFF_XDL_SET(options, COMPACTION_HEURISTIC);
-	else if (!strcmp(arg, "--no-compaction-heuristic"))
+		DIFF_XDL_CLR(options, INDENT_HEURISTIC);
+	} else if (!strcmp(arg, "--no-compaction-heuristic"))
 		DIFF_XDL_CLR(options, COMPACTION_HEURISTIC);
 	else if (!strcmp(arg, "--patience"))
 		options->xdl_opts = DIFF_WITH_ALG(options, PATIENCE_DIFF);
diff --git a/git-add--interactive.perl b/git-add--interactive.perl
index 642cce1..ee3d812 100755
--- a/git-add--interactive.perl
+++ b/git-add--interactive.perl
@@ -45,6 +45,7 @@ my ($diff_new_color) =
 my $normal_color = $repo->get_color("", "reset");
 
 my $diff_algorithm = $repo->config('diff.algorithm');
+my $diff_indent_heuristic = $repo->config_bool('diff.indentheuristic');
 my $diff_compaction_heuristic = $repo->config_bool('diff.compactionheuristic');
 my $diff_filter = $repo->config('interactive.difffilter');
 
@@ -750,7 +751,9 @@ sub parse_diff {
 	if (defined $diff_algorithm) {
 		splice @diff_cmd, 1, 0, "--diff-algorithm=${diff_algorithm}";
 	}
-	if ($diff_compaction_heuristic) {
+	if ($diff_indent_heuristic) {
+		splice @diff_cmd, 1, 0, "--indent-heuristic";
+	} elsif ($diff_compaction_heuristic) {
 		splice @diff_cmd, 1, 0, "--compaction-heuristic";
 	}
 	if (defined $patch_mode_revision) {
diff --git a/xdiff/xdiff.h b/xdiff/xdiff.h
index 7423f77..8db16d4 100644
--- a/xdiff/xdiff.h
+++ b/xdiff/xdiff.h
@@ -42,6 +42,7 @@ extern "C" {
 #define XDF_IGNORE_BLANK_LINES (1 << 7)
 
 #define XDF_COMPACTION_HEURISTIC (1 << 8)
+#define XDF_INDENT_HEURISTIC (1 << 9)
 
 #define XDL_EMIT_FUNCNAMES (1 << 0)
 #define XDL_EMIT_FUNCCONTEXT (1 << 2)
diff --git a/xdiff/xdiffi.c b/xdiff/xdiffi.c
index 44fded6..dc79628 100644
--- a/xdiff/xdiffi.c
+++ b/xdiff/xdiffi.c
@@ -414,6 +414,286 @@ static int recs_match(xrecord_t *rec1, xrecord_t *rec2, long flags)
 }
 
 /*
+ * If a line is indented more than this, get_indent() just returns this value.
+ * This avoids having to do absurd amounts of work for data that are not
+ * human-readable text, and also ensures that the output of get_indent fits within
+ * an int.
+ */
+#define MAX_INDENT 200
+
+/*
+ * Return the amount of indentation of the specified line, treating TAB as 8
+ * columns. Return -1 if line is empty or contains only whitespace. Clamp the
+ * output value at MAX_INDENT.
+ */
+static int get_indent(xrecord_t *rec)
+{
+	long i;
+	int ret = 0;
+
+	for (i = 0; i < rec->size; i++) {
+		char c = rec->ptr[i];
+
+		if (!XDL_ISSPACE(c))
+			return ret;
+		else if (c == ' ')
+			ret += 1;
+		else if (c == '\t')
+			ret += 8 - ret % 8;
+		/* ignore other whitespace characters */
+
+		if (ret >= MAX_INDENT)
+			return MAX_INDENT;
+	}
+
+	/* The line contains only whitespace. */
+	return -1;
+}
+
+/*
+ * If more than this number of consecutive blank rows are found, just return this
+ * value. This avoids requiring O(N^2) work for pathological cases, and also
+ * ensures that the output of score_split fits in an int.
+ */
+#define MAX_BLANKS 20
+
+/* Characteristics measured about a hypothetical split position. */
+struct split_measurement {
+	/*
+	 * Is the split at the end of the file (aside from any blank lines)?
+	 */
+	int end_of_file;
+
+	/*
+	 * How much is the line immediately following the split indented (or -1 if
+	 * the line is blank):
+	 */
+	int indent;
+
+	/*
+	 * How many consecutive lines above the split are blank?
+	 */
+	int pre_blank;
+
+	/*
+	 * How much is the nearest non-blank line above the split indented (or -1
+	 * if there is no such line)?
+	 */
+	int pre_indent;
+
+	/*
+	 * How many lines after the line following the split are blank?
+	 */
+	int post_blank;
+
+	/*
+	 * How much is the nearest non-blank line after the line following the
+	 * split indented (or -1 if there is no such line)?
+	 */
+	int post_indent;
+};
+
+struct split_score {
+	/* The effective indent of this split (smaller is preferred). */
+	int effective_indent;
+
+	/* Penalty for this split (smaller is preferred). */
+	int penalty;
+};
+
+/*
+ * Fill m with information about a hypothetical split of xdf above line split.
+ */
+static void measure_split(const xdfile_t *xdf, long split,
+			  struct split_measurement *m)
+{
+	long i;
+
+	if (split >= xdf->nrec) {
+		m->end_of_file = 1;
+		m->indent = -1;
+	} else {
+		m->end_of_file = 0;
+		m->indent = get_indent(xdf->recs[split]);
+	}
+
+	m->pre_blank = 0;
+	m->pre_indent = -1;
+	for (i = split - 1; i >= 0; i--) {
+		m->pre_indent = get_indent(xdf->recs[i]);
+		if (m->pre_indent != -1)
+			break;
+		m->pre_blank += 1;
+		if (m->pre_blank == MAX_BLANKS) {
+			m->pre_indent = 0;
+			break;
+		}
+	}
+
+	m->post_blank = 0;
+	m->post_indent = -1;
+	for (i = split + 1; i < xdf->nrec; i++) {
+		m->post_indent = get_indent(xdf->recs[i]);
+		if (m->post_indent != -1)
+			break;
+		m->post_blank += 1;
+		if (m->post_blank == MAX_BLANKS) {
+			m->post_indent = 0;
+			break;
+		}
+	}
+}
+
+/*
+ * The empirically-determined weight factors used by score_split() below.
+ * Larger values means that the position is a less favorable place to split.
+ *
+ * Note that scores are only ever compared against each other, so multiplying
+ * all of these weight/penalty values by the same factor wouldn't change the
+ * heuristic's behavior. Still, we need to set that arbitrary scale *somehow*.
+ * In practice, these numbers are chosen to be large enough that they can be
+ * adjusted relative to each other with sufficient precision despite using
+ * integer math.
+ */
+
+/* Penalty if there are no non-blank lines before the split */
+#define START_OF_FILE_PENALTY 1
+
+/* Penalty if there are no non-blank lines after the split */
+#define END_OF_FILE_PENALTY 21
+
+/* Multiplier for the number of blank lines around the split */
+#define TOTAL_BLANK_WEIGHT (-30)
+
+/* Multiplier for the number of blank lines after the split */
+#define POST_BLANK_WEIGHT 6
+
+/*
+ * Penalties applied if the line is indented more than its predecessor
+ */
+#define RELATIVE_INDENT_PENALTY (-4)
+#define RELATIVE_INDENT_WITH_BLANK_PENALTY 10
+
+/*
+ * Penalties applied if the line is indented less than both its predecessor and
+ * its successor
+ */
+#define RELATIVE_OUTDENT_PENALTY 24
+#define RELATIVE_OUTDENT_WITH_BLANK_PENALTY 17
+
+/*
+ * Penalties applied if the line is indented less than its predecessor but not
+ * less than its successor
+ */
+#define RELATIVE_DEDENT_PENALTY 23
+#define RELATIVE_DEDENT_WITH_BLANK_PENALTY 17
+
+/*
+ * We only consider whether the sum of the effective indents for splits are
+ * less than (-1), equal to (0), or greater than (+1) each other. The resulting
+ * value is multiplied by the following weight and combined with the penalty to
+ * determine the better of two scores.
+ */
+#define INDENT_WEIGHT 60
+
+/*
+ * Compute a badness score for the hypothetical split whose measurements are
+ * stored in m. The weight factors were determined empirically using the tools and
+ * corpus described in
+ *
+ *     https://github.com/mhagger/diff-slider-tools
+ *
+ * Also see that project if you want to improve the weights based on, for example,
+ * a larger or more diverse corpus.
+ */
+static void score_add_split(const struct split_measurement *m, struct split_score *s)
+{
+	/*
+	 * A place to accumulate penalty factors (positive makes this index more
+	 * favored):
+	 */
+	int post_blank, total_blank, indent, any_blanks;
+
+	if (m->pre_indent == -1 && m->pre_blank == 0)
+		s->penalty += START_OF_FILE_PENALTY;
+
+	if (m->end_of_file)
+		s->penalty += END_OF_FILE_PENALTY;
+
+	/*
+	 * Set post_blank to the number of blank lines following the split,
+	 * including the line immediately after the split:
+	 */
+	post_blank = (m->indent == -1) ? 1 + m->post_blank : 0;
+	total_blank = m->pre_blank + post_blank;
+
+	/* Penalties based on nearby blank lines: */
+	s->penalty += TOTAL_BLANK_WEIGHT * total_blank;
+	s->penalty += POST_BLANK_WEIGHT * post_blank;
+
+	if (m->indent != -1)
+		indent = m->indent;
+	else
+		indent = m->post_indent;
+
+	any_blanks = (total_blank != 0);
+
+	/* Note that the effective indent is -1 at the end of the file: */
+	s->effective_indent += indent;
+
+	if (indent == -1) {
+		/* No additional adjustments needed. */
+	} else if (m->pre_indent == -1) {
+		/* No additional adjustments needed. */
+	} else if (indent > m->pre_indent) {
+		/*
+		 * The line is indented more than its predecessor.
+		 */
+		s->penalty += any_blanks ?
+			RELATIVE_INDENT_WITH_BLANK_PENALTY :
+			RELATIVE_INDENT_PENALTY;
+	} else if (indent == m->pre_indent) {
+		/*
+		 * The line has the same indentation level as its predecessor.
+		 * No additional adjustments needed.
+		 */
+	} else {
+		/*
+		 * The line is indented less than its predecessor. It could be
+		 * the block terminator of the previous block, but it could
+		 * also be the start of a new block (e.g., an "else" block, or
+		 * maybe the previous block didn't have a block terminator).
+		 * Try to distinguish those cases based on what comes next:
+		 */
+		if (m->post_indent != -1 && m->post_indent > indent) {
+			/*
+			 * The following line is indented more. So it is likely
+			 * that this line is the start of a block.
+			 */
+			s->penalty += any_blanks ?
+				RELATIVE_OUTDENT_WITH_BLANK_PENALTY :
+				RELATIVE_OUTDENT_PENALTY;
+		} else {
+			/*
+			 * That was probably the end of a block.
+			 */
+			s->penalty += any_blanks ?
+				RELATIVE_DEDENT_WITH_BLANK_PENALTY :
+				RELATIVE_DEDENT_PENALTY;
+		}
+	}
+}
+
+int score_cmp(struct split_score *s1, struct split_score *s2)
+{
+	/* -1 if s1.effective_indent < s2->effective_indent, etc. */
+	int cmp_indents = ((s1->effective_indent > s2->effective_indent) -
+			   (s1->effective_indent < s2->effective_indent));
+
+	return INDENT_WEIGHT * cmp_indents + (s1->penalty - s2->penalty);
+}
+
+/*
  * Represent a group of changed lines in an xdfile_t (i.e., a contiguous group
  * of lines that was inserted or deleted from the corresponding version of the
  * file). We consider there to be such a group at the beginning of the file, at
@@ -604,6 +884,14 @@ int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) {
 			}
 		} while (groupsize != g.end - g.start);
 
+		/*
+		 * If the group can be shifted, then we can possibly use this
+		 * freedom to produce a more intuitive diff.
+		 *
+		 * The group is currently shifted as far down as possible, so the
+		 * heuristics below only have to handle upwards shifts.
+		 */
+
 		if (g.end == earliest_end) {
 			/* no shifting was possible */
 		} else if (end_matching_other != -1) {
@@ -633,6 +921,43 @@ int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) {
 				if (group_previous(xdfo, &go))
 					xdl_bug("group sync broken sliding to blank line");
 			}
+		} else if (flags & XDF_INDENT_HEURISTIC) {
+			/*
+			 * Indent heuristic: a group of pure add/delete lines
+			 * implies two splits, one between the end of the "before"
+			 * context and the start of the group, and another between
+			 * the end of the group and the beginning of the "after"
+			 * context. Some splits are aesthetically better and some
+			 * are worse. We compute a badness "score" for each split,
+			 * and add the scores for the two splits to define a
+			 * "score" for each position that the group can be shifted
+			 * to. Then we pick the shift with the lowest score.
+			 */
+			long shift, best_shift = -1;
+			struct split_score best_score;
+
+			for (shift = earliest_end; shift <= g.end; shift++) {
+				struct split_measurement m;
+				struct split_score score = {0, 0};
+
+				measure_split(xdf, shift, &m);
+				score_add_split(&m, &score);
+				measure_split(xdf, shift - groupsize, &m);
+				score_add_split(&m, &score);
+				if (best_shift == -1 ||
+				    score_cmp(&score, &best_score) <= 0) {
+					best_score.effective_indent = score.effective_indent;
+					best_score.penalty = score.penalty;
+					best_shift = shift;
+				}
+			}
+
+			while (g.end > best_shift) {
+				if (group_slide_up(xdf, &g, flags))
+					xdl_bug("best shift unreached");
+				if (group_previous(xdfo, &go))
+					xdl_bug("group sync broken sliding to blank line");
+			}
 		}
 
 	next:
-- 
2.9.3
Michael Haggerty· Aug 22, 2016, 11:22 UTC · re: Michael Haggerty · lore

[PATCH v2 3/7] is_blank_line(): take a single xrecord_t as argument

There is no reason for it to take an array and index as argument, as it only accesses a single element of the array.

Signed-off-by: Michael Haggerty <mhagger@alum.mit.edu>
---
 xdiff/xdiffi.c | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)
Show changes to xdiff/xdiffi.c +4 −4
diff --git a/xdiff/xdiffi.c b/xdiff/xdiffi.c
index 61deed8..ed2df64 100644
--- a/xdiff/xdiffi.c
+++ b/xdiff/xdiffi.c
@@ -400,9 +400,9 @@ static xdchange_t *xdl_add_change(xdchange_t *xscr, long i1, long i2, long chg1,
 }
 
 
-static int is_blank_line(xrecord_t **recs, long ix, long flags)
+static int is_blank_line(xrecord_t *rec, long flags)
 {
-	return xdl_blankline(recs[ix]->ptr, recs[ix]->size, flags);
+	return xdl_blankline(rec->ptr, rec->size, flags);
 }
 
 static int recs_match(xrecord_t **recs, long ixs, long ix, long flags)
@@ -485,7 +485,7 @@ int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) {
 			 * the group.
 			 */
 			while (ix < nrec && recs_match(recs, ixs, ix, flags)) {
-				blank_lines += is_blank_line(recs, ix, flags);
+				blank_lines += is_blank_line(recs[ix], flags);
 
 				rchg[ixs++] = 0;
 				rchg[ix++] = 1;
@@ -524,7 +524,7 @@ int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) {
 			 * back only if at all.
 			 */
 			while (ixs > 0 &&
-			       !is_blank_line(recs, ix - 1, flags) &&
+			       !is_blank_line(recs[ix - 1], flags) &&
 			       recs_match(recs, ixs - 1, ix - 1, flags)) {
 				rchg[--ixs] = 1;
 				rchg[--ix] = 0;
-- 
2.9.3
Michael Haggerty· Aug 22, 2016, 11:22 UTC · re: Michael Haggerty · lore

[PATCH v2 5/7] xdl_change_compact(): introduce the concept of a change group

The idea of xdl_change_compact() is fairly simple:
* Proceed through groups of changed lines in the file to be compacted,
  keeping track of the corresponding location in the "other" file.
* If possible, slide the group up and down to try to give the most
  aesthetically pleasing diff. Whenever it is slid, the current location
  in the other file needs to be adjusted.

But these simple concepts are obfuscated by a lot of index handling that is written in terse, subtle, and varied patterns. I found it very hard to convince myself that the function was correct.

So introduce a "struct group" that represents a group of changed lines in a file. Add some functions that perform elementary operations on groups:

* Initialize a group to the first group in a file
* Move to the next or previous group in a file
* Slide a group up or down

Even though the resulting code is longer, I think it is easier to understand and review. Its performance is not changed appreciably (though it would be if `group_next()` and `group_previous()` were not inlined).

...and in fact, the rewriting helped me discover another bug in the --compaction-heuristic code: The update of blank_lines was never done for the highest possible position of the group. This means that it could fail to slide the group to its highest possible position, even if that position had a blank line as its last line. So for example, it yielded the following diff:

    $ git diff --no-index --compaction-heuristic a.txt b.txt
    diff --git a/a.txt b/b.txt
    index e53969f..0d60c5fe 100644
    --- a/a.txt
    +++ b/b.txt
    @@ -1,3 +1,7 @@
     1
     A
    +
    +B
    +
    +A
     2

when in fact the following diff is better (according to the rules of --compaction-heuristic):

    $ git diff --no-index --compaction-heuristic a.txt b.txt
    diff --git a/a.txt b/b.txt
    index e53969f..0d60c5fe 100644
    --- a/a.txt
    +++ b/b.txt
    @@ -1,3 +1,7 @@
     1
    +A
    +
    +B
    +
     A
     2
The new code gives the bottom answer.
Signed-off-by: Michael Haggerty <mhagger@alum.mit.edu>
---
 xdiff/xdiffi.c | 293 +++++++++++++++++++++++++++++++++++++++------------------
 1 file changed, 203 insertions(+), 90 deletions(-)
Show changes to xdiff/xdiffi.c +203 −90
diff --git a/xdiff/xdiffi.c b/xdiff/xdiffi.c
index 8a5832a..44fded6 100644
--- a/xdiff/xdiffi.c
+++ b/xdiff/xdiffi.c
@@ -413,126 +413,239 @@ static int recs_match(xrecord_t *rec1, xrecord_t *rec2, long flags)
 			     flags));
 }
 
-int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) {
-	long ix, ixo, ixs, ixref, grpsiz, nrec = xdf->nrec;
-	char *rchg = xdf->rchg, *rchgo = xdfo->rchg;
-	unsigned int blank_lines;
-	xrecord_t **recs = xdf->recs;
+/*
+ * Represent a group of changed lines in an xdfile_t (i.e., a contiguous group
+ * of lines that was inserted or deleted from the corresponding version of the
+ * file). We consider there to be such a group at the beginning of the file, at
+ * the end of the file, and between any two unchanged lines, though most such
+ * groups will usually be empty.
+ *
+ * If the first line in a group is equal to the line following the group, then
+ * the group can be slid down. Similarly, if the last line in a group is equal
+ * to the line preceding the group, then the group can be slid up. See
+ * group_slide_down() and group_slide_up().
+ *
+ * Note that loops that are testing for changed lines in xdf->rchg do not need
+ * index bounding since the array is prepared with a zero at position -1 and N.
+ */
+struct group {
+	/*
+	 * The index of the first changed line in the group, or the index of
+	 * the unchanged line above which the (empty) group is located.
+	 */
+	long start;
 
 	/*
-	 * This is the same of what GNU diff does. Move back and forward
-	 * change groups for a consistent and pretty diff output. This also
-	 * helps in finding joinable change groups and reduce the diff size.
+	 * The index of the first unchanged line after the group. For an empty
+	 * group, end is equal to start.
 	 */
-	for (ix = ixo = 0;;) {
-		/*
-		 * Find the first changed line in the to-be-compacted file.
-		 * We need to keep track of both indexes, so if we find a
-		 * changed lines group on the other file, while scanning the
-		 * to-be-compacted file, we need to skip it properly. Note
-		 * that loops that are testing for changed lines on rchg* do
-		 * not need index bounding since the array is prepared with
-		 * a zero at position -1 and N.
-		 */
-		for (; ix < nrec && !rchg[ix]; ix++)
-			while (rchgo[ixo++]);
-		if (ix == nrec)
-			break;
+	long end;
+};
+
+/*
+ * Initialize g to point at the first group in xdf.
+ */
+static void group_init(xdfile_t *xdf, struct group *g)
+{
+	g->start = g->end = 0;
+	while (xdf->rchg[g->end])
+		g->end++;
+}
+
+/*
+ * Move g to describe the next (possibly empty) group in xdf and return 0. If g
+ * is already at the end of the file, do nothing and return -1.
+ */
+static inline int group_next(xdfile_t *xdf, struct group *g)
+{
+	if (g->end == xdf->nrec)
+		return -1;
+
+	g->start = g->end + 1;
+	for (g->end = g->start; xdf->rchg[g->end]; g->end++)
+		;
+
+	return 0;
+}
+
+/*
+ * Move g to describe the previous (possibly empty) group in xdf and return 0.
+ * If g is already at the beginning of the file, do nothing and return -1.
+ */
+static inline int group_previous(xdfile_t *xdf, struct group *g)
+{
+	if (g->start == 0)
+		return -1;
+
+	g->end = g->start - 1;
+	for (g->start = g->end; xdf->rchg[g->start - 1]; g->start--)
+		;
+
+	return 0;
+}
+
+/*
+ * If g can be slid toward the end of the file, do so, and if it bumps into a
+ * following group, expand this group to include it. Return 0 on success or -1
+ * if g cannot be slid down.
+ */
+static int group_slide_down(xdfile_t *xdf, struct group *g, long flags)
+{
+	if (g->end < xdf->nrec &&
+	    recs_match(xdf->recs[g->start], xdf->recs[g->end], flags)) {
+		xdf->rchg[g->start++] = 0;
+		xdf->rchg[g->end++] = 1;
+
+		while (xdf->rchg[g->end])
+			g->end++;
+
+		return 0;
+	} else {
+		return -1;
+	}
+}
+
+/*
+ * If g can be slid toward the beginning of the file, do so, and if it bumps
+ * into a previous group, expand this group to include it. Return 0 on success
+ * or -1 if g cannot be slid up.
+ */
+static int group_slide_up(xdfile_t *xdf, struct group *g, long flags)
+{
+	if (g->start > 0 &&
+	    recs_match(xdf->recs[g->start - 1], xdf->recs[g->end - 1], flags)) {
+		xdf->rchg[--g->start] = 1;
+		xdf->rchg[--g->end] = 0;
+
+		while (xdf->rchg[g->start - 1])
+			g->start--;
+
+		return 0;
+	} else {
+		return -1;
+	}
+}
+
+static void xdl_bug(const char *msg)
+{
+	fprintf(stderr, "BUG: %s\n", msg);
+	exit(1);
+}
+
+/*
+ * Move back and forward change groups for a consistent and pretty diff output.
+ * This also helps in finding joinable change groups and reducing the diff
+ * size.
+ */
+int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) {
+	struct group g, go;
+	long earliest_end, end_matching_other;
+	long groupsize;
+	unsigned int blank_lines;
+
+	group_init(xdf, &g);
+	group_init(xdfo, &go);
+
+	while (1) {
+		/* If the group is empty in the to-be-compacted file, skip it: */
+		if (g.end == g.start)
+			goto next;
 
 		/*
-		 * Record the start of a changed-group in the to-be-compacted file
-		 * and find the end of it, on both to-be-compacted and other file
-		 * indexes (ix and ixo).
+		 * Now shift the change up and then down as far as possible in
+		 * each direction. If it bumps into any other changes, merge them.
 		 */
-		ixs = ix;
-		for (ix++; rchg[ix]; ix++);
-		for (; rchgo[ixo]; ixo++);
-
 		do {
-			grpsiz = ix - ixs;
-			blank_lines = 0;
+			groupsize = g.end - g.start;
 
 			/*
-			 * If the line before the current change group, is equal to
-			 * the last line of the current change group, shift backward
-			 * the group.
+			 * Keep track of the last "end" index that causes this
+			 * group to align with a group of changed lines in the
+			 * other file. -1 indicates that we haven't found such
+			 * a match yet:
 			 */
-			while (ixs > 0 && recs_match(recs[ixs - 1], recs[ix - 1], flags)) {
-				rchg[--ixs] = 1;
-				rchg[--ix] = 0;
-
-				/*
-				 * This change might have joined two change groups,
-				 * so we try to take this scenario in account by moving
-				 * the start index accordingly (and so the other-file
-				 * end-of-group index).
-				 */
-				for (; rchg[ixs - 1]; ixs--);
-				while (rchgo[--ixo]);
-			}
+			end_matching_other = -1;
 
 			/*
-			 * Record the end-of-group position in case we are matched
-			 * with a group of changes in the other file (that is, the
-			 * change record before the end-of-group index in the other
-			 * file is set).
+			 * Boolean value that records whether there are any blank
+			 * lines that could be made to be the last line of this
+			 * group.
 			 */
-			ixref = rchgo[ixo - 1] ? ix: nrec;
+			blank_lines = 0;
+
+			/* Shift the group backward as much as possible: */
+			while (!group_slide_up(xdf, &g, flags))
+				if (group_previous(xdfo, &go))
+					xdl_bug("group sync broken sliding up");
 
 			/*
-			 * If the first line of the current change group, is equal to
-			 * the line next of the current change group, shift forward
-			 * the group.
+			 * This is this highest that this group can be shifted.
+			 * Record its end index:
 			 */
-			while (ix < nrec && recs_match(recs[ixs], recs[ix], flags)) {
-				blank_lines += is_blank_line(recs[ix], flags);
-
-				rchg[ixs++] = 0;
-				rchg[ix++] = 1;
-
-				/*
-				 * This change might have joined two change groups,
-				 * so we try to take this scenario in account by moving
-				 * the start index accordingly (and so the other-file
-				 * end-of-group index). Keep tracking the reference
-				 * index in case we are shifting together with a
-				 * corresponding group of changes in the other file.
-				 */
-				for (; rchg[ix]; ix++);
-				while (rchgo[++ixo])
-					ixref = ix;
+			earliest_end = g.end;
+
+			if (go.end > go.start)
+				end_matching_other = g.end;
+
+			/* Now shift the group forward as far as possible: */
+			while (1) {
+				if (!blank_lines)
+					blank_lines = is_blank_line(
+							xdf->recs[g.end - 1],
+							flags);
+
+				if (group_slide_down(xdf, &g, flags))
+					break;
+				if (group_next(xdfo, &go))
+					xdl_bug("group sync broken sliding down");
+
+				if (go.end > go.start)
+					end_matching_other = g.end;
 			}
-		} while (grpsiz != ix - ixs);
+		} while (groupsize != g.end - g.start);
 
-		if (ixref < ix) {
+		if (g.end == earliest_end) {
+			/* no shifting was possible */
+		} else if (end_matching_other != -1) {
 			/*
-			 * Try to move back the possibly merged group of changes, to match
-			 * the recorded position in the other file.
+			 * Move the possibly merged group of changes back to line
+			 * up with the last group of changes from the other file
+			 * that it can align with.
 			 */
-			while (ixref < ix) {
-				rchg[--ixs] = 1;
-				rchg[--ix] = 0;
-				while (rchgo[--ixo]);
+			while (go.end == go.start) {
+				if (group_slide_up(xdf, &g, flags))
+					xdl_bug("match disappeared");
+				if (group_previous(xdfo, &go))
+					xdl_bug("group sync broken sliding to match");
 			}
 		} else if ((flags & XDF_COMPACTION_HEURISTIC) && blank_lines) {
 			/*
-			 * The group can be slid up to make its last line a
-			 * blank line. Do so.
+			 * Compaction heuristic: if it is possible to shift the
+			 * group to make its bottom line a blank line, do so.
 			 *
 			 * As we already shifted the group forward as far as
-			 * possible in the earlier loop, we need to shift it
-			 * back only if at all.
+			 * possible in the earlier loop, we only need to handle
+			 * backward shifts, not forward ones.
 			 */
-			while (ixs > 0 &&
-			       !is_blank_line(recs[ix - 1], flags) &&
-			       recs_match(recs[ixs - 1], recs[ix - 1], flags)) {
-				rchg[--ixs] = 1;
-				rchg[--ix] = 0;
-				while (rchgo[--ixo]);
+			while (!is_blank_line(xdf->recs[g.end - 1], flags)) {
+				if (group_slide_up(xdf, &g, flags))
+					xdl_bug("blank line disappeared");
+				if (group_previous(xdfo, &go))
+					xdl_bug("group sync broken sliding to blank line");
 			}
 		}
+
+	next:
+		/* Move past the just-processed group: */
+		if (group_next(xdf, &g))
+			break;
+		if (group_next(xdfo, &go))
+			xdl_bug("group sync broken moving to next group");
 	}
 
+	if (!group_next(xdfo, &go))
+		xdl_bug("group sync broken at end of file");
+
 	return 0;
 }
 
-- 
2.9.3
Junio C Hamano· Aug 23, 2016, 21:35 UTC · re: Michael Haggerty · lore

Re: [PATCH v2 5/7] xdl_change_compact(): introduce the concept of a change group

Michael Haggerty <mhagger@alum.mit.edu> writes:
Show 23 quoted lines
> The idea of xdl_change_compact() is fairly simple:
>
> * Proceed through groups of changed lines in the file to be compacted,
>   keeping track of the corresponding location in the "other" file.
>
> * If possible, slide the group up and down to try to give the most
>   aesthetically pleasing diff. Whenever it is slid, the current location
>   in the other file needs to be adjusted.
>
> But these simple concepts are obfuscated by a lot of index handling that
> is written in terse, subtle, and varied patterns. I found it very hard
> to convince myself that the function was correct.
>
> So introduce a "struct group" that represents a group of changed lines
> in a file. Add some functions that perform elementary operations on
> groups:
>
> * Initialize a group to the first group in a file
> * Move to the next or previous group in a file
> * Slide a group up or down
>
> Even though the resulting code is longer, I think it is easier to
> understand and review.

Yup. The important thing is that the length of the core logic of sliding up and down becomes easier to read, because it shrinks; the mechanics of sliding up and down may need more lines with boilderplate, but they are isolated "do one thing and do it well" helpers.

Nice.
Michael Haggerty· Aug 22, 2016, 11:22 UTC · re: Michael Haggerty · lore

[PATCH v2 4/7] recs_match(): take two xrecord_t pointers as arguments

There is no reason for it to take an array and two indexes as argument, as it only accesses two elements of the array.

Signed-off-by: Michael Haggerty <mhagger@alum.mit.edu>
---
 xdiff/xdiffi.c | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)
Show changes to xdiff/xdiffi.c +7 −7
diff --git a/xdiff/xdiffi.c b/xdiff/xdiffi.c
index ed2df64..8a5832a 100644
--- a/xdiff/xdiffi.c
+++ b/xdiff/xdiffi.c
@@ -405,11 +405,11 @@ static int is_blank_line(xrecord_t *rec, long flags)
 	return xdl_blankline(rec->ptr, rec->size, flags);
 }
 
-static int recs_match(xrecord_t **recs, long ixs, long ix, long flags)
+static int recs_match(xrecord_t *rec1, xrecord_t *rec2, long flags)
 {
-	return (recs[ixs]->ha == recs[ix]->ha &&
-		xdl_recmatch(recs[ixs]->ptr, recs[ixs]->size,
-			     recs[ix]->ptr, recs[ix]->size,
+	return (rec1->ha == rec2->ha &&
+		xdl_recmatch(rec1->ptr, rec1->size,
+			     rec2->ptr, rec2->size,
 			     flags));
 }
 
@@ -457,7 +457,7 @@ int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) {
 			 * the last line of the current change group, shift backward
 			 * the group.
 			 */
-			while (ixs > 0 && recs_match(recs, ixs - 1, ix - 1, flags)) {
+			while (ixs > 0 && recs_match(recs[ixs - 1], recs[ix - 1], flags)) {
 				rchg[--ixs] = 1;
 				rchg[--ix] = 0;
 
@@ -484,7 +484,7 @@ int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) {
 			 * the line next of the current change group, shift forward
 			 * the group.
 			 */
-			while (ix < nrec && recs_match(recs, ixs, ix, flags)) {
+			while (ix < nrec && recs_match(recs[ixs], recs[ix], flags)) {
 				blank_lines += is_blank_line(recs[ix], flags);
 
 				rchg[ixs++] = 0;
@@ -525,7 +525,7 @@ int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) {
 			 */
 			while (ixs > 0 &&
 			       !is_blank_line(recs[ix - 1], flags) &&
-			       recs_match(recs, ixs - 1, ix - 1, flags)) {
+			       recs_match(recs[ixs - 1], recs[ix - 1], flags)) {
 				rchg[--ixs] = 1;
 				rchg[--ix] = 0;
 				while (rchgo[--ixo]);
-- 
2.9.3
Michael Haggerty· Aug 22, 2016, 11:22 UTC · re: Michael Haggerty · lore

[PATCH v2 7/7] blame: actually use the diff opts parsed from the command line

"git blame" already parsed generic diff options from the command line via diff_opt_parse(), but instead of passing the resulting xdl_opts to xdi_diff(), it sent its own xdl_opts, which only reflected the values of the self-parsed options "-w" and "--minimal". Instead, rely on diff_opt_parse() to parse all of the diff options, including "-w" and "--minimal", and pass the resulting xdl_opts to xdi_diff().

Signed-off-by: Michael Haggerty <mhagger@alum.mit.edu>
---
Somebody who knows more about how diff operations are configured
should please review this. I'm not certain that the change as
implemented won't have other unwanted side-effects, though of course
I checked that the test suite runs correctly.
 builtin/blame.c        |  11 ++--
 t/t4059-diff-indent.sh | 160 +++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 165 insertions(+), 6 deletions(-)
 create mode 100755 t/t4059-diff-indent.sh
Show changes to 2 files +165 −6

builtin/blame.c, t/t4059-diff-indent.sh

diff --git a/builtin/blame.c b/builtin/blame.c
index 7ec7823..cde2d15 100644
--- a/builtin/blame.c
+++ b/builtin/blame.c
@@ -48,11 +48,12 @@ static int show_root;
 static int reverse;
 static int blank_boundary;
 static int incremental;
-static int xdl_opts;
 static int abbrev = -1;
 static int no_whole_file_rename;
 static int show_progress;
 
+static struct rev_info revs;
+
 static struct date_mode blame_date_mode = { DATE_ISO8601 };
 static size_t blame_date_width;
 
@@ -137,11 +138,12 @@ struct progress_info {
 static int diff_hunks(mmfile_t *file_a, mmfile_t *file_b,
 		      xdl_emit_hunk_consume_func_t hunk_func, void *cb_data)
 {
-	xpparam_t xpp = {0};
+	xpparam_t xpp;
 	xdemitconf_t xecfg = {0};
 	xdemitcb_t ecb = {NULL};
 
-	xpp.flags = xdl_opts;
+	memset(&xpp, 0, sizeof(xpp));
+	xpp.flags = revs.diffopt.xdl_opts;
 	xecfg.hunk_func = hunk_func;
 	ecb.priv = cb_data;
 	return xdi_diff(file_a, file_b, &xpp, &xecfg, &ecb);
@@ -2517,7 +2519,6 @@ static int blame_move_callback(const struct option *option, const char *arg, int
 
 int cmd_blame(int argc, const char **argv, const char *prefix)
 {
-	struct rev_info revs;
 	const char *path;
 	struct scoreboard sb;
 	struct origin *o;
@@ -2548,8 +2549,6 @@ int cmd_blame(int argc, const char **argv, const char *prefix)
 		OPT_BIT('l', NULL, &output_option, N_("Show long commit SHA1 (Default: off)"), OUTPUT_LONG_OBJECT_NAME),
 		OPT_BIT('s', NULL, &output_option, N_("Suppress author name and timestamp (Default: off)"), OUTPUT_NO_AUTHOR),
 		OPT_BIT('e', "show-email", &output_option, N_("Show author email instead of name (Default: off)"), OUTPUT_SHOW_EMAIL),
-		OPT_BIT('w', NULL, &xdl_opts, N_("Ignore whitespace differences"), XDF_IGNORE_WHITESPACE),
-		OPT_BIT(0, "minimal", &xdl_opts, N_("Spend extra cycles to find better match"), XDF_NEED_MINIMAL),
 		OPT_STRING('S', NULL, &revs_file, N_("file"), N_("Use revisions from <file> instead of calling git-rev-list")),
 		OPT_STRING(0, "contents", &contents_from, N_("file"), N_("Use <file>'s contents as the final image")),
 		{ OPTION_CALLBACK, 'C', NULL, &opt, N_("score"), N_("Find line copies within and across files"), PARSE_OPT_OPTARG, blame_copy_callback },
diff --git a/t/t4059-diff-indent.sh b/t/t4059-diff-indent.sh
new file mode 100755
index 0000000..8b6c7ef
--- /dev/null
+++ b/t/t4059-diff-indent.sh
@@ -0,0 +1,160 @@
+#!/bin/sh
+
+test_description='Test diff indent heuristic.
+
+'
+. ./test-lib.sh
+. "$TEST_DIRECTORY"/diff-lib.sh
+
+# Compare two diff outputs. Ignore "index" lines, because we don't
+# care about SHA-1s or file modes.
+compare_diff () {
+	sed -e "/^index /d" <"$1" >.tmp-1
+	sed -e "/^index /d" <"$2" >.tmp-2
+	test_cmp .tmp-1 .tmp-2 && rm -f .tmp-1 .tmp-2
+}
+
+test_expect_success 'diff: favor trailing blank lines' '
+	cat <<-\EOF >old &&
+	1
+	2
+	a
+
+	b
+	3
+	4
+	EOF
+
+	cat <<-\EOF >new &&
+	1
+	2
+	a
+
+	b
+	a
+
+	b
+	3
+	4
+	EOF
+
+	tr "_" " " <<-\EOF >expect &&
+	diff --git a/old b/new
+	--- a/old
+	+++ b/new
+	@@ -3,5 +3,8 @@
+	 a
+	_
+	 b
+	+a
+	+
+	+b
+	 3
+	 4
+	EOF
+
+	tr "_" " " <<-\EOF >expect-compacted &&
+	diff --git a/old b/new
+	--- a/old
+	+++ b/new
+	@@ -2,6 +2,9 @@
+	 2
+	 a
+	_
+	+b
+	+a
+	+
+	 b
+	 3
+	 4
+	EOF
+
+	test_must_fail git diff --no-index old new >out &&
+	compare_diff expect out &&
+
+	test_must_fail git diff --no-index --indent-heuristic old new >out-compacted &&
+	compare_diff expect-compacted out-compacted &&
+
+	test_must_fail git -c diff.indentHeuristic=true diff --no-index old new >out-compacted2 &&
+	compare_diff expect-compacted out-compacted2 &&
+
+	test_must_fail git diff --indent-heuristic --patience --no-index old new >out-compacted3 &&
+	compare_diff expect-compacted out-compacted3 &&
+
+	test_must_fail git diff --indent-heuristic --histogram --no-index old new >out-compacted4 &&
+	compare_diff expect-compacted out-compacted4
+'
+
+test_expect_success 'diff: keep functions together' '
+	cat <<-\EOF >old &&
+	1
+	2
+	/* function */
+	foo() {
+	    foo
+	}
+
+	3
+	4
+	EOF
+
+	cat <<-\EOF >new &&
+	1
+	2
+	/* function */
+	bar() {
+	    foo
+	}
+
+	/* function */
+	foo() {
+	    foo
+	}
+
+	3
+	4
+	EOF
+
+	tr "_" " " <<-\EOF >expect &&
+	diff --git a/old b/new
+	--- a/old
+	+++ b/new
+	@@ -1,6 +1,11 @@
+	 1
+	 2
+	 /* function */
+	+bar() {
+	+    foo
+	+}
+	+
+	+/* function */
+	 foo() {
+	     foo
+	 }
+	EOF
+
+	tr "_" " " <<-\EOF >expect-compacted &&
+	diff --git a/old b/new
+	--- a/old
+	+++ b/new
+	@@ -1,5 +1,10 @@
+	 1
+	 2
+	+/* function */
+	+bar() {
+	+    foo
+	+}
+	+
+	 /* function */
+	 foo() {
+	     foo
+	EOF
+
+	test_must_fail git diff --no-index old new >out &&
+	compare_diff expect out &&
+
+	test_must_fail git diff --no-index --indent-heuristic old new >out-compacted &&
+	compare_diff expect-compacted out-compacted
+'
+
+test_done
-- 
2.9.3
René Scharfe· Aug 23, 2016, 09:56 UTC · re: Michael Haggerty · lore

Re: [PATCH v2 7/7] blame: actually use the diff opts parsed from the command line

Am 22.08.2016 um 13:22 schrieb Michael Haggerty:
Show 6 quoted lines
> "git blame" already parsed generic diff options from the command line
> via diff_opt_parse(), but instead of passing the resulting xdl_opts to
> xdi_diff(), it sent its own xdl_opts, which only reflected the values of
> the self-parsed options "-w" and "--minimal". Instead, rely on
> diff_opt_parse() to parse all of the diff options, including "-w" and
> "--minimal", and pass the resulting xdl_opts to xdi_diff().

Sounds useful: It allows more fine-grained control over which whitespace changes to ignore and which diff algorithm to use. There is a bit of overlap (e.g. with -b meaning show blank boundaries vs. ignore whitespace changes), but with your patch blame's own options still take precedence, so there should be no unpleasant surprises.

Show 6 quoted lines
> Signed-off-by: Michael Haggerty <mhagger@alum.mit.edu>
> ---
> Somebody who knows more about how diff operations are configured
> should please review this. I'm not certain that the change as
> implemented won't have other unwanted side-effects, though of course
> I checked that the test suite runs correctly.
I don't qualify, but I'll comment anyway..
>  builtin/blame.c        |  11 ++--
>  t/t4059-diff-indent.sh | 160 +++++++++++++++++++++++++++++++++++++++++++++++++
>  2 files changed, 165 insertions(+), 6 deletions(-)
>  create mode 100755 t/t4059-diff-indent.sh

This new test doesn't call git blame. Does it belong to a different commit? And shouldn't the change to blame.c stand on its own, outside of this series?

Show 30 quoted lines
> diff --git a/builtin/blame.c b/builtin/blame.c
> index 7ec7823..cde2d15 100644
> --- a/builtin/blame.c
> +++ b/builtin/blame.c
> @@ -48,11 +48,12 @@ static int show_root;
>  static int reverse;
>  static int blank_boundary;
>  static int incremental;
> -static int xdl_opts;
>  static int abbrev = -1;
>  static int no_whole_file_rename;
>  static int show_progress;
>
> +static struct rev_info revs;
> +
>  static struct date_mode blame_date_mode = { DATE_ISO8601 };
>  static size_t blame_date_width;
>
> @@ -137,11 +138,12 @@ struct progress_info {
>  static int diff_hunks(mmfile_t *file_a, mmfile_t *file_b,
>  		      xdl_emit_hunk_consume_func_t hunk_func, void *cb_data)
>  {
> -	xpparam_t xpp = {0};
> +	xpparam_t xpp;
>  	xdemitconf_t xecfg = {0};
>  	xdemitcb_t ecb = {NULL};
>
> -	xpp.flags = xdl_opts;
> +	memset(&xpp, 0, sizeof(xpp));
> +	xpp.flags = revs.diffopt.xdl_opts;

Why call memset instead of using a static initializer? The intent of this patch is just to change the .flags assignment, isn't it?

Show 17 quoted lines
>  	xecfg.hunk_func = hunk_func;
>  	ecb.priv = cb_data;
>  	return xdi_diff(file_a, file_b, &xpp, &xecfg, &ecb);
> @@ -2517,7 +2519,6 @@ static int blame_move_callback(const struct option *option, const char *arg, int
>
>  int cmd_blame(int argc, const char **argv, const char *prefix)
>  {
> -	struct rev_info revs;
>  	const char *path;
>  	struct scoreboard sb;
>  	struct origin *o;
> @@ -2548,8 +2549,6 @@ int cmd_blame(int argc, const char **argv, const char *prefix)
>  		OPT_BIT('l', NULL, &output_option, N_("Show long commit SHA1 (Default: off)"), OUTPUT_LONG_OBJECT_NAME),
>  		OPT_BIT('s', NULL, &output_option, N_("Suppress author name and timestamp (Default: off)"), OUTPUT_NO_AUTHOR),
>  		OPT_BIT('e', "show-email", &output_option, N_("Show author email instead of name (Default: off)"), OUTPUT_SHOW_EMAIL),
> -		OPT_BIT('w', NULL, &xdl_opts, N_("Ignore whitespace differences"), XDF_IGNORE_WHITESPACE),
> -		OPT_BIT(0, "minimal", &xdl_opts, N_("Spend extra cycles to find better match"), XDF_NEED_MINIMAL),

This removes -w and --minimal from blame's short help; diff options should be mentioned somehow in exchange for that loss. Or perhaps they should be mentioned in git-rev-list(1)? (git blame -h points to git-rev-list(1) already.)

Documentation/git-blame.txt needs an update as well.
>  		OPT_STRING('S', NULL, &revs_file, N_("file"), N_("Use revisions from <file> instead of calling git-rev-list")),
>  		OPT_STRING(0, "contents", &contents_from, N_("file"), N_("Use <file>'s contents as the final image")),
>  		{ OPTION_CALLBACK, 'C', NULL, &opt, N_("score"), N_("Find line copies within and across files"), PARSE_OPT_OPTARG, blame_copy_callback },
Michael Haggerty· Sep 5, 2016, 04:12 UTC · re: René Scharfe · lore

Re: [PATCH v2 7/7] blame: actually use the diff opts parsed from the command line

On 08/23/2016 11:56 AM, René Scharfe wrote:
Show 32 quoted lines
> Am 22.08.2016 um 13:22 schrieb Michael Haggerty:
>> "git blame" already parsed generic diff options from the command line
>> via diff_opt_parse(), but instead of passing the resulting xdl_opts to
>> xdi_diff(), it sent its own xdl_opts, which only reflected the values of
>> the self-parsed options "-w" and "--minimal". Instead, rely on
>> diff_opt_parse() to parse all of the diff options, including "-w" and
>> "--minimal", and pass the resulting xdl_opts to xdi_diff().
> 
> Sounds useful: It allows more fine-grained control over which whitespace
> changes to ignore and which diff algorithm to use.  There is a bit of
> overlap (e.g. with -b meaning show blank boundaries vs. ignore
> whitespace changes), but with your patch blame's own options still take
> precedence, so there should be no unpleasant surprises.
> 
>> Signed-off-by: Michael Haggerty <mhagger@alum.mit.edu>
>> ---
>> Somebody who knows more about how diff operations are configured
>> should please review this. I'm not certain that the change as
>> implemented won't have other unwanted side-effects, though of course
>> I checked that the test suite runs correctly.
> 
> I don't qualify, but I'll comment anyway..
> 
>>  builtin/blame.c        |  11 ++--
>>  t/t4059-diff-indent.sh | 160
>> +++++++++++++++++++++++++++++++++++++++++++++++++
>>  2 files changed, 165 insertions(+), 6 deletions(-)
>>  create mode 100755 t/t4059-diff-indent.sh
> 
> This new test doesn't call git blame.  Does it belong to a different
> commit?  And shouldn't the change to blame.c stand on its own, outside
> of this series?

Thanks for catching this. I squashed the test incorrectly; it was meant to be part of the previous commit. I'll fix it in v3.

The reason that I would prefer to change `blame` as part of this patch series is that I think it would be disconcerting for `git diff` and `git blame` to use different heuristics when computing diffs. It would make their output inconsistent.

But probably that means passing just the new option to `git blame` rather than passing all possible `diff` options through.

> [...]

Your other comments may be moot given that changing `git blame` in this way seems to have been a bad idea in the first place [1]. But I'll keep them in mind if a new version contains similar code.

Thanks, Michael

[1] http://public-inbox.org/git/xmqqtwebwhbg.fsf@gitster.mtv.corp.google.com/

Junio C Hamano· Sep 7, 2016, 17:58 UTC · re: Michael Haggerty · lore

Re: [PATCH v2 7/7] blame: actually use the diff opts parsed from the command line

Michael Haggerty <mhagger@alum.mit.edu> writes:
> The reason that I would prefer to change `blame` as part of this patch
> series is that I think it would be disconcerting for `git diff` and `git
> blame` to use different heuristics when computing diffs. It would make
> their output inconsistent.

I do think it is the right thing to do. With your shifting heuristics, "git diff" would attribute an addition of a whole block more correctly, e.g.

	 }
        +foo {
        +	bar
        +       baz
	+}

instead of attributing the tail of the new thing to the old author, and the "blame" should take advantage of the better heuristics as well.

Junio C Hamano· Aug 23, 2016, 17:01 UTC · re: Michael Haggerty · lore

Re: [PATCH v2 7/7] blame: actually use the diff opts parsed from the command line

Michael Haggerty <mhagger@alum.mit.edu> writes:
> Somebody who knows more about how diff operations are configured
> should please review this. I'm not certain that the change as
> implemented won't have other unwanted side-effects, though of course
> I checked that the test suite runs correctly.
Generally, I think this is a bad idea.  
We run "diff-tree" internally for multiple purposes:
 - When the same path we are drilling down appears, we use diff-tree
   to compare that path alone, to obtain textual diff, so that we
   can say "what did not appear in the textual diff output are
   attributable to the parent commit, we can exonerate this child".
   Even if the command line to "git blame" had "-Sfoo", you do not
   want to pass it down to this invocation.  If the child did not
   change the number of occurrences of string "foo" in the path
   being drilled down, it will pass down the blame for all lines to
   the parent.  And copying -R from the command line to this
   invocation does not make any sense.  Copying -C -C from the
   command line will defeat the whole purpose of having find_origin
   vs find_rename distinction, I would think, for this step.
 - When the path we have been drilling down for no longer appears in
   the parent, we use diff-tree with rename detection to find where
   the file _could_ have come from.
   It is a good idea to allow the similarity threshold to be tweaked
   from the command line.  Copying -R from the command line to this
   invocation may make sense, but I think you would break this step
   if you copied -C/-C -C
 - When -C/-C -C/-C -C -C is given from the command line, after
   running the "pass the blame to our primary suspect", we run
   tree-level diff-tree with find-copy option, only to enumerate
   paths that appear in the parent.  We pick pieces from these paths
   by doing blob-level diffs in diff_hunks() ourselves.  Copying -C
   from the command line would be useful if you are only doing a
   single '-C' (because it would allow you to tweak the similarity
   threshold), but otherwise it would probably break the command.

The design principle taken in "git blame" is to leave the decision on what diff options do or do not make sense for these particular invocations of the internal "diff-tree", and have "blame" give the selected options to the internal "diff-tree" invocations. "-w" happens to use xdl_opts that will eventually be passed down to diff machinery but you should consider it an accidental optimization (i.e. "blame" designer considered that it makes sense to give whitespace-ignoring comparison at the leaf-level "diff between blobs", and found that the most efficient way to do so is to just take "-w" from the command line and set the ignore-whitespace bit used to drive the internal "diff-tree" invocation). If we _were_ adding -R<num> to allow the users to affect similarity threshold, we would parse it in "git blame" command line option processing, and would give that _ONLY_ to the second invocation above, i.e. the one done in find_rename() but not in others like in find_origin.

← back to recent threads