#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;
                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")) {
        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;
                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);
 
--- /dev/null
+#!/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 'prepare' '
+       cat <<-\EOF >spaces.txt &&
+       1
+       2
+       a
+
+       b
+       3
+       4
+       EOF
+
+       cat <<-\EOF >functions.c &&
+       1
+       2
+       /* function */
+       foo() {
+           foo
+       }
+
+       3
+       4
+       EOF
+
+       git add spaces.txt functions.c &&
+       test_tick &&
+       git commit -m initial &&
+       git branch old &&
+
+       cat <<-\EOF >spaces.txt &&
+       1
+       2
+       a
+
+       b
+       a
+
+       b
+       3
+       4
+       EOF
+
+       cat <<-\EOF >functions.c &&
+       1
+       2
+       /* function */
+       bar() {
+           foo
+       }
+
+       /* function */
+       foo() {
+           foo
+       }
+
+       3
+       4
+       EOF
+
+       git add spaces.txt functions.c &&
+       test_tick &&
+       git commit -m initial &&
+       git branch new &&
+
+       tr "_" " " <<-\EOF >spaces-expect &&
+       diff --git a/spaces.txt b/spaces.txt
+       --- a/spaces.txt
+       +++ b/spaces.txt
+       @@ -3,5 +3,8 @@
+        a
+       _
+        b
+       +a
+       +
+       +b
+        3
+        4
+       EOF
+
+       tr "_" " " <<-\EOF >spaces-compacted-expect &&
+       diff --git a/spaces.txt b/spaces.txt
+       --- a/spaces.txt
+       +++ b/spaces.txt
+       @@ -2,6 +2,9 @@
+        2
+        a
+       _
+       +b
+       +a
+       +
+        b
+        3
+        4
+       EOF
+
+       tr "_" " " <<-\EOF >functions-expect &&
+       diff --git a/functions.c b/functions.c
+       --- a/functions.c
+       +++ b/functions.c
+       @@ -1,6 +1,11 @@
+        1
+        2
+        /* function */
+       +bar() {
+       +    foo
+       +}
+       +
+       +/* function */
+        foo() {
+            foo
+        }
+       EOF
+
+       tr "_" " " <<-\EOF >functions-compacted-expect
+       diff --git a/functions.c b/functions.c
+       --- a/functions.c
+       +++ b/functions.c
+       @@ -1,5 +1,10 @@
+        1
+        2
+       +/* function */
+       +bar() {
+       +    foo
+       +}
+       +
+        /* function */
+        foo() {
+            foo
+       EOF
+'
+
+test_expect_success 'diff: ugly spaces' '
+       git diff old new -- spaces.txt >out &&
+       compare_diff spaces-expect out
+'
+
+test_expect_success 'diff: nice spaces with --indent-heuristic' '
+       git diff --indent-heuristic old new -- spaces.txt >out-compacted &&
+       compare_diff spaces-compacted-expect out-compacted
+'
+
+test_expect_success 'diff: nice spaces with diff.indentHeuristic' '
+       git -c diff.indentHeuristic=true diff old new -- spaces.txt >out-compacted2 &&
+       compare_diff spaces-compacted-expect out-compacted2
+'
+
+test_expect_success 'diff: --no-indent-heuristic overrides config' '
+       git -c diff.indentHeuristic=true diff --no-indent-heuristic old new -- spaces.txt >out2 &&
+       compare_diff spaces-expect out2
+'
+
+test_expect_success 'diff: --indent-heuristic with --patience' '
+       git diff --indent-heuristic --patience old new -- spaces.txt >out-compacted3 &&
+       compare_diff spaces-compacted-expect out-compacted3
+'
+
+test_expect_success 'diff: --indent-heuristic with --histogram' '
+       git diff --indent-heuristic --histogram old new -- spaces.txt >out-compacted4 &&
+       compare_diff spaces-compacted-expect out-compacted4
+'
+
+test_expect_success 'diff: ugly functions' '
+       git diff old new -- functions.c >out &&
+       compare_diff functions-expect out
+'
+
+test_expect_success 'diff: nice functions with --indent-heuristic' '
+       git diff --indent-heuristic old new -- functions.c >out-compacted &&
+       compare_diff functions-compacted-expect out-compacted
+'
+
+test_done
 
                             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;
+               }
+       }
+}
+
+static 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
                        }
                } 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) {
                                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: