Code

Merge branch 'jl/status-ignore-submodules'
authorJunio C Hamano <gitster@pobox.com>
Wed, 30 Jun 2010 18:55:39 +0000 (11:55 -0700)
committerJunio C Hamano <gitster@pobox.com>
Wed, 30 Jun 2010 18:55:39 +0000 (11:55 -0700)
* jl/status-ignore-submodules:
  Add the option "--ignore-submodules" to "git status"
  git submodule: ignore dirty submodules for summary and status

Conflicts:
builtin/commit.c
t/t7508-status.sh
wt-status.c
wt-status.h

1  2 
Documentation/git-status.txt
builtin/commit.c
diff.c
git-submodule.sh
t/t7508-status.sh
wt-status.c
wt-status.h

index fd0fe7cb56cf3ba163cef467f5fc9c35b0ea963b,a617ce746c19efe6c5efe371974cc995b1ea458a..2fd054c1040ce43949382e4744a0d7a814a8d9bd
@@@ -27,10 -27,6 +27,10 @@@ OPTION
  --short::
        Give the output in the short-format.
  
 +-b::
 +--branch::
 +      Show the branch and tracking info even in short-format.
 +
  --porcelain::
        Give the output in a stable, easy-to-parse format for scripts.
        Currently this is identical to --short output, but is guaranteed
@@@ -53,6 -49,17 +53,17 @@@ See linkgit:git-config[1] for configura
  used to change the default for when the option is not
  specified.
  
+ --ignore-submodules[=<when>]::
+       Ignore changes to submodules when looking for changes. <when> can be
+       either "untracked", "dirty" or "all", which is the default. When
+       "untracked" is used submodules are not considered dirty when they only
+       contain untracked content (but they are still scanned for modified
+       content). Using "dirty" ignores all changes to the work tree of submodules,
+       only changes to the commits stored in the superproject are shown (this was
+       the behavior before 1.7.0). Using "all" hides all changes to submodules
+       (and suppresses the output of submodule summaries when the config option
+       `status.submodulesummary` is set).
  -z::
        Terminate entries with NUL, instead of LF.  This implies
        the `--porcelain` output format if no other format is given.
@@@ -124,10 -131,6 +135,10 @@@ Ignored files are not listed
      ?           ?    untracked
      -------------------------------------------------
  
 +If -b is used the short-format status is preceded by a line
 +
 +## branchname tracking info
 +
  There is an alternate -z format recommended for machine parsing.  In
  that format, the status field is the same, but some other things
  change.  First, the '->' is omitted from rename entries and the field
@@@ -136,7 -139,7 +147,7 @@@ order is reversed (e.g 'from -> to' bec
  and the terminating newline (but a space still separates the status
  field from the first filename).  Third, filenames containing special
  characters are not specially formatted; no quoting or
 -backslash-escaping is performed.
 +backslash-escaping is performed. Fourth, there is no branch line.
  
  CONFIGURATION
  -------------
diff --combined builtin/commit.c
index c6b053a508facc2029df489d069e0667cfeb28fe,01817509262138bc453de401f63913d61f5b47d0..c101f006f6b5d7d185aa97081fb9717de92b09cd
@@@ -48,11 -48,6 +48,11 @@@ static const char implicit_ident_advice
  "\n"
  "    git commit --amend --author='Your Name <you@example.com>'\n";
  
 +static const char empty_amend_advice[] =
 +"You asked to amend the most recent commit, but doing so would make\n"
 +"it empty. You can repeat your command with --allow-empty, or you can\n"
 +"remove the commit entirely with \"git reset HEAD^\".\n";
 +
  static unsigned char head_sha1[20];
  
  static char *use_message_buffer;
@@@ -62,7 -57,7 +62,7 @@@ static struct lock_file false_lock; /* 
  static enum {
        COMMIT_AS_IS = 1,
        COMMIT_NORMAL,
 -      COMMIT_PARTIAL,
 +      COMMIT_PARTIAL
  } commit_style;
  
  static const char *logfile, *force_author;
@@@ -71,8 -66,8 +71,8 @@@ static char *edit_message, *use_message
  static char *author_name, *author_email, *author_date;
  static int all, edit_flag, also, interactive, only, amend, signoff;
  static int quiet, verbose, no_verify, allow_empty, dry_run, renew_authorship;
 -static int no_post_rewrite;
 +static int no_post_rewrite, allow_empty_message;
- static char *untracked_files_arg, *force_date;
+ static char *untracked_files_arg, *force_date, *ignore_submodule_arg;
  /*
   * The default commit message cleanup mode will remove the lines
   * beginning with # (shell comments) and leading and trailing
  static enum {
        CLEANUP_SPACE,
        CLEANUP_NONE,
 -      CLEANUP_ALL,
 +      CLEANUP_ALL
  } cleanup_mode;
  static char *cleanup_arg;
  
  static int use_editor = 1, initial_commit, in_merge, include_status = 1;
 +static int show_ignored_in_status;
  static const char *only_include_assumed;
  static struct strbuf message;
  
@@@ -96,9 -90,8 +96,9 @@@ static int null_termination
  static enum {
        STATUS_FORMAT_LONG,
        STATUS_FORMAT_SHORT,
 -      STATUS_FORMAT_PORCELAIN,
 +      STATUS_FORMAT_PORCELAIN
  } status_format = STATUS_FORMAT_LONG;
 +static int status_show_branch;
  
  static int opt_parse_m(const struct option *opt, const char *arg, int unset)
  {
@@@ -140,7 -133,6 +140,7 @@@ static struct option builtin_commit_opt
        OPT_BOOLEAN(0, "dry-run", &dry_run, "show what would be committed"),
        OPT_SET_INT(0, "short", &status_format, "show status concisely",
                    STATUS_FORMAT_SHORT),
 +      OPT_BOOLEAN(0, "branch", &status_show_branch, "show branch information"),
        OPT_SET_INT(0, "porcelain", &status_format,
                    "show porcelain output format", STATUS_FORMAT_PORCELAIN),
        OPT_BOOLEAN('z', "null", &null_termination,
        OPT_BOOLEAN(0, "amend", &amend, "amend previous commit"),
        OPT_BOOLEAN(0, "no-post-rewrite", &no_post_rewrite, "bypass post-rewrite hook"),
        { OPTION_STRING, 'u', "untracked-files", &untracked_files_arg, "mode", "show untracked files, optional modes: all, normal, no. (Default: all)", PARSE_OPT_OPTARG, NULL, (intptr_t)"all" },
 -      OPT_BOOLEAN(0, "allow-empty", &allow_empty, "ok to record an empty change"),
        /* end commit contents options */
  
 +      { OPTION_BOOLEAN, 0, "allow-empty", &allow_empty, NULL,
 +        "ok to record an empty change",
 +        PARSE_OPT_NOARG | PARSE_OPT_HIDDEN },
 +      { OPTION_BOOLEAN, 0, "allow-empty-message", &allow_empty_message, NULL,
 +        "ok to record a change with an empty message",
 +        PARSE_OPT_NOARG | PARSE_OPT_HIDDEN },
 +
        OPT_END()
  };
  
@@@ -219,7 -205,7 +219,7 @@@ static int list_paths(struct string_lis
                        continue;
                if (!match_pathspec(pattern, ce->name, ce_namelen(ce), 0, m))
                        continue;
 -              item = string_list_insert(ce->name, list);
 +              item = string_list_insert(list, ce->name);
                if (ce_skip_worktree(ce))
                        item->util = item; /* better a valid pointer than a fake one */
        }
@@@ -431,7 -417,7 +431,7 @@@ static int run_status(FILE *fp, const c
  
        switch (status_format) {
        case STATUS_FORMAT_SHORT:
 -              wt_shortstatus_print(s, null_termination);
 +              wt_shortstatus_print(s, null_termination, status_show_branch);
                break;
        case STATUS_FORMAT_PORCELAIN:
                wt_porcelain_print(s, null_termination);
@@@ -469,21 -455,15 +469,21 @@@ static void determine_author_info(void
                if (!a)
                        die("invalid commit: %s", use_message);
  
 -              lb = strstr(a + 8, " <");
 -              rb = strstr(a + 8, "> ");
 -              eol = strchr(a + 8, '\n');
 -              if (!lb || !rb || !eol)
 +              lb = strchrnul(a + strlen("\nauthor "), '<');
 +              rb = strchrnul(lb, '>');
 +              eol = strchrnul(rb, '\n');
 +              if (!*lb || !*rb || !*eol)
                        die("invalid commit: %s", use_message);
  
 -              name = xstrndup(a + 8, lb - (a + 8));
 -              email = xstrndup(lb + 2, rb - (lb + 2));
 -              date = xstrndup(rb + 2, eol - (rb + 2));
 +              if (lb == a + strlen("\nauthor "))
 +                      /* \nauthor <foo@example.com> */
 +                      name = xcalloc(1, 1);
 +              else
 +                      name = xmemdupz(a + strlen("\nauthor "),
 +                                      (lb - strlen(" ") -
 +                                       (a + strlen("\nauthor "))));
 +              email = xmemdupz(lb + strlen("<"), rb - (lb + strlen("<")));
 +              date = xmemdupz(rb + strlen("> "), eol - (rb + strlen("> ")));
        }
  
        if (force_author) {
@@@ -713,8 -693,6 +713,8 @@@ static int prepare_to_commit(const cha
        if (!commitable && !in_merge && !allow_empty &&
            !(amend && is_a_merge(head_sha1))) {
                run_status(stdout, index_file, prefix, 0, s);
 +              if (amend)
 +                      fputs(empty_amend_advice, stderr);
                return 0;
        }
  
  
        if (use_editor) {
                char index[PATH_MAX];
 -              const char *env[2] = { index, NULL };
 +              const char *env[2] = { NULL };
 +              env[0] =  index;
                snprintf(index, sizeof(index), "GIT_INDEX_FILE=%s", index_file);
                if (launch_editor(git_path(commit_editmsg), NULL, env)) {
                        fprintf(stderr,
@@@ -1040,14 -1017,11 +1040,14 @@@ static int git_status_config(const cha
  int cmd_status(int argc, const char **argv, const char *prefix)
  {
        struct wt_status s;
 +      int fd;
        unsigned char sha1[20];
        static struct option builtin_status_options[] = {
                OPT__VERBOSE(&verbose),
                OPT_SET_INT('s', "short", &status_format,
                            "show status concisely", STATUS_FORMAT_SHORT),
 +              OPT_BOOLEAN('b', "branch", &status_show_branch,
 +                          "show branch information"),
                OPT_SET_INT(0, "porcelain", &status_format,
                            "show porcelain output format",
                            STATUS_FORMAT_PORCELAIN),
                  "mode",
                  "show untracked files, optional modes: all, normal, no. (Default: all)",
                  PARSE_OPT_OPTARG, NULL, (intptr_t)"all" },
 +              OPT_BOOLEAN(0, "ignored", &show_ignored_in_status,
 +                          "show ignored files"),
+               { OPTION_STRING, 0, "ignore-submodules", &ignore_submodule_arg, "when",
+                 "ignore changes to submodules, optional when: all, dirty, untracked. (Default: all)",
+                 PARSE_OPT_OPTARG, NULL, (intptr_t)"all" },
                OPT_END(),
        };
  
                             builtin_status_options,
                             builtin_status_usage, 0);
        handle_untracked_files_arg(&s);
 -
 +      if (show_ignored_in_status)
 +              s.show_ignored_files = 1;
        if (*argv)
                s.pathspec = get_pathspec(prefix, argv);
  
        read_cache_preload(s.pathspec);
        refresh_index(&the_index, REFRESH_QUIET|REFRESH_UNMERGED, s.pathspec, NULL, NULL);
 +
 +      fd = hold_locked_index(&index_lock, 0);
 +      if (0 <= fd) {
 +              if (!write_cache(fd, active_cache, active_nr))
 +                      commit_locked_index(&index_lock);
 +              rollback_lock_file(&index_lock);
 +      }
 +
        s.is_initial = get_sha1(s.reference, sha1) ? 1 : 0;
        s.in_merge = in_merge;
+       s.ignore_submodule_arg = ignore_submodule_arg;
        wt_status_collect(&s);
  
        if (s.relative_paths)
  
        switch (status_format) {
        case STATUS_FORMAT_SHORT:
 -              wt_shortstatus_print(&s, null_termination);
 +              wt_shortstatus_print(&s, null_termination, status_show_branch);
                break;
        case STATUS_FORMAT_PORCELAIN:
                wt_porcelain_print(&s, null_termination);
                break;
        case STATUS_FORMAT_LONG:
                s.verbose = verbose;
+               s.ignore_submodule_arg = ignore_submodule_arg;
                wt_status_print(&s);
                break;
        }
@@@ -1175,11 -1143,13 +1180,11 @@@ static void print_summary(const char *p
                initial_commit ? " (root-commit)" : "");
  
        if (!log_tree_commit(&rev, commit)) {
 -              struct pretty_print_context ctx = {0};
 -              struct strbuf buf = STRBUF_INIT;
 -              ctx.date_mode = DATE_NORMAL;
 -              format_commit_message(commit, format.buf + 7, &buf, &ctx);
 -              printf("%s\n", buf.buf);
 -              strbuf_release(&buf);
 +              rev.always_show_header = 1;
 +              rev.use_terminator = 1;
 +              log_tree_commit(&rev, commit);
        }
 +
        strbuf_release(&format);
  }
  
@@@ -1267,16 -1237,13 +1272,16 @@@ int cmd_commit(int argc, const char **a
        }
  
        /* Determine parents */
 +      reflog_msg = getenv("GIT_REFLOG_ACTION");
        if (initial_commit) {
 -              reflog_msg = "commit (initial)";
 +              if (!reflog_msg)
 +                      reflog_msg = "commit (initial)";
        } else if (amend) {
                struct commit_list *c;
                struct commit *commit;
  
 -              reflog_msg = "commit (amend)";
 +              if (!reflog_msg)
 +                      reflog_msg = "commit (amend)";
                commit = lookup_commit(head_sha1);
                if (!commit || parse_commit(commit))
                        die("could not parse HEAD commit");
                struct strbuf m = STRBUF_INIT;
                FILE *fp;
  
 -              reflog_msg = "commit (merge)";
 +              if (!reflog_msg)
 +                      reflog_msg = "commit (merge)";
                pptr = &commit_list_insert(lookup_commit(head_sha1), pptr)->next;
                fp = fopen(git_path("MERGE_HEAD"), "r");
                if (fp == NULL)
                if (allow_fast_forward)
                        parents = reduce_heads(parents);
        } else {
 -              reflog_msg = "commit";
 +              if (!reflog_msg)
 +                      reflog_msg = "commit";
                pptr = &commit_list_insert(lookup_commit(head_sha1), pptr)->next;
        }
  
  
        if (cleanup_mode != CLEANUP_NONE)
                stripspace(&sb, cleanup_mode == CLEANUP_ALL);
 -      if (message_is_empty(&sb)) {
 +      if (message_is_empty(&sb) && !allow_empty_message) {
                rollback_index_files();
                fprintf(stderr, "Aborting commit due to empty commit message.\n");
                exit(1);
diff --combined diff.c
index 9d70f9d731325287dd7e9f83e5e3b44ad1dfb8d9,36eae91a757788c254c7822052f735f6192ac187..3aa695df62d2f13beed4b4ac54e3ea568194ae02
--- 1/diff.c
--- 2/diff.c
+++ b/diff.c
@@@ -30,7 -30,6 +30,7 @@@ static const char *diff_word_regex_cfg
  static const char *external_diff_cmd_cfg;
  int diff_auto_refresh_index = 1;
  static int diff_mnemonic_prefix;
 +static int diff_no_prefix;
  
  static char diff_colors[][COLOR_MAXLEN] = {
        GIT_COLOR_RESET,
@@@ -44,6 -43,9 +44,6 @@@
        GIT_COLOR_NORMAL,       /* FUNCINFO */
  };
  
 -static void diff_filespec_load_driver(struct diff_filespec *one);
 -static char *run_textconv(const char *, struct diff_filespec *, size_t *);
 -
  static int parse_diff_color_slot(const char *var, int ofs)
  {
        if (!strcasecmp(var+ofs, "plain"))
@@@ -98,10 -100,6 +98,10 @@@ int git_diff_ui_config(const char *var
                diff_mnemonic_prefix = git_config_bool(var, value);
                return 0;
        }
 +      if (!strcmp(var, "diff.noprefix")) {
 +              diff_no_prefix = git_config_bool(var, value);
 +              return 0;
 +      }
        if (!strcmp(var, "diff.external"))
                return git_config_string(&external_diff_cmd_cfg, var, value);
        if (!strcmp(var, "diff.wordregex"))
@@@ -195,8 -193,8 +195,8 @@@ struct emit_callback 
        sane_truncate_fn truncate;
        const char **label_path;
        struct diff_words_data *diff_words;
 +      struct diff_options *opt;
        int *found_changesp;
 -      FILE *file;
        struct strbuf *header;
  };
  
@@@ -283,19 -281,11 +283,19 @@@ static void check_blank_at_eof(mmfile_
        ecbdata->blank_at_eof_in_postimage = (at - l2) + 1;
  }
  
 -static void emit_line_0(FILE *file, const char *set, const char *reset,
 +static void emit_line_0(struct diff_options *o, const char *set, const char *reset,
                        int first, const char *line, int len)
  {
        int has_trailing_newline, has_trailing_carriage_return;
        int nofirst;
 +      FILE *file = o->file;
 +
 +      if (o->output_prefix) {
 +              struct strbuf *msg = NULL;
 +              msg = o->output_prefix(o, o->output_prefix_data);
 +              assert(msg);
 +              fwrite(msg->buf, msg->len, 1, file);
 +      }
  
        if (len == 0) {
                has_trailing_newline = (first == '\n');
                fputc('\n', file);
  }
  
 -static void emit_line(FILE *file, const char *set, const char *reset,
 +static void emit_line(struct diff_options *o, const char *set, const char *reset,
                      const char *line, int len)
  {
 -      emit_line_0(file, set, reset, line[0], line+1, len-1);
 +      emit_line_0(o, set, reset, line[0], line+1, len-1);
  }
  
  static int new_blank_line_at_eof(struct emit_callback *ecbdata, const char *line, int len)
@@@ -350,15 -340,15 +350,15 @@@ static void emit_add_line(const char *r
        const char *set = diff_get_color(ecbdata->color_diff, DIFF_FILE_NEW);
  
        if (!*ws)
 -              emit_line_0(ecbdata->file, set, reset, '+', line, len);
 +              emit_line_0(ecbdata->opt, set, reset, '+', line, len);
        else if (new_blank_line_at_eof(ecbdata, line, len))
                /* Blank line at EOF - paint '+' as well */
 -              emit_line_0(ecbdata->file, ws, reset, '+', line, len);
 +              emit_line_0(ecbdata->opt, ws, reset, '+', line, len);
        else {
                /* Emit just the prefix, then the rest. */
 -              emit_line_0(ecbdata->file, set, reset, '+', "", 0);
 +              emit_line_0(ecbdata->opt, set, reset, '+', "", 0);
                ws_check_emit(line, len, ecbdata->ws_rule,
 -                            ecbdata->file, set, reset, ws);
 +                            ecbdata->opt->file, set, reset, ws);
        }
  }
  
@@@ -371,9 -361,6 +371,9 @@@ static void emit_hunk_header(struct emi
        const char *reset = diff_get_color(ecbdata->color_diff, DIFF_RESET);
        static const char atat[2] = { '@', '@' };
        const char *cp, *ep;
 +      struct strbuf msgbuf = STRBUF_INIT;
 +      int org_len = len;
 +      int i = 1;
  
        /*
         * As a hunk header must begin with "@@ -<old>, +<new> @@",
        if (len < 10 ||
            memcmp(line, atat, 2) ||
            !(ep = memmem(line + 2, len - 2, atat, 2))) {
 -              emit_line(ecbdata->file, plain, reset, line, len);
 +              emit_line(ecbdata->opt, plain, reset, line, len);
                return;
        }
        ep += 2; /* skip over @@ */
  
        /* The hunk header in fraginfo color */
 -      emit_line(ecbdata->file, frag, reset, line, ep - line);
 +      strbuf_add(&msgbuf, frag, strlen(frag));
 +      strbuf_add(&msgbuf, line, ep - line);
 +      strbuf_add(&msgbuf, reset, strlen(reset));
 +
 +      /*
 +       * trailing "\r\n"
 +       */
 +      for ( ; i < 3; i++)
 +              if (line[len - i] == '\r' || line[len - i] == '\n')
 +                      len--;
  
        /* blank before the func header */
        for (cp = ep; ep - line < len; ep++)
                if (*ep != ' ' && *ep != '\t')
                        break;
 -      if (ep != cp)
 -              emit_line(ecbdata->file, plain, reset, cp, ep - cp);
 +      if (ep != cp) {
 +              strbuf_add(&msgbuf, plain, strlen(plain));
 +              strbuf_add(&msgbuf, cp, ep - cp);
 +              strbuf_add(&msgbuf, reset, strlen(reset));
 +      }
 +
 +      if (ep < line + len) {
 +              strbuf_add(&msgbuf, func, strlen(func));
 +              strbuf_add(&msgbuf, ep, line + len - ep);
 +              strbuf_add(&msgbuf, reset, strlen(reset));
 +      }
  
 -      if (ep < line + len)
 -              emit_line(ecbdata->file, func, reset, ep, line + len - ep);
 +      strbuf_add(&msgbuf, line + len, org_len - len);
 +      emit_line(ecbdata->opt, "", "", msgbuf.buf, msgbuf.len);
 +      strbuf_release(&msgbuf);
  }
  
  static struct diff_tempfile *claim_diff_tempfile(void) {
@@@ -477,7 -445,7 +477,7 @@@ static void emit_rewrite_lines(struct e
                len = endp ? (endp - data + 1) : size;
                if (prefix != '+') {
                        ecb->lno_in_preimage++;
 -                      emit_line_0(ecb->file, old, reset, '-',
 +                      emit_line_0(ecb->opt, old, reset, '-',
                                    data, len);
                } else {
                        ecb->lno_in_postimage++;
        if (!endp) {
                const char *plain = diff_get_color(ecb->color_diff,
                                                   DIFF_PLAIN);
 -              emit_line_0(ecb->file, plain, reset, '\\',
 +              emit_line_0(ecb->opt, plain, reset, '\\',
                            nneof, strlen(nneof));
        }
  }
@@@ -498,8 -466,8 +498,8 @@@ static void emit_rewrite_diff(const cha
                              const char *name_b,
                              struct diff_filespec *one,
                              struct diff_filespec *two,
 -                            const char *textconv_one,
 -                            const char *textconv_two,
 +                            struct userdiff_driver *textconv_one,
 +                            struct userdiff_driver *textconv_two,
                              struct diff_options *o)
  {
        int lc_a, lc_b;
        const char *reset = diff_get_color(color_diff, DIFF_RESET);
        static struct strbuf a_name = STRBUF_INIT, b_name = STRBUF_INIT;
        const char *a_prefix, *b_prefix;
 -      const char *data_one, *data_two;
 +      char *data_one, *data_two;
        size_t size_one, size_two;
        struct emit_callback ecbdata;
 +      char *line_prefix = "";
 +      struct strbuf *msgbuf;
 +
 +      if (o && o->output_prefix) {
 +              msgbuf = o->output_prefix(o, o->output_prefix_data);
 +              line_prefix = msgbuf->buf;
 +      }
  
        if (diff_mnemonic_prefix && DIFF_OPT_TST(o, REVERSE_DIFF)) {
                a_prefix = o->b_prefix;
        quote_two_c_style(&a_name, a_prefix, name_a, 0);
        quote_two_c_style(&b_name, b_prefix, name_b, 0);
  
 -      diff_populate_filespec(one, 0);
 -      diff_populate_filespec(two, 0);
 -      if (textconv_one) {
 -              data_one = run_textconv(textconv_one, one, &size_one);
 -              if (!data_one)
 -                      die("unable to read files to diff");
 -      }
 -      else {
 -              data_one = one->data;
 -              size_one = one->size;
 -      }
 -      if (textconv_two) {
 -              data_two = run_textconv(textconv_two, two, &size_two);
 -              if (!data_two)
 -                      die("unable to read files to diff");
 -      }
 -      else {
 -              data_two = two->data;
 -              size_two = two->size;
 -      }
 +      size_one = fill_textconv(textconv_one, one, &data_one);
 +      size_two = fill_textconv(textconv_two, two, &data_two);
  
        memset(&ecbdata, 0, sizeof(ecbdata));
        ecbdata.color_diff = color_diff;
        ecbdata.found_changesp = &o->found_changes;
        ecbdata.ws_rule = whitespace_rule(name_b ? name_b : name_a);
 -      ecbdata.file = o->file;
 +      ecbdata.opt = o;
        if (ecbdata.ws_rule & WS_BLANK_AT_EOF) {
                mmfile_t mf1, mf2;
                mf1.ptr = (char *)data_one;
        lc_a = count_lines(data_one, size_one);
        lc_b = count_lines(data_two, size_two);
        fprintf(o->file,
 -              "%s--- %s%s%s\n%s+++ %s%s%s\n%s@@ -",
 -              metainfo, a_name.buf, name_a_tab, reset,
 -              metainfo, b_name.buf, name_b_tab, reset, fraginfo);
 +              "%s%s--- %s%s%s\n%s%s+++ %s%s%s\n%s%s@@ -",
 +              line_prefix, metainfo, a_name.buf, name_a_tab, reset,
 +              line_prefix, metainfo, b_name.buf, name_b_tab, reset,
 +              line_prefix, fraginfo);
        print_line_count(o->file, lc_a);
        fprintf(o->file, " +");
        print_line_count(o->file, lc_b);
@@@ -599,134 -577,23 +599,134 @@@ static void diff_words_append(char *lin
        buffer->text.ptr[buffer->text.size] = '\0';
  }
  
 +struct diff_words_style_elem
 +{
 +      const char *prefix;
 +      const char *suffix;
 +      const char *color; /* NULL; filled in by the setup code if
 +                          * color is enabled */
 +};
 +
 +struct diff_words_style
 +{
 +      enum diff_words_type type;
 +      struct diff_words_style_elem new, old, ctx;
 +      const char *newline;
 +};
 +
 +struct diff_words_style diff_words_styles[] = {
 +      { DIFF_WORDS_PORCELAIN, {"+", "\n"}, {"-", "\n"}, {" ", "\n"}, "~\n" },
 +      { DIFF_WORDS_PLAIN, {"{+", "+}"}, {"[-", "-]"}, {"", ""}, "\n" },
 +      { DIFF_WORDS_COLOR, {"", ""}, {"", ""}, {"", ""}, "\n" }
 +};
 +
  struct diff_words_data {
        struct diff_words_buffer minus, plus;
        const char *current_plus;
 -      FILE *file;
 +      int last_minus;
 +      struct diff_options *opt;
        regex_t *word_regex;
 +      enum diff_words_type type;
 +      struct diff_words_style *style;
  };
  
 +static int fn_out_diff_words_write_helper(FILE *fp,
 +                                        struct diff_words_style_elem *st_el,
 +                                        const char *newline,
 +                                        size_t count, const char *buf,
 +                                        const char *line_prefix)
 +{
 +      int print = 0;
 +
 +      while (count) {
 +              char *p = memchr(buf, '\n', count);
 +              if (print)
 +                      fputs(line_prefix, fp);
 +              if (p != buf) {
 +                      if (st_el->color && fputs(st_el->color, fp) < 0)
 +                              return -1;
 +                      if (fputs(st_el->prefix, fp) < 0 ||
 +                          fwrite(buf, p ? p - buf : count, 1, fp) != 1 ||
 +                          fputs(st_el->suffix, fp) < 0)
 +                              return -1;
 +                      if (st_el->color && *st_el->color
 +                          && fputs(GIT_COLOR_RESET, fp) < 0)
 +                              return -1;
 +              }
 +              if (!p)
 +                      return 0;
 +              if (fputs(newline, fp) < 0)
 +                      return -1;
 +              count -= p + 1 - buf;
 +              buf = p + 1;
 +              print = 1;
 +      }
 +      return 0;
 +}
 +
 +/*
 + * '--color-words' algorithm can be described as:
 + *
 + *   1. collect a the minus/plus lines of a diff hunk, divided into
 + *      minus-lines and plus-lines;
 + *
 + *   2. break both minus-lines and plus-lines into words and
 + *      place them into two mmfile_t with one word for each line;
 + *
 + *   3. use xdiff to run diff on the two mmfile_t to get the words level diff;
 + *
 + * And for the common parts of the both file, we output the plus side text.
 + * diff_words->current_plus is used to trace the current position of the plus file
 + * which printed. diff_words->last_minus is used to trace the last minus word
 + * printed.
 + *
 + * For '--graph' to work with '--color-words', we need to output the graph prefix
 + * on each line of color words output. Generally, there are two conditions on
 + * which we should output the prefix.
 + *
 + *   1. diff_words->last_minus == 0 &&
 + *      diff_words->current_plus == diff_words->plus.text.ptr
 + *
 + *      that is: the plus text must start as a new line, and if there is no minus
 + *      word printed, a graph prefix must be printed.
 + *
 + *   2. diff_words->current_plus > diff_words->plus.text.ptr &&
 + *      *(diff_words->current_plus - 1) == '\n'
 + *
 + *      that is: a graph prefix must be printed following a '\n'
 + */
 +static int color_words_output_graph_prefix(struct diff_words_data *diff_words)
 +{
 +      if ((diff_words->last_minus == 0 &&
 +              diff_words->current_plus == diff_words->plus.text.ptr) ||
 +              (diff_words->current_plus > diff_words->plus.text.ptr &&
 +              *(diff_words->current_plus - 1) == '\n')) {
 +              return 1;
 +      } else {
 +              return 0;
 +      }
 +}
 +
  static void fn_out_diff_words_aux(void *priv, char *line, unsigned long len)
  {
        struct diff_words_data *diff_words = priv;
 +      struct diff_words_style *style = diff_words->style;
        int minus_first, minus_len, plus_first, plus_len;
        const char *minus_begin, *minus_end, *plus_begin, *plus_end;
 +      struct diff_options *opt = diff_words->opt;
 +      struct strbuf *msgbuf;
 +      char *line_prefix = "";
  
        if (line[0] != '@' || parse_hunk_header(line, len,
                        &minus_first, &minus_len, &plus_first, &plus_len))
                return;
  
 +      assert(opt);
 +      if (opt->output_prefix) {
 +              msgbuf = opt->output_prefix(opt, opt->output_prefix_data);
 +              line_prefix = msgbuf->buf;
 +      }
 +
        /* POSIX requires that first be decremented by one if len == 0... */
        if (minus_len) {
                minus_begin = diff_words->minus.orig[minus_first].begin;
        } else
                plus_begin = plus_end = diff_words->plus.orig[plus_first].end;
  
 -      if (diff_words->current_plus != plus_begin)
 -              fwrite(diff_words->current_plus,
 -                              plus_begin - diff_words->current_plus, 1,
 -                              diff_words->file);
 -      if (minus_begin != minus_end)
 -              color_fwrite_lines(diff_words->file,
 -                              diff_get_color(1, DIFF_FILE_OLD),
 -                              minus_end - minus_begin, minus_begin);
 -      if (plus_begin != plus_end)
 -              color_fwrite_lines(diff_words->file,
 -                              diff_get_color(1, DIFF_FILE_NEW),
 -                              plus_end - plus_begin, plus_begin);
 +      if (color_words_output_graph_prefix(diff_words)) {
 +              fputs(line_prefix, diff_words->opt->file);
 +      }
 +      if (diff_words->current_plus != plus_begin) {
 +              fn_out_diff_words_write_helper(diff_words->opt->file,
 +                              &style->ctx, style->newline,
 +                              plus_begin - diff_words->current_plus,
 +                              diff_words->current_plus, line_prefix);
 +              if (*(plus_begin - 1) == '\n')
 +                      fputs(line_prefix, diff_words->opt->file);
 +      }
 +      if (minus_begin != minus_end) {
 +              fn_out_diff_words_write_helper(diff_words->opt->file,
 +                              &style->old, style->newline,
 +                              minus_end - minus_begin, minus_begin,
 +                              line_prefix);
 +      }
 +      if (plus_begin != plus_end) {
 +              fn_out_diff_words_write_helper(diff_words->opt->file,
 +                              &style->new, style->newline,
 +                              plus_end - plus_begin, plus_begin,
 +                              line_prefix);
 +      }
  
        diff_words->current_plus = plus_end;
 +      diff_words->last_minus = minus_first;
  }
  
  /* This function starts looking at *begin, and returns 0 iff a word was found. */
@@@ -846,37 -701,23 +846,37 @@@ static void diff_words_show(struct diff
        xpparam_t xpp;
        xdemitconf_t xecfg;
        mmfile_t minus, plus;
 +      struct diff_words_style *style = diff_words->style;
 +
 +      struct diff_options *opt = diff_words->opt;
 +      struct strbuf *msgbuf;
 +      char *line_prefix = "";
 +
 +      assert(opt);
 +      if (opt->output_prefix) {
 +              msgbuf = opt->output_prefix(opt, opt->output_prefix_data);
 +              line_prefix = msgbuf->buf;
 +      }
  
        /* special case: only removal */
        if (!diff_words->plus.text.size) {
 -              color_fwrite_lines(diff_words->file,
 -                      diff_get_color(1, DIFF_FILE_OLD),
 -                      diff_words->minus.text.size, diff_words->minus.text.ptr);
 +              fputs(line_prefix, diff_words->opt->file);
 +              fn_out_diff_words_write_helper(diff_words->opt->file,
 +                      &style->old, style->newline,
 +                      diff_words->minus.text.size,
 +                      diff_words->minus.text.ptr, line_prefix);
                diff_words->minus.text.size = 0;
                return;
        }
  
        diff_words->current_plus = diff_words->plus.text.ptr;
 +      diff_words->last_minus = 0;
  
        memset(&xpp, 0, sizeof(xpp));
        memset(&xecfg, 0, sizeof(xecfg));
        diff_words_fill(&diff_words->minus, &minus, diff_words->word_regex);
        diff_words_fill(&diff_words->plus, &plus, diff_words->word_regex);
 -      xpp.flags = XDF_NEED_MINIMAL;
 +      xpp.flags = 0;
        /* as only the hunk header will be parsed, we need a 0-context */
        xecfg.ctxlen = 0;
        xdi_diff_outf(&minus, &plus, fn_out_diff_words_aux, diff_words,
        free(minus.ptr);
        free(plus.ptr);
        if (diff_words->current_plus != diff_words->plus.text.ptr +
 -                      diff_words->plus.text.size)
 -              fwrite(diff_words->current_plus,
 +                      diff_words->plus.text.size) {
 +              if (color_words_output_graph_prefix(diff_words))
 +                      fputs(line_prefix, diff_words->opt->file);
 +              fn_out_diff_words_write_helper(diff_words->opt->file,
 +                      &style->ctx, style->newline,
                        diff_words->plus.text.ptr + diff_words->plus.text.size
 -                      - diff_words->current_plus, 1,
 -                      diff_words->file);
 +                      - diff_words->current_plus, diff_words->current_plus,
 +                      line_prefix);
 +      }
        diff_words->minus.text.size = diff_words->plus.text.size = 0;
  }
  
@@@ -964,17 -801,9 +964,17 @@@ static void fn_out_consume(void *priv, 
        const char *meta = diff_get_color(ecbdata->color_diff, DIFF_METAINFO);
        const char *plain = diff_get_color(ecbdata->color_diff, DIFF_PLAIN);
        const char *reset = diff_get_color(ecbdata->color_diff, DIFF_RESET);
 +      struct diff_options *o = ecbdata->opt;
 +      char *line_prefix = "";
 +      struct strbuf *msgbuf;
 +
 +      if (o && o->output_prefix) {
 +              msgbuf = o->output_prefix(o, o->output_prefix_data);
 +              line_prefix = msgbuf->buf;
 +      }
  
        if (ecbdata->header) {
 -              fprintf(ecbdata->file, "%s", ecbdata->header->buf);
 +              fprintf(ecbdata->opt->file, "%s", ecbdata->header->buf);
                strbuf_reset(ecbdata->header);
                ecbdata->header = NULL;
        }
                name_a_tab = strchr(ecbdata->label_path[0], ' ') ? "\t" : "";
                name_b_tab = strchr(ecbdata->label_path[1], ' ') ? "\t" : "";
  
 -              fprintf(ecbdata->file, "%s--- %s%s%s\n",
 -                      meta, ecbdata->label_path[0], reset, name_a_tab);
 -              fprintf(ecbdata->file, "%s+++ %s%s%s\n",
 -                      meta, ecbdata->label_path[1], reset, name_b_tab);
 +              fprintf(ecbdata->opt->file, "%s%s--- %s%s%s\n",
 +                      line_prefix, meta, ecbdata->label_path[0], reset, name_a_tab);
 +              fprintf(ecbdata->opt->file, "%s%s+++ %s%s%s\n",
 +                      line_prefix, meta, ecbdata->label_path[1], reset, name_b_tab);
                ecbdata->label_path[0] = ecbdata->label_path[1] = NULL;
        }
  
                find_lno(line, ecbdata);
                emit_hunk_header(ecbdata, line, len);
                if (line[len-1] != '\n')
 -                      putc('\n', ecbdata->file);
 +                      putc('\n', ecbdata->opt->file);
                return;
        }
  
        if (len < 1) {
 -              emit_line(ecbdata->file, reset, reset, line, len);
 +              emit_line(ecbdata->opt, reset, reset, line, len);
 +              if (ecbdata->diff_words
 +                  && ecbdata->diff_words->type == DIFF_WORDS_PORCELAIN)
 +                      fputs("~\n", ecbdata->opt->file);
                return;
        }
  
                        return;
                }
                diff_words_flush(ecbdata);
 -              line++;
 -              len--;
 -              emit_line(ecbdata->file, plain, reset, line, len);
 +              if (ecbdata->diff_words->type == DIFF_WORDS_PORCELAIN) {
 +                      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);
 +              }
                return;
        }
  
                ecbdata->lno_in_preimage++;
                if (line[0] == ' ')
                        ecbdata->lno_in_postimage++;
 -              emit_line(ecbdata->file, color, reset, line, len);
 +              emit_line(ecbdata->opt, color, reset, line, len);
        } else {
                ecbdata->lno_in_postimage++;
                emit_add_line(reset, ecbdata, line + 1, len - 1);
@@@ -1226,17 -1048,10 +1226,17 @@@ static void show_stats(struct diffstat_
        int total_files = data->nr;
        int width, name_width;
        const char *reset, *set, *add_c, *del_c;
 +      const char *line_prefix = "";
 +      struct strbuf *msg = NULL;
  
        if (data->nr == 0)
                return;
  
 +      if (options->output_prefix) {
 +              msg = options->output_prefix(options, options->output_prefix_data);
 +              line_prefix = msg->buf;
 +      }
 +
        width = options->stat_width ? options->stat_width : 80;
        name_width = options->stat_name_width ? options->stat_name_width : 50;
  
                }
  
                if (data->files[i]->is_binary) {
 +                      fprintf(options->file, "%s", line_prefix);
                        show_name(options->file, prefix, name, len);
                        fprintf(options->file, "  Bin ");
                        fprintf(options->file, "%s%"PRIuMAX"%s",
                        continue;
                }
                else if (data->files[i]->is_unmerged) {
 +                      fprintf(options->file, "%s", line_prefix);
                        show_name(options->file, prefix, name, len);
                        fprintf(options->file, "  Unmerged\n");
                        continue;
                        add = scale_linear(add, width, max_change);
                        del = scale_linear(del, width, max_change);
                }
 +              fprintf(options->file, "%s", line_prefix);
                show_name(options->file, prefix, name, len);
                fprintf(options->file, "%5"PRIuMAX"%s", added + deleted,
                                added + deleted ? " " : "");
                show_graph(options->file, '-', del, del_c, reset);
                fprintf(options->file, "\n");
        }
 +      fprintf(options->file, "%s", line_prefix);
        fprintf(options->file,
               " %d files changed, %d insertions(+), %d deletions(-)\n",
               total_files, adds, dels);
@@@ -1377,12 -1188,6 +1377,12 @@@ static void show_shortstats(struct diff
                        }
                }
        }
 +      if (options->output_prefix) {
 +              struct strbuf *msg = NULL;
 +              msg = options->output_prefix(options,
 +                              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);
  }
@@@ -1397,13 -1202,6 +1397,13 @@@ static void show_numstat(struct diffsta
        for (i = 0; i < data->nr; i++) {
                struct diffstat_file *file = data->files[i];
  
 +              if (options->output_prefix) {
 +                      struct strbuf *msg = NULL;
 +                      msg = options->output_prefix(options,
 +                                      options->output_prefix_data);
 +                      fprintf(options->file, "%s", msg->buf);
 +              }
 +
                if (file->is_binary)
                        fprintf(options->file, "-\t-\t");
                else
@@@ -1439,18 -1237,10 +1439,18 @@@ struct dirstat_dir 
        int alloc, nr, percent, cumulative;
  };
  
 -static long gather_dirstat(FILE *file, struct dirstat_dir *dir, unsigned long changed, const char *base, int baselen)
 +static long gather_dirstat(struct diff_options *opt, struct dirstat_dir *dir,
 +              unsigned long changed, const char *base, int baselen)
  {
        unsigned long this_dir = 0;
        unsigned int sources = 0;
 +      const char *line_prefix = "";
 +      struct strbuf *msg = NULL;
 +
 +      if (opt->output_prefix) {
 +              msg = opt->output_prefix(opt, opt->output_prefix_data);
 +              line_prefix = msg->buf;
 +      }
  
        while (dir->nr) {
                struct dirstat_file *f = dir->files;
                slash = strchr(f->name + baselen, '/');
                if (slash) {
                        int newbaselen = slash + 1 - f->name;
 -                      this = gather_dirstat(file, dir, changed, f->name, newbaselen);
 +                      this = gather_dirstat(opt, dir, changed, f->name, newbaselen);
                        sources++;
                } else {
                        this = f->changed;
                if (permille) {
                        int percent = permille / 10;
                        if (percent >= dir->percent) {
 -                              fprintf(file, "%4d.%01d%% %.*s\n", percent, permille % 10, baselen, base);
 +                              fprintf(opt->file, "%s%4d.%01d%% %.*s\n", line_prefix,
 +                                      percent, permille % 10, baselen, base);
                                if (!dir->cumulative)
                                        return 0;
                        }
@@@ -1568,7 -1357,7 +1568,7 @@@ static void show_dirstat(struct diff_op
  
        /* Show all directories with more than x% of the changes */
        qsort(dir.files, dir.nr, sizeof(dir.files[0]), dirstat_compare);
 -      gather_dirstat(options->file, &dir, changed, "", 0);
 +      gather_dirstat(options, &dir, changed, "", 0);
  }
  
  static void free_diffstat_info(struct diffstat_t *diffstat)
@@@ -1626,15 -1415,6 +1626,15 @@@ static void checkdiff_consume(void *pri
        const char *reset = diff_get_color(color_diff, DIFF_RESET);
        const char *set = diff_get_color(color_diff, DIFF_FILE_NEW);
        char *err;
 +      char *line_prefix = "";
 +      struct strbuf *msgbuf;
 +
 +      assert(data->o);
 +      if (data->o->output_prefix) {
 +              msgbuf = data->o->output_prefix(data->o,
 +                      data->o->output_prefix_data);
 +              line_prefix = msgbuf->buf;
 +      }
  
        if (line[0] == '+') {
                unsigned bad;
                if (is_conflict_marker(line + 1, marker_size, len - 1)) {
                        data->status |= 1;
                        fprintf(data->o->file,
 -                              "%s:%d: leftover conflict marker\n",
 -                              data->filename, data->lineno);
 +                              "%s%s:%d: leftover conflict marker\n",
 +                              line_prefix, data->filename, data->lineno);
                }
                bad = ws_check(line + 1, len - 1, data->ws_rule);
                if (!bad)
                        return;
                data->status |= bad;
                err = whitespace_error_string(bad);
 -              fprintf(data->o->file, "%s:%d: %s.\n",
 -                      data->filename, data->lineno, err);
 +              fprintf(data->o->file, "%s%s:%d: %s.\n",
 +                      line_prefix, data->filename, data->lineno, err);
                free(err);
 -              emit_line(data->o->file, set, reset, line, 1);
 +              emit_line(data->o, set, reset, line, 1);
                ws_check_emit(line + 1, len - 1, data->ws_rule,
                              data->o->file, set, reset, ws);
        } else if (line[0] == ' ') {
@@@ -1691,7 -1471,7 +1691,7 @@@ static unsigned char *deflate_it(char *
        return deflated;
  }
  
 -static void emit_binary_diff_body(FILE *file, mmfile_t *one, mmfile_t *two)
 +static void emit_binary_diff_body(FILE *file, mmfile_t *one, mmfile_t *two, char *prefix)
  {
        void *cp;
        void *delta;
        }
  
        if (delta && delta_size < deflate_size) {
 -              fprintf(file, "delta %lu\n", orig_size);
 +              fprintf(file, "%sdelta %lu\n", prefix, orig_size);
                free(deflated);
                data = delta;
                data_size = delta_size;
        }
        else {
 -              fprintf(file, "literal %lu\n", two->size);
 +              fprintf(file, "%sliteral %lu\n", prefix, two->size);
                free(delta);
                data = deflated;
                data_size = deflate_size;
                        line[0] = bytes - 26 + 'a' - 1;
                encode_85(line + 1, cp, bytes);
                cp = (char *) cp + bytes;
 +              fprintf(file, "%s", prefix);
                fputs(line, file);
                fputc('\n', file);
        }
 -      fprintf(file, "\n");
 +      fprintf(file, "%s\n", prefix);
        free(data);
  }
  
 -static void emit_binary_diff(FILE *file, mmfile_t *one, mmfile_t *two)
 +static void emit_binary_diff(FILE *file, mmfile_t *one, mmfile_t *two, char *prefix)
  {
 -      fprintf(file, "GIT binary patch\n");
 -      emit_binary_diff_body(file, one, two);
 -      emit_binary_diff_body(file, two, one);
 +      fprintf(file, "%sGIT binary patch\n", prefix);
 +      emit_binary_diff_body(file, one, two, prefix);
 +      emit_binary_diff_body(file, two, one, prefix);
  }
  
  static void diff_filespec_load_driver(struct diff_filespec *one)
@@@ -1806,26 -1585,14 +1806,26 @@@ void diff_set_mnemonic_prefix(struct di
                options->b_prefix = b;
  }
  
 -static const char *get_textconv(struct diff_filespec *one)
 +struct userdiff_driver *get_textconv(struct diff_filespec *one)
  {
        if (!DIFF_FILE_VALID(one))
                return NULL;
        if (!S_ISREG(one->mode))
                return NULL;
        diff_filespec_load_driver(one);
 -      return one->driver->textconv;
 +      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;
  }
  
  static void builtin_diff(const char *name_a,
                         struct diff_filespec *one,
                         struct diff_filespec *two,
                         const char *xfrm_msg,
 +                       int must_show_header,
                         struct diff_options *o,
                         int complete_rewrite)
  {
        const char *set = diff_get_color_opt(o, DIFF_METAINFO);
        const char *reset = diff_get_color_opt(o, DIFF_RESET);
        const char *a_prefix, *b_prefix;
 -      const char *textconv_one = NULL, *textconv_two = NULL;
 +      struct userdiff_driver *textconv_one = NULL;
 +      struct userdiff_driver *textconv_two = NULL;
        struct strbuf header = STRBUF_INIT;
 +      struct strbuf *msgbuf;
 +      char *line_prefix = "";
 +
 +      if (o->output_prefix) {
 +              msgbuf = o->output_prefix(o, o->output_prefix_data);
 +              line_prefix = msgbuf->buf;
 +      }
  
        if (DIFF_OPT_TST(o, SUBMODULE_LOG) &&
                        (!one->mode || S_ISGITLINK(one->mode)) &&
        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, "%sdiff --git %s %s%s\n", set, a_one, b_two, reset);
 +      strbuf_addf(&header, "%s%sdiff --git %s %s%s\n", line_prefix, set, a_one, b_two, reset);
        if (lbl[0][0] == '/') {
                /* /dev/null */
 -              strbuf_addf(&header, "%snew file mode %06o%s\n", set, two->mode, reset);
 -              if (xfrm_msg && xfrm_msg[0])
 -                      strbuf_addf(&header, "%s%s%s\n", set, xfrm_msg, reset);
 +              strbuf_addf(&header, "%s%snew file mode %06o%s\n", line_prefix, set, two->mode, reset);
 +              if (xfrm_msg)
 +                      strbuf_addstr(&header, xfrm_msg);
 +              must_show_header = 1;
        }
        else if (lbl[1][0] == '/') {
 -              strbuf_addf(&header, "%sdeleted file mode %06o%s\n", set, one->mode, reset);
 -              if (xfrm_msg && xfrm_msg[0])
 -                      strbuf_addf(&header, "%s%s%s\n", set, xfrm_msg, reset);
 +              strbuf_addf(&header, "%s%sdeleted file mode %06o%s\n", line_prefix, set, one->mode, reset);
 +              if (xfrm_msg)
 +                      strbuf_addstr(&header, xfrm_msg);
 +              must_show_header = 1;
        }
        else {
                if (one->mode != two->mode) {
 -                      strbuf_addf(&header, "%sold mode %06o%s\n", set, one->mode, reset);
 -                      strbuf_addf(&header, "%snew mode %06o%s\n", set, two->mode, reset);
 +                      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);
 +                      must_show_header = 1;
                }
 -              if (xfrm_msg && xfrm_msg[0])
 -                      strbuf_addf(&header, "%s%s%s\n", set, xfrm_msg, reset);
 +              if (xfrm_msg)
 +                      strbuf_addstr(&header, xfrm_msg);
  
                /*
                 * we do not run diff between different kind
                }
        }
  
 -      if (fill_mmfile(&mf1, one) < 0 || fill_mmfile(&mf2, two) < 0)
 -              die("unable to read files to diff");
 -
        if (!DIFF_OPT_TST(o, TEXT) &&
 -          ( (diff_filespec_is_binary(one) && !textconv_one) ||
 -            (diff_filespec_is_binary(two) && !textconv_two) )) {
 +          ( (!textconv_one && diff_filespec_is_binary(one)) ||
 +            (!textconv_two && diff_filespec_is_binary(two)) )) {
 +              if (fill_mmfile(&mf1, one) < 0 || fill_mmfile(&mf2, two) < 0)
 +                      die("unable to read files to diff");
                /* Quite common confusing case */
                if (mf1.size == mf2.size &&
 -                  !memcmp(mf1.ptr, mf2.ptr, mf1.size))
 +                  !memcmp(mf1.ptr, mf2.ptr, mf1.size)) {
 +                      if (must_show_header)
 +                              fprintf(o->file, "%s", header.buf);
                        goto free_ab_and_return;
 +              }
                fprintf(o->file, "%s", header.buf);
                strbuf_reset(&header);
                if (DIFF_OPT_TST(o, BINARY))
 -                      emit_binary_diff(o->file, &mf1, &mf2);
 +                      emit_binary_diff(o->file, &mf1, &mf2, line_prefix);
                else
 -                      fprintf(o->file, "Binary files %s and %s differ\n",
 -                              lbl[0], lbl[1]);
 +                      fprintf(o->file, "%sBinary files %s and %s differ\n",
 +                              line_prefix, lbl[0], lbl[1]);
                o->found_changes = 1;
        }
        else {
                struct emit_callback ecbdata;
                const struct userdiff_funcname *pe;
  
 -              if (!DIFF_XDL_TST(o, WHITESPACE_FLAGS)) {
 +              if (!DIFF_XDL_TST(o, WHITESPACE_FLAGS) || must_show_header) {
                        fprintf(o->file, "%s", header.buf);
                        strbuf_reset(&header);
                }
  
 -              if (textconv_one) {
 -                      size_t size;
 -                      mf1.ptr = run_textconv(textconv_one, one, &size);
 -                      if (!mf1.ptr)
 -                              die("unable to read files to diff");
 -                      mf1.size = size;
 -              }
 -              if (textconv_two) {
 -                      size_t size;
 -                      mf2.ptr = run_textconv(textconv_two, two, &size);
 -                      if (!mf2.ptr)
 -                              die("unable to read files to diff");
 -                      mf2.size = size;
 -              }
 +              mf1.size = fill_textconv(textconv_one, one, &mf1.ptr);
 +              mf2.size = fill_textconv(textconv_two, two, &mf2.ptr);
  
                pe = diff_funcname_pattern(one);
                if (!pe)
                ecbdata.ws_rule = whitespace_rule(name_b ? name_b : name_a);
                if (ecbdata.ws_rule & WS_BLANK_AT_EOF)
                        check_blank_at_eof(&mf1, &mf2, &ecbdata);
 -              ecbdata.file = o->file;
 +              ecbdata.opt = o;
                ecbdata.header = header.len ? &header : NULL;
 -              xpp.flags = XDF_NEED_MINIMAL | o->xdl_opts;
 +              xpp.flags = o->xdl_opts;
                xecfg.ctxlen = o->context;
                xecfg.interhunkctxlen = o->interhunkcontext;
                xecfg.flags = XDL_EMIT_FUNCNAMES;
                        xecfg.ctxlen = strtoul(diffopts + 10, NULL, 10);
                else if (!prefixcmp(diffopts, "-u"))
                        xecfg.ctxlen = strtoul(diffopts + 2, NULL, 10);
 -              if (DIFF_OPT_TST(o, COLOR_DIFF_WORDS)) {
 +              if (o->word_diff) {
 +                      int i;
 +
                        ecbdata.diff_words =
                                xcalloc(1, sizeof(struct diff_words_data));
 -                      ecbdata.diff_words->file = o->file;
 +                      ecbdata.diff_words->type = o->word_diff;
 +                      ecbdata.diff_words->opt = o;
                        if (!o->word_regex)
                                o->word_regex = userdiff_word_regex(one);
                        if (!o->word_regex)
                                        die ("Invalid regular expression: %s",
                                                        o->word_regex);
                        }
 +                      for (i = 0; i < ARRAY_SIZE(diff_words_styles); i++) {
 +                              if (o->word_diff == diff_words_styles[i].type) {
 +                                      ecbdata.diff_words->style =
 +                                              &diff_words_styles[i];
 +                                      break;
 +                              }
 +                      }
 +                      if (DIFF_OPT_TST(o, COLOR_DIFF)) {
 +                              struct diff_words_style *st = ecbdata.diff_words->style;
 +                              st->old.color = diff_get_color_opt(o, DIFF_FILE_OLD);
 +                              st->new.color = diff_get_color_opt(o, DIFF_FILE_NEW);
 +                              st->ctx.color = diff_get_color_opt(o, DIFF_PLAIN);
 +                      }
                }
                xdi_diff_outf(&mf1, &mf2, fn_out_consume, &ecbdata,
                              &xpp, &xecfg);
 -              if (DIFF_OPT_TST(o, COLOR_DIFF_WORDS))
 +              if (o->word_diff)
                        free_diff_words_data(&ecbdata);
                if (textconv_one)
                        free(mf1.ptr);
@@@ -2085,7 -1834,7 +2085,7 @@@ static void builtin_diffstat(const cha
  
                memset(&xpp, 0, sizeof(xpp));
                memset(&xecfg, 0, sizeof(xecfg));
 -              xpp.flags = XDF_NEED_MINIMAL | o->xdl_opts;
 +              xpp.flags = o->xdl_opts;
                xdi_diff_outf(&mf1, &mf2, diffstat_consume, diffstat,
                              &xpp, &xecfg);
        }
@@@ -2133,7 -1882,7 +2133,7 @@@ static void builtin_checkdiff(const cha
                memset(&xpp, 0, sizeof(xpp));
                memset(&xecfg, 0, sizeof(xecfg));
                xecfg.ctxlen = 1; /* at least one context line */
 -              xpp.flags = XDF_NEED_MINIMAL;
 +              xpp.flags = 0;
                xdi_diff_outf(&mf1, &mf2, checkdiff_consume, &data,
                              &xpp, &xecfg);
  
@@@ -2570,53 -2319,36 +2570,53 @@@ static void fill_metainfo(struct strbu
                          struct diff_filespec *one,
                          struct diff_filespec *two,
                          struct diff_options *o,
 -                        struct diff_filepair *p)
 +                        struct diff_filepair *p,
 +                        int *must_show_header,
 +                        int use_color)
  {
 +      const char *set = diff_get_color(use_color, DIFF_METAINFO);
 +      const char *reset = diff_get_color(use_color, DIFF_RESET);
 +      struct strbuf *msgbuf;
 +      char *line_prefix = "";
 +
 +      *must_show_header = 1;
 +      if (o->output_prefix) {
 +              msgbuf = o->output_prefix(o, o->output_prefix_data);
 +              line_prefix = msgbuf->buf;
 +      }
        strbuf_init(msg, PATH_MAX * 2 + 300);
        switch (p->status) {
        case DIFF_STATUS_COPIED:
 -              strbuf_addf(msg, "similarity index %d%%", similarity_index(p));
 -              strbuf_addstr(msg, "\ncopy from ");
 +              strbuf_addf(msg, "%s%ssimilarity index %d%%",
 +                          line_prefix, set, similarity_index(p));
 +              strbuf_addf(msg, "%s\n%s%scopy from ",
 +                          reset,  line_prefix, set);
                quote_c_style(name, msg, NULL, 0);
 -              strbuf_addstr(msg, "\ncopy to ");
 +              strbuf_addf(msg, "%s\n%s%scopy to ", reset, line_prefix, set);
                quote_c_style(other, msg, NULL, 0);
 -              strbuf_addch(msg, '\n');
 +              strbuf_addf(msg, "%s\n", reset);
                break;
        case DIFF_STATUS_RENAMED:
 -              strbuf_addf(msg, "similarity index %d%%", similarity_index(p));
 -              strbuf_addstr(msg, "\nrename from ");
 +              strbuf_addf(msg, "%s%ssimilarity index %d%%",
 +                          line_prefix, set, similarity_index(p));
 +              strbuf_addf(msg, "%s\n%s%srename from ",
 +                          reset, line_prefix, set);
                quote_c_style(name, msg, NULL, 0);
 -              strbuf_addstr(msg, "\nrename to ");
 +              strbuf_addf(msg, "%s\n%s%srename to ",
 +                          reset, line_prefix, set);
                quote_c_style(other, msg, NULL, 0);
 -              strbuf_addch(msg, '\n');
 +              strbuf_addf(msg, "%s\n", reset);
                break;
        case DIFF_STATUS_MODIFIED:
                if (p->score) {
 -                      strbuf_addf(msg, "dissimilarity index %d%%\n",
 -                                  similarity_index(p));
 +                      strbuf_addf(msg, "%s%sdissimilarity index %d%%%s\n",
 +                                  line_prefix,
 +                                  set, similarity_index(p), reset);
                        break;
                }
                /* fallthru */
        default:
 -              /* nothing */
 -              ;
 +              *must_show_header = 0;
        }
        if (one && two && hashcmp(one->sha1, two->sha1)) {
                int abbrev = DIFF_OPT_TST(o, FULL_INDEX) ? 40 : DEFAULT_ABBREV;
                            (!fill_mmfile(&mf, two) && diff_filespec_is_binary(two)))
                                abbrev = 40;
                }
 -              strbuf_addf(msg, "index %.*s..%.*s",
 -                          abbrev, sha1_to_hex(one->sha1),
 -                          abbrev, sha1_to_hex(two->sha1));
 +              strbuf_addf(msg, "%s%sindex %s..", set,
 +                          line_prefix,
 +                          find_unique_abbrev(one->sha1, abbrev));
 +              strbuf_addstr(msg, find_unique_abbrev(two->sha1, abbrev));
                if (one->mode == two->mode)
                        strbuf_addf(msg, " %06o", one->mode);
 -              strbuf_addch(msg, '\n');
 +              strbuf_addf(msg, "%s\n", reset);
        }
 -      if (msg->len)
 -              strbuf_setlen(msg, msg->len - 1);
  }
  
  static void run_diff_cmd(const char *pgm,
  {
        const char *xfrm_msg = NULL;
        int complete_rewrite = (p->status == DIFF_STATUS_MODIFIED) && p->score;
 -
 -      if (msg) {
 -              fill_metainfo(msg, name, other, one, two, o, p);
 -              xfrm_msg = msg->len ? msg->buf : NULL;
 -      }
 +      int must_show_header = 0;
  
        if (!DIFF_OPT_TST(o, ALLOW_EXTERNAL))
                pgm = NULL;
                        pgm = drv->external;
        }
  
 +      if (msg) {
 +              /*
 +               * don't use colors when the header is intended for an
 +               * external diff driver
 +               */
 +              fill_metainfo(msg, name, other, one, two, o, p,
 +                            &must_show_header,
 +                            DIFF_OPT_TST(o, COLOR_DIFF) && !pgm);
 +              xfrm_msg = msg->len ? msg->buf : NULL;
 +      }
 +
        if (pgm) {
                run_external_diff(pgm, name, other, one, two, xfrm_msg,
                                  complete_rewrite);
        }
        if (one && two)
                builtin_diff(name, other ? other : name,
 -                           one, two, xfrm_msg, o, complete_rewrite);
 +                           one, two, xfrm_msg, must_show_header,
 +                           o, complete_rewrite);
        else
                fprintf(o->file, "* Unmerged path %s\n", name);
  }
@@@ -2815,7 -2540,6 +2815,7 @@@ static void run_checkdiff(struct diff_f
  void diff_setup(struct diff_options *options)
  {
        memset(options, 0, sizeof(*options));
 +      memset(&diff_queued_diff, 0, sizeof(diff_queued_diff));
  
        options->file = stdout;
  
                DIFF_OPT_SET(options, COLOR_DIFF);
        options->detect_rename = diff_detect_rename_default;
  
 -      if (!diff_mnemonic_prefix) {
 +      if (diff_no_prefix) {
 +              options->a_prefix = options->b_prefix = "";
 +      } else if (!diff_mnemonic_prefix) {
                options->a_prefix = "a/";
                options->b_prefix = "b/";
        }
@@@ -2996,7 -2718,7 +2996,7 @@@ int diff_opt_parse(struct diff_options 
        const char *arg = av[0];
  
        /* Output format options */
 -      if (!strcmp(arg, "-p") || !strcmp(arg, "-u"))
 +      if (!strcmp(arg, "-p") || !strcmp(arg, "-u") || !strcmp(arg, "--patch"))
                options->output_format |= DIFF_FORMAT_PATCH;
        else if (opt_arg(arg, 'U', "unified", &options->context))
                options->output_format |= DIFF_FORMAT_PATCH;
                DIFF_OPT_CLR(options, COLOR_DIFF);
        else if (!strcmp(arg, "--color-words")) {
                DIFF_OPT_SET(options, COLOR_DIFF);
 -              DIFF_OPT_SET(options, COLOR_DIFF_WORDS);
 +              options->word_diff = DIFF_WORDS_COLOR;
        }
        else if (!prefixcmp(arg, "--color-words=")) {
                DIFF_OPT_SET(options, COLOR_DIFF);
 -              DIFF_OPT_SET(options, COLOR_DIFF_WORDS);
 +              options->word_diff = DIFF_WORDS_COLOR;
                options->word_regex = arg + 14;
        }
 +      else if (!strcmp(arg, "--word-diff")) {
 +              if (options->word_diff == DIFF_WORDS_NONE)
 +                      options->word_diff = DIFF_WORDS_PLAIN;
 +      }
 +      else if (!prefixcmp(arg, "--word-diff=")) {
 +              const char *type = arg + 12;
 +              if (!strcmp(type, "plain"))
 +                      options->word_diff = DIFF_WORDS_PLAIN;
 +              else if (!strcmp(type, "color")) {
 +                      DIFF_OPT_SET(options, COLOR_DIFF);
 +                      options->word_diff = DIFF_WORDS_COLOR;
 +              }
 +              else if (!strcmp(type, "porcelain"))
 +                      options->word_diff = DIFF_WORDS_PORCELAIN;
 +              else if (!strcmp(type, "none"))
 +                      options->word_diff = DIFF_WORDS_NONE;
 +              else
 +                      die("bad --word-diff argument: %s", type);
 +      }
 +      else if (!prefixcmp(arg, "--word-diff-regex=")) {
 +              if (options->word_diff == DIFF_WORDS_NONE)
 +                      options->word_diff = DIFF_WORDS_PLAIN;
 +              options->word_regex = arg + 18;
 +      }
        else if (!strcmp(arg, "--exit-code"))
                DIFF_OPT_SET(options, EXIT_WITH_STATUS);
        else if (!strcmp(arg, "--quiet"))
        else if (!strcmp(arg, "--no-textconv"))
                DIFF_OPT_CLR(options, ALLOW_TEXTCONV);
        else if (!strcmp(arg, "--ignore-submodules"))
-               DIFF_OPT_SET(options, IGNORE_SUBMODULES);
-       else if (!prefixcmp(arg, "--ignore-submodules=")) {
-               if (!strcmp(arg + 20, "all"))
-                       DIFF_OPT_SET(options, IGNORE_SUBMODULES);
-               else if (!strcmp(arg + 20, "untracked"))
-                       DIFF_OPT_SET(options, IGNORE_UNTRACKED_IN_SUBMODULES);
-               else if (!strcmp(arg + 20, "dirty"))
-                       DIFF_OPT_SET(options, IGNORE_DIRTY_SUBMODULES);
-               else
-                       die("bad --ignore-submodules argument: %s", arg + 20);
-       } else if (!strcmp(arg, "--submodule"))
+               handle_ignore_submodules_arg(options, "all");
+       else if (!prefixcmp(arg, "--ignore-submodules="))
+               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"))
@@@ -3350,11 -3041,6 +3343,11 @@@ static void diff_flush_raw(struct diff_
  {
        int line_termination = opt->line_termination;
        int inter_name_termination = line_termination ? '\t' : '\0';
 +      if (opt->output_prefix) {
 +              struct strbuf *msg = NULL;
 +              msg = opt->output_prefix(opt, opt->output_prefix_data);
 +              fprintf(opt->file, "%s", msg->buf);
 +      }
  
        if (!(opt->output_format & DIFF_FORMAT_NAME_STATUS)) {
                fprintf(opt->file, ":%06o %06o %s ", p->one->mode, p->two->mode,
@@@ -3600,62 -3286,48 +3593,62 @@@ static void show_file_mode_name(FILE *f
  }
  
  
 -static void show_mode_change(FILE *file, struct diff_filepair *p, int show_name)
 +static void show_mode_change(FILE *file, struct diff_filepair *p, int show_name,
 +              const char *line_prefix)
  {
        if (p->one->mode && p->two->mode && p->one->mode != p->two->mode) {
 -              fprintf(file, " mode change %06o => %06o%c", p->one->mode, p->two->mode,
 -                      show_name ? ' ' : '\n');
 +              fprintf(file, "%s mode change %06o => %06o%c", line_prefix, p->one->mode,
 +                      p->two->mode, show_name ? ' ' : '\n');
                if (show_name) {
                        write_name_quoted(p->two->path, file, '\n');
                }
        }
  }
  
 -static void show_rename_copy(FILE *file, const char *renamecopy, struct diff_filepair *p)
 +static void show_rename_copy(FILE *file, const char *renamecopy, struct diff_filepair *p,
 +                      const char *line_prefix)
  {
        char *names = pprint_rename(p->one->path, p->two->path);
  
        fprintf(file, " %s %s (%d%%)\n", renamecopy, names, similarity_index(p));
        free(names);
 -      show_mode_change(file, p, 0);
 +      show_mode_change(file, p, 0, line_prefix);
  }
  
 -static void diff_summary(FILE *file, struct diff_filepair *p)
 +static void diff_summary(struct diff_options *opt, struct diff_filepair *p)
  {
 +      FILE *file = opt->file;
 +      char *line_prefix = "";
 +
 +      if (opt->output_prefix) {
 +              struct strbuf *buf = opt->output_prefix(opt, opt->output_prefix_data);
 +              line_prefix = buf->buf;
 +      }
 +
        switch(p->status) {
        case DIFF_STATUS_DELETED:
 +              fputs(line_prefix, file);
                show_file_mode_name(file, "delete", p->one);
                break;
        case DIFF_STATUS_ADDED:
 +              fputs(line_prefix, file);
                show_file_mode_name(file, "create", p->two);
                break;
        case DIFF_STATUS_COPIED:
 -              show_rename_copy(file, "copy", p);
 +              fputs(line_prefix, file);
 +              show_rename_copy(file, "copy", p, line_prefix);
                break;
        case DIFF_STATUS_RENAMED:
 -              show_rename_copy(file, "rename", p);
 +              fputs(line_prefix, file);
 +              show_rename_copy(file, "rename", p, line_prefix);
                break;
        default:
                if (p->score) {
 -                      fputs(" rewrite ", file);
 +                      fprintf(file, "%s rewrite ", line_prefix);
                        write_name_quoted(p->two->path, file, ' ');
                        fprintf(file, "(%d%%)\n", similarity_index(p));
                }
 -              show_mode_change(file, p, !p->score);
 +              show_mode_change(file, p, !p->score, line_prefix);
                break;
        }
  }
@@@ -3766,7 -3438,7 +3759,7 @@@ static int diff_get_patch_id(struct dif
                                        len2, p->two->path);
                git_SHA1_Update(&ctx, buffer, len1);
  
 -              xpp.flags = XDF_NEED_MINIMAL;
 +              xpp.flags = 0;
                xecfg.ctxlen = 3;
                xecfg.flags = XDL_EMIT_FUNCNAMES;
                xdi_diff_outf(&mf1, &mf2, patch_id_consume, &data,
@@@ -3787,7 -3459,8 +3780,7 @@@ int diff_flush_patch_id(struct diff_opt
                diff_free_filepair(q->queue[i]);
  
        free(q->queue);
 -      q->queue = NULL;
 -      q->nr = q->alloc = 0;
 +      DIFF_QUEUE_CLEAR(q);
  
        return result;
  }
@@@ -3864,9 -3537,8 +3857,9 @@@ void diff_flush(struct diff_options *op
                show_dirstat(options);
  
        if (output_format & DIFF_FORMAT_SUMMARY && !is_summary_empty(q)) {
 -              for (i = 0; i < q->nr; i++)
 -                      diff_summary(options->file, q->queue[i]);
 +              for (i = 0; i < q->nr; i++) {
 +                      diff_summary(options, q->queue[i]);
 +              }
                separator++;
        }
  
                diff_free_filepair(q->queue[i]);
  free_queue:
        free(q->queue);
 -      q->queue = NULL;
 -      q->nr = q->alloc = 0;
 +      DIFF_QUEUE_CLEAR(q);
        if (options->close_file)
                fclose(options->file);
  
@@@ -3938,7 -3611,8 +3931,7 @@@ static void diffcore_apply_filter(cons
        int i;
        struct diff_queue_struct *q = &diff_queued_diff;
        struct diff_queue_struct outq;
 -      outq.queue = NULL;
 -      outq.nr = outq.alloc = 0;
 +      DIFF_QUEUE_CLEAR(&outq);
  
        if (!filter)
                return;
@@@ -4006,7 -3680,8 +3999,7 @@@ static void diffcore_skip_stat_unmatch(
        int i;
        struct diff_queue_struct *q = &diff_queued_diff;
        struct diff_queue_struct outq;
 -      outq.queue = NULL;
 -      outq.nr = outq.alloc = 0;
 +      DIFF_QUEUE_CLEAR(&outq);
  
        for (i = 0; i < q->nr; i++) {
                struct diff_filepair *p = q->queue[i];
@@@ -4067,12 -3742,6 +4060,12 @@@ void diffcore_fix_diff_index(struct dif
  
  void diffcore_std(struct diff_options *options)
  {
 +      /* We never run this function more than one time, because the
 +       * rename/copy detection logic can only run once.
 +       */
 +      if (diff_queued_diff.run)
 +              return;
 +
        if (options->skip_stat_unmatch)
                diffcore_skip_stat_unmatch(options);
        if (options->break_opt != -1)
                DIFF_OPT_SET(options, HAS_CHANGES);
        else
                DIFF_OPT_CLR(options, HAS_CHANGES);
 +
 +      diff_queued_diff.run = 1;
  }
  
  int diff_result_code(struct diff_options *opt, int status)
@@@ -4247,47 -3914,3 +4240,47 @@@ static char *run_textconv(const char *p
  
        return strbuf_detach(&buf, outsize);
  }
 +
 +size_t fill_textconv(struct userdiff_driver *driver,
 +                   struct diff_filespec *df,
 +                   char **outbuf)
 +{
 +      size_t size;
 +
 +      if (!driver || !driver->textconv) {
 +              if (!DIFF_FILE_VALID(df)) {
 +                      *outbuf = "";
 +                      return 0;
 +              }
 +              if (diff_populate_filespec(df, 0))
 +                      die("unable to read files to diff");
 +              *outbuf = df->data;
 +              return df->size;
 +      }
 +
 +      if (driver->textconv_cache) {
 +              *outbuf = notes_cache_get(driver->textconv_cache, df->sha1,
 +                                        &size);
 +              if (*outbuf)
 +                      return size;
 +      }
 +
 +      *outbuf = run_textconv(driver->textconv, df, &size);
 +      if (!*outbuf)
 +              die("unable to read files to diff");
 +
 +      if (driver->textconv_cache) {
 +              /* ignore errors, as we might be in a readonly repository */
 +              notes_cache_put(driver->textconv_cache, df->sha1, *outbuf,
 +                              size);
 +              /*
 +               * we could save up changes and flush them all at the end,
 +               * but we would need an extra call after all diffing is done.
 +               * Since generating a cache entry is the slow path anyway,
 +               * this extra overhead probably isn't a big deal.
 +               */
 +              notes_cache_write(driver->textconv_cache);
 +      }
 +
 +      return size;
 +}
diff --combined git-submodule.sh
index 8c562a72e6e95fce60b78c015623212b7fce1dab,5e7b1279c428d46bf83fa122bc72e11195f1ee12..d9950c2b7fadef0876867e11ef26283ca1cfaf1e
@@@ -271,8 -271,6 +271,8 @@@ cmd_foreach(
                shift
        done
  
 +      toplevel=$(pwd)
 +
        module_list |
        while read mode sha1 stage path
        do
@@@ -580,7 -578,7 +580,7 @@@ cmd_summary() 
  
        cd_to_toplevel
        # Get modified modules cared by user
-       modules=$(git $diff_cmd $cached --raw $head -- "$@" |
+       modules=$(git $diff_cmd $cached --ignore-submodules=dirty --raw $head -- "$@" |
                sane_egrep '^:([0-7]* )?160000' |
                while read mod_src mod_dst sha1_src sha1_dst status name
                do
  
        test -z "$modules" && return
  
-       git $diff_cmd $cached --raw $head -- $modules |
+       git $diff_cmd $cached --ignore-submodules=dirty --raw $head -- $modules |
        sane_egrep '^:([0-7]* )?160000' |
        cut -c2- |
        while read mod_src mod_dst sha1_src sha1_dst status name
                                range=$sha1_dst
                        fi
                        GIT_DIR="$name/.git" \
 -                      git log --pretty=oneline --first-parent $range | wc -l
 +                      git rev-list --first-parent $range -- | wc -l
                        )
                        total_commits=" ($(($total_commits + 0)))"
                        ;;
@@@ -760,7 -758,7 +760,7 @@@ cmd_status(
                        continue;
                fi
                set_name_rev "$path" "$sha1"
-               if git diff-files --quiet -- "$path"
+               if git diff-files --ignore-submodules=dirty --quiet -- "$path"
                then
                        say " $sha1 $displaypath$revname"
                else
diff --combined t/t7508-status.sh
index 9e081073fbed00da362137446d78a7be7f86350b,63d437e0e6b5a959b77c78072af1942d73fafe49..a72fe3ae640378350102a07124aee201fb1c637b
@@@ -68,34 -68,6 +68,34 @@@ test_expect_success 'status (2)' 
  
  '
  
 +cat >expect <<\EOF
 +# On branch master
 +# Changes to be committed:
 +#     new file:   dir2/added
 +#
 +# Changed but not updated:
 +#     modified:   dir1/modified
 +#
 +# Untracked files:
 +#     dir1/untracked
 +#     dir2/modified
 +#     dir2/untracked
 +#     expect
 +#     output
 +#     untracked
 +EOF
 +
 +git config advice.statusHints false
 +
 +test_expect_success 'status (advice.statusHints false)' '
 +
 +      git status >output &&
 +      test_cmp expect output
 +
 +'
 +
 +git config --unset advice.statusHints
 +
  cat >expect <<\EOF
   M dir1/modified
  A  dir2/added
  ?? untracked
  EOF
  
 -test_expect_success 'status -s (2)' '
 +test_expect_success 'status -s' '
  
        git status -s >output &&
        test_cmp expect output
  
  '
  
 +cat >expect <<\EOF
 +## master
 + M dir1/modified
 +A  dir2/added
 +?? dir1/untracked
 +?? dir2/modified
 +?? dir2/untracked
 +?? expect
 +?? output
 +?? untracked
 +EOF
 +
 +test_expect_success 'status -s -b' '
 +
 +      git status -s -b >output &&
 +      test_cmp expect output
 +
 +'
 +
  cat >expect <<EOF
  # On branch master
  # Changes to be committed:
@@@ -162,23 -115,6 +162,23 @@@ test_expect_success 'status (status.sho
        test_cmp expect output
  '
  
 +cat >expect <<EOF
 +# On branch master
 +# Changes to be committed:
 +#     new file:   dir2/added
 +#
 +# Changed but not updated:
 +#     modified:   dir1/modified
 +#
 +# Untracked files not listed
 +EOF
 +git config advice.statusHints false
 +test_expect_success 'status -uno (advice.statusHints false)' '
 +      git status -uno >output &&
 +      test_cmp expect output
 +'
 +git config --unset advice.statusHints
 +
  cat >expect << EOF
   M dir1/modified
  A  dir2/added
@@@ -455,25 -391,6 +455,25 @@@ test_expect_success 'status -s with col
  
  '
  
 +cat >expect <<\EOF
 +## <GREEN>master<RESET>
 + <RED>M<RESET> dir1/modified
 +<GREEN>A<RESET>  dir2/added
 +<BLUE>??<RESET> dir1/untracked
 +<BLUE>??<RESET> dir2/modified
 +<BLUE>??<RESET> dir2/untracked
 +<BLUE>??<RESET> expect
 +<BLUE>??<RESET> output
 +<BLUE>??<RESET> untracked
 +EOF
 +
 +test_expect_success 'status -s -b with color.status' '
 +
 +      git status -s -b | test_decode_color >output &&
 +      test_cmp expect output
 +
 +'
 +
  cat >expect <<\EOF
   M dir1/modified
  A  dir2/added
@@@ -507,13 -424,6 +507,13 @@@ test_expect_success 'status --porcelai
  git config --unset color.status
  git config --unset color.ui
  
 +test_expect_success 'status --porcelain ignores -b' '
 +
 +      git status --porcelain -b >output &&
 +      test_cmp expect output
 +
 +'
 +
  cat >expect <<\EOF
  # On branch master
  # Changes to be committed:
@@@ -586,16 -496,6 +586,16 @@@ test_expect_success 'dry-run of partia
        test_cmp expect output
  '
  
 +cat >expect <<EOF
 +:100644 100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0000000000000000000000000000000000000000 M    dir1/modified
 +EOF
 +test_expect_success 'status refreshes the index' '
 +      touch dir2/added &&
 +      git status &&
 +      git diff-files >output &&
 +      test_cmp expect output
 +'
 +
  test_expect_success 'setup status submodule summary' '
        test_create_repo sm && (
                cd sm &&
@@@ -793,19 -693,131 +793,146 @@@ test_expect_success 'commit --dry-run s
        test_cmp expect output
  '
  
 +test_expect_success POSIXPERM 'status succeeds in a read-only repository' '
 +      (
 +              chmod a-w .git &&
 +              # make dir1/tracked stat-dirty
 +              >dir1/tracked1 && mv -f dir1/tracked1 dir1/tracked &&
 +              git status -s >output &&
 +              ! grep dir1/tracked output &&
 +              # make sure "status" succeeded without writing index out
 +              git diff-files | grep dir1/tracked
 +      )
 +      status=$?
 +      chmod 775 .git
 +      (exit $status)
 +'
 +
+ cat > expect << EOF
+ # On branch master
+ # Changed but not updated:
+ #   (use "git add <file>..." to update what will be committed)
+ #   (use "git checkout -- <file>..." to discard changes in working directory)
+ #
+ #     modified:   dir1/modified
+ #
+ # Untracked files:
+ #   (use "git add <file>..." to include in what will be committed)
+ #
+ #     dir1/untracked
+ #     dir2/modified
+ #     dir2/untracked
+ #     expect
+ #     output
+ #     untracked
+ no changes added to commit (use "git add" and/or "git commit -a")
+ EOF
+ test_expect_success '--ignore-submodules=untracked suppresses submodules with untracked content' '
+       echo modified > sm/untracked &&
+       git status --ignore-submodules=untracked > output &&
+       test_cmp expect output
+ '
+ test_expect_success '--ignore-submodules=dirty suppresses submodules with untracked content' '
+       git status --ignore-submodules=dirty > output &&
+       test_cmp expect output
+ '
+ test_expect_success '--ignore-submodules=dirty suppresses submodules with modified content' '
+       echo modified > sm/foo &&
+       git status --ignore-submodules=dirty > output &&
+       test_cmp expect output
+ '
+ cat > expect << EOF
+ # On branch master
+ # Changed but not updated:
+ #   (use "git add <file>..." to update what will be committed)
+ #   (use "git checkout -- <file>..." to discard changes in working directory)
+ #   (commit or discard the untracked or modified content in submodules)
+ #
+ #     modified:   dir1/modified
+ #     modified:   sm (modified content)
+ #
+ # Untracked files:
+ #   (use "git add <file>..." to include in what will be committed)
+ #
+ #     dir1/untracked
+ #     dir2/modified
+ #     dir2/untracked
+ #     expect
+ #     output
+ #     untracked
+ no changes added to commit (use "git add" and/or "git commit -a")
+ EOF
+ test_expect_success "--ignore-submodules=untracked doesn't suppress submodules with modified content" '
+       git status --ignore-submodules=untracked > output &&
+       test_cmp expect output
+ '
+ head2=$(cd sm && git commit -q -m "2nd commit" foo && git rev-parse --short=7 --verify HEAD)
+ cat > expect << EOF
+ # On branch master
+ # Changed but not updated:
+ #   (use "git add <file>..." to update what will be committed)
+ #   (use "git checkout -- <file>..." to discard changes in working directory)
+ #
+ #     modified:   dir1/modified
+ #     modified:   sm (new commits)
+ #
+ # Submodules changed but not updated:
+ #
+ # * sm $head...$head2 (1):
+ #   > 2nd commit
+ #
+ # Untracked files:
+ #   (use "git add <file>..." to include in what will be committed)
+ #
+ #     dir1/untracked
+ #     dir2/modified
+ #     dir2/untracked
+ #     expect
+ #     output
+ #     untracked
+ no changes added to commit (use "git add" and/or "git commit -a")
+ EOF
+ test_expect_success "--ignore-submodules=untracked doesn't suppress submodule summary" '
+       git status --ignore-submodules=untracked > output &&
+       test_cmp expect output
+ '
+ test_expect_success "--ignore-submodules=dirty doesn't suppress submodule summary" '
+       git status --ignore-submodules=dirty > output &&
+       test_cmp expect output
+ '
+ cat > expect << EOF
+ # On branch master
+ # Changed but not updated:
+ #   (use "git add <file>..." to update what will be committed)
+ #   (use "git checkout -- <file>..." to discard changes in working directory)
+ #
+ #     modified:   dir1/modified
+ #
+ # Untracked files:
+ #   (use "git add <file>..." to include in what will be committed)
+ #
+ #     dir1/untracked
+ #     dir2/modified
+ #     dir2/untracked
+ #     expect
+ #     output
+ #     untracked
+ no changes added to commit (use "git add" and/or "git commit -a")
+ EOF
+ test_expect_success "--ignore-submodules=all suppresses submodule summary" '
+       git status --ignore-submodules=all > output &&
+       test_cmp expect output
+ '
  test_done
diff --combined wt-status.c
index 38754ad735e9e6b090eb23990d82926cd2d82396,894d66f8fc3e1175080345f0f068ee828663e664..2f9e33c8fa172b129fd956abc4f74a5bf5543ba7
@@@ -9,7 -9,7 +9,8 @@@
  #include "quote.h"
  #include "run-command.h"
  #include "remote.h"
 +#include "refs.h"
+ #include "submodule.h"
  
  static char default_wt_status_colors[][COLOR_MAXLEN] = {
        GIT_COLOR_NORMAL, /* WT_STATUS_HEADER */
@@@ -18,8 -18,6 +19,8 @@@
        GIT_COLOR_RED,    /* WT_STATUS_UNTRACKED */
        GIT_COLOR_RED,    /* WT_STATUS_NOBRANCH */
        GIT_COLOR_RED,    /* WT_STATUS_UNMERGED */
 +      GIT_COLOR_GREEN,  /* WT_STATUS_LOCAL_BRANCH */
 +      GIT_COLOR_RED,    /* WT_STATUS_REMOTE_BRANCH */
  };
  
  static const char *color(int slot, struct wt_status *s)
@@@ -45,7 -43,6 +46,7 @@@ void wt_status_prepare(struct wt_statu
        s->index_file = get_index_file();
        s->change.strdup_strings = 1;
        s->untracked.strdup_strings = 1;
 +      s->ignored.strdup_strings = 1;
  }
  
  static void wt_status_print_unmerged_header(struct wt_status *s)
@@@ -100,15 -97,13 +101,15 @@@ static void wt_status_print_dirty_heade
        color_fprintf_ln(s->fp, c, "#");
  }
  
 -static void wt_status_print_untracked_header(struct wt_status *s)
 +static void wt_status_print_other_header(struct wt_status *s,
 +                                       const char *what,
 +                                       const char *how)
  {
        const char *c = color(WT_STATUS_HEADER, s);
 -      color_fprintf_ln(s->fp, c, "# Untracked files:");
 +      color_fprintf_ln(s->fp, c, "# %s files:", what);
        if (!advice_status_hints)
                return;
 -      color_fprintf_ln(s->fp, c, "#   (use \"git add <file>...\" to include in what will be committed)");
 +      color_fprintf_ln(s->fp, c, "#   (use \"git %s <file>...\" to include in what will be committed)", how);
        color_fprintf_ln(s->fp, c, "#");
  }
  
@@@ -235,7 -230,7 +236,7 @@@ static void wt_status_collect_changed_c
                struct wt_status_change_data *d;
  
                p = q->queue[i];
 -              it = string_list_insert(p->one->path, &s->change);
 +              it = string_list_insert(&s->change, p->one->path);
                d = it->util;
                if (!d) {
                        d = xcalloc(1, sizeof(*d));
@@@ -282,7 -277,7 +283,7 @@@ static void wt_status_collect_updated_c
                struct wt_status_change_data *d;
  
                p = q->queue[i];
 -              it = string_list_insert(p->two->path, &s->change);
 +              it = string_list_insert(&s->change, p->two->path);
                d = it->util;
                if (!d) {
                        d = xcalloc(1, sizeof(*d));
@@@ -312,6 -307,8 +313,8 @@@ static void wt_status_collect_changes_w
        DIFF_OPT_SET(&rev.diffopt, DIRTY_SUBMODULES);
        if (!s->show_untracked_files)
                DIFF_OPT_SET(&rev.diffopt, IGNORE_UNTRACKED_IN_SUBMODULES);
+       if (s->ignore_submodule_arg)
+               handle_ignore_submodules_arg(&rev.diffopt, s->ignore_submodule_arg);
        rev.diffopt.format_callback = wt_status_collect_changed_cb;
        rev.diffopt.format_callback_data = s;
        rev.prune_data = s->pathspec;
@@@ -328,6 -325,9 +331,9 @@@ static void wt_status_collect_changes_i
        opt.def = s->is_initial ? EMPTY_TREE_SHA1_HEX : s->reference;
        setup_revisions(0, NULL, &rev, &opt);
  
+       if (s->ignore_submodule_arg)
+               handle_ignore_submodules_arg(&rev.diffopt, s->ignore_submodule_arg);
        rev.diffopt.output_format |= DIFF_FORMAT_CALLBACK;
        rev.diffopt.format_callback = wt_status_collect_updated_cb;
        rev.diffopt.format_callback_data = s;
@@@ -349,7 -349,7 +355,7 @@@ static void wt_status_collect_changes_i
  
                if (!ce_path_match(ce, s->pathspec))
                        continue;
 -              it = string_list_insert(ce->name, &s->change);
 +              it = string_list_insert(&s->change, ce->name);
                d = it->util;
                if (!d) {
                        d = xcalloc(1, sizeof(*d));
@@@ -384,26 -384,9 +390,26 @@@ static void wt_status_collect_untracked
                        continue;
                if (!match_pathspec(s->pathspec, ent->name, ent->len, 0, NULL))
                        continue;
 -              s->workdir_untracked = 1;
 -              string_list_insert(ent->name, &s->untracked);
 +              string_list_insert(&s->untracked, ent->name);
 +              free(ent);
        }
 +
 +      if (s->show_ignored_files) {
 +              dir.nr = 0;
 +              dir.flags = DIR_SHOW_IGNORED | DIR_SHOW_OTHER_DIRECTORIES;
 +              fill_directory(&dir, s->pathspec);
 +              for (i = 0; i < dir.nr; i++) {
 +                      struct dir_entry *ent = dir.entries[i];
 +                      if (!cache_name_is_other(ent->name, ent->len))
 +                              continue;
 +                      if (!match_pathspec(s->pathspec, ent->name, ent->len, 0, NULL))
 +                              continue;
 +                      string_list_insert(&s->ignored, ent->name);
 +                      free(ent);
 +              }
 +      }
 +
 +      free(dir.entries);
  }
  
  void wt_status_collect(struct wt_status *s)
@@@ -521,18 -504,17 +527,18 @@@ static void wt_status_print_submodule_s
        struct child_process sm_summary;
        char summary_limit[64];
        char index[PATH_MAX];
 -      const char *env[] = { index, NULL };
 -      const char *argv[] = {
 -              "submodule",
 -              "summary",
 -              uncommitted ? "--files" : "--cached",
 -              "--for-status",
 -              "--summary-limit",
 -              summary_limit,
 -              uncommitted ? NULL : (s->amend ? "HEAD^" : "HEAD"),
 -              NULL
 -      };
 +      const char *env[] = { NULL, NULL };
 +      const char *argv[8];
 +
 +      env[0] =        index;
 +      argv[0] =       "submodule";
 +      argv[1] =       "summary";
 +      argv[2] =       uncommitted ? "--files" : "--cached";
 +      argv[3] =       "--for-status";
 +      argv[4] =       "--summary-limit";
 +      argv[5] =       summary_limit;
 +      argv[6] =       uncommitted ? NULL : (s->amend ? "HEAD^" : "HEAD");
 +      argv[7] =       NULL;
  
        sprintf(summary_limit, "%d", s->submodule_summary);
        snprintf(index, sizeof(index), "GIT_INDEX_FILE=%s", s->index_file);
        run_command(&sm_summary);
  }
  
 -static void wt_status_print_untracked(struct wt_status *s)
 +static void wt_status_print_other(struct wt_status *s,
 +                                struct string_list *l,
 +                                const char *what,
 +                                const char *how)
  {
        int i;
        struct strbuf buf = STRBUF_INIT;
        if (!s->untracked.nr)
                return;
  
 -      wt_status_print_untracked_header(s);
 -      for (i = 0; i < s->untracked.nr; i++) {
 +      wt_status_print_other_header(s, what, how);
 +
 +      for (i = 0; i < l->nr; i++) {
                struct string_list_item *it;
 -              it = &(s->untracked.items[i]);
 +              it = &(l->items[i]);
                color_fprintf(s->fp, color(WT_STATUS_HEADER, s), "#\t");
                color_fprintf_ln(s->fp, color(WT_STATUS_UNTRACKED, s), "%s",
                                 quote_path(it->string, strlen(it->string),
@@@ -646,18 -624,16 +652,20 @@@ void wt_status_print(struct wt_status *
        wt_status_print_updated(s);
        wt_status_print_unmerged(s);
        wt_status_print_changed(s);
-       if (s->submodule_summary) {
+       if (s->submodule_summary &&
+           (!s->ignore_submodule_arg ||
+            strcmp(s->ignore_submodule_arg, "all"))) {
                wt_status_print_submodule_summary(s, 0);  /* staged */
                wt_status_print_submodule_summary(s, 1);  /* unstaged */
        }
 -      if (s->show_untracked_files)
 -              wt_status_print_untracked(s);
 -      else if (s->commitable)
 -               fprintf(s->fp, "# Untracked files not listed (use -u option to show untracked files)\n");
 +      if (s->show_untracked_files) {
 +              wt_status_print_other(s, &s->untracked, "Untracked", "add");
 +              if (s->show_ignored_files)
 +                      wt_status_print_other(s, &s->ignored, "Ignored", "add -f");
 +      } else if (s->commitable)
 +              fprintf(s->fp, "# Untracked files not listed%s\n",
 +                      advice_status_hints
 +                      ? " (use -u option to show untracked files)" : "");
  
        if (s->verbose)
                wt_status_print_verbose(s);
                else if (s->nowarn)
                        ; /* nothing */
                else if (s->workdir_dirty)
 -                      printf("no changes added to commit (use \"git add\" and/or \"git commit -a\")\n");
 +                      printf("no changes added to commit%s\n",
 +                              advice_status_hints
 +                              ? " (use \"git add\" and/or \"git commit -a\")" : "");
                else if (s->untracked.nr)
 -                      printf("nothing added to commit but untracked files present (use \"git add\" to track)\n");
 +                      printf("nothing added to commit but untracked files present%s\n",
 +                              advice_status_hints
 +                              ? " (use \"git add\" to track)" : "");
                else if (s->is_initial)
 -                      printf("nothing to commit (create/copy files and use \"git add\" to track)\n");
 +                      printf("nothing to commit%s\n", advice_status_hints
 +                              ? " (create/copy files and use \"git add\" to track)" : "");
                else if (!s->show_untracked_files)
 -                      printf("nothing to commit (use -u to show untracked files)\n");
 +                      printf("nothing to commit%s\n", advice_status_hints
 +                              ? " (use -u to show untracked files)" : "");
                else
 -                      printf("nothing to commit (working directory clean)\n");
 +                      printf("nothing to commit%s\n", advice_status_hints
 +                              ? " (working directory clean)" : "");
        }
  }
  
@@@ -745,84 -714,24 +753,84 @@@ static void wt_shortstatus_status(int n
        }
  }
  
 -static void wt_shortstatus_untracked(int null_termination, struct string_list_item *it,
 -                          struct wt_status *s)
 +static void wt_shortstatus_other(int null_termination, struct string_list_item *it,
 +                               struct wt_status *s, const char *sign)
  {
        if (null_termination) {
 -              fprintf(stdout, "?? %s%c", it->string, 0);
 +              fprintf(stdout, "%s %s%c", sign, it->string, 0);
        } else {
                struct strbuf onebuf = STRBUF_INIT;
                const char *one;
                one = quote_path(it->string, -1, &onebuf, s->prefix);
 -              color_fprintf(s->fp, color(WT_STATUS_UNTRACKED, s), "??");
 +              color_fprintf(s->fp, color(WT_STATUS_UNTRACKED, s), "%s", sign);
                printf(" %s\n", one);
                strbuf_release(&onebuf);
        }
  }
  
 -void wt_shortstatus_print(struct wt_status *s, int null_termination)
 +static void wt_shortstatus_print_tracking(struct wt_status *s)
 +{
 +      struct branch *branch;
 +      const char *header_color = color(WT_STATUS_HEADER, s);
 +      const char *branch_color_local = color(WT_STATUS_LOCAL_BRANCH, s);
 +      const char *branch_color_remote = color(WT_STATUS_REMOTE_BRANCH, s);
 +
 +      const char *base;
 +      const char *branch_name;
 +      int num_ours, num_theirs;
 +
 +      color_fprintf(s->fp, color(WT_STATUS_HEADER, s), "## ");
 +
 +      if (!s->branch)
 +              return;
 +      branch_name = s->branch;
 +
 +      if (!prefixcmp(branch_name, "refs/heads/"))
 +              branch_name += 11;
 +      else if (!strcmp(branch_name, "HEAD")) {
 +              branch_name = "HEAD (no branch)";
 +              branch_color_local = color(WT_STATUS_NOBRANCH, s);
 +      }
 +
 +      branch = branch_get(s->branch + 11);
 +      if (s->is_initial)
 +              color_fprintf(s->fp, header_color, "Initial commit on ");
 +      if (!stat_tracking_info(branch, &num_ours, &num_theirs)) {
 +              color_fprintf_ln(s->fp, branch_color_local,
 +                      "%s", branch_name);
 +              return;
 +      }
 +
 +      base = branch->merge[0]->dst;
 +      base = shorten_unambiguous_ref(base, 0);
 +      color_fprintf(s->fp, branch_color_local, "%s", branch_name);
 +      color_fprintf(s->fp, header_color, "...");
 +      color_fprintf(s->fp, branch_color_remote, "%s", base);
 +
 +      color_fprintf(s->fp, header_color, " [");
 +      if (!num_ours) {
 +              color_fprintf(s->fp, header_color, "behind ");
 +              color_fprintf(s->fp, branch_color_remote, "%d", num_theirs);
 +      } else if (!num_theirs) {
 +              color_fprintf(s->fp, header_color, "ahead ");
 +              color_fprintf(s->fp, branch_color_local, "%d", num_ours);
 +      } else {
 +              color_fprintf(s->fp, header_color, "ahead ");
 +              color_fprintf(s->fp, branch_color_local, "%d", num_ours);
 +              color_fprintf(s->fp, header_color, ", behind ");
 +              color_fprintf(s->fp, branch_color_remote, "%d", num_theirs);
 +      }
 +
 +      color_fprintf_ln(s->fp, header_color, "]");
 +}
 +
 +void wt_shortstatus_print(struct wt_status *s, int null_termination, int show_branch)
  {
        int i;
 +
 +      if (show_branch)
 +              wt_shortstatus_print_tracking(s);
 +
        for (i = 0; i < s->change.nr; i++) {
                struct wt_status_change_data *d;
                struct string_list_item *it;
                struct string_list_item *it;
  
                it = &(s->untracked.items[i]);
 -              wt_shortstatus_untracked(null_termination, it, s);
 +              wt_shortstatus_other(null_termination, it, s, "??");
 +      }
 +      for (i = 0; i < s->ignored.nr; i++) {
 +              struct string_list_item *it;
 +
 +              it = &(s->ignored.items[i]);
 +              wt_shortstatus_other(null_termination, it, s, "!!");
        }
  }
  
@@@ -853,5 -756,5 +861,5 @@@ void wt_porcelain_print(struct wt_statu
        s->use_color = 0;
        s->relative_paths = 0;
        s->prefix = NULL;
 -      wt_shortstatus_print(s, null_termination);
 +      wt_shortstatus_print(s, null_termination, 0);
  }
diff --combined wt-status.h
index 4cd74c4b32f51dbe575b0a04a894a520df79e9bf,192909691e70622fac8054716d5663ae128cc683..9df9c9fad2512d7c1d7d6456cdd645d641b5a089
@@@ -12,8 -12,6 +12,8 @@@ enum color_wt_status 
        WT_STATUS_UNTRACKED,
        WT_STATUS_NOBRANCH,
        WT_STATUS_UNMERGED,
 +      WT_STATUS_LOCAL_BRANCH,
 +      WT_STATUS_REMOTE_BRANCH
  };
  
  enum untracked_status_type {
@@@ -43,26 -41,26 +43,27 @@@ struct wt_status 
        int use_color;
        int relative_paths;
        int submodule_summary;
 +      int show_ignored_files;
        enum untracked_status_type show_untracked_files;
 -      char color_palette[WT_STATUS_UNMERGED+1][COLOR_MAXLEN];
+       const char *ignore_submodule_arg;
 +      char color_palette[WT_STATUS_REMOTE_BRANCH+1][COLOR_MAXLEN];
  
        /* These are computed during processing of the individual sections */
        int commitable;
        int workdir_dirty;
 -      int workdir_untracked;
        const char *index_file;
        FILE *fp;
        const char *prefix;
        struct string_list change;
        struct string_list untracked;
 +      struct string_list ignored;
  };
  
  void wt_status_prepare(struct wt_status *s);
  void wt_status_print(struct wt_status *s);
  void wt_status_collect(struct wt_status *s);
  
 -void wt_shortstatus_print(struct wt_status *s, int null_termination);
 +void wt_shortstatus_print(struct wt_status *s, int null_termination, int show_branch);
  void wt_porcelain_print(struct wt_status *s, int null_termination);
  
  #endif /* STATUS_H */