Code

Merge branch 'jc/diff-algo-cleanup'
[git.git] / diff.c
diff --git a/diff.c b/diff.c
index 52cda7a0da154029eac675cea8acdbbe5b45ebf8..f43b581f41617af61d985fe1f07e99c45f5ebcc5 100644 (file)
--- a/diff.c
+++ b/diff.c
@@ -31,6 +31,7 @@ static const char *external_diff_cmd_cfg;
 int diff_auto_refresh_index = 1;
 static int diff_mnemonic_prefix;
 static int diff_no_prefix;
+static int diff_stat_graph_width;
 static int diff_dirstat_permille_default = 30;
 static struct diff_options default_diff_options;
 
@@ -156,6 +157,10 @@ int git_diff_ui_config(const char *var, const char *value, void *cb)
                diff_no_prefix = git_config_bool(var, value);
                return 0;
        }
+       if (!strcmp(var, "diff.statgraphwidth")) {
+               diff_stat_graph_width = git_config_int(var, value);
+               return 0;
+       }
        if (!strcmp(var, "diff.external"))
                return git_config_string(&external_diff_cmd_cfg, var, value);
        if (!strcmp(var, "diff.wordregex"))
@@ -177,11 +182,8 @@ int git_diff_basic_config(const char *var, const char *value, void *cb)
                return 0;
        }
 
-       switch (userdiff_config(var, value)) {
-               case 0: break;
-               case -1: return -1;
-               default: return 0;
-       }
+       if (userdiff_config(var, value) < 0)
+               return -1;
 
        if (!prefixcmp(var, "diff.color.") || !prefixcmp(var, "color.diff.")) {
                int slot = parse_diff_color_slot(var, 11);
@@ -1113,6 +1115,15 @@ static void fn_out_consume(void *priv, char *line, unsigned long len)
                        diff_words_append(line, len,
                                          &ecbdata->diff_words->plus);
                        return;
+               } else if (!prefixcmp(line, "\\ ")) {
+                       /*
+                        * Eat the "no newline at eof" marker as if we
+                        * saw a "+" or "-" line with nothing on it,
+                        * and return without diff_words_flush() to
+                        * defer processing. If this is the end of
+                        * preimage, more "+" lines may come after it.
+                        */
+                       return;
                }
                diff_words_flush(ecbdata);
                if (ecbdata->diff_words->type == DIFF_WORDS_PORCELAIN) {
@@ -1267,13 +1278,15 @@ const char mime_boundary_leader[] = "------------";
 
 static int scale_linear(int it, int width, int max_change)
 {
+       if (!it)
+               return 0;
        /*
-        * make sure that at least one '-' is printed if there were deletions,
-        * and likewise for '+'.
+        * make sure that at least one '-' or '+' is printed if
+        * there is any change to this path. The easiest way is to
+        * scale linearly as if the alloted width is one column shorter
+        * than it is, and then add 1 to the result.
         */
-       if (max_change < 2)
-               return it;
-       return ((it - 1) * (width - 1) + max_change - 1) / (max_change - 1);
+       return 1 + (it * (width - 1) / max_change);
 }
 
 static void show_name(FILE *file,
@@ -1313,12 +1326,61 @@ static void fill_print_name(struct diffstat_file *file)
        file->print_name = pname;
 }
 
+int print_stat_summary(FILE *fp, int files, int insertions, int deletions)
+{
+       struct strbuf sb = STRBUF_INIT;
+       int ret;
+
+       if (!files) {
+               assert(insertions == 0 && deletions == 0);
+               return fputs(_(" 0 files changed\n"), fp);
+       }
+
+       strbuf_addf(&sb,
+                   Q_(" %d file changed", " %d files changed", files),
+                   files);
+
+       /*
+        * For binary diff, the caller may want to print "x files
+        * changed" with insertions == 0 && deletions == 0.
+        *
+        * Not omitting "0 insertions(+), 0 deletions(-)" in this case
+        * is probably less confusing (i.e skip over "2 files changed
+        * but nothing about added/removed lines? Is this a bug in Git?").
+        */
+       if (insertions || deletions == 0) {
+               /*
+                * TRANSLATORS: "+" in (+) is a line addition marker;
+                * do not translate it.
+                */
+               strbuf_addf(&sb,
+                           Q_(", %d insertion(+)", ", %d insertions(+)",
+                              insertions),
+                           insertions);
+       }
+
+       if (deletions || insertions == 0) {
+               /*
+                * TRANSLATORS: "-" in (-) is a line removal marker;
+                * do not translate it.
+                */
+               strbuf_addf(&sb,
+                           Q_(", %d deletion(-)", ", %d deletions(-)",
+                              deletions),
+                           deletions);
+       }
+       strbuf_addch(&sb, '\n');
+       ret = fputs(sb.buf, fp);
+       strbuf_release(&sb);
+       return ret;
+}
+
 static void show_stats(struct diffstat_t *data, struct diff_options *options)
 {
        int i, len, add, del, adds = 0, dels = 0;
        uintmax_t max_change = 0, max_len = 0;
        int total_files = data->nr;
-       int width, name_width, count;
+       int width, name_width, graph_width, number_width = 4, count;
        const char *reset, *add_c, *del_c;
        const char *line_prefix = "";
        int extra_shown = 0;
@@ -1332,25 +1394,15 @@ static void show_stats(struct diffstat_t *data, struct diff_options *options)
                line_prefix = msg->buf;
        }
 
-       width = options->stat_width ? options->stat_width : 80;
-       name_width = options->stat_name_width ? options->stat_name_width : 50;
        count = options->stat_count ? options->stat_count : data->nr;
 
-       /* Sanity: give at least 5 columns to the graph,
-        * but leave at least 10 columns for the name.
-        */
-       if (width < 25)
-               width = 25;
-       if (name_width < 10)
-               name_width = 10;
-       else if (width < name_width + 15)
-               name_width = width - 15;
-
-       /* Find the longest filename and max number of changes */
        reset = diff_get_color_opt(options, DIFF_RESET);
        add_c = diff_get_color_opt(options, DIFF_FILE_NEW);
        del_c = diff_get_color_opt(options, DIFF_FILE_OLD);
 
+       /*
+        * Find the longest filename and max number of changes
+        */
        for (i = 0; (i < count) && (i < data->nr); i++) {
                struct diffstat_file *file = data->files[i];
                uintmax_t change = file->added + file->deleted;
@@ -1371,19 +1423,72 @@ static void show_stats(struct diffstat_t *data, struct diff_options *options)
        }
        count = i; /* min(count, data->nr) */
 
-       /* Compute the width of the graph part;
-        * 10 is for one blank at the beginning of the line plus
-        * " | count " between the name and the graph.
+       /*
+        * We have width = stat_width or term_columns() columns total.
+        * We want a maximum of min(max_len, stat_name_width) for the name part.
+        * We want a maximum of min(max_change, stat_graph_width) for the +- part.
+        * We also need 1 for " " and 4 + decimal_width(max_change)
+        * for " | NNNN " and one the empty column at the end, altogether
+        * 6 + decimal_width(max_change).
         *
-        * From here on, name_width is the width of the name area,
-        * and width is the width of the graph area.
+        * If there's not enough space, we will use the smaller of
+        * stat_name_width (if set) and 5/8*width for the filename,
+        * and the rest for constant elements + graph part, but no more
+        * than stat_graph_width for the graph part.
+        * (5/8 gives 50 for filename and 30 for the constant parts + graph
+        * for the standard terminal size).
+        *
+        * In other words: stat_width limits the maximum width, and
+        * stat_name_width fixes the maximum width of the filename,
+        * and is also used to divide available columns if there
+        * aren't enough.
         */
-       name_width = (name_width < max_len) ? name_width : max_len;
-       if (width < (name_width + 10) + max_change)
-               width = width - (name_width + 10);
+
+       if (options->stat_width == -1)
+               width = term_columns();
        else
-               width = max_change;
+               width = options->stat_width ? options->stat_width : 80;
+
+       if (options->stat_graph_width == -1)
+               options->stat_graph_width = diff_stat_graph_width;
+
+       /*
+        * Guarantee 3/8*16==6 for the graph part
+        * and 5/8*16==10 for the filename part
+        */
+       if (width < 16 + 6 + number_width)
+               width = 16 + 6 + number_width;
+
+       /*
+        * First assign sizes that are wanted, ignoring available width.
+        */
+       graph_width = (options->stat_graph_width &&
+                      options->stat_graph_width < max_change) ?
+               options->stat_graph_width : max_change;
+       name_width = (options->stat_name_width > 0 &&
+                     options->stat_name_width < max_len) ?
+               options->stat_name_width : max_len;
+
+       /*
+        * Adjust adjustable widths not to exceed maximum width
+        */
+       if (name_width + number_width + 6 + graph_width > width) {
+               if (graph_width > width * 3/8 - number_width - 6)
+                       graph_width = width * 3/8 - number_width - 6;
+               if (options->stat_graph_width &&
+                   graph_width > options->stat_graph_width)
+                       graph_width = options->stat_graph_width;
+               if (name_width > width - number_width - 6 - graph_width)
+                       name_width = width - number_width - 6 - graph_width;
+               else
+                       graph_width = width - number_width - 6 - name_width;
+       }
 
+       /*
+        * From here name_width is the width of the name area,
+        * and graph_width is the width of the graph area.
+        * max_change is used to scale graph properly.
+        */
        for (i = 0; i < count; i++) {
                const char *prefix = "";
                char *name = data->files[i]->print_name;
@@ -1439,9 +1544,20 @@ static void show_stats(struct diffstat_t *data, struct diff_options *options)
                adds += add;
                dels += del;
 
-               if (width <= max_change) {
-                       add = scale_linear(add, width, max_change);
-                       del = scale_linear(del, width, max_change);
+               if (graph_width <= max_change) {
+                       int total = add + del;
+
+                       total = scale_linear(add + del, graph_width, max_change);
+                       if (total < 2 && add && del)
+                               /* width >= 2 due to the sanity check */
+                               total = 2;
+                       if (add < del) {
+                               add = scale_linear(add, graph_width, max_change);
+                               del = total - add;
+                       } else {
+                               del = scale_linear(del, graph_width, max_change);
+                               add = total - del;
+                       }
                }
                fprintf(options->file, "%s", line_prefix);
                show_name(options->file, prefix, name, len);
@@ -1466,9 +1582,7 @@ static void show_stats(struct diffstat_t *data, struct diff_options *options)
                extra_shown = 1;
        }
        fprintf(options->file, "%s", line_prefix);
-       fprintf(options->file,
-              " %d files changed, %d insertions(+), %d deletions(-)\n",
-              total_files, adds, dels);
+       print_stat_summary(options->file, total_files, adds, dels);
 }
 
 static void show_shortstats(struct diffstat_t *data, struct diff_options *options)
@@ -1498,8 +1612,7 @@ static void show_shortstats(struct diffstat_t *data, struct diff_options *option
                                options->output_prefix_data);
                fprintf(options->file, "%s", msg->buf);
        }
-       fprintf(options->file, " %d files changed, %d insertions(+), %d deletions(-)\n",
-              total_files, adds, dels);
+       print_stat_summary(options->file, total_files, adds, dels);
 }
 
 static void show_numstat(struct diffstat_t *data, struct diff_options *options)
@@ -2142,7 +2255,7 @@ static void builtin_diff(const char *name_a,
                struct emit_callback ecbdata;
                const struct userdiff_funcname *pe;
 
-               if (!DIFF_XDL_TST(o, WHITESPACE_FLAGS) || must_show_header) {
+               if (must_show_header) {
                        fprintf(o->file, "%s", header.buf);
                        strbuf_reset(&header);
                }
@@ -3234,6 +3347,7 @@ static int stat_opt(struct diff_options *options, const char **av)
        char *end;
        int width = options->stat_width;
        int name_width = options->stat_name_width;
+       int graph_width = options->stat_graph_width;
        int count = options->stat_count;
        int argcount = 1;
 
@@ -3262,6 +3376,16 @@ static int stat_opt(struct diff_options *options, const char **av)
                                name_width = strtoul(av[1], &end, 10);
                                argcount = 2;
                        }
+               } else if (!prefixcmp(arg, "-graph-width")) {
+                       arg += strlen("-graph-width");
+                       if (*arg == '=')
+                               graph_width = strtoul(arg + 1, &end, 10);
+                       else if (!*arg && !av[1])
+                               die("Option '--stat-graph-width' requires a value");
+                       else if (!*arg) {
+                               graph_width = strtoul(av[1], &end, 10);
+                               argcount = 2;
+                       }
                } else if (!prefixcmp(arg, "-count")) {
                        arg += strlen("-count");
                        if (*arg == '=')
@@ -3287,6 +3411,7 @@ static int stat_opt(struct diff_options *options, const char **av)
                return 0;
        options->output_format |= DIFF_FORMAT_DIFFSTAT;
        options->stat_name_width = name_width;
+       options->stat_graph_width = graph_width;
        options->stat_width = width;
        options->stat_count = count;
        return argcount;