Code

Merge branch 'mg/diff-stat-count'
authorJunio C Hamano <gitster@pobox.com>
Thu, 30 Jun 2011 00:03:10 +0000 (17:03 -0700)
committerJunio C Hamano <gitster@pobox.com>
Thu, 30 Jun 2011 00:03:10 +0000 (17:03 -0700)
* mg/diff-stat-count:
  diff --stat-count: finishing touches
  diff-options.txt: describe --stat-{width,name-width,count}
  diff: introduce --stat-lines to limit the stat lines
  diff.c: omit hidden entries from namelen calculation with --stat

1  2 
Documentation/diff-options.txt
diff.c
diff.h

index 24f189f96b760dc9201a202827351ca82aab6780,f6c046a09adc5e20cdf26d70386fffa6693ef49a..4235302ea48eea4daac9512f680fc4cfd877fb23
@@@ -48,11 -48,17 +48,17 @@@ endif::git-format-patch[
  --patience::
        Generate a diff using the "patience diff" algorithm.
  
- --stat[=<width>[,<name-width>]]::
+ --stat[=<width>[,<name-width>[,<count>]]]::
        Generate a diffstat.  You can override the default
        output width for 80-column terminal by `--stat=<width>`.
        The width of the filename part can be controlled by
        giving another width to it separated by a comma.
+       By giving a third parameter `<count>`, you can limit the
+       output to the first `<count>` lines, followed by
+       `...` if there are more.
+ +
+ These parameters can also be set individually with `--stat-width=<width>`,
+ `--stat-name-width=<name-width>` and `--stat-count=<count>`.
  
  --numstat::
        Similar to `\--stat`, but shows number of added and
        number of modified files, as well as number of added and deleted
        lines.
  
 ---dirstat[=<limit>]::
 -      Output the distribution of relative amount of changes (number of lines added or
 -      removed) for each sub-directory. Directories with changes below
 -      a cut-off percent (3% by default) are not shown. The cut-off percent
 -      can be set with `--dirstat=<limit>`. Changes in a child directory are not
 -      counted for the parent directory, unless `--cumulative` is used.
 +--dirstat[=<param1,param2,...>]::
 +      Output the distribution of relative amount of changes for each
 +      sub-directory. The behavior of `--dirstat` can be customized by
 +      passing it a comma separated list of parameters.
 +      The defaults are controlled by the `diff.dirstat` configuration
 +      variable (see linkgit:git-config[1]).
 +      The following parameters are available:
  +
 -Note that the `--dirstat` option computes the changes while ignoring
 -the amount of pure code movements within a file.  In other words,
 -rearranging lines in a file is not counted as much as other changes.
 -
 ---dirstat-by-file[=<limit>]::
 -      Same as `--dirstat`, but counts changed files instead of lines.
 +--
 +`changes`;;
 +      Compute the dirstat numbers by counting the lines that have been
 +      removed from the source, or added to the destination. This ignores
 +      the amount of pure code movements within a file.  In other words,
 +      rearranging lines in a file is not counted as much as other changes.
 +      This is the default behavior when no parameter is given.
 +`lines`;;
 +      Compute the dirstat numbers by doing the regular line-based diff
 +      analysis, and summing the removed/added line counts. (For binary
 +      files, count 64-byte chunks instead, since binary files have no
 +      natural concept of lines). This is a more expensive `--dirstat`
 +      behavior than the `changes` behavior, but it does count rearranged
 +      lines within a file as much as other changes. The resulting output
 +      is consistent with what you get from the other `--*stat` options.
 +`files`;;
 +      Compute the dirstat numbers by counting the number of files changed.
 +      Each changed file counts equally in the dirstat analysis. This is
 +      the computationally cheapest `--dirstat` behavior, since it does
 +      not have to look at the file contents at all.
 +`cumulative`;;
 +      Count changes in a child directory for the parent directory as well.
 +      Note that when using `cumulative`, the sum of the percentages
 +      reported may exceed 100%. The default (non-cumulative) behavior can
 +      be specified with the `noncumulative` parameter.
 +<limit>;;
 +      An integer parameter specifies a cut-off percent (3% by default).
 +      Directories contributing less than this percentage of the changes
 +      are not shown in the output.
 +--
 ++
 +Example: The following will count changed files, while ignoring
 +directories with less than 10% of the total amount of changed files,
 +and accumulating child directory counts in the parent directories:
 +`--dirstat=files,10,cumulative`.
  
  --summary::
        Output a condensed summary of extended header information
@@@ -154,19 -130,12 +160,19 @@@ any of those replacements occurred
  
  --color[=<when>]::
        Show colored diff.
 -      The value must be always (the default), never, or auto.
 +      The value must be `always` (the default for `<when>`), `never`, or `auto`.
 +      The default value is `never`.
 +ifdef::git-diff[]
 +      It can be changed by the `color.ui` and `color.diff`
 +      configuration settings.
 +endif::git-diff[]
  
  --no-color::
 -      Turn off colored diff, even when the configuration file
 -      gives the default to color output.
 -      Same as `--color=never`.
 +      Turn off colored diff.
 +ifdef::git-diff[]
 +      This can be used to override configuration settings.
 +endif::git-diff[]
 +      It is the same as `--color=never`.
  
  --word-diff[=<mode>]::
        Show a word diff, using the <mode> to delimit changed words.
@@@ -224,14 -193,10 +230,14 @@@ endif::git-format-patch[
  
  ifndef::git-format-patch[]
  --check::
 -      Warn if changes introduce trailing whitespace
 -      or an indent that uses a space before a tab. Exits with
 -      non-zero status if problems are found. Not compatible with
 -      --exit-code.
 +      Warn if changes introduce whitespace errors.  What are
 +      considered whitespace errors is controlled by `core.whitespace`
 +      configuration.  By default, trailing whitespaces (including
 +      lines that solely consist of whitespaces) and a space character
 +      that is immediately followed by a tab character inside the
 +      initial indent of the line are considered whitespace errors.
 +      Exits with non-zero status if problems are found. Not compatible
 +      with --exit-code.
  endif::git-format-patch[]
  
  --full-index::
@@@ -284,7 -249,7 +290,7 @@@ ifdef::git-log[
        For following files across renames while traversing history, see
        `--follow`.
  endif::git-log[]
 -      If `n` is specified, it is a is a threshold on the similarity
 +      If `n` is specified, it is a threshold on the similarity
        index (i.e. amount of addition/deletions compared to the
        file's size). For example, `-M90%` means git should consider a
        delete/add pair to be a rename if more than 90% of the file
diff --combined diff.c
index 5dd9049c7d354110534b2770fbf6cbc8e74e359e,542ff42e5aa28b2a235466e07795bd2e6214afd7..918c70ca457bcdca54b9d524ff14b60d88b5a631
--- 1/diff.c
--- 2/diff.c
+++ b/diff.c
@@@ -31,7 -31,6 +31,7 @@@ static const char *external_diff_cmd_cf
  int diff_auto_refresh_index = 1;
  static int diff_mnemonic_prefix;
  static int diff_no_prefix;
 +static int diff_dirstat_permille_default = 30;
  static struct diff_options default_diff_options;
  
  static char diff_colors[][COLOR_MAXLEN] = {
@@@ -67,58 -66,6 +67,58 @@@ static int parse_diff_color_slot(const 
        return -1;
  }
  
 +static int parse_dirstat_params(struct diff_options *options, const char *params,
 +                              struct strbuf *errmsg)
 +{
 +      const char *p = params;
 +      int p_len, ret = 0;
 +
 +      while (*p) {
 +              p_len = strchrnul(p, ',') - p;
 +              if (!memcmp(p, "changes", p_len)) {
 +                      DIFF_OPT_CLR(options, DIRSTAT_BY_LINE);
 +                      DIFF_OPT_CLR(options, DIRSTAT_BY_FILE);
 +              } else if (!memcmp(p, "lines", p_len)) {
 +                      DIFF_OPT_SET(options, DIRSTAT_BY_LINE);
 +                      DIFF_OPT_CLR(options, DIRSTAT_BY_FILE);
 +              } else if (!memcmp(p, "files", p_len)) {
 +                      DIFF_OPT_CLR(options, DIRSTAT_BY_LINE);
 +                      DIFF_OPT_SET(options, DIRSTAT_BY_FILE);
 +              } else if (!memcmp(p, "noncumulative", p_len)) {
 +                      DIFF_OPT_CLR(options, DIRSTAT_CUMULATIVE);
 +              } else if (!memcmp(p, "cumulative", p_len)) {
 +                      DIFF_OPT_SET(options, DIRSTAT_CUMULATIVE);
 +              } else if (isdigit(*p)) {
 +                      char *end;
 +                      int permille = strtoul(p, &end, 10) * 10;
 +                      if (*end == '.' && isdigit(*++end)) {
 +                              /* only use first digit */
 +                              permille += *end - '0';
 +                              /* .. and ignore any further digits */
 +                              while (isdigit(*++end))
 +                                      ; /* nothing */
 +                      }
 +                      if (end - p == p_len)
 +                              options->dirstat_permille = permille;
 +                      else {
 +                              strbuf_addf(errmsg, _("  Failed to parse dirstat cut-off percentage '%.*s'\n"),
 +                                          p_len, p);
 +                              ret++;
 +                      }
 +              } else {
 +                      strbuf_addf(errmsg, _("  Unknown dirstat parameter '%.*s'\n"),
 +                                  p_len, p);
 +                      ret++;
 +              }
 +
 +              p += p_len;
 +
 +              if (*p)
 +                      p++; /* more parameters, swallow separator */
 +      }
 +      return ret;
 +}
 +
  static int git_config_rename(const char *var, const char *value)
  {
        if (!value)
@@@ -198,17 -145,6 +198,17 @@@ int git_diff_basic_config(const char *v
                return 0;
        }
  
 +      if (!strcmp(var, "diff.dirstat")) {
 +              struct strbuf errmsg = STRBUF_INIT;
 +              default_diff_options.dirstat_permille = diff_dirstat_permille_default;
 +              if (parse_dirstat_params(&default_diff_options, value, &errmsg))
 +                      warning(_("Found errors in 'diff.dirstat' config variable:\n%s"),
 +                              errmsg.buf);
 +              strbuf_release(&errmsg);
 +              diff_dirstat_permille_default = default_diff_options.dirstat_permille;
 +              return 0;
 +      }
 +
        if (!prefixcmp(var, "submodule."))
                return parse_submodule_config_option(var, value);
  
@@@ -1117,16 -1053,8 +1117,16 @@@ static void fn_out_consume(void *priv, 
                        emit_line(ecbdata->opt, plain, reset, line, len);
                        fputs("~\n", ecbdata->opt->file);
                } else {
 -                      /* don't print the prefix character */
 -                      emit_line(ecbdata->opt, plain, reset, line+1, len-1);
 +                      /*
 +                       * Skip the prefix character, if any.  With
 +                       * diff_suppress_blank_empty, there may be
 +                       * none.
 +                       */
 +                      if (line[0] != '\n') {
 +                            line++;
 +                            len--;
 +                      }
 +                      emit_line(ecbdata->opt, plain, reset, line, len);
                }
                return;
        }
@@@ -1316,9 -1244,10 +1316,10 @@@ static void show_stats(struct diffstat_
        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;
+       int width, name_width, count;
        const char *reset, *add_c, *del_c;
        const char *line_prefix = "";
+       int extra_shown = 0;
        struct strbuf *msg = NULL;
  
        if (data->nr == 0)
  
        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.
        add_c = diff_get_color_opt(options, DIFF_FILE_NEW);
        del_c = diff_get_color_opt(options, DIFF_FILE_OLD);
  
-       for (i = 0; i < data->nr; i++) {
+       for (i = 0; (i < count) && (i < data->nr); i++) {
                struct diffstat_file *file = data->files[i];
                uintmax_t change = file->added + file->deleted;
+               if (!data->files[i]->is_renamed &&
+                        (change == 0)) {
+                       count++; /* not shown == room for one more */
+                       continue;
+               }
                fill_print_name(file);
                len = strlen(file->print_name);
                if (max_len < len)
                if (max_change < change)
                        max_change = change;
        }
+       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
        else
                width = max_change;
  
-       for (i = 0; i < data->nr; i++) {
+       for (i = 0; i < count; i++) {
                const char *prefix = "";
                char *name = data->files[i]->print_name;
                uintmax_t added = data->files[i]->added;
                uintmax_t deleted = data->files[i]->deleted;
                int name_len;
  
+               if (!data->files[i]->is_renamed &&
+                        (added + deleted == 0)) {
+                       total_files--;
+                       continue;
+               }
                /*
                 * "scale" the filename
                 */
                        fprintf(options->file, "  Unmerged\n");
                        continue;
                }
-               else if (!data->files[i]->is_renamed &&
-                        (added + deleted == 0)) {
-                       total_files--;
-                       continue;
-               }
  
                /*
                 * scale the add/delete
                show_graph(options->file, '-', del, del_c, reset);
                fprintf(options->file, "\n");
        }
+       for (i = count; i < data->nr; i++) {
+               uintmax_t added = data->files[i]->added;
+               uintmax_t deleted = data->files[i]->deleted;
+               if (!data->files[i]->is_renamed &&
+                        (added + deleted == 0)) {
+                       total_files--;
+                       continue;
+               }
+               adds += added;
+               dels += deleted;
+               if (!extra_shown)
+                       fprintf(options->file, "%s ...\n", line_prefix);
+               extra_shown = 1;
+       }
        fprintf(options->file, "%s", line_prefix);
        fprintf(options->file,
               " %d files changed, %d insertions(+), %d deletions(-)\n",
@@@ -1527,7 -1477,7 +1549,7 @@@ struct dirstat_file 
  
  struct dirstat_dir {
        struct dirstat_file *files;
 -      int alloc, nr, percent, cumulative;
 +      int alloc, nr, permille, cumulative;
  };
  
  static long gather_dirstat(struct diff_options *opt, struct dirstat_dir *dir,
         *    under this directory (sources == 1).
         */
        if (baselen && sources != 1) {
 -              int permille = this_dir * 1000 / changed;
 -              if (permille) {
 -                      int percent = permille / 10;
 -                      if (percent >= dir->percent) {
 +              if (this_dir) {
 +                      int permille = this_dir * 1000 / changed;
 +                      if (permille >= dir->permille) {
                                fprintf(opt->file, "%s%4d.%01d%% %.*s\n", line_prefix,
 -                                      percent, permille % 10, baselen, base);
 +                                      permille / 10, permille % 10, baselen, base);
                                if (!dir->cumulative)
                                        return 0;
                        }
@@@ -1604,7 -1555,7 +1626,7 @@@ static void show_dirstat(struct diff_op
        dir.files = NULL;
        dir.alloc = 0;
        dir.nr = 0;
 -      dir.percent = options->dirstat_percent;
 +      dir.permille = options->dirstat_permille;
        dir.cumulative = DIFF_OPT_TST(options, DIRSTAT_CUMULATIVE);
  
        changed = 0;
@@@ -1693,50 -1644,6 +1715,50 @@@ found_damage
        gather_dirstat(options, &dir, changed, "", 0);
  }
  
 +static void show_dirstat_by_line(struct diffstat_t *data, struct diff_options *options)
 +{
 +      int i;
 +      unsigned long changed;
 +      struct dirstat_dir dir;
 +
 +      if (data->nr == 0)
 +              return;
 +
 +      dir.files = NULL;
 +      dir.alloc = 0;
 +      dir.nr = 0;
 +      dir.permille = options->dirstat_permille;
 +      dir.cumulative = DIFF_OPT_TST(options, DIRSTAT_CUMULATIVE);
 +
 +      changed = 0;
 +      for (i = 0; i < data->nr; i++) {
 +              struct diffstat_file *file = data->files[i];
 +              unsigned long damage = file->added + file->deleted;
 +              if (file->is_binary)
 +                      /*
 +                       * binary files counts bytes, not lines. Must find some
 +                       * way to normalize binary bytes vs. textual lines.
 +                       * The following heuristic assumes that there are 64
 +                       * bytes per "line".
 +                       * This is stupid and ugly, but very cheap...
 +                       */
 +                      damage = (damage + 63) / 64;
 +              ALLOC_GROW(dir.files, dir.nr + 1, dir.alloc);
 +              dir.files[dir.nr].name = file->name;
 +              dir.files[dir.nr].changed = damage;
 +              changed += damage;
 +              dir.nr++;
 +      }
 +
 +      /* This can happen even with many files, if everything was renames */
 +      if (!changed)
 +              return;
 +
 +      /* Show all directories with more than x% of the changes */
 +      qsort(dir.files, dir.nr, sizeof(dir.files[0]), dirstat_compare);
 +      gather_dirstat(options, &dir, changed, "", 0);
 +}
 +
  static void free_diffstat_info(struct diffstat_t *diffstat)
  {
        int i;
@@@ -1984,7 -1891,19 +2006,7 @@@ struct userdiff_driver *get_textconv(st
                return NULL;
  
        diff_filespec_load_driver(one);
 -      if (!one->driver->textconv)
 -              return NULL;
 -
 -      if (one->driver->textconv_want_cache && !one->driver->textconv_cache) {
 -              struct notes_cache *c = xmalloc(sizeof(*c));
 -              struct strbuf name = STRBUF_INIT;
 -
 -              strbuf_addf(&name, "textconv/%s", one->driver->name);
 -              notes_cache_init(c, name.buf, one->driver->textconv);
 -              one->driver->textconv_cache = c;
 -      }
 -
 -      return one->driver;
 +      return userdiff_get_textconv(one->driver);
  }
  
  static void builtin_diff(const char *name_a,
@@@ -2994,7 -2913,7 +3016,7 @@@ void diff_setup(struct diff_options *op
        options->line_termination = '\n';
        options->break_opt = -1;
        options->rename_limit = -1;
 -      options->dirstat_percent = 3;
 +      options->dirstat_permille = diff_dirstat_permille_default;
        options->context = 3;
  
        options->change = diff_change;
@@@ -3208,6 -3127,7 +3230,7 @@@ static int stat_opt(struct diff_option
        char *end;
        int width = options->stat_width;
        int name_width = options->stat_name_width;
+       int count = options->stat_count;
        int argcount = 1;
  
        arg += strlen("--stat");
                                name_width = strtoul(av[1], &end, 10);
                                argcount = 2;
                        }
+               } else if (!prefixcmp(arg, "-count")) {
+                       arg += strlen("-count");
+                       if (*arg == '=')
+                               count = strtoul(arg + 1, &end, 10);
+                       else if (!*arg && !av[1])
+                               die("Option '--stat-count' requires a value");
+                       else if (!*arg) {
+                               count = strtoul(av[1], &end, 10);
+                               argcount = 2;
+                       }
                }
                break;
        case '=':
                width = strtoul(arg+1, &end, 10);
                if (*end == ',')
                        name_width = strtoul(end+1, &end, 10);
+               if (*end == ',')
+                       count = strtoul(end+1, &end, 10);
        }
  
        /* Important! This checks all the error cases! */
        options->output_format |= DIFF_FORMAT_DIFFSTAT;
        options->stat_name_width = name_width;
        options->stat_width = width;
+       options->stat_count = count;
        return argcount;
  }
  
 +static int parse_dirstat_opt(struct diff_options *options, const char *params)
 +{
 +      struct strbuf errmsg = STRBUF_INIT;
 +      if (parse_dirstat_params(options, params, &errmsg))
 +              die(_("Failed to parse --dirstat/-X option parameter:\n%s"),
 +                  errmsg.buf);
 +      strbuf_release(&errmsg);
 +      /*
 +       * The caller knows a dirstat-related option is given from the command
 +       * line; allow it to say "return this_function();"
 +       */
 +      options->output_format |= DIFF_FORMAT_DIRSTAT;
 +      return 1;
 +}
 +
  int diff_opt_parse(struct diff_options *options, const char **av, int ac)
  {
        const char *arg = av[0];
                options->output_format |= DIFF_FORMAT_NUMSTAT;
        else if (!strcmp(arg, "--shortstat"))
                options->output_format |= DIFF_FORMAT_SHORTSTAT;
 -      else if (opt_arg(arg, 'X', "dirstat", &options->dirstat_percent))
 -              options->output_format |= DIFF_FORMAT_DIRSTAT;
 -      else if (!strcmp(arg, "--cumulative")) {
 -              options->output_format |= DIFF_FORMAT_DIRSTAT;
 -              DIFF_OPT_SET(options, DIRSTAT_CUMULATIVE);
 -      } else if (opt_arg(arg, 0, "dirstat-by-file",
 -                         &options->dirstat_percent)) {
 -              options->output_format |= DIFF_FORMAT_DIRSTAT;
 -              DIFF_OPT_SET(options, DIRSTAT_BY_FILE);
 +      else if (!strcmp(arg, "-X") || !strcmp(arg, "--dirstat"))
 +              return parse_dirstat_opt(options, "");
 +      else if (!prefixcmp(arg, "-X"))
 +              return parse_dirstat_opt(options, arg + 2);
 +      else if (!prefixcmp(arg, "--dirstat="))
 +              return parse_dirstat_opt(options, arg + 10);
 +      else if (!strcmp(arg, "--cumulative"))
 +              return parse_dirstat_opt(options, "cumulative");
 +      else if (!strcmp(arg, "--dirstat-by-file"))
 +              return parse_dirstat_opt(options, "files");
 +      else if (!prefixcmp(arg, "--dirstat-by-file=")) {
 +              parse_dirstat_opt(options, "files");
 +              return parse_dirstat_opt(options, arg + 18);
        }
        else if (!strcmp(arg, "--check"))
                options->output_format |= DIFF_FORMAT_CHECKDIFF;
        else if (!strcmp(arg, "-s"))
                options->output_format |= DIFF_FORMAT_NO_OUTPUT;
        else if (!prefixcmp(arg, "--stat"))
-               /* --stat, --stat-width, or --stat-name-width */
+               /* --stat, --stat-width, --stat-name-width, or --stat-count */
                return stat_opt(options, av);
  
        /* renames options */
@@@ -4145,7 -4059,6 +4181,7 @@@ void diff_flush(struct diff_options *op
        struct diff_queue_struct *q = &diff_queued_diff;
        int i, output_format = options->output_format;
        int separator = 0;
 +      int dirstat_by_line = 0;
  
        /*
         * Order: raw, stat, summary, patch
                separator++;
        }
  
 -      if (output_format & (DIFF_FORMAT_DIFFSTAT|DIFF_FORMAT_SHORTSTAT|DIFF_FORMAT_NUMSTAT)) {
 +      if (output_format & DIFF_FORMAT_DIRSTAT && DIFF_OPT_TST(options, DIRSTAT_BY_LINE))
 +              dirstat_by_line = 1;
 +
 +      if (output_format & (DIFF_FORMAT_DIFFSTAT|DIFF_FORMAT_SHORTSTAT|DIFF_FORMAT_NUMSTAT) ||
 +          dirstat_by_line) {
                struct diffstat_t diffstat;
  
                memset(&diffstat, 0, sizeof(struct diffstat_t));
                        show_stats(&diffstat, options);
                if (output_format & DIFF_FORMAT_SHORTSTAT)
                        show_shortstats(&diffstat, options);
 +              if (output_format & DIFF_FORMAT_DIRSTAT)
 +                      show_dirstat_by_line(&diffstat, options);
                free_diffstat_info(&diffstat);
                separator++;
        }
 -      if (output_format & DIFF_FORMAT_DIRSTAT)
 +      if ((output_format & DIFF_FORMAT_DIRSTAT) && !dirstat_by_line)
                show_dirstat(options);
  
        if (output_format & DIFF_FORMAT_SUMMARY && !is_summary_empty(q)) {
@@@ -4444,13 -4351,6 +4480,13 @@@ int diff_result_code(struct diff_option
        return result;
  }
  
 +int diff_can_quit_early(struct diff_options *opt)
 +{
 +      return (DIFF_OPT_TST(opt, QUICK) &&
 +              !opt->filter &&
 +              DIFF_OPT_TST(opt, HAS_CHANGES));
 +}
 +
  /*
   * Shall changes to this submodule be ignored?
   *
@@@ -4552,20 -4452,20 +4588,20 @@@ void diff_change(struct diff_options *o
                DIFF_OPT_SET(options, HAS_CHANGES);
  }
  
 -void diff_unmerge(struct diff_options *options,
 -                const char *path,
 -                unsigned mode, const unsigned char *sha1)
 +struct diff_filepair *diff_unmerge(struct diff_options *options, const char *path)
  {
 +      struct diff_filepair *pair;
        struct diff_filespec *one, *two;
  
        if (options->prefix &&
            strncmp(path, options->prefix, options->prefix_length))
 -              return;
 +              return NULL;
  
        one = alloc_filespec(path);
        two = alloc_filespec(path);
 -      fill_filespec(one, sha1, mode);
 -      diff_queue(&diff_queued_diff, one, two)->is_unmerged = 1;
 +      pair = diff_queue(&diff_queued_diff, one, two);
 +      pair->is_unmerged = 1;
 +      return pair;
  }
  
  static char *run_textconv(const char *pgm, struct diff_filespec *spec,
diff --combined diff.h
index 6d303c1d50aa799ec90aeb64b2a1b1f55811dd46,59608f9605839c6caa5a4abca0f5f031a08740bd..b920a20f80936ab66f5fe88c950dd35e38e1e856
--- 1/diff.h
--- 2/diff.h
+++ b/diff.h
@@@ -78,7 -78,6 +78,7 @@@ typedef struct strbuf *(*diff_prefix_fn
  #define DIFF_OPT_IGNORE_UNTRACKED_IN_SUBMODULES (1 << 25)
  #define DIFF_OPT_IGNORE_DIRTY_SUBMODULES (1 << 26)
  #define DIFF_OPT_OVERRIDE_SUBMODULE_CONFIG (1 << 27)
 +#define DIFF_OPT_DIRSTAT_BY_LINE     (1 << 28)
  
  #define DIFF_OPT_TST(opts, flag)    ((opts)->flags & DIFF_OPT_##flag)
  #define DIFF_OPT_SET(opts, flag)    ((opts)->flags |= DIFF_OPT_##flag)
@@@ -115,7 -114,7 +115,7 @@@ struct diff_options 
        int needed_rename_limit;
        int degraded_cc_to_c;
        int show_rename_progress;
 -      int dirstat_percent;
 +      int dirstat_permille;
        int setup;
        int abbrev;
        const char *prefix;
  
        int stat_width;
        int stat_name_width;
+       int stat_count;
        const char *word_regex;
        enum diff_words_type word_diff;
  
@@@ -198,8 -198,6 +199,8 @@@ extern void diff_tree_combined_merge(co
  
  void diff_set_mnemonic_prefix(struct diff_options *options, const char *a, const char *b);
  
 +extern int diff_can_quit_early(struct diff_options *);
 +
  extern void diff_addremove(struct diff_options *,
                           int addremove,
                           unsigned mode,
@@@ -213,7 -211,10 +214,7 @@@ extern void diff_change(struct diff_opt
                        const char *fullpath,
                        unsigned dirty_submodule1, unsigned dirty_submodule2);
  
 -extern void diff_unmerge(struct diff_options *,
 -                       const char *path,
 -                       unsigned mode,
 -                       const unsigned char *sha1);
 +extern struct diff_filepair *diff_unmerge(struct diff_options *, const char *path);
  
  #define DIFF_SETUP_REVERSE            1
  #define DIFF_SETUP_USE_CACHE          2