Merge branch 'jc/blame-no-follow'
authorJunio C Hamano <gitster@pobox.com>
Mon, 14 Jan 2013 16:15:51 +0000 (08:15 -0800)
committerJunio C Hamano <gitster@pobox.com>
Mon, 14 Jan 2013 16:15:51 +0000 (08:15 -0800)
Teaches "--no-follow" option to "git blame" to disable its
whole-file rename detection.

* jc/blame-no-follow:
blame: pay attention to --no-follow
diff: accept --no-follow option

1  2 
builtin/blame.c
diff.c
diff --combined builtin/blame.c
index cfae5699051312943d90212fc9cbfda2aafb2288,bfa6086b064387a68135a2e7b9c8f9c9a65b5134..bc6c899d302d3027a0531b613d4c2dbf706d558d
  #include "utf8.h"
  #include "userdiff.h"
  
 -static char blame_usage[] = "git blame [options] [rev-opts] [rev] [--] file";
 +static char blame_usage[] = N_("git blame [options] [rev-opts] [rev] [--] file");
  
  static const char *blame_opt_usage[] = {
        blame_usage,
        "",
 -      "[rev-opts] are documented in git-rev-list(1)",
 +      N_("[rev-opts] are documented in git-rev-list(1)"),
        NULL
  };
  
@@@ -42,6 -42,7 +42,7 @@@ static int blank_boundary
  static int incremental;
  static int xdl_opts;
  static int abbrev = -1;
+ static int no_whole_file_rename;
  
  static enum date_mode blame_date_mode = DATE_ISO8601;
  static size_t blame_date_width;
@@@ -1226,7 -1227,7 +1227,7 @@@ static void pass_blame(struct scoreboar
         * The first pass looks for unrenamed path to optimize for
         * common cases, then we look for renames in the second pass.
         */
-       for (pass = 0; pass < 2; pass++) {
+       for (pass = 0; pass < 2 - no_whole_file_rename; pass++) {
                struct origin *(*find)(struct scoreboard *,
                                       struct commit *, struct origin *);
                find = pass ? find_rename : find_origin;
@@@ -1425,7 -1426,7 +1426,7 @@@ static void get_commit_info(struct comm
                            int detailed)
  {
        int len;
 -      const char *subject;
 +      const char *subject, *encoding;
        char *reencoded, *message;
        static char author_name[1024];
        static char author_mail[1024];
                        die("Cannot read commit %s",
                            sha1_to_hex(commit->object.sha1));
        }
 -      reencoded = reencode_commit_message(commit, NULL);
 +      encoding = get_log_output_encoding();
 +      reencoded = logmsg_reencode(commit, encoding);
        message   = reencoded ? reencoded : commit->buffer;
        ret->author = author_name;
        ret->author_mail = author_mail;
@@@ -2070,55 -2070,6 +2071,55 @@@ static int git_blame_config(const char 
        return git_default_config(var, value, cb);
  }
  
 +static void verify_working_tree_path(struct commit *work_tree, const char *path)
 +{
 +      struct commit_list *parents;
 +
 +      for (parents = work_tree->parents; parents; parents = parents->next) {
 +              const unsigned char *commit_sha1 = parents->item->object.sha1;
 +              unsigned char blob_sha1[20];
 +              unsigned mode;
 +
 +              if (!get_tree_entry(commit_sha1, path, blob_sha1, &mode) &&
 +                  sha1_object_info(blob_sha1, NULL) == OBJ_BLOB)
 +                      return;
 +      }
 +      die("no such path '%s' in HEAD", path);
 +}
 +
 +static struct commit_list **append_parent(struct commit_list **tail, const unsigned char *sha1)
 +{
 +      struct commit *parent;
 +
 +      parent = lookup_commit_reference(sha1);
 +      if (!parent)
 +              die("no such commit %s", sha1_to_hex(sha1));
 +      return &commit_list_insert(parent, tail)->next;
 +}
 +
 +static void append_merge_parents(struct commit_list **tail)
 +{
 +      int merge_head;
 +      const char *merge_head_file = git_path("MERGE_HEAD");
 +      struct strbuf line = STRBUF_INIT;
 +
 +      merge_head = open(merge_head_file, O_RDONLY);
 +      if (merge_head < 0) {
 +              if (errno == ENOENT)
 +                      return;
 +              die("cannot open '%s' for reading", merge_head_file);
 +      }
 +
 +      while (!strbuf_getwholeline_fd(&line, merge_head, '\n')) {
 +              unsigned char sha1[20];
 +              if (line.len < 40 || get_sha1_hex(line.buf, sha1))
 +                      die("unknown line in '%s': %s", merge_head_file, line.buf);
 +              tail = append_parent(tail, sha1);
 +      }
 +      close(merge_head);
 +      strbuf_release(&line);
 +}
 +
  /*
   * Prepare a dummy commit that represents the work tree (or staged) item.
   * Note that annotating work tree item never works in the reverse.
@@@ -2129,7 -2080,6 +2130,7 @@@ static struct commit *fake_working_tree
  {
        struct commit *commit;
        struct origin *origin;
 +      struct commit_list **parent_tail, *parent;
        unsigned char head_sha1[20];
        struct strbuf buf = STRBUF_INIT;
        const char *ident;
        int size, len;
        struct cache_entry *ce;
        unsigned mode;
 -
 -      if (get_sha1("HEAD", head_sha1))
 -              die("No such ref: HEAD");
 +      struct strbuf msg = STRBUF_INIT;
  
        time(&now);
        commit = xcalloc(1, sizeof(*commit));
 -      commit->parents = xcalloc(1, sizeof(*commit->parents));
 -      commit->parents->item = lookup_commit_reference(head_sha1);
        commit->object.parsed = 1;
        commit->date = now;
        commit->object.type = OBJ_COMMIT;
 +      parent_tail = &commit->parents;
 +
 +      if (!resolve_ref_unsafe("HEAD", head_sha1, 1, NULL))
 +              die("no such ref: HEAD");
 +
 +      parent_tail = append_parent(parent_tail, head_sha1);
 +      append_merge_parents(parent_tail);
 +      verify_working_tree_path(commit, path);
  
        origin = make_origin(commit, path);
  
 +      ident = fmt_ident("Not Committed Yet", "not.committed.yet", NULL, 0);
 +      strbuf_addstr(&msg, "tree 0000000000000000000000000000000000000000\n");
 +      for (parent = commit->parents; parent; parent = parent->next)
 +              strbuf_addf(&msg, "parent %s\n",
 +                          sha1_to_hex(parent->item->object.sha1));
 +      strbuf_addf(&msg,
 +                  "author %s\n"
 +                  "committer %s\n\n"
 +                  "Version of %s from %s\n",
 +                  ident, ident, path,
 +                  (!contents_from ? path :
 +                   (!strcmp(contents_from, "-") ? "standard input" : contents_from)));
 +      commit->buffer = strbuf_detach(&msg, NULL);
 +
        if (!contents_from || strcmp("-", contents_from)) {
                struct stat st;
                const char *read_from;
        }
        else {
                /* Reading from stdin */
 -              contents_from = "standard input";
                mode = 0;
                if (strbuf_read(&buf, 0, 0) < 0)
                        die_errno("failed to read from stdin");
        ce = xcalloc(1, size);
        hashcpy(ce->sha1, origin->blob_sha1);
        memcpy(ce->name, path, len);
 -      ce->ce_flags = create_ce_flags(len, 0);
 +      ce->ce_flags = create_ce_flags(0);
 +      ce->ce_namelen = len;
        ce->ce_mode = create_ce_mode(mode);
        add_cache_entry(ce, ADD_CACHE_OK_TO_ADD|ADD_CACHE_OK_TO_REPLACE);
  
         */
        cache_tree_invalidate_path(active_cache_tree, path);
  
 -      commit->buffer = xmalloc(400);
 -      ident = fmt_ident("Not Committed Yet", "not.committed.yet", NULL, 0);
 -      snprintf(commit->buffer, 400,
 -              "tree 0000000000000000000000000000000000000000\n"
 -              "parent %s\n"
 -              "author %s\n"
 -              "committer %s\n\n"
 -              "Version of %s from %s\n",
 -              sha1_to_hex(head_sha1),
 -              ident, ident, path, contents_from ? contents_from : path);
        return commit;
  }
  
@@@ -2371,27 -2313,27 +2372,27 @@@ int cmd_blame(int argc, const char **ar
        static const char *revs_file = NULL;
        static const char *contents_from = NULL;
        static const struct option options[] = {
 -              OPT_BOOLEAN(0, "incremental", &incremental, "Show blame entries as we find them, incrementally"),
 -              OPT_BOOLEAN('b', NULL, &blank_boundary, "Show blank SHA-1 for boundary commits (Default: off)"),
 -              OPT_BOOLEAN(0, "root", &show_root, "Do not treat root commits as boundaries (Default: off)"),
 -              OPT_BOOLEAN(0, "show-stats", &show_stats, "Show work cost statistics"),
 -              OPT_BIT(0, "score-debug", &output_option, "Show output score for blame entries", OUTPUT_SHOW_SCORE),
 -              OPT_BIT('f', "show-name", &output_option, "Show original filename (Default: auto)", OUTPUT_SHOW_NAME),
 -              OPT_BIT('n', "show-number", &output_option, "Show original linenumber (Default: off)", OUTPUT_SHOW_NUMBER),
 -              OPT_BIT('p', "porcelain", &output_option, "Show in a format designed for machine consumption", OUTPUT_PORCELAIN),
 -              OPT_BIT(0, "line-porcelain", &output_option, "Show porcelain format with per-line commit information", OUTPUT_PORCELAIN|OUTPUT_LINE_PORCELAIN),
 -              OPT_BIT('c', NULL, &output_option, "Use the same output mode as git-annotate (Default: off)", OUTPUT_ANNOTATE_COMPAT),
 -              OPT_BIT('t', NULL, &output_option, "Show raw timestamp (Default: off)", OUTPUT_RAW_TIMESTAMP),
 -              OPT_BIT('l', NULL, &output_option, "Show long commit SHA1 (Default: off)", OUTPUT_LONG_OBJECT_NAME),
 -              OPT_BIT('s', NULL, &output_option, "Suppress author name and timestamp (Default: off)", OUTPUT_NO_AUTHOR),
 -              OPT_BIT('e', "show-email", &output_option, "Show author email instead of name (Default: off)", OUTPUT_SHOW_EMAIL),
 -              OPT_BIT('w', NULL, &xdl_opts, "Ignore whitespace differences", XDF_IGNORE_WHITESPACE),
 -              OPT_BIT(0, "minimal", &xdl_opts, "Spend extra cycles to find better match", XDF_NEED_MINIMAL),
 -              OPT_STRING('S', NULL, &revs_file, "file", "Use revisions from <file> instead of calling git-rev-list"),
 -              OPT_STRING(0, "contents", &contents_from, "file", "Use <file>'s contents as the final image"),
 -              { OPTION_CALLBACK, 'C', NULL, &opt, "score", "Find line copies within and across files", PARSE_OPT_OPTARG, blame_copy_callback },
 -              { OPTION_CALLBACK, 'M', NULL, &opt, "score", "Find line movements within and across files", PARSE_OPT_OPTARG, blame_move_callback },
 -              OPT_CALLBACK('L', NULL, &bottomtop, "n,m", "Process only line range n,m, counting from 1", blame_bottomtop_callback),
 +              OPT_BOOLEAN(0, "incremental", &incremental, N_("Show blame entries as we find them, incrementally")),
 +              OPT_BOOLEAN('b', NULL, &blank_boundary, N_("Show blank SHA-1 for boundary commits (Default: off)")),
 +              OPT_BOOLEAN(0, "root", &show_root, N_("Do not treat root commits as boundaries (Default: off)")),
 +              OPT_BOOLEAN(0, "show-stats", &show_stats, N_("Show work cost statistics")),
 +              OPT_BIT(0, "score-debug", &output_option, N_("Show output score for blame entries"), OUTPUT_SHOW_SCORE),
 +              OPT_BIT('f', "show-name", &output_option, N_("Show original filename (Default: auto)"), OUTPUT_SHOW_NAME),
 +              OPT_BIT('n', "show-number", &output_option, N_("Show original linenumber (Default: off)"), OUTPUT_SHOW_NUMBER),
 +              OPT_BIT('p', "porcelain", &output_option, N_("Show in a format designed for machine consumption"), OUTPUT_PORCELAIN),
 +              OPT_BIT(0, "line-porcelain", &output_option, N_("Show porcelain format with per-line commit information"), OUTPUT_PORCELAIN|OUTPUT_LINE_PORCELAIN),
 +              OPT_BIT('c', NULL, &output_option, N_("Use the same output mode as git-annotate (Default: off)"), OUTPUT_ANNOTATE_COMPAT),
 +              OPT_BIT('t', NULL, &output_option, N_("Show raw timestamp (Default: off)"), OUTPUT_RAW_TIMESTAMP),
 +              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 },
 +              { OPTION_CALLBACK, 'M', NULL, &opt, N_("score"), N_("Find line movements within and across files"), PARSE_OPT_OPTARG, blame_move_callback },
 +              OPT_CALLBACK('L', NULL, &bottomtop, N_("n,m"), N_("Process only line range n,m, counting from 1"), blame_bottomtop_callback),
                OPT__ABBREV(&abbrev),
                OPT_END()
        };
        init_revisions(&revs, NULL);
        revs.date_mode = blame_date_mode;
        DIFF_OPT_SET(&revs.diffopt, ALLOW_TEXTCONV);
+       DIFF_OPT_SET(&revs.diffopt, FOLLOW_RENAMES);
  
        save_commit_buffer = 0;
        dashdash_pos = 0;
                parse_revision_opt(&revs, &ctx, options, blame_opt_usage);
        }
  parse_done:
+       no_whole_file_rename = !DIFF_OPT_TST(&revs.diffopt, FOLLOW_RENAMES);
+       DIFF_OPT_CLR(&revs.diffopt, FOLLOW_RENAMES);
        argc = parse_options_end(&ctx);
  
        if (0 < abbrev)
diff --combined diff.c
index 732d4c227595873bb94224da618ce5612a7415af,32ebcbbf4f4d21be34a76542f8c25acfe6b3ad7b..348f71b46256657860a2f7f79598e1d9b19d6c54
--- 1/diff.c
--- 2/diff.c
+++ b/diff.c
@@@ -15,7 -15,6 +15,7 @@@
  #include "sigchain.h"
  #include "submodule.h"
  #include "ll-merge.h"
 +#include "string-list.h"
  
  #ifdef NO_FAST_WORKING_DIRECTORY
  #define FAST_WORKING_DIRECTORY 0
@@@ -26,8 -25,7 +26,8 @@@
  static int diff_detect_rename_default;
  static int diff_rename_limit_default = 400;
  static int diff_suppress_blank_empty;
 -int diff_use_color_default = -1;
 +static int diff_use_color_default = -1;
 +static int diff_context_default = 3;
  static const char *diff_word_regex_cfg;
  static const char *external_diff_cmd_cfg;
  int diff_auto_refresh_index = 1;
@@@ -70,30 -68,26 +70,30 @@@ static int parse_diff_color_slot(const 
        return -1;
  }
  
 -static int parse_dirstat_params(struct diff_options *options, const char *params,
 +static int parse_dirstat_params(struct diff_options *options, const char *params_string,
                                struct strbuf *errmsg)
  {
 -      const char *p = params;
 -      int p_len, ret = 0;
 +      char *params_copy = xstrdup(params_string);
 +      struct string_list params = STRING_LIST_INIT_NODUP;
 +      int ret = 0;
 +      int i;
  
 -      while (*p) {
 -              p_len = strchrnul(p, ',') - p;
 -              if (!memcmp(p, "changes", p_len)) {
 +      if (*params_copy)
 +              string_list_split_in_place(&params, params_copy, ',', -1);
 +      for (i = 0; i < params.nr; i++) {
 +              const char *p = params.items[i].string;
 +              if (!strcmp(p, "changes")) {
                        DIFF_OPT_CLR(options, DIRSTAT_BY_LINE);
                        DIFF_OPT_CLR(options, DIRSTAT_BY_FILE);
 -              } else if (!memcmp(p, "lines", p_len)) {
 +              } else if (!strcmp(p, "lines")) {
                        DIFF_OPT_SET(options, DIRSTAT_BY_LINE);
                        DIFF_OPT_CLR(options, DIRSTAT_BY_FILE);
 -              } else if (!memcmp(p, "files", p_len)) {
 +              } else if (!strcmp(p, "files")) {
                        DIFF_OPT_CLR(options, DIRSTAT_BY_LINE);
                        DIFF_OPT_SET(options, DIRSTAT_BY_FILE);
 -              } else if (!memcmp(p, "noncumulative", p_len)) {
 +              } else if (!strcmp(p, "noncumulative")) {
                        DIFF_OPT_CLR(options, DIRSTAT_CUMULATIVE);
 -              } else if (!memcmp(p, "cumulative", p_len)) {
 +              } else if (!strcmp(p, "cumulative")) {
                        DIFF_OPT_SET(options, DIRSTAT_CUMULATIVE);
                } else if (isdigit(*p)) {
                        char *end;
                                while (isdigit(*++end))
                                        ; /* nothing */
                        }
 -                      if (end - p == p_len)
 +                      if (!*end)
                                options->dirstat_permille = permille;
                        else {
 -                              strbuf_addf(errmsg, _("  Failed to parse dirstat cut-off percentage '%.*s'\n"),
 -                                          p_len, p);
 +                              strbuf_addf(errmsg, _("  Failed to parse dirstat cut-off percentage '%s'\n"),
 +                                          p);
                                ret++;
                        }
                } else {
 -                      strbuf_addf(errmsg, _("  Unknown dirstat parameter '%.*s'\n"),
 -                                  p_len, p);
 +                      strbuf_addf(errmsg, _("  Unknown dirstat parameter '%s'\n"), p);
                        ret++;
                }
  
 -              p += p_len;
 -
 -              if (*p)
 -                      p++; /* more parameters, swallow separator */
        }
 +      string_list_clear(&params, 0);
 +      free(params_copy);
        return ret;
  }
  
 +static int parse_submodule_params(struct diff_options *options, const char *value)
 +{
 +      if (!strcmp(value, "log"))
 +              DIFF_OPT_SET(options, SUBMODULE_LOG);
 +      else if (!strcmp(value, "short"))
 +              DIFF_OPT_CLR(options, SUBMODULE_LOG);
 +      else
 +              return -1;
 +      return 0;
 +}
 +
  static int git_config_rename(const char *var, const char *value)
  {
        if (!value)
@@@ -155,12 -141,6 +155,12 @@@ int git_diff_ui_config(const char *var
                diff_use_color_default = git_config_colorbool(var, value);
                return 0;
        }
 +      if (!strcmp(var, "diff.context")) {
 +              diff_context_default = git_config_int(var, value);
 +              if (diff_context_default < 0)
 +                      return -1;
 +              return 0;
 +      }
        if (!strcmp(var, "diff.renames")) {
                diff_detect_rename_default = git_config_rename(var, value);
                return 0;
        if (!strcmp(var, "diff.ignoresubmodules"))
                handle_ignore_submodules_arg(&default_diff_options, value);
  
 +      if (!strcmp(var, "diff.submodule")) {
 +              if (parse_submodule_params(&default_diff_options, value))
 +                      warning(_("Unknown value for 'diff.submodule' config variable: '%s'"),
 +                              value);
 +              return 0;
 +      }
 +
        if (git_color_config(var, value, cb) < 0)
                return -1;
  
@@@ -1327,7 -1300,6 +1327,7 @@@ struct diffstat_t 
                unsigned is_unmerged:1;
                unsigned is_binary:1;
                unsigned is_renamed:1;
 +              unsigned is_interesting:1;
                uintmax_t added, deleted;
        } **files;
  };
@@@ -1426,11 -1398,11 +1426,11 @@@ int print_stat_summary(FILE *fp, int fi
  
        if (!files) {
                assert(insertions == 0 && deletions == 0);
 -              return fputs(_(" 0 files changed\n"), fp);
 +              return fprintf(fp, "%s\n", " 0 files changed");
        }
  
        strbuf_addf(&sb,
 -                  Q_(" %d file changed", " %d files changed", files),
 +                  (files == 1) ? " %d file changed" : " %d files changed",
                    files);
  
        /*
                 * do not translate it.
                 */
                strbuf_addf(&sb,
 -                          Q_(", %d insertion(+)", ", %d insertions(+)",
 -                             insertions),
 +                          (insertions == 1) ? ", %d insertion(+)" : ", %d insertions(+)",
                            insertions);
        }
  
                 * do not translate it.
                 */
                strbuf_addf(&sb,
 -                          Q_(", %d deletion(-)", ", %d deletions(-)",
 -                             deletions),
 +                          (deletions == 1) ? ", %d deletion(-)" : ", %d deletions(-)",
                            deletions);
        }
        strbuf_addch(&sb, '\n');
@@@ -1497,8 -1471,8 +1497,8 @@@ static void show_stats(struct diffstat_
        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)) {
 +
 +              if (!file->is_interesting && (change == 0)) {
                        count++; /* not shown == room for one more */
                        continue;
                }
                if (max_change < change)
                        max_change = change;
        }
 -      count = i; /* min(count, data->nr) */
 +      count = i; /* where we can stop scanning in data->files[] */
  
        /*
         * We have width = stat_width or term_columns() columns total.
         */
        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;
 +              struct diffstat_file *file = data->files[i];
 +              char *name = file->print_name;
 +              uintmax_t added = file->added;
 +              uintmax_t deleted = file->deleted;
                int name_len;
  
 -              if (!data->files[i]->is_renamed &&
 -                       (added + deleted == 0)) {
 -                      total_files--;
 +              if (!file->is_interesting && (added + deleted == 0))
                        continue;
 -              }
 +
                /*
                 * "scale" the filename
                 */
                                name = slash;
                }
  
 -              if (data->files[i]->is_binary) {
 +              if (file->is_binary) {
                        fprintf(options->file, "%s", line_prefix);
                        show_name(options->file, prefix, name, len);
                        fprintf(options->file, " %*s", number_width, "Bin");
                        fprintf(options->file, "\n");
                        continue;
                }
 -              else if (data->files[i]->is_unmerged) {
 +              else if (file->is_unmerged) {
                        fprintf(options->file, "%s", line_prefix);
                        show_name(options->file, prefix, name, len);
                        fprintf(options->file, " Unmerged\n");
                 */
                add = added;
                del = deleted;
 -              adds += add;
 -              dels += del;
  
                if (graph_width <= max_change) {
                        int total = add + del;
                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)) {
 +
 +      for (i = 0; i < data->nr; i++) {
 +              struct diffstat_file *file = data->files[i];
 +              uintmax_t added = file->added;
 +              uintmax_t deleted = file->deleted;
 +
 +              if (file->is_unmerged ||
 +                  (!file->is_interesting && (added + deleted == 0))) {
                        total_files--;
                        continue;
                }
 -              adds += added;
 -              dels += deleted;
 +
 +              if (!file->is_binary) {
 +                      adds += added;
 +                      dels += deleted;
 +              }
 +              if (i < count)
 +                      continue;
                if (!extra_shown)
                        fprintf(options->file, "%s ...\n", line_prefix);
                extra_shown = 1;
@@@ -1728,8 -1697,9 +1728,8 @@@ static void show_shortstats(struct diff
                int added = data->files[i]->added;
                int deleted= data->files[i]->deleted;
  
 -              if (data->files[i]->is_unmerged)
 -                      continue;
 -              if (!data->files[i]->is_renamed && (added + deleted == 0)) {
 +              if (data->files[i]->is_unmerged ||
 +                  (!data->files[i]->is_interesting && (added + deleted == 0))) {
                        total_files--;
                } else if (!data->files[i]->is_binary) { /* don't count bytes */
                        adds += added;
@@@ -2245,7 -2215,7 +2245,7 @@@ static void builtin_diff(const char *na
        mmfile_t mf1, mf2;
        const char *lbl[2];
        char *a_one, *b_two;
 -      const char *set = diff_get_color_opt(o, DIFF_METAINFO);
 +      const char *meta = diff_get_color_opt(o, DIFF_METAINFO);
        const char *reset = diff_get_color_opt(o, DIFF_RESET);
        const char *a_prefix, *b_prefix;
        struct userdiff_driver *textconv_one = NULL;
                const char *add = diff_get_color_opt(o, DIFF_FILE_NEW);
                show_submodule_summary(o->file, one ? one->path : two->path,
                                one->sha1, two->sha1, two->dirty_submodule,
 -                              del, add, reset);
 +                              meta, del, add, reset);
                return;
        }
  
        b_two = quote_two(b_prefix, name_b + (*name_b == '/'));
        lbl[0] = DIFF_FILE_VALID(one) ? a_one : "/dev/null";
        lbl[1] = DIFF_FILE_VALID(two) ? b_two : "/dev/null";
 -      strbuf_addf(&header, "%s%sdiff --git %s %s%s\n", line_prefix, set, a_one, b_two, reset);
 +      strbuf_addf(&header, "%s%sdiff --git %s %s%s\n", line_prefix, meta, a_one, b_two, reset);
        if (lbl[0][0] == '/') {
                /* /dev/null */
 -              strbuf_addf(&header, "%s%snew file mode %06o%s\n", line_prefix, set, two->mode, reset);
 +              strbuf_addf(&header, "%s%snew file mode %06o%s\n", line_prefix, meta, two->mode, reset);
                if (xfrm_msg)
                        strbuf_addstr(&header, xfrm_msg);
                must_show_header = 1;
        }
        else if (lbl[1][0] == '/') {
 -              strbuf_addf(&header, "%s%sdeleted file mode %06o%s\n", line_prefix, set, one->mode, reset);
 +              strbuf_addf(&header, "%s%sdeleted file mode %06o%s\n", line_prefix, meta, one->mode, reset);
                if (xfrm_msg)
                        strbuf_addstr(&header, xfrm_msg);
                must_show_header = 1;
        }
        else {
                if (one->mode != two->mode) {
 -                      strbuf_addf(&header, "%s%sold mode %06o%s\n", line_prefix, set, one->mode, reset);
 -                      strbuf_addf(&header, "%s%snew mode %06o%s\n", line_prefix, set, two->mode, reset);
 +                      strbuf_addf(&header, "%s%sold mode %06o%s\n", line_prefix, meta, one->mode, reset);
 +                      strbuf_addf(&header, "%s%snew mode %06o%s\n", line_prefix, meta, two->mode, reset);
                        must_show_header = 1;
                }
                if (xfrm_msg)
@@@ -2429,20 -2399,13 +2429,20 @@@ static void builtin_diffstat(const cha
                             struct diff_filespec *two,
                             struct diffstat_t *diffstat,
                             struct diff_options *o,
 -                           int complete_rewrite)
 +                           struct diff_filepair *p)
  {
        mmfile_t mf1, mf2;
        struct diffstat_file *data;
        int same_contents;
 +      int complete_rewrite = 0;
 +
 +      if (!DIFF_PAIR_UNMERGED(p)) {
 +              if (p->status == DIFF_STATUS_MODIFIED && p->score)
 +                      complete_rewrite = 1;
 +      }
  
        data = diffstat_add(diffstat, name_a, name_b);
 +      data->is_interesting = p->status != DIFF_STATUS_UNKNOWN;
  
        if (!one || !two) {
                data->is_unmerged = 1;
@@@ -3153,10 -3116,11 +3153,10 @@@ static void run_diffstat(struct diff_fi
  {
        const char *name;
        const char *other;
 -      int complete_rewrite = 0;
  
        if (DIFF_PAIR_UNMERGED(p)) {
                /* unmerged */
 -              builtin_diffstat(p->one->path, NULL, NULL, NULL, diffstat, o, 0);
 +              builtin_diffstat(p->one->path, NULL, NULL, NULL, diffstat, o, p);
                return;
        }
  
        diff_fill_sha1_info(p->one);
        diff_fill_sha1_info(p->two);
  
 -      if (p->status == DIFF_STATUS_MODIFIED && p->score)
 -              complete_rewrite = 1;
 -      builtin_diffstat(name, other, p->one, p->two, diffstat, o, complete_rewrite);
 +      builtin_diffstat(name, other, p->one, p->two, diffstat, o, p);
  }
  
  static void run_checkdiff(struct diff_filepair *p, struct diff_options *o)
@@@ -3206,7 -3172,7 +3206,7 @@@ void diff_setup(struct diff_options *op
        options->break_opt = -1;
        options->rename_limit = -1;
        options->dirstat_permille = diff_dirstat_permille_default;
 -      options->context = 3;
 +      options->context = diff_context_default;
        DIFF_OPT_SET(options, RENAME_EMPTY);
  
        options->change = diff_change;
@@@ -3502,14 -3468,6 +3502,14 @@@ static int parse_dirstat_opt(struct dif
        return 1;
  }
  
 +static int parse_submodule_opt(struct diff_options *options, const char *value)
 +{
 +      if (parse_submodule_params(options, value))
 +              die(_("Failed to parse --submodule option parameter: '%s'"),
 +                      value);
 +      return 1;
 +}
 +
  int diff_opt_parse(struct diff_options *options, const char **av, int ac)
  {
        const char *arg = av[0];
                DIFF_OPT_SET(options, FIND_COPIES_HARDER);
        else if (!strcmp(arg, "--follow"))
                DIFF_OPT_SET(options, FOLLOW_RENAMES);
+       else if (!strcmp(arg, "--no-follow"))
+               DIFF_OPT_CLR(options, FOLLOW_RENAMES);
        else if (!strcmp(arg, "--color"))
                options->use_color = 1;
        else if (!prefixcmp(arg, "--color=")) {
                handle_ignore_submodules_arg(options, arg + 20);
        } else if (!strcmp(arg, "--submodule"))
                DIFF_OPT_SET(options, SUBMODULE_LOG);
 -      else if (!prefixcmp(arg, "--submodule=")) {
 -              if (!strcmp(arg + 12, "log"))
 -                      DIFF_OPT_SET(options, SUBMODULE_LOG);
 -      }
 +      else if (!prefixcmp(arg, "--submodule="))
 +              return parse_submodule_opt(options, arg + 12);
  
        /* misc options */
        else if (!strcmp(arg, "-z"))
@@@ -4913,19 -4875,3 +4915,19 @@@ size_t fill_textconv(struct userdiff_dr
  
        return size;
  }
 +
 +void setup_diff_pager(struct diff_options *opt)
 +{
 +      /*
 +       * If the user asked for our exit code, then either they want --quiet
 +       * or --exit-code. We should definitely not bother with a pager in the
 +       * former case, as we will generate no output. Since we still properly
 +       * report our exit code even when a pager is run, we _could_ run a
 +       * pager with --exit-code. But since we have not done so historically,
 +       * and because it is easy to find people oneline advising "git diff
 +       * --exit-code" in hooks and other scripts, we do not do so.
 +       */
 +      if (!DIFF_OPT_TST(opt, EXIT_WITH_STATUS) &&
 +          check_pager_config("diff") != 0)
 +              setup_pager();
 +}