OSDN Git Service

Merge branch 'nd/ita-wt-renames-in-status'
authorJunio C Hamano <gitster@pobox.com>
Tue, 23 Jan 2018 21:16:28 +0000 (13:16 -0800)
committerJunio C Hamano <gitster@pobox.com>
Tue, 23 Jan 2018 21:16:28 +0000 (13:16 -0800)
"git status" after moving a path in the working tree (hence making
it appear "removed") and then adding with the -N option (hence
making that appear "added") detected it as a rename, but did not
report the  old and new pathnames correctly.

* nd/ita-wt-renames-in-status:
  wt-status.c: handle worktree renames
  wt-status.c: rename rename-related fields in wt_status_change_data
  wt-status.c: catch unhandled diff status codes
  wt-status.c: coding style fix
  Use DIFF_DETECT_RENAME for detect_rename assignments
  t2203: test status output with porcelain v2 format

1  2 
Documentation/git-status.txt
builtin/commit.c
diff.c
t/t2203-add-intent.sh
wt-status.c
wt-status.h

@@@ -32,9 -32,6 +32,9 @@@ OPTION
  --branch::
        Show the branch and tracking info even in short-format.
  
 +--show-stash::
 +      Show the number of entries currently stashed away.
 +
  --porcelain[=<version>]::
        Give the output in an easy-to-parse format for scripts.
        This is similar to the short output, but will remain stable
@@@ -97,27 -94,8 +97,27 @@@ configuration variable documented in li
        (and suppresses the output of submodule summaries when the config option
        `status.submoduleSummary` is set).
  
 ---ignored::
 +--ignored[=<mode>]::
        Show ignored files as well.
 ++
 +The mode parameter is used to specify the handling of ignored files.
 +It is optional: it defaults to 'traditional'.
 ++
 +The possible options are:
 ++
 +      - 'traditional' - Shows ignored files and directories, unless
 +                        --untracked-files=all is specifed, in which case
 +                        individual files in ignored directories are
 +                        displayed.
 +      - 'no'          - Show no ignored files.
 +      - 'matching'    - Shows ignored files and directories matching an
 +                        ignore pattern.
 ++
 +When 'matching' mode is specified, paths that explicity match an
 +ignored pattern are shown. If a directory matches an ignore pattern,
 +then it is shown, but not paths contained in the ignored directory. If
 +a directory does not match an ignore pattern, but all contents are
 +ignored, then the directory is not shown, but all contents are shown.
  
  -z::
        Terminate entries with NUL, instead of LF.  This implies
        without options are equivalent to 'always' and 'never'
        respectively.
  
 +<pathspec>...::
 +      See the 'pathspec' entry in linkgit:gitglossary[7].
  
  OUTPUT
  ------
@@@ -149,14 -125,15 +149,15 @@@ the status.relativePaths config option 
  Short Format
  ~~~~~~~~~~~~
  
- In the short-format, the status of each path is shown as
+ In the short-format, the status of each path is shown as one of these
+ forms
  
-       XY PATH1 -> PATH2
+       XY PATH
+       XY ORIG_PATH -> PATH
  
- where `PATH1` is the path in the `HEAD`, and the " `-> PATH2`" part is
- shown only when `PATH1` corresponds to a different path in the
- index/worktree (i.e. the file is renamed). The `XY` is a two-letter
- status code.
+ where `ORIG_PATH` is where the renamed/copied contents came
+ from. `ORIG_PATH` is only shown when the entry is renamed or
+ copied. The `XY` is a two-letter status code.
  
  The fields (including the `->`) are separated from each other by a
  single space. If a filename contains whitespace or other nonprintable
@@@ -192,6 -169,8 +193,8 @@@ in which case `XY` are `!!`
      [MARC]           index and work tree matches
      [ MARC]     M    work tree changed since index
      [ MARC]     D    deleted in work tree
+     [ D]        R    renamed in work tree
+     [ D]        C    copied in work tree
      -------------------------------------------------
      D           D    unmerged, both deleted
      A           U    unmerged, added by us
@@@ -309,13 -288,13 +312,13 @@@ Renamed or copied entries have the foll
                of similarity between the source and target of the
                move or copy). For example "R100" or "C75".
      <path>      The pathname.  In a renamed/copied entry, this
-               is the path in the index and in the working tree.
+               is the target path.
      <sep>       When the `-z` option is used, the 2 pathnames are separated
                with a NUL (ASCII 0x00) byte; otherwise, a tab (ASCII 0x09)
                byte separates them.
-     <origPath>  The pathname in the commit at HEAD.  This is only
-               present in a renamed/copied entry, and tells
-               where the renamed/copied contents came from.
+     <origPath>  The pathname in the commit at HEAD or in the index.
+               This is only present in a renamed/copied entry, and
+               tells where the renamed/copied contents came from.
      --------------------------------------------------------
  
  Unmerged entries have the following format; the first character is
@@@ -387,19 -366,6 +390,19 @@@ ignored submodules you can either use t
  line option or the 'git submodule summary' command, which shows a similar
  output but does not honor these settings.
  
 +BACKGROUND REFRESH
 +------------------
 +
 +By default, `git status` will automatically refresh the index, updating
 +the cached stat information from the working tree and writing out the
 +result. Writing out the updated index is an optimization that isn't
 +strictly necessary (`status` computes the values for itself, but writing
 +them out is just to save subsequent programs from repeating our
 +computation). When `status` is run in the background, the lock held
 +during the write may conflict with other simultaneous processes, causing
 +them to fail. Scripts running `status` in the background should consider
 +using `git --no-optional-locks status` (see linkgit:git[1] for details).
 +
  SEE ALSO
  --------
  linkgit:gitignore[5]
diff --combined builtin/commit.c
@@@ -6,7 -6,6 +6,7 @@@
   */
  
  #include "cache.h"
 +#include "config.h"
  #include "lockfile.h"
  #include "cache-tree.h"
  #include "color.h"
@@@ -118,7 -117,7 +118,7 @@@ static int edit_flag = -1; /* unspecifi
  static int quiet, verbose, no_verify, allow_empty, dry_run, renew_authorship;
  static int config_commit_verbose = -1; /* unspecified */
  static int no_post_rewrite, allow_empty_message;
 -static char *untracked_files_arg, *force_date, *ignore_submodule_arg;
 +static char *untracked_files_arg, *force_date, *ignore_submodule_arg, *ignored_arg;
  static char *sign_commit;
  
  /*
@@@ -139,7 -138,8 +139,7 @@@ static const char *cleanup_arg
  static enum commit_whence whence;
  static int sequencer_in_use;
  static int use_editor = 1, include_status = 1;
 -static int show_ignored_in_status, have_option_m;
 -static const char *only_include_assumed;
 +static int have_option_m;
  static struct strbuf message = STRBUF_INIT;
  
  static enum wt_status_format status_format = STATUS_FORMAT_UNSPECIFIED;
@@@ -195,6 -195,7 +195,6 @@@ static void determine_whence(struct wt_
  static void status_init_config(struct wt_status *s, config_fn_t fn)
  {
        wt_status_prepare(s);
 -      gitmodules_config();
        git_config(fn, s);
        determine_whence(s);
        init_diff_ui_defaults();
@@@ -252,8 -253,7 +252,8 @@@ static int list_paths(struct string_lis
  
        if (with_tree) {
                char *max_prefix = common_prefix(pattern);
 -              overlay_tree_on_cache(with_tree, max_prefix ? max_prefix : prefix);
 +              overlay_tree_on_index(&the_index, with_tree,
 +                                    max_prefix ? max_prefix : prefix);
                free(max_prefix);
        }
  
@@@ -313,7 -313,7 +313,7 @@@ static void create_base_index(const str
        opts.dst_index = &the_index;
  
        opts.fn = oneway_merge;
 -      tree = parse_tree_indirect(current_head->object.oid.hash);
 +      tree = parse_tree_indirect(&current_head->object.oid);
        if (!tree)
                die(_("failed to unpack HEAD tree object"));
        parse_tree(tree);
@@@ -335,7 -335,7 +335,7 @@@ static void refresh_cache_or_die(int re
  static const char *prepare_index(int argc, const char **argv, const char *prefix,
                                 const struct commit *current_head, int is_status)
  {
 -      struct string_list partial;
 +      struct string_list partial = STRING_LIST_INIT_DUP;
        struct pathspec pathspec;
        int refresh_flags = REFRESH_QUIET;
        const char *ret;
  
                refresh_cache_or_die(refresh_flags);
  
 -              if (write_locked_index(&the_index, &index_lock, CLOSE_LOCK))
 +              if (write_locked_index(&the_index, &index_lock, 0))
                        die(_("unable to create temporary index"));
  
                old_index_env = getenv(INDEX_ENVIRONMENT);
                if (update_main_cache_tree(WRITE_TREE_SILENT) == 0) {
                        if (reopen_lock_file(&index_lock) < 0)
                                die(_("unable to write index file"));
 -                      if (write_locked_index(&the_index, &index_lock, CLOSE_LOCK))
 +                      if (write_locked_index(&the_index, &index_lock, 0))
                                die(_("unable to update temporary index"));
                } else
                        warning(_("Failed to update main cache tree"));
  
                commit_style = COMMIT_NORMAL;
 -              return get_lock_file_path(&index_lock);
 +              ret = get_lock_file_path(&index_lock);
 +              goto out;
        }
  
        /*
                add_files_to_cache(also ? prefix : NULL, &pathspec, 0);
                refresh_cache_or_die(refresh_flags);
                update_main_cache_tree(WRITE_TREE_SILENT);
 -              if (write_locked_index(&the_index, &index_lock, CLOSE_LOCK))
 +              if (write_locked_index(&the_index, &index_lock, 0))
                        die(_("unable to write new_index file"));
                commit_style = COMMIT_NORMAL;
 -              return get_lock_file_path(&index_lock);
 +              ret = get_lock_file_path(&index_lock);
 +              goto out;
        }
  
        /*
                        rollback_lock_file(&index_lock);
                }
                commit_style = COMMIT_AS_IS;
 -              return get_index_file();
 +              ret = get_index_file();
 +              goto out;
        }
  
        /*
                        die(_("cannot do a partial commit during a cherry-pick."));
        }
  
 -      string_list_init(&partial, 1);
        if (list_paths(&partial, !current_head ? NULL : "HEAD", prefix, &pathspec))
                exit(1);
  
        add_remove_files(&partial);
        refresh_cache(REFRESH_QUIET);
        update_main_cache_tree(WRITE_TREE_SILENT);
 -      if (write_locked_index(&the_index, &index_lock, CLOSE_LOCK))
 +      if (write_locked_index(&the_index, &index_lock, 0))
                die(_("unable to write new_index file"));
  
        hold_lock_file_for_update(&false_lock,
        add_remove_files(&partial);
        refresh_cache(REFRESH_QUIET);
  
 -      if (write_locked_index(&the_index, &false_lock, CLOSE_LOCK))
 +      if (write_locked_index(&the_index, &false_lock, 0))
                die(_("unable to write temporary index file"));
  
        discard_cache();
        ret = get_lock_file_path(&false_lock);
        read_cache_from(ret);
 +out:
 +      string_list_clear(&partial, 0);
 +      clear_pathspec(&pathspec);
        return ret;
  }
  
@@@ -514,7 -509,7 +514,7 @@@ static int run_status(FILE *fp, const c
        s->index_file = index_file;
        s->fp = fp;
        s->nowarn = nowarn;
 -      s->is_initial = get_sha1(s->reference, oid.hash) ? 1 : 0;
 +      s->is_initial = get_oid(s->reference, &oid) ? 1 : 0;
        if (!s->is_initial)
                hashcpy(s->sha1_commit, oid.hash);
        s->status_format = status_format;
@@@ -846,6 -841,9 +846,6 @@@ static int prepare_to_commit(const cha
                                  "with '%c' will be kept; you may remove them"
                                  " yourself if you want to.\n"
                                  "An empty message aborts the commit.\n"), comment_line_char);
 -              if (only_include_assumed)
 -                      status_printf_ln(s, GIT_COLOR_NORMAL,
 -                                      "%s", only_include_assumed);
  
                /*
                 * These should never fail because they come from our own
                                (int)(ci.name_end - ci.name_begin), ci.name_begin,
                                (int)(ci.mail_end - ci.mail_begin), ci.mail_begin);
  
 -              if (ident_shown)
 -                      status_printf_ln(s, GIT_COLOR_NORMAL, "%s", "");
 +              status_printf_ln(s, GIT_COLOR_NORMAL, "%s", ""); /* Add new line for clarity */
  
                saved_color_setting = s->use_color;
                s->use_color = 0;
                if (amend)
                        parent = "HEAD^1";
  
 -              if (get_sha1(parent, oid.hash)) {
 +              if (get_oid(parent, &oid)) {
                        int i, ita_nr = 0;
  
                        for (i = 0; i < active_nr; i++)
                         * submodules which were manually staged, which would
                         * be really confusing.
                         */
 -                      int diff_flags = DIFF_OPT_OVERRIDE_SUBMODULE_CONFIG;
 +                      struct diff_flags flags = DIFF_FLAGS_INIT;
 +                      flags.override_submodule_config = 1;
                        if (ignore_submodule_arg &&
                            !strcmp(ignore_submodule_arg, "all"))
 -                              diff_flags |= DIFF_OPT_IGNORE_SUBMODULES;
 -                      commitable = index_differs_from(parent, diff_flags, 1);
 +                              flags.ignore_submodules = 1;
 +                      commitable = index_differs_from(parent, &flags, 1);
                }
        }
        strbuf_release(&committer_ident);
                return 0;
        }
  
 -      /*
 -       * Re-read the index as pre-commit hook could have updated it,
 -       * and write it out as a tree.  We must do this before we invoke
 -       * the editor and after we invoke run_status above.
 -       */
 -      discard_cache();
 +      if (!no_verify && find_hook("pre-commit")) {
 +              /*
 +               * Re-read the index as pre-commit hook could have updated it,
 +               * and write it out as a tree.  We must do this before we invoke
 +               * the editor and after we invoke run_status above.
 +               */
 +              discard_cache();
 +      }
        read_cache_from(index_file);
 +
        if (update_main_cache_tree(0)) {
                error(_("Error building trees"));
                return 0;
@@@ -1076,19 -1071,6 +1076,19 @@@ static const char *find_author_by_nickn
        die(_("--author '%s' is not 'Name <email>' and matches no existing author"), name);
  }
  
 +static void handle_ignored_arg(struct wt_status *s)
 +{
 +      if (!ignored_arg)
 +              ; /* default already initialized */
 +      else if (!strcmp(ignored_arg, "traditional"))
 +              s->show_ignored_mode = SHOW_TRADITIONAL_IGNORED;
 +      else if (!strcmp(ignored_arg, "no"))
 +              s->show_ignored_mode = SHOW_NO_IGNORED;
 +      else if (!strcmp(ignored_arg, "matching"))
 +              s->show_ignored_mode = SHOW_MATCHING_IGNORED;
 +      else
 +              die(_("Invalid ignored mode '%s'"), ignored_arg);
 +}
  
  static void handle_untracked_files_arg(struct wt_status *s)
  {
@@@ -1226,6 -1208,8 +1226,6 @@@ static int parse_and_validate_options(i
                die(_("Only one of --include/--only/--all/--interactive/--patch can be used."));
        if (argc == 0 && (also || (only && !amend && !allow_empty)))
                die(_("No paths with --include/--only does not make sense."));
 -      if (argc > 0 && !also && !only)
 -              only_include_assumed = _("Explicit paths specified without -i or -o; assuming --only paths...");
        if (!cleanup_arg || !strcmp(cleanup_arg, "default"))
                cleanup_mode = use_editor ? CLEANUP_ALL : CLEANUP_SPACE;
        else if (!strcmp(cleanup_arg, "verbatim"))
@@@ -1279,10 -1263,6 +1279,10 @@@ static int parse_status_slot(const cha
                return WT_STATUS_NOBRANCH;
        if (!strcasecmp(slot, "unmerged"))
                return WT_STATUS_UNMERGED;
 +      if (!strcasecmp(slot, "localBranch"))
 +              return WT_STATUS_LOCAL_BRANCH;
 +      if (!strcasecmp(slot, "remoteBranch"))
 +              return WT_STATUS_REMOTE_BRANCH;
        return -1;
  }
  
@@@ -1311,10 -1291,6 +1311,10 @@@ static int git_status_config(const cha
                status_deferred_config.show_branch = git_config_bool(k, v);
                return 0;
        }
 +      if (!strcmp(k, "status.showstash")) {
 +              s->show_stash = git_config_bool(k, v);
 +              return 0;
 +      }
        if (!strcmp(k, "status.color") || !strcmp(k, "color.status")) {
                s->use_color = git_config_colorbool(k, v);
                return 0;
@@@ -1363,8 -1339,6 +1363,8 @@@ int cmd_status(int argc, const char **a
                            N_("show status concisely"), STATUS_FORMAT_SHORT),
                OPT_BOOL('b', "branch", &s.show_branch,
                         N_("show branch information")),
 +              OPT_BOOL(0, "show-stash", &s.show_stash,
 +                       N_("show stash information")),
                { OPTION_CALLBACK, 0, "porcelain", &status_format,
                  N_("version"), N_("machine-readable output"),
                  PARSE_OPT_OPTARG, opt_parse_porcelain },
                  N_("mode"),
                  N_("show untracked files, optional modes: all, normal, no. (Default: all)"),
                  PARSE_OPT_OPTARG, NULL, (intptr_t)"all" },
 -              OPT_BOOL(0, "ignored", &show_ignored_in_status,
 -                       N_("show ignored files")),
 +              { OPTION_STRING, 0, "ignored", &ignored_arg,
 +                N_("mode"),
 +                N_("show ignored files, optional modes: traditional, matching, no. (Default: traditional)"),
 +                PARSE_OPT_OPTARG, NULL, (intptr_t)"traditional" },
                { OPTION_STRING, 0, "ignore-submodules", &ignore_submodule_arg, N_("when"),
                  N_("ignore changes to submodules, optional when: all, dirty, untracked. (Default: all)"),
                  PARSE_OPT_OPTARG, NULL, (intptr_t)"all" },
        finalize_deferred_config(&s);
  
        handle_untracked_files_arg(&s);
 -      if (show_ignored_in_status)
 -              s.show_ignored_files = 1;
 +      handle_ignored_arg(&s);
 +
 +      if (s.show_ignored_mode == SHOW_MATCHING_IGNORED &&
 +          s.show_untracked_files == SHOW_NO_UNTRACKED_FILES)
 +              die(_("Unsupported combination of ignored and untracked-files arguments"));
 +
        parse_pathspec(&s.pathspec, 0,
                       PATHSPEC_PREFER_FULL,
                       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 (use_optional_locks())
 +              fd = hold_locked_index(&index_lock, 0);
 +      else
 +              fd = -1;
  
 -      s.is_initial = get_sha1(s.reference, oid.hash) ? 1 : 0;
 +      s.is_initial = get_oid(s.reference, &oid) ? 1 : 0;
        if (!s.is_initial)
                hashcpy(s.sha1_commit, oid.hash);
  
@@@ -1459,12 -1424,13 +1459,12 @@@ static void print_summary(const char *p
        struct rev_info rev;
        struct commit *commit;
        struct strbuf format = STRBUF_INIT;
 -      struct object_id junk_oid;
        const char *head;
        struct pretty_print_context pctx = {0};
        struct strbuf author_ident = STRBUF_INIT;
        struct strbuf committer_ident = STRBUF_INIT;
  
 -      commit = lookup_commit(oid->hash);
 +      commit = lookup_commit(oid);
        if (!commit)
                die(_("couldn't look up newly created commit"));
        if (parse_commit(commit))
        rev.show_root_diff = 1;
        get_commit_format(format.buf, &rev);
        rev.always_show_header = 0;
-       rev.diffopt.detect_rename = 1;
+       rev.diffopt.detect_rename = DIFF_DETECT_RENAME;
        rev.diffopt.break_opt = 0;
        diff_setup_done(&rev.diffopt);
  
 -      head = resolve_ref_unsafe("HEAD", 0, junk_oid.hash, NULL);
 +      head = resolve_ref_unsafe("HEAD", 0, NULL, NULL);
 +      if (!head)
 +              die_errno(_("unable to resolve HEAD after creating commit"));
        if (!strcmp(head, "HEAD"))
                head = _("detached HEAD");
        else
@@@ -1684,14 -1648,13 +1684,14 @@@ int cmd_commit(int argc, const char **a
                usage_with_options(builtin_commit_usage, builtin_commit_options);
  
        status_init_config(&s, git_commit_config);
 +      s.commit_template = 1;
        status_format = STATUS_FORMAT_NONE; /* Ignore status.short */
        s.colopts = 0;
  
 -      if (get_sha1("HEAD", oid.hash))
 +      if (get_oid("HEAD", &oid))
                current_head = NULL;
        else {
 -              current_head = lookup_commit_or_die(oid.hash, "HEAD");
 +              current_head = lookup_commit_or_die(&oid, "HEAD");
                if (parse_commit(current_head))
                        die(_("could not parse HEAD commit"));
        }
                if (!reflog_msg)
                        reflog_msg = "commit (merge)";
                pptr = commit_list_append(current_head, pptr);
 -              fp = fopen(git_path_merge_head(), "r");
 -              if (fp == NULL)
 -                      die_errno(_("could not open '%s' for reading"),
 -                                git_path_merge_head());
 +              fp = xfopen(git_path_merge_head(), "r");
                while (strbuf_getline_lf(&m, fp) != EOF) {
                        struct commit *parent;
  
                                allow_fast_forward = 0;
                }
                if (allow_fast_forward)
 -                      parents = reduce_heads(parents);
 +                      reduce_heads_replace(&parents);
        } else {
                if (!reflog_msg)
                        reflog_msg = (whence == FROM_CHERRY_PICK)
        if (verbose || /* Truncate the message just before the diff, if any. */
            cleanup_mode == CLEANUP_SCISSORS)
                strbuf_setlen(&sb, wt_status_locate_end(sb.buf, sb.len));
 -
        if (cleanup_mode != CLEANUP_NONE)
                strbuf_stripspace(&sb, cleanup_mode == CLEANUP_ALL);
 -      if (template_untouched(&sb) && !allow_empty_message) {
 +
 +      if (message_is_empty(&sb) && !allow_empty_message) {
                rollback_index_files();
 -              fprintf(stderr, _("Aborting commit; you did not edit the message.\n"));
 +              fprintf(stderr, _("Aborting commit due to empty commit message.\n"));
                exit(1);
        }
 -      if (message_is_empty(&sb) && !allow_empty_message) {
 +      if (template_untouched(&sb) && !allow_empty_message) {
                rollback_index_files();
 -              fprintf(stderr, _("Aborting commit due to empty commit message.\n"));
 +              fprintf(stderr, _("Aborting commit; you did not edit the message.\n"));
                exit(1);
        }
  
                append_merge_tag_headers(parents, &tail);
        }
  
 -      if (commit_tree_extended(sb.buf, sb.len, active_cache_tree->sha1,
 +      if (commit_tree_extended(sb.buf, sb.len, active_cache_tree->oid.hash,
                         parents, oid.hash, author_ident.buf, sign_commit, extra)) {
                rollback_index_files();
                die(_("failed to write commit object"));
  
        transaction = ref_transaction_begin(&err);
        if (!transaction ||
 -          ref_transaction_update(transaction, "HEAD", oid.hash,
 +          ref_transaction_update(transaction, "HEAD", &oid,
                                   current_head
 -                                 ? current_head->object.oid.hash : null_sha1,
 +                                 ? &current_head->object.oid : &null_oid,
                                   0, sb.buf, &err) ||
            ref_transaction_commit(transaction, &err)) {
                rollback_index_files();
                cfg = init_copy_notes_for_rewrite("amend");
                if (cfg) {
                        /* we are amending, so current_head is not NULL */
 -                      copy_note_for_rewrite(cfg, current_head->object.oid.hash, oid.hash);
 +                      copy_note_for_rewrite(cfg, &current_head->object.oid, &oid);
                        finish_copy_notes_for_rewrite(cfg, "Notes added by 'git commit --amend'");
                }
                run_rewrite_hook(&current_head->object.oid, &oid);
        if (!quiet)
                print_summary(prefix, &oid, !current_head);
  
 -      strbuf_release(&err);
 +      UNLEAK(err);
 +      UNLEAK(sb);
        return 0;
  }
diff --combined diff.c
--- 1/diff.c
--- 2/diff.c
+++ b/diff.c
@@@ -2,7 -2,6 +2,7 @@@
   * Copyright (C) 2005 Junio C Hamano
   */
  #include "cache.h"
 +#include "config.h"
  #include "tempfile.h"
  #include "quote.h"
  #include "diff.h"
  #include "userdiff.h"
  #include "submodule-config.h"
  #include "submodule.h"
 +#include "hashmap.h"
  #include "ll-merge.h"
  #include "string-list.h"
  #include "argv-array.h"
  #include "graph.h"
 +#include "packfile.h"
  
  #ifdef NO_FAST_WORKING_DIRECTORY
  #define FAST_WORKING_DIRECTORY 0
  #endif
  
  static int diff_detect_rename_default;
 -static int diff_indent_heuristic; /* experimental */
 +static int diff_indent_heuristic = 1;
  static int diff_rename_limit_default = 400;
  static int diff_suppress_blank_empty;
  static int diff_use_color_default = -1;
 +static int diff_color_moved_default;
  static int diff_context_default = 3;
  static int diff_interhunk_context_default;
  static const char *diff_word_regex_cfg;
@@@ -59,14 -55,6 +59,14 @@@ static char diff_colors[][COLOR_MAXLEN
        GIT_COLOR_YELLOW,       /* COMMIT */
        GIT_COLOR_BG_RED,       /* WHITESPACE */
        GIT_COLOR_NORMAL,       /* FUNCINFO */
 +      GIT_COLOR_BOLD_MAGENTA, /* OLD_MOVED */
 +      GIT_COLOR_BOLD_BLUE,    /* OLD_MOVED ALTERNATIVE */
 +      GIT_COLOR_FAINT,        /* OLD_MOVED_DIM */
 +      GIT_COLOR_FAINT_ITALIC, /* OLD_MOVED_ALTERNATIVE_DIM */
 +      GIT_COLOR_BOLD_CYAN,    /* NEW_MOVED */
 +      GIT_COLOR_BOLD_YELLOW,  /* NEW_MOVED ALTERNATIVE */
 +      GIT_COLOR_FAINT,        /* NEW_MOVED_DIM */
 +      GIT_COLOR_FAINT_ITALIC, /* NEW_MOVED_ALTERNATIVE_DIM */
  };
  
  static NORETURN void die_want_option(const char *option_name)
@@@ -92,22 -80,6 +92,22 @@@ static int parse_diff_color_slot(const 
                return DIFF_WHITESPACE;
        if (!strcasecmp(var, "func"))
                return DIFF_FUNCINFO;
 +      if (!strcasecmp(var, "oldmoved"))
 +              return DIFF_FILE_OLD_MOVED;
 +      if (!strcasecmp(var, "oldmovedalternative"))
 +              return DIFF_FILE_OLD_MOVED_ALT;
 +      if (!strcasecmp(var, "oldmoveddimmed"))
 +              return DIFF_FILE_OLD_MOVED_DIM;
 +      if (!strcasecmp(var, "oldmovedalternativedimmed"))
 +              return DIFF_FILE_OLD_MOVED_ALT_DIM;
 +      if (!strcasecmp(var, "newmoved"))
 +              return DIFF_FILE_NEW_MOVED;
 +      if (!strcasecmp(var, "newmovedalternative"))
 +              return DIFF_FILE_NEW_MOVED_ALT;
 +      if (!strcasecmp(var, "newmoveddimmed"))
 +              return DIFF_FILE_NEW_MOVED_DIM;
 +      if (!strcasecmp(var, "newmovedalternativedimmed"))
 +              return DIFF_FILE_NEW_MOVED_ALT_DIM;
        return -1;
  }
  
@@@ -124,18 -96,18 +124,18 @@@ static int parse_dirstat_params(struct 
        for (i = 0; i < params.nr; i++) {
                const char *p = params.items[i].string;
                if (!strcmp(p, "changes")) {
 -                      DIFF_OPT_CLR(options, DIRSTAT_BY_LINE);
 -                      DIFF_OPT_CLR(options, DIRSTAT_BY_FILE);
 +                      options->flags.dirstat_by_line = 0;
 +                      options->flags.dirstat_by_file = 0;
                } else if (!strcmp(p, "lines")) {
 -                      DIFF_OPT_SET(options, DIRSTAT_BY_LINE);
 -                      DIFF_OPT_CLR(options, DIRSTAT_BY_FILE);
 +                      options->flags.dirstat_by_line = 1;
 +                      options->flags.dirstat_by_file = 0;
                } else if (!strcmp(p, "files")) {
 -                      DIFF_OPT_CLR(options, DIRSTAT_BY_LINE);
 -                      DIFF_OPT_SET(options, DIRSTAT_BY_FILE);
 +                      options->flags.dirstat_by_line = 0;
 +                      options->flags.dirstat_by_file = 1;
                } else if (!strcmp(p, "noncumulative")) {
 -                      DIFF_OPT_CLR(options, DIRSTAT_CUMULATIVE);
 +                      options->flags.dirstat_cumulative = 0;
                } else if (!strcmp(p, "cumulative")) {
 -                      DIFF_OPT_SET(options, DIRSTAT_CUMULATIVE);
 +                      options->flags.dirstat_cumulative = 1;
                } else if (isdigit(*p)) {
                        char *end;
                        int permille = strtoul(p, &end, 10) * 10;
@@@ -246,7 -218,7 +246,7 @@@ static int parse_ws_error_highlight(con
   */
  void init_diff_ui_defaults(void)
  {
-       diff_detect_rename_default = 1;
+       diff_detect_rename_default = DIFF_DETECT_RENAME;
  }
  
  int git_diff_heuristic_config(const char *var, const char *value, void *cb)
        return 0;
  }
  
 +static int parse_color_moved(const char *arg)
 +{
 +      switch (git_parse_maybe_bool(arg)) {
 +      case 0:
 +              return COLOR_MOVED_NO;
 +      case 1:
 +              return COLOR_MOVED_DEFAULT;
 +      default:
 +              break;
 +      }
 +
 +      if (!strcmp(arg, "no"))
 +              return COLOR_MOVED_NO;
 +      else if (!strcmp(arg, "plain"))
 +              return COLOR_MOVED_PLAIN;
 +      else if (!strcmp(arg, "zebra"))
 +              return COLOR_MOVED_ZEBRA;
 +      else if (!strcmp(arg, "default"))
 +              return COLOR_MOVED_DEFAULT;
 +      else if (!strcmp(arg, "dimmed_zebra"))
 +              return COLOR_MOVED_ZEBRA_DIM;
 +      else
 +              return error(_("color moved setting must be one of 'no', 'default', 'zebra', 'dimmed_zebra', 'plain'"));
 +}
 +
  int git_diff_ui_config(const char *var, const char *value, void *cb)
  {
        if (!strcmp(var, "diff.color") || !strcmp(var, "color.diff")) {
                diff_use_color_default = git_config_colorbool(var, value);
                return 0;
        }
 +      if (!strcmp(var, "diff.colormoved")) {
 +              int cm = parse_color_moved(value);
 +              if (cm < 0)
 +                      return -1;
 +              diff_color_moved_default = cm;
 +              return 0;
 +      }
        if (!strcmp(var, "diff.context")) {
                diff_context_default = git_config_int(var, value);
                if (diff_context_default < 0)
                return 0;
        }
  
 -      if (git_diff_heuristic_config(var, value, cb) < 0)
 -              return -1;
 -
        if (!strcmp(var, "diff.wserrorhighlight")) {
                int val = parse_ws_error_highlight(value);
                if (val < 0)
@@@ -405,8 -348,8 +405,8 @@@ int git_diff_basic_config(const char *v
                return 0;
        }
  
 -      if (starts_with(var, "submodule."))
 -              return parse_submodule_config_option(var, value);
 +      if (git_diff_heuristic_config(var, value, cb) < 0)
 +              return -1;
  
        return git_default_config(var, value, cb);
  }
@@@ -462,9 -405,11 +462,9 @@@ static struct diff_tempfile 
         * If this diff_tempfile instance refers to a temporary file,
         * this tempfile object is used to manage its lifetime.
         */
 -      struct tempfile tempfile;
 +      struct tempfile *tempfile;
  } diff_temp[2];
  
 -typedef unsigned long (*sane_truncate_fn)(char *line, unsigned long len);
 -
  struct emit_callback {
        int color_diff;
        unsigned ws_rule;
        int blank_at_eof_in_postimage;
        int lno_in_preimage;
        int lno_in_postimage;
 -      sane_truncate_fn truncate;
        const char **label_path;
        struct diff_words_data *diff_words;
        struct diff_options *opt;
@@@ -613,668 -559,68 +613,668 @@@ static void emit_line(struct diff_optio
        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)
 +enum diff_symbol {
 +      DIFF_SYMBOL_BINARY_DIFF_HEADER,
 +      DIFF_SYMBOL_BINARY_DIFF_HEADER_DELTA,
 +      DIFF_SYMBOL_BINARY_DIFF_HEADER_LITERAL,
 +      DIFF_SYMBOL_BINARY_DIFF_BODY,
 +      DIFF_SYMBOL_BINARY_DIFF_FOOTER,
 +      DIFF_SYMBOL_STATS_SUMMARY_NO_FILES,
 +      DIFF_SYMBOL_STATS_SUMMARY_ABBREV,
 +      DIFF_SYMBOL_STATS_SUMMARY_INSERTS_DELETES,
 +      DIFF_SYMBOL_STATS_LINE,
 +      DIFF_SYMBOL_WORD_DIFF,
 +      DIFF_SYMBOL_STAT_SEP,
 +      DIFF_SYMBOL_SUMMARY,
 +      DIFF_SYMBOL_SUBMODULE_ADD,
 +      DIFF_SYMBOL_SUBMODULE_DEL,
 +      DIFF_SYMBOL_SUBMODULE_UNTRACKED,
 +      DIFF_SYMBOL_SUBMODULE_MODIFIED,
 +      DIFF_SYMBOL_SUBMODULE_HEADER,
 +      DIFF_SYMBOL_SUBMODULE_ERROR,
 +      DIFF_SYMBOL_SUBMODULE_PIPETHROUGH,
 +      DIFF_SYMBOL_REWRITE_DIFF,
 +      DIFF_SYMBOL_BINARY_FILES,
 +      DIFF_SYMBOL_HEADER,
 +      DIFF_SYMBOL_FILEPAIR_PLUS,
 +      DIFF_SYMBOL_FILEPAIR_MINUS,
 +      DIFF_SYMBOL_WORDS_PORCELAIN,
 +      DIFF_SYMBOL_WORDS,
 +      DIFF_SYMBOL_CONTEXT,
 +      DIFF_SYMBOL_CONTEXT_INCOMPLETE,
 +      DIFF_SYMBOL_PLUS,
 +      DIFF_SYMBOL_MINUS,
 +      DIFF_SYMBOL_NO_LF_EOF,
 +      DIFF_SYMBOL_CONTEXT_FRAGINFO,
 +      DIFF_SYMBOL_CONTEXT_MARKER,
 +      DIFF_SYMBOL_SEPARATOR
 +};
 +/*
 + * Flags for content lines:
 + * 0..12 are whitespace rules
 + * 13-15 are WSEH_NEW | WSEH_OLD | WSEH_CONTEXT
 + * 16 is marking if the line is blank at EOF
 + */
 +#define DIFF_SYMBOL_CONTENT_BLANK_LINE_EOF    (1<<16)
 +#define DIFF_SYMBOL_MOVED_LINE                        (1<<17)
 +#define DIFF_SYMBOL_MOVED_LINE_ALT            (1<<18)
 +#define DIFF_SYMBOL_MOVED_LINE_UNINTERESTING  (1<<19)
 +#define DIFF_SYMBOL_CONTENT_WS_MASK (WSEH_NEW | WSEH_OLD | WSEH_CONTEXT | WS_RULE_MASK)
 +
 +/*
 + * This struct is used when we need to buffer the output of the diff output.
 + *
 + * NEEDSWORK: Instead of storing a copy of the line, add an offset pointer
 + * into the pre/post image file. This pointer could be a union with the
 + * line pointer. By storing an offset into the file instead of the literal line,
 + * we can decrease the memory footprint for the buffered output. At first we
 + * may want to only have indirection for the content lines, but we could also
 + * enhance the state for emitting prefabricated lines, e.g. the similarity
 + * score line or hunk/file headers would only need to store a number or path
 + * and then the output can be constructed later on depending on state.
 + */
 +struct emitted_diff_symbol {
 +      const char *line;
 +      int len;
 +      int flags;
 +      enum diff_symbol s;
 +};
 +#define EMITTED_DIFF_SYMBOL_INIT {NULL}
 +
 +struct emitted_diff_symbols {
 +      struct emitted_diff_symbol *buf;
 +      int nr, alloc;
 +};
 +#define EMITTED_DIFF_SYMBOLS_INIT {NULL, 0, 0}
 +
 +static void append_emitted_diff_symbol(struct diff_options *o,
 +                                     struct emitted_diff_symbol *e)
  {
 -      if (!((ecbdata->ws_rule & WS_BLANK_AT_EOF) &&
 -            ecbdata->blank_at_eof_in_preimage &&
 -            ecbdata->blank_at_eof_in_postimage &&
 -            ecbdata->blank_at_eof_in_preimage <= ecbdata->lno_in_preimage &&
 -            ecbdata->blank_at_eof_in_postimage <= ecbdata->lno_in_postimage))
 -              return 0;
 -      return ws_blank_line(line, len, ecbdata->ws_rule);
 +      struct emitted_diff_symbol *f;
 +
 +      ALLOC_GROW(o->emitted_symbols->buf,
 +                 o->emitted_symbols->nr + 1,
 +                 o->emitted_symbols->alloc);
 +      f = &o->emitted_symbols->buf[o->emitted_symbols->nr++];
 +
 +      memcpy(f, e, sizeof(struct emitted_diff_symbol));
 +      f->line = e->line ? xmemdupz(e->line, e->len) : NULL;
  }
  
 -static void emit_line_checked(const char *reset,
 -                            struct emit_callback *ecbdata,
 -                            const char *line, int len,
 -                            enum color_diff color,
 -                            unsigned ws_error_highlight,
 -                            char sign)
 +struct moved_entry {
 +      struct hashmap_entry ent;
 +      const struct emitted_diff_symbol *es;
 +      struct moved_entry *next_line;
 +};
 +
 +static int moved_entry_cmp(const struct diff_options *diffopt,
 +                         const struct moved_entry *a,
 +                         const struct moved_entry *b,
 +                         const void *keydata)
 +{
 +      return !xdiff_compare_lines(a->es->line, a->es->len,
 +                                  b->es->line, b->es->len,
 +                                  diffopt->xdl_opts);
 +}
 +
 +static struct moved_entry *prepare_entry(struct diff_options *o,
 +                                       int line_no)
 +{
 +      struct moved_entry *ret = xmalloc(sizeof(*ret));
 +      struct emitted_diff_symbol *l = &o->emitted_symbols->buf[line_no];
 +
 +      ret->ent.hash = xdiff_hash_string(l->line, l->len, o->xdl_opts);
 +      ret->es = l;
 +      ret->next_line = NULL;
 +
 +      return ret;
 +}
 +
 +static void add_lines_to_move_detection(struct diff_options *o,
 +                                      struct hashmap *add_lines,
 +                                      struct hashmap *del_lines)
 +{
 +      struct moved_entry *prev_line = NULL;
 +
 +      int n;
 +      for (n = 0; n < o->emitted_symbols->nr; n++) {
 +              struct hashmap *hm;
 +              struct moved_entry *key;
 +
 +              switch (o->emitted_symbols->buf[n].s) {
 +              case DIFF_SYMBOL_PLUS:
 +                      hm = add_lines;
 +                      break;
 +              case DIFF_SYMBOL_MINUS:
 +                      hm = del_lines;
 +                      break;
 +              default:
 +                      prev_line = NULL;
 +                      continue;
 +              }
 +
 +              key = prepare_entry(o, n);
 +              if (prev_line && prev_line->es->s == o->emitted_symbols->buf[n].s)
 +                      prev_line->next_line = key;
 +
 +              hashmap_add(hm, key);
 +              prev_line = key;
 +      }
 +}
 +
 +static int shrink_potential_moved_blocks(struct moved_entry **pmb,
 +                                       int pmb_nr)
 +{
 +      int lp, rp;
 +
 +      /* Shrink the set of potential block to the remaining running */
 +      for (lp = 0, rp = pmb_nr - 1; lp <= rp;) {
 +              while (lp < pmb_nr && pmb[lp])
 +                      lp++;
 +              /* lp points at the first NULL now */
 +
 +              while (rp > -1 && !pmb[rp])
 +                      rp--;
 +              /* rp points at the last non-NULL */
 +
 +              if (lp < pmb_nr && rp > -1 && lp < rp) {
 +                      pmb[lp] = pmb[rp];
 +                      pmb[rp] = NULL;
 +                      rp--;
 +                      lp++;
 +              }
 +      }
 +
 +      /* Remember the number of running sets */
 +      return rp + 1;
 +}
 +
 +/*
 + * If o->color_moved is COLOR_MOVED_PLAIN, this function does nothing.
 + *
 + * Otherwise, if the last block has fewer alphanumeric characters than
 + * COLOR_MOVED_MIN_ALNUM_COUNT, unset DIFF_SYMBOL_MOVED_LINE on all lines in
 + * that block.
 + *
 + * The last block consists of the (n - block_length)'th line up to but not
 + * including the nth line.
 + *
 + * NEEDSWORK: This uses the same heuristic as blame_entry_score() in blame.c.
 + * Think of a way to unify them.
 + */
 +static void adjust_last_block(struct diff_options *o, int n, int block_length)
 +{
 +      int i, alnum_count = 0;
 +      if (o->color_moved == COLOR_MOVED_PLAIN)
 +              return;
 +      for (i = 1; i < block_length + 1; i++) {
 +              const char *c = o->emitted_symbols->buf[n - i].line;
 +              for (; *c; c++) {
 +                      if (!isalnum(*c))
 +                              continue;
 +                      alnum_count++;
 +                      if (alnum_count >= COLOR_MOVED_MIN_ALNUM_COUNT)
 +                              return;
 +              }
 +      }
 +      for (i = 1; i < block_length + 1; i++)
 +              o->emitted_symbols->buf[n - i].flags &= ~DIFF_SYMBOL_MOVED_LINE;
 +}
 +
 +/* Find blocks of moved code, delegate actual coloring decision to helper */
 +static void mark_color_as_moved(struct diff_options *o,
 +                              struct hashmap *add_lines,
 +                              struct hashmap *del_lines)
 +{
 +      struct moved_entry **pmb = NULL; /* potentially moved blocks */
 +      int pmb_nr = 0, pmb_alloc = 0;
 +      int n, flipped_block = 1, block_length = 0;
 +
 +
 +      for (n = 0; n < o->emitted_symbols->nr; n++) {
 +              struct hashmap *hm = NULL;
 +              struct moved_entry *key;
 +              struct moved_entry *match = NULL;
 +              struct emitted_diff_symbol *l = &o->emitted_symbols->buf[n];
 +              int i;
 +
 +              switch (l->s) {
 +              case DIFF_SYMBOL_PLUS:
 +                      hm = del_lines;
 +                      key = prepare_entry(o, n);
 +                      match = hashmap_get(hm, key, o);
 +                      free(key);
 +                      break;
 +              case DIFF_SYMBOL_MINUS:
 +                      hm = add_lines;
 +                      key = prepare_entry(o, n);
 +                      match = hashmap_get(hm, key, o);
 +                      free(key);
 +                      break;
 +              default:
 +                      flipped_block = 1;
 +              }
 +
 +              if (!match) {
 +                      adjust_last_block(o, n, block_length);
 +                      pmb_nr = 0;
 +                      block_length = 0;
 +                      continue;
 +              }
 +
 +              l->flags |= DIFF_SYMBOL_MOVED_LINE;
 +
 +              if (o->color_moved == COLOR_MOVED_PLAIN)
 +                      continue;
 +
 +              /* Check any potential block runs, advance each or nullify */
 +              for (i = 0; i < pmb_nr; i++) {
 +                      struct moved_entry *p = pmb[i];
 +                      struct moved_entry *pnext = (p && p->next_line) ?
 +                                      p->next_line : NULL;
 +                      if (pnext && !hm->cmpfn(o, pnext, match, NULL)) {
 +                              pmb[i] = p->next_line;
 +                      } else {
 +                              pmb[i] = NULL;
 +                      }
 +              }
 +
 +              pmb_nr = shrink_potential_moved_blocks(pmb, pmb_nr);
 +
 +              if (pmb_nr == 0) {
 +                      /*
 +                       * The current line is the start of a new block.
 +                       * Setup the set of potential blocks.
 +                       */
 +                      for (; match; match = hashmap_get_next(hm, match)) {
 +                              ALLOC_GROW(pmb, pmb_nr + 1, pmb_alloc);
 +                              pmb[pmb_nr++] = match;
 +                      }
 +
 +                      flipped_block = (flipped_block + 1) % 2;
 +
 +                      adjust_last_block(o, n, block_length);
 +                      block_length = 0;
 +              }
 +
 +              block_length++;
 +
 +              if (flipped_block)
 +                      l->flags |= DIFF_SYMBOL_MOVED_LINE_ALT;
 +      }
 +      adjust_last_block(o, n, block_length);
 +
 +      free(pmb);
 +}
 +
 +#define DIFF_SYMBOL_MOVED_LINE_ZEBRA_MASK \
 +  (DIFF_SYMBOL_MOVED_LINE | DIFF_SYMBOL_MOVED_LINE_ALT)
 +static void dim_moved_lines(struct diff_options *o)
 +{
 +      int n;
 +      for (n = 0; n < o->emitted_symbols->nr; n++) {
 +              struct emitted_diff_symbol *prev = (n != 0) ?
 +                              &o->emitted_symbols->buf[n - 1] : NULL;
 +              struct emitted_diff_symbol *l = &o->emitted_symbols->buf[n];
 +              struct emitted_diff_symbol *next =
 +                              (n < o->emitted_symbols->nr - 1) ?
 +                              &o->emitted_symbols->buf[n + 1] : NULL;
 +
 +              /* Not a plus or minus line? */
 +              if (l->s != DIFF_SYMBOL_PLUS && l->s != DIFF_SYMBOL_MINUS)
 +                      continue;
 +
 +              /* Not a moved line? */
 +              if (!(l->flags & DIFF_SYMBOL_MOVED_LINE))
 +                      continue;
 +
 +              /*
 +               * If prev or next are not a plus or minus line,
 +               * pretend they don't exist
 +               */
 +              if (prev && prev->s != DIFF_SYMBOL_PLUS &&
 +                          prev->s != DIFF_SYMBOL_MINUS)
 +                      prev = NULL;
 +              if (next && next->s != DIFF_SYMBOL_PLUS &&
 +                          next->s != DIFF_SYMBOL_MINUS)
 +                      next = NULL;
 +
 +              /* Inside a block? */
 +              if ((prev &&
 +                  (prev->flags & DIFF_SYMBOL_MOVED_LINE_ZEBRA_MASK) ==
 +                  (l->flags & DIFF_SYMBOL_MOVED_LINE_ZEBRA_MASK)) &&
 +                  (next &&
 +                  (next->flags & DIFF_SYMBOL_MOVED_LINE_ZEBRA_MASK) ==
 +                  (l->flags & DIFF_SYMBOL_MOVED_LINE_ZEBRA_MASK))) {
 +                      l->flags |= DIFF_SYMBOL_MOVED_LINE_UNINTERESTING;
 +                      continue;
 +              }
 +
 +              /* Check if we are at an interesting bound: */
 +              if (prev && (prev->flags & DIFF_SYMBOL_MOVED_LINE) &&
 +                  (prev->flags & DIFF_SYMBOL_MOVED_LINE_ALT) !=
 +                     (l->flags & DIFF_SYMBOL_MOVED_LINE_ALT))
 +                      continue;
 +              if (next && (next->flags & DIFF_SYMBOL_MOVED_LINE) &&
 +                  (next->flags & DIFF_SYMBOL_MOVED_LINE_ALT) !=
 +                     (l->flags & DIFF_SYMBOL_MOVED_LINE_ALT))
 +                      continue;
 +
 +              /*
 +               * The boundary to prev and next are not interesting,
 +               * so this line is not interesting as a whole
 +               */
 +              l->flags |= DIFF_SYMBOL_MOVED_LINE_UNINTERESTING;
 +      }
 +}
 +
 +static void emit_line_ws_markup(struct diff_options *o,
 +                              const char *set, const char *reset,
 +                              const char *line, int len, char sign,
 +                              unsigned ws_rule, int blank_at_eof)
  {
 -      const char *set = diff_get_color(ecbdata->color_diff, color);
        const char *ws = NULL;
  
 -      if (ecbdata->opt->ws_error_highlight & ws_error_highlight) {
 -              ws = diff_get_color(ecbdata->color_diff, DIFF_WHITESPACE);
 +      if (o->ws_error_highlight & ws_rule) {
 +              ws = diff_get_color_opt(o, DIFF_WHITESPACE);
                if (!*ws)
                        ws = NULL;
        }
  
        if (!ws)
 -              emit_line_0(ecbdata->opt, set, reset, sign, line, len);
 -      else if (sign == '+' && new_blank_line_at_eof(ecbdata, line, len))
 +              emit_line_0(o, set, reset, sign, line, len);
 +      else if (blank_at_eof)
                /* Blank line at EOF - paint '+' as well */
 -              emit_line_0(ecbdata->opt, ws, reset, sign, line, len);
 +              emit_line_0(o, ws, reset, sign, line, len);
        else {
                /* Emit just the prefix, then the rest. */
 -              emit_line_0(ecbdata->opt, set, reset, sign, "", 0);
 -              ws_check_emit(line, len, ecbdata->ws_rule,
 -                            ecbdata->opt->file, set, reset, ws);
 +              emit_line_0(o, set, reset, sign, "", 0);
 +              ws_check_emit(line, len, ws_rule,
 +                            o->file, set, reset, ws);
        }
  }
  
 +static void emit_diff_symbol_from_struct(struct diff_options *o,
 +                                       struct emitted_diff_symbol *eds)
 +{
 +      static const char *nneof = " No newline at end of file\n";
 +      const char *context, *reset, *set, *meta, *fraginfo;
 +      struct strbuf sb = STRBUF_INIT;
 +
 +      enum diff_symbol s = eds->s;
 +      const char *line = eds->line;
 +      int len = eds->len;
 +      unsigned flags = eds->flags;
 +
 +      switch (s) {
 +      case DIFF_SYMBOL_NO_LF_EOF:
 +              context = diff_get_color_opt(o, DIFF_CONTEXT);
 +              reset = diff_get_color_opt(o, DIFF_RESET);
 +              putc('\n', o->file);
 +              emit_line_0(o, context, reset, '\\',
 +                          nneof, strlen(nneof));
 +              break;
 +      case DIFF_SYMBOL_SUBMODULE_HEADER:
 +      case DIFF_SYMBOL_SUBMODULE_ERROR:
 +      case DIFF_SYMBOL_SUBMODULE_PIPETHROUGH:
 +      case DIFF_SYMBOL_STATS_SUMMARY_INSERTS_DELETES:
 +      case DIFF_SYMBOL_SUMMARY:
 +      case DIFF_SYMBOL_STATS_LINE:
 +      case DIFF_SYMBOL_BINARY_DIFF_BODY:
 +      case DIFF_SYMBOL_CONTEXT_FRAGINFO:
 +              emit_line(o, "", "", line, len);
 +              break;
 +      case DIFF_SYMBOL_CONTEXT_INCOMPLETE:
 +      case DIFF_SYMBOL_CONTEXT_MARKER:
 +              context = diff_get_color_opt(o, DIFF_CONTEXT);
 +              reset = diff_get_color_opt(o, DIFF_RESET);
 +              emit_line(o, context, reset, line, len);
 +              break;
 +      case DIFF_SYMBOL_SEPARATOR:
 +              fprintf(o->file, "%s%c",
 +                      diff_line_prefix(o),
 +                      o->line_termination);
 +              break;
 +      case DIFF_SYMBOL_CONTEXT:
 +              set = diff_get_color_opt(o, DIFF_CONTEXT);
 +              reset = diff_get_color_opt(o, DIFF_RESET);
 +              emit_line_ws_markup(o, set, reset, line, len, ' ',
 +                                  flags & (DIFF_SYMBOL_CONTENT_WS_MASK), 0);
 +              break;
 +      case DIFF_SYMBOL_PLUS:
 +              switch (flags & (DIFF_SYMBOL_MOVED_LINE |
 +                               DIFF_SYMBOL_MOVED_LINE_ALT |
 +                               DIFF_SYMBOL_MOVED_LINE_UNINTERESTING)) {
 +              case DIFF_SYMBOL_MOVED_LINE |
 +                   DIFF_SYMBOL_MOVED_LINE_ALT |
 +                   DIFF_SYMBOL_MOVED_LINE_UNINTERESTING:
 +                      set = diff_get_color_opt(o, DIFF_FILE_NEW_MOVED_ALT_DIM);
 +                      break;
 +              case DIFF_SYMBOL_MOVED_LINE |
 +                   DIFF_SYMBOL_MOVED_LINE_ALT:
 +                      set = diff_get_color_opt(o, DIFF_FILE_NEW_MOVED_ALT);
 +                      break;
 +              case DIFF_SYMBOL_MOVED_LINE |
 +                   DIFF_SYMBOL_MOVED_LINE_UNINTERESTING:
 +                      set = diff_get_color_opt(o, DIFF_FILE_NEW_MOVED_DIM);
 +                      break;
 +              case DIFF_SYMBOL_MOVED_LINE:
 +                      set = diff_get_color_opt(o, DIFF_FILE_NEW_MOVED);
 +                      break;
 +              default:
 +                      set = diff_get_color_opt(o, DIFF_FILE_NEW);
 +              }
 +              reset = diff_get_color_opt(o, DIFF_RESET);
 +              emit_line_ws_markup(o, set, reset, line, len, '+',
 +                                  flags & DIFF_SYMBOL_CONTENT_WS_MASK,
 +                                  flags & DIFF_SYMBOL_CONTENT_BLANK_LINE_EOF);
 +              break;
 +      case DIFF_SYMBOL_MINUS:
 +              switch (flags & (DIFF_SYMBOL_MOVED_LINE |
 +                               DIFF_SYMBOL_MOVED_LINE_ALT |
 +                               DIFF_SYMBOL_MOVED_LINE_UNINTERESTING)) {
 +              case DIFF_SYMBOL_MOVED_LINE |
 +                   DIFF_SYMBOL_MOVED_LINE_ALT |
 +                   DIFF_SYMBOL_MOVED_LINE_UNINTERESTING:
 +                      set = diff_get_color_opt(o, DIFF_FILE_OLD_MOVED_ALT_DIM);
 +                      break;
 +              case DIFF_SYMBOL_MOVED_LINE |
 +                   DIFF_SYMBOL_MOVED_LINE_ALT:
 +                      set = diff_get_color_opt(o, DIFF_FILE_OLD_MOVED_ALT);
 +                      break;
 +              case DIFF_SYMBOL_MOVED_LINE |
 +                   DIFF_SYMBOL_MOVED_LINE_UNINTERESTING:
 +                      set = diff_get_color_opt(o, DIFF_FILE_OLD_MOVED_DIM);
 +                      break;
 +              case DIFF_SYMBOL_MOVED_LINE:
 +                      set = diff_get_color_opt(o, DIFF_FILE_OLD_MOVED);
 +                      break;
 +              default:
 +                      set = diff_get_color_opt(o, DIFF_FILE_OLD);
 +              }
 +              reset = diff_get_color_opt(o, DIFF_RESET);
 +              emit_line_ws_markup(o, set, reset, line, len, '-',
 +                                  flags & DIFF_SYMBOL_CONTENT_WS_MASK, 0);
 +              break;
 +      case DIFF_SYMBOL_WORDS_PORCELAIN:
 +              context = diff_get_color_opt(o, DIFF_CONTEXT);
 +              reset = diff_get_color_opt(o, DIFF_RESET);
 +              emit_line(o, context, reset, line, len);
 +              fputs("~\n", o->file);
 +              break;
 +      case DIFF_SYMBOL_WORDS:
 +              context = diff_get_color_opt(o, DIFF_CONTEXT);
 +              reset = diff_get_color_opt(o, DIFF_RESET);
 +              /*
 +               * Skip the prefix character, if any.  With
 +               * diff_suppress_blank_empty, there may be
 +               * none.
 +               */
 +              if (line[0] != '\n') {
 +                      line++;
 +                      len--;
 +              }
 +              emit_line(o, context, reset, line, len);
 +              break;
 +      case DIFF_SYMBOL_FILEPAIR_PLUS:
 +              meta = diff_get_color_opt(o, DIFF_METAINFO);
 +              reset = diff_get_color_opt(o, DIFF_RESET);
 +              fprintf(o->file, "%s%s+++ %s%s%s\n", diff_line_prefix(o), meta,
 +                      line, reset,
 +                      strchr(line, ' ') ? "\t" : "");
 +              break;
 +      case DIFF_SYMBOL_FILEPAIR_MINUS:
 +              meta = diff_get_color_opt(o, DIFF_METAINFO);
 +              reset = diff_get_color_opt(o, DIFF_RESET);
 +              fprintf(o->file, "%s%s--- %s%s%s\n", diff_line_prefix(o), meta,
 +                      line, reset,
 +                      strchr(line, ' ') ? "\t" : "");
 +              break;
 +      case DIFF_SYMBOL_BINARY_FILES:
 +      case DIFF_SYMBOL_HEADER:
 +              fprintf(o->file, "%s", line);
 +              break;
 +      case DIFF_SYMBOL_BINARY_DIFF_HEADER:
 +              fprintf(o->file, "%sGIT binary patch\n", diff_line_prefix(o));
 +              break;
 +      case DIFF_SYMBOL_BINARY_DIFF_HEADER_DELTA:
 +              fprintf(o->file, "%sdelta %s\n", diff_line_prefix(o), line);
 +              break;
 +      case DIFF_SYMBOL_BINARY_DIFF_HEADER_LITERAL:
 +              fprintf(o->file, "%sliteral %s\n", diff_line_prefix(o), line);
 +              break;
 +      case DIFF_SYMBOL_BINARY_DIFF_FOOTER:
 +              fputs(diff_line_prefix(o), o->file);
 +              fputc('\n', o->file);
 +              break;
 +      case DIFF_SYMBOL_REWRITE_DIFF:
 +              fraginfo = diff_get_color(o->use_color, DIFF_FRAGINFO);
 +              reset = diff_get_color_opt(o, DIFF_RESET);
 +              emit_line(o, fraginfo, reset, line, len);
 +              break;
 +      case DIFF_SYMBOL_SUBMODULE_ADD:
 +              set = diff_get_color_opt(o, DIFF_FILE_NEW);
 +              reset = diff_get_color_opt(o, DIFF_RESET);
 +              emit_line(o, set, reset, line, len);
 +              break;
 +      case DIFF_SYMBOL_SUBMODULE_DEL:
 +              set = diff_get_color_opt(o, DIFF_FILE_OLD);
 +              reset = diff_get_color_opt(o, DIFF_RESET);
 +              emit_line(o, set, reset, line, len);
 +              break;
 +      case DIFF_SYMBOL_SUBMODULE_UNTRACKED:
 +              fprintf(o->file, "%sSubmodule %s contains untracked content\n",
 +                      diff_line_prefix(o), line);
 +              break;
 +      case DIFF_SYMBOL_SUBMODULE_MODIFIED:
 +              fprintf(o->file, "%sSubmodule %s contains modified content\n",
 +                      diff_line_prefix(o), line);
 +              break;
 +      case DIFF_SYMBOL_STATS_SUMMARY_NO_FILES:
 +              emit_line(o, "", "", " 0 files changed\n",
 +                        strlen(" 0 files changed\n"));
 +              break;
 +      case DIFF_SYMBOL_STATS_SUMMARY_ABBREV:
 +              emit_line(o, "", "", " ...\n", strlen(" ...\n"));
 +              break;
 +      case DIFF_SYMBOL_WORD_DIFF:
 +              fprintf(o->file, "%.*s", len, line);
 +              break;
 +      case DIFF_SYMBOL_STAT_SEP:
 +              fputs(o->stat_sep, o->file);
 +              break;
 +      default:
 +              die("BUG: unknown diff symbol");
 +      }
 +      strbuf_release(&sb);
 +}
 +
 +static void emit_diff_symbol(struct diff_options *o, enum diff_symbol s,
 +                           const char *line, int len, unsigned flags)
 +{
 +      struct emitted_diff_symbol e = {line, len, flags, s};
 +
 +      if (o->emitted_symbols)
 +              append_emitted_diff_symbol(o, &e);
 +      else
 +              emit_diff_symbol_from_struct(o, &e);
 +}
 +
 +void diff_emit_submodule_del(struct diff_options *o, const char *line)
 +{
 +      emit_diff_symbol(o, DIFF_SYMBOL_SUBMODULE_DEL, line, strlen(line), 0);
 +}
 +
 +void diff_emit_submodule_add(struct diff_options *o, const char *line)
 +{
 +      emit_diff_symbol(o, DIFF_SYMBOL_SUBMODULE_ADD, line, strlen(line), 0);
 +}
 +
 +void diff_emit_submodule_untracked(struct diff_options *o, const char *path)
 +{
 +      emit_diff_symbol(o, DIFF_SYMBOL_SUBMODULE_UNTRACKED,
 +                       path, strlen(path), 0);
 +}
 +
 +void diff_emit_submodule_modified(struct diff_options *o, const char *path)
 +{
 +      emit_diff_symbol(o, DIFF_SYMBOL_SUBMODULE_MODIFIED,
 +                       path, strlen(path), 0);
 +}
 +
 +void diff_emit_submodule_header(struct diff_options *o, const char *header)
 +{
 +      emit_diff_symbol(o, DIFF_SYMBOL_SUBMODULE_HEADER,
 +                       header, strlen(header), 0);
 +}
 +
 +void diff_emit_submodule_error(struct diff_options *o, const char *err)
 +{
 +      emit_diff_symbol(o, DIFF_SYMBOL_SUBMODULE_ERROR, err, strlen(err), 0);
 +}
 +
 +void diff_emit_submodule_pipethrough(struct diff_options *o,
 +                                   const char *line, int len)
 +{
 +      emit_diff_symbol(o, DIFF_SYMBOL_SUBMODULE_PIPETHROUGH, line, len, 0);
 +}
 +
 +static int new_blank_line_at_eof(struct emit_callback *ecbdata, const char *line, int len)
 +{
 +      if (!((ecbdata->ws_rule & WS_BLANK_AT_EOF) &&
 +            ecbdata->blank_at_eof_in_preimage &&
 +            ecbdata->blank_at_eof_in_postimage &&
 +            ecbdata->blank_at_eof_in_preimage <= ecbdata->lno_in_preimage &&
 +            ecbdata->blank_at_eof_in_postimage <= ecbdata->lno_in_postimage))
 +              return 0;
 +      return ws_blank_line(line, len, ecbdata->ws_rule);
 +}
 +
  static void emit_add_line(const char *reset,
                          struct emit_callback *ecbdata,
                          const char *line, int len)
  {
 -      emit_line_checked(reset, ecbdata, line, len,
 -                        DIFF_FILE_NEW, WSEH_NEW, '+');
 +      unsigned flags = WSEH_NEW | ecbdata->ws_rule;
 +      if (new_blank_line_at_eof(ecbdata, line, len))
 +              flags |= DIFF_SYMBOL_CONTENT_BLANK_LINE_EOF;
 +
 +      emit_diff_symbol(ecbdata->opt, DIFF_SYMBOL_PLUS, line, len, flags);
  }
  
  static void emit_del_line(const char *reset,
                          struct emit_callback *ecbdata,
                          const char *line, int len)
  {
 -      emit_line_checked(reset, ecbdata, line, len,
 -                        DIFF_FILE_OLD, WSEH_OLD, '-');
 +      unsigned flags = WSEH_OLD | ecbdata->ws_rule;
 +      emit_diff_symbol(ecbdata->opt, DIFF_SYMBOL_MINUS, line, len, flags);
  }
  
  static void emit_context_line(const char *reset,
                              struct emit_callback *ecbdata,
                              const char *line, int len)
  {
 -      emit_line_checked(reset, ecbdata, line, len,
 -                        DIFF_CONTEXT, WSEH_CONTEXT, ' ');
 +      unsigned flags = WSEH_CONTEXT | ecbdata->ws_rule;
 +      emit_diff_symbol(ecbdata->opt, DIFF_SYMBOL_CONTEXT, line, len, flags);
  }
  
  static void emit_hunk_header(struct emit_callback *ecbdata,
        if (len < 10 ||
            memcmp(line, atat, 2) ||
            !(ep = memmem(line + 2, len - 2, atat, 2))) {
 -              emit_line(ecbdata->opt, context, reset, line, len);
 +              emit_diff_symbol(ecbdata->opt,
 +                               DIFF_SYMBOL_CONTEXT_MARKER, line, len, 0);
                return;
        }
        ep += 2; /* skip over @@ */
        }
  
        strbuf_add(&msgbuf, line + len, org_len - len);
 -      emit_line(ecbdata->opt, "", "", msgbuf.buf, msgbuf.len);
 +      strbuf_complete_line(&msgbuf);
 +      emit_diff_symbol(ecbdata->opt,
 +                       DIFF_SYMBOL_CONTEXT_FRAGINFO, msgbuf.buf, msgbuf.len, 0);
        strbuf_release(&msgbuf);
  }
  
@@@ -1350,23 -693,23 +1350,23 @@@ static void remove_tempfile(void
  {
        int i;
        for (i = 0; i < ARRAY_SIZE(diff_temp); i++) {
 -              if (is_tempfile_active(&diff_temp[i].tempfile))
 +              if (is_tempfile_active(diff_temp[i].tempfile))
                        delete_tempfile(&diff_temp[i].tempfile);
                diff_temp[i].name = NULL;
        }
  }
  
 -static void print_line_count(FILE *file, int count)
 +static void add_line_count(struct strbuf *out, int count)
  {
        switch (count) {
        case 0:
 -              fprintf(file, "0,0");
 +              strbuf_addstr(out, "0,0");
                break;
        case 1:
 -              fprintf(file, "1");
 +              strbuf_addstr(out, "1");
                break;
        default:
 -              fprintf(file, "1,%d", count);
 +              strbuf_addf(out, "1,%d", count);
                break;
        }
  }
@@@ -1375,6 -718,7 +1375,6 @@@ static void emit_rewrite_lines(struct e
                               int prefix, const char *data, int size)
  {
        const char *endp = NULL;
 -      static const char *nneof = " No newline at end of file\n";
        const char *reset = diff_get_color(ecb->color_diff, DIFF_RESET);
  
        while (0 < size) {
                size -= len;
                data += len;
        }
 -      if (!endp) {
 -              const char *context = diff_get_color(ecb->color_diff,
 -                                                   DIFF_CONTEXT);
 -              putc('\n', ecb->opt->file);
 -              emit_line_0(ecb->opt, context, reset, '\\',
 -                          nneof, strlen(nneof));
 -      }
 +      if (!endp)
 +              emit_diff_symbol(ecb->opt, DIFF_SYMBOL_NO_LF_EOF, NULL, 0, 0);
  }
  
  static void emit_rewrite_diff(const char *name_a,
                              struct diff_options *o)
  {
        int lc_a, lc_b;
 -      const char *name_a_tab, *name_b_tab;
 -      const char *metainfo = diff_get_color(o->use_color, DIFF_METAINFO);
 -      const char *fraginfo = diff_get_color(o->use_color, DIFF_FRAGINFO);
 -      const char *reset = diff_get_color(o->use_color, DIFF_RESET);
        static struct strbuf a_name = STRBUF_INIT, b_name = STRBUF_INIT;
        const char *a_prefix, *b_prefix;
        char *data_one, *data_two;
        size_t size_one, size_two;
        struct emit_callback ecbdata;
 -      const char *line_prefix = diff_line_prefix(o);
 +      struct strbuf out = STRBUF_INIT;
  
 -      if (diff_mnemonic_prefix && DIFF_OPT_TST(o, REVERSE_DIFF)) {
 +      if (diff_mnemonic_prefix && o->flags.reverse_diff) {
                a_prefix = o->b_prefix;
                b_prefix = o->a_prefix;
        } else {
  
        name_a += (*name_a == '/');
        name_b += (*name_b == '/');
 -      name_a_tab = strchr(name_a, ' ') ? "\t" : "";
 -      name_b_tab = strchr(name_b, ' ') ? "\t" : "";
  
        strbuf_reset(&a_name);
        strbuf_reset(&b_name);
  
        lc_a = count_lines(data_one, size_one);
        lc_b = count_lines(data_two, size_two);
 -      fprintf(o->file,
 -              "%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);
 +
 +      emit_diff_symbol(o, DIFF_SYMBOL_FILEPAIR_MINUS,
 +                       a_name.buf, a_name.len, 0);
 +      emit_diff_symbol(o, DIFF_SYMBOL_FILEPAIR_PLUS,
 +                       b_name.buf, b_name.len, 0);
 +
 +      strbuf_addstr(&out, "@@ -");
        if (!o->irreversible_delete)
 -              print_line_count(o->file, lc_a);
 +              add_line_count(&out, lc_a);
        else
 -              fprintf(o->file, "?,?");
 -      fprintf(o->file, " +");
 -      print_line_count(o->file, lc_b);
 -      fprintf(o->file, " @@%s\n", reset);
 +              strbuf_addstr(&out, "?,?");
 +      strbuf_addstr(&out, " +");
 +      add_line_count(&out, lc_b);
 +      strbuf_addstr(&out, " @@\n");
 +      emit_diff_symbol(o, DIFF_SYMBOL_REWRITE_DIFF, out.buf, out.len, 0);
 +      strbuf_release(&out);
 +
        if (lc_a && !o->irreversible_delete)
                emit_rewrite_lines(&ecbdata, '-', data_one, size_one);
        if (lc_b)
  
  struct diff_words_buffer {
        mmfile_t text;
 -      long alloc;
 +      unsigned long alloc;
        struct diff_words_orig {
                const char *begin, *end;
        } *orig;
@@@ -1524,49 -874,37 +1524,49 @@@ struct diff_words_data 
        struct diff_words_style *style;
  };
  
 -static int fn_out_diff_words_write_helper(FILE *fp,
 +static int fn_out_diff_words_write_helper(struct diff_options *o,
                                          struct diff_words_style_elem *st_el,
                                          const char *newline,
 -                                        size_t count, const char *buf,
 -                                        const char *line_prefix)
 +                                        size_t count, const char *buf)
  {
        int print = 0;
 +      struct strbuf sb = STRBUF_INIT;
  
        while (count) {
                char *p = memchr(buf, '\n', count);
                if (print)
 -                      fputs(line_prefix, fp);
 +                      strbuf_addstr(&sb, diff_line_prefix(o));
 +
                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;
 +                      const char *reset = st_el->color && *st_el->color ?
 +                                          GIT_COLOR_RESET : NULL;
 +                      if (st_el->color && *st_el->color)
 +                              strbuf_addstr(&sb, st_el->color);
 +                      strbuf_addstr(&sb, st_el->prefix);
 +                      strbuf_add(&sb, buf, p ? p - buf : count);
 +                      strbuf_addstr(&sb, st_el->suffix);
 +                      if (reset)
 +                              strbuf_addstr(&sb, reset);
                }
                if (!p)
 -                      return 0;
 -              if (fputs(newline, fp) < 0)
 -                      return -1;
 +                      goto out;
 +
 +              strbuf_addstr(&sb, newline);
                count -= p + 1 - buf;
                buf = p + 1;
                print = 1;
 +              if (count) {
 +                      emit_diff_symbol(o, DIFF_SYMBOL_WORD_DIFF,
 +                                       sb.buf, sb.len, 0);
 +                      strbuf_reset(&sb);
 +              }
        }
 +
 +out:
 +      if (sb.len)
 +              emit_diff_symbol(o, DIFF_SYMBOL_WORD_DIFF,
 +                               sb.buf, sb.len, 0);
 +      strbuf_release(&sb);
        return 0;
  }
  
@@@ -1648,20 -986,24 +1648,20 @@@ static void fn_out_diff_words_aux(void 
                fputs(line_prefix, diff_words->opt->file);
        }
        if (diff_words->current_plus != plus_begin) {
 -              fn_out_diff_words_write_helper(diff_words->opt->file,
 +              fn_out_diff_words_write_helper(diff_words->opt,
                                &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);
 +                              diff_words->current_plus);
        }
        if (minus_begin != minus_end) {
 -              fn_out_diff_words_write_helper(diff_words->opt->file,
 +              fn_out_diff_words_write_helper(diff_words->opt,
                                &style->old, style->newline,
 -                              minus_end - minus_begin, minus_begin,
 -                              line_prefix);
 +                              minus_end - minus_begin, minus_begin);
        }
        if (plus_begin != plus_end) {
 -              fn_out_diff_words_write_helper(diff_words->opt->file,
 +              fn_out_diff_words_write_helper(diff_words->opt,
                                &style->new, style->newline,
 -                              plus_end - plus_begin, plus_begin,
 -                              line_prefix);
 +                              plus_end - plus_begin, plus_begin);
        }
  
        diff_words->current_plus = plus_end;
@@@ -1755,12 -1097,11 +1755,12 @@@ static void diff_words_show(struct diff
  
        /* special case: only removal */
        if (!diff_words->plus.text.size) {
 -              fputs(line_prefix, diff_words->opt->file);
 -              fn_out_diff_words_write_helper(diff_words->opt->file,
 +              emit_diff_symbol(diff_words->opt, DIFF_SYMBOL_WORD_DIFF,
 +                               line_prefix, strlen(line_prefix), 0);
 +              fn_out_diff_words_write_helper(diff_words->opt,
                        &style->old, style->newline,
                        diff_words->minus.text.size,
 -                      diff_words->minus.text.ptr, line_prefix);
 +                      diff_words->minus.text.ptr);
                diff_words->minus.text.size = 0;
                return;
        }
        if (diff_words->current_plus != diff_words->plus.text.ptr +
                        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,
 +                      emit_diff_symbol(diff_words->opt, DIFF_SYMBOL_WORD_DIFF,
 +                                       line_prefix, strlen(line_prefix), 0);
 +              fn_out_diff_words_write_helper(diff_words->opt,
                        &style->ctx, style->newline,
                        diff_words->plus.text.ptr + diff_words->plus.text.size
 -                      - diff_words->current_plus, diff_words->current_plus,
 -                      line_prefix);
 +                      - diff_words->current_plus, diff_words->current_plus);
        }
        diff_words->minus.text.size = diff_words->plus.text.size = 0;
  }
  /* In "color-words" mode, show word-diff of words accumulated in the buffer */
  static void diff_words_flush(struct emit_callback *ecbdata)
  {
 +      struct diff_options *wo = ecbdata->diff_words->opt;
 +
        if (ecbdata->diff_words->minus.text.size ||
            ecbdata->diff_words->plus.text.size)
                diff_words_show(ecbdata->diff_words);
 +
 +      if (wo->emitted_symbols) {
 +              struct diff_options *o = ecbdata->opt;
 +              struct emitted_diff_symbols *wol = wo->emitted_symbols;
 +              int i;
 +
 +              /*
 +               * NEEDSWORK:
 +               * Instead of appending each, concat all words to a line?
 +               */
 +              for (i = 0; i < wol->nr; i++)
 +                      append_emitted_diff_symbol(o, &wol->buf[i]);
 +
 +              for (i = 0; i < wol->nr; i++)
 +                      free((void *)wol->buf[i].line);
 +
 +              wol->nr = 0;
 +      }
  }
  
  static void diff_filespec_load_driver(struct diff_filespec *one)
@@@ -1854,11 -1175,6 +1854,11 @@@ static void init_diff_words_data(struc
                xcalloc(1, sizeof(struct diff_words_data));
        ecbdata->diff_words->type = o->word_diff;
        ecbdata->diff_words->opt = o;
 +
 +      if (orig_opts->emitted_symbols)
 +              o->emitted_symbols =
 +                      xcalloc(1, sizeof(struct emitted_diff_symbols));
 +
        if (!o->word_regex)
                o->word_regex = userdiff_word_regex(one);
        if (!o->word_regex)
@@@ -1893,7 -1209,6 +1893,7 @@@ static void free_diff_words_data(struc
  {
        if (ecbdata->diff_words) {
                diff_words_flush(ecbdata);
 +              free (ecbdata->diff_words->opt->emitted_symbols);
                free (ecbdata->diff_words->opt);
                free (ecbdata->diff_words->minus.text.ptr);
                free (ecbdata->diff_words->minus.orig);
                        regfree(ecbdata->diff_words->word_regex);
                        free(ecbdata->diff_words->word_regex);
                }
 -              free(ecbdata->diff_words);
 -              ecbdata->diff_words = NULL;
 +              FREE_AND_NULL(ecbdata->diff_words);
        }
  }
  
@@@ -1930,6 -1246,8 +1930,6 @@@ static unsigned long sane_truncate_line
        unsigned long allot;
        size_t l = len;
  
 -      if (ecb->truncate)
 -              return ecb->truncate(line, len);
        cp = line;
        allot = l;
        while (0 < l) {
@@@ -1958,25 -1276,30 +1958,25 @@@ static void find_lno(const char *line, 
  static void fn_out_consume(void *priv, char *line, unsigned long len)
  {
        struct emit_callback *ecbdata = priv;
 -      const char *meta = diff_get_color(ecbdata->color_diff, DIFF_METAINFO);
 -      const char *context = diff_get_color(ecbdata->color_diff, DIFF_CONTEXT);
        const char *reset = diff_get_color(ecbdata->color_diff, DIFF_RESET);
        struct diff_options *o = ecbdata->opt;
 -      const char *line_prefix = diff_line_prefix(o);
  
        o->found_changes = 1;
  
        if (ecbdata->header) {
 -              fprintf(o->file, "%s", ecbdata->header->buf);
 +              emit_diff_symbol(o, DIFF_SYMBOL_HEADER,
 +                               ecbdata->header->buf, ecbdata->header->len, 0);
                strbuf_reset(ecbdata->header);
                ecbdata->header = NULL;
        }
  
        if (ecbdata->label_path[0]) {
 -              const char *name_a_tab, *name_b_tab;
 -
 -              name_a_tab = strchr(ecbdata->label_path[0], ' ') ? "\t" : "";
 -              name_b_tab = strchr(ecbdata->label_path[1], ' ') ? "\t" : "";
 -
 -              fprintf(o->file, "%s%s--- %s%s%s\n",
 -                      line_prefix, meta, ecbdata->label_path[0], reset, name_a_tab);
 -              fprintf(o->file, "%s%s+++ %s%s%s\n",
 -                      line_prefix, meta, ecbdata->label_path[1], reset, name_b_tab);
 +              emit_diff_symbol(o, DIFF_SYMBOL_FILEPAIR_MINUS,
 +                               ecbdata->label_path[0],
 +                               strlen(ecbdata->label_path[0]), 0);
 +              emit_diff_symbol(o, DIFF_SYMBOL_FILEPAIR_PLUS,
 +                               ecbdata->label_path[1],
 +                               strlen(ecbdata->label_path[1]), 0);
                ecbdata->label_path[0] = ecbdata->label_path[1] = NULL;
        }
  
                len = sane_truncate_line(ecbdata, line, len);
                find_lno(line, ecbdata);
                emit_hunk_header(ecbdata, line, len);
 -              if (line[len-1] != '\n')
 -                      putc('\n', o->file);
                return;
        }
  
        if (ecbdata->diff_words) {
 +              enum diff_symbol s =
 +                      ecbdata->diff_words->type == DIFF_WORDS_PORCELAIN ?
 +                      DIFF_SYMBOL_WORDS_PORCELAIN : DIFF_SYMBOL_WORDS;
                if (line[0] == '-') {
                        diff_words_append(line, len,
                                          &ecbdata->diff_words->minus);
                        return;
                }
                diff_words_flush(ecbdata);
 -              if (ecbdata->diff_words->type == DIFF_WORDS_PORCELAIN) {
 -                      emit_line(o, context, reset, line, len);
 -                      fputs("~\n", o->file);
 -              } else {
 -                      /*
 -                       * Skip the prefix character, if any.  With
 -                       * diff_suppress_blank_empty, there may be
 -                       * none.
 -                       */
 -                      if (line[0] != '\n') {
 -                            line++;
 -                            len--;
 -                      }
 -                      emit_line(o, context, reset, line, len);
 -              }
 +              emit_diff_symbol(o, s, line, len, 0);
                return;
        }
  
        default:
                /* incomplete line at the end */
                ecbdata->lno_in_preimage++;
 -              emit_line(o, diff_get_color(ecbdata->color_diff, DIFF_CONTEXT),
 -                        reset, line, len);
 +              emit_diff_symbol(o, DIFF_SYMBOL_CONTEXT_INCOMPLETE,
 +                               line, len, 0);
                break;
        }
  }
@@@ -2185,14 -1521,20 +2185,14 @@@ static int scale_linear(int it, int wid
        return 1 + (it * (width - 1) / max_change);
  }
  
 -static void show_name(FILE *file,
 -                    const char *prefix, const char *name, int len)
 -{
 -      fprintf(file, " %s%-*s |", prefix, len, name);
 -}
 -
 -static void show_graph(FILE *file, char ch, int cnt, const char *set, const char *reset)
 +static void show_graph(struct strbuf *out, char ch, int cnt,
 +                     const char *set, const char *reset)
  {
        if (cnt <= 0)
                return;
 -      fprintf(file, "%s", set);
 -      while (cnt--)
 -              putc(ch, file);
 -      fprintf(file, "%s", reset);
 +      strbuf_addstr(out, set);
 +      strbuf_addchars(out, ch, cnt);
 +      strbuf_addstr(out, reset);
  }
  
  static void fill_print_name(struct diffstat_file *file)
        file->print_name = pname;
  }
  
 -int print_stat_summary(FILE *fp, int files, int insertions, int deletions)
 +static void print_stat_summary_inserts_deletes(struct diff_options *options,
 +              int files, int insertions, int deletions)
  {
        struct strbuf sb = STRBUF_INIT;
 -      int ret;
  
        if (!files) {
                assert(insertions == 0 && deletions == 0);
 -              return fprintf(fp, "%s\n", " 0 files changed");
 +              emit_diff_symbol(options, DIFF_SYMBOL_STATS_SUMMARY_NO_FILES,
 +                               NULL, 0, 0);
 +              return;
        }
  
        strbuf_addf(&sb,
                            deletions);
        }
        strbuf_addch(&sb, '\n');
 -      ret = fputs(sb.buf, fp);
 +      emit_diff_symbol(options, DIFF_SYMBOL_STATS_SUMMARY_INSERTS_DELETES,
 +                       sb.buf, sb.len, 0);
        strbuf_release(&sb);
 -      return ret;
 +}
 +
 +void print_stat_summary(FILE *fp, int files,
 +                      int insertions, int deletions)
 +{
 +      struct diff_options o;
 +      memset(&o, 0, sizeof(o));
 +      o.file = fp;
 +
 +      print_stat_summary_inserts_deletes(&o, files, insertions, deletions);
  }
  
  static void show_stats(struct diffstat_t *data, struct diff_options *options)
        int total_files = data->nr, count;
        int width, name_width, graph_width, number_width = 0, bin_width = 0;
        const char *reset, *add_c, *del_c;
 -      const char *line_prefix = "";
        int extra_shown = 0;
 +      const char *line_prefix = diff_line_prefix(options);
 +      struct strbuf out = STRBUF_INIT;
  
        if (data->nr == 0)
                return;
  
 -      line_prefix = diff_line_prefix(options);
        count = options->stat_count ? options->stat_count : data->nr;
  
        reset = diff_get_color_opt(options, DIFF_RESET);
                }
  
                if (file->is_binary) {
 -                      fprintf(options->file, "%s", line_prefix);
 -                      show_name(options->file, prefix, name, len);
 -                      fprintf(options->file, " %*s", number_width, "Bin");
 +                      strbuf_addf(&out, " %s%-*s |", prefix, len, name);
 +                      strbuf_addf(&out, " %*s", number_width, "Bin");
                        if (!added && !deleted) {
 -                              putc('\n', options->file);
 +                              strbuf_addch(&out, '\n');
 +                              emit_diff_symbol(options, DIFF_SYMBOL_STATS_LINE,
 +                                               out.buf, out.len, 0);
 +                              strbuf_reset(&out);
                                continue;
                        }
 -                      fprintf(options->file, " %s%"PRIuMAX"%s",
 +                      strbuf_addf(&out, " %s%"PRIuMAX"%s",
                                del_c, deleted, reset);
 -                      fprintf(options->file, " -> ");
 -                      fprintf(options->file, "%s%"PRIuMAX"%s",
 +                      strbuf_addstr(&out, " -> ");
 +                      strbuf_addf(&out, "%s%"PRIuMAX"%s",
                                add_c, added, reset);
 -                      fprintf(options->file, " bytes");
 -                      fprintf(options->file, "\n");
 +                      strbuf_addstr(&out, " bytes\n");
 +                      emit_diff_symbol(options, DIFF_SYMBOL_STATS_LINE,
 +                                       out.buf, out.len, 0);
 +                      strbuf_reset(&out);
                        continue;
                }
                else if (file->is_unmerged) {
 -                      fprintf(options->file, "%s", line_prefix);
 -                      show_name(options->file, prefix, name, len);
 -                      fprintf(options->file, " Unmerged\n");
 +                      strbuf_addf(&out, " %s%-*s |", prefix, len, name);
 +                      strbuf_addstr(&out, " Unmerged\n");
 +                      emit_diff_symbol(options, DIFF_SYMBOL_STATS_LINE,
 +                                       out.buf, out.len, 0);
 +                      strbuf_reset(&out);
                        continue;
                }
  
                                add = total - del;
                        }
                }
 -              fprintf(options->file, "%s", line_prefix);
 -              show_name(options->file, prefix, name, len);
 -              fprintf(options->file, " %*"PRIuMAX"%s",
 +              strbuf_addf(&out, " %s%-*s |", prefix, len, name);
 +              strbuf_addf(&out, " %*"PRIuMAX"%s",
                        number_width, added + deleted,
                        added + deleted ? " " : "");
 -              show_graph(options->file, '+', add, add_c, reset);
 -              show_graph(options->file, '-', del, del_c, reset);
 -              fprintf(options->file, "\n");
 +              show_graph(&out, '+', add, add_c, reset);
 +              show_graph(&out, '-', del, del_c, reset);
 +              strbuf_addch(&out, '\n');
 +              emit_diff_symbol(options, DIFF_SYMBOL_STATS_LINE,
 +                               out.buf, out.len, 0);
 +              strbuf_reset(&out);
        }
  
        for (i = 0; i < data->nr; i++) {
                if (i < count)
                        continue;
                if (!extra_shown)
 -                      fprintf(options->file, "%s ...\n", line_prefix);
 +                      emit_diff_symbol(options,
 +                                       DIFF_SYMBOL_STATS_SUMMARY_ABBREV,
 +                                       NULL, 0, 0);
                extra_shown = 1;
        }
 -      fprintf(options->file, "%s", line_prefix);
 -      print_stat_summary(options->file, total_files, adds, dels);
 +
 +      print_stat_summary_inserts_deletes(options, total_files, adds, dels);
 +      strbuf_release(&out);
  }
  
  static void show_shortstats(struct diffstat_t *data, struct diff_options *options)
  
        for (i = 0; i < data->nr; i++) {
                int added = data->files[i]->added;
 -              int deleted= data->files[i]->deleted;
 +              int deleted = data->files[i]->deleted;
  
                if (data->files[i]->is_unmerged ||
                    (!data->files[i]->is_interesting && (added + deleted == 0))) {
                        dels += deleted;
                }
        }
 -      fprintf(options->file, "%s", diff_line_prefix(options));
 -      print_stat_summary(options->file, total_files, adds, dels);
 +      print_stat_summary_inserts_deletes(options, total_files, adds, dels);
  }
  
  static void show_numstat(struct diffstat_t *data, struct diff_options *options)
@@@ -2660,7 -1980,7 +2660,7 @@@ static void show_dirstat(struct diff_op
        dir.alloc = 0;
        dir.nr = 0;
        dir.permille = options->dirstat_permille;
 -      dir.cumulative = DIFF_OPT_TST(options, DIRSTAT_CUMULATIVE);
 +      dir.cumulative = options->flags.dirstat_cumulative;
  
        changed = 0;
        for (i = 0; i < q->nr; i++) {
                        goto found_damage;
                }
  
 -              if (DIFF_OPT_TST(options, DIRSTAT_BY_FILE)) {
 +              if (options->flags.dirstat_by_file) {
                        /*
                         * In --dirstat-by-file mode, we don't really need to
                         * look at the actual file contents at all.
@@@ -2761,7 -2081,7 +2761,7 @@@ static void show_dirstat_by_line(struc
        dir.alloc = 0;
        dir.nr = 0;
        dir.permille = options->dirstat_permille;
 -      dir.cumulative = DIFF_OPT_TST(options, DIRSTAT_CUMULATIVE);
 +      dir.cumulative = options->flags.dirstat_cumulative;
  
        changed = 0;
        for (i = 0; i < data->nr; i++) {
                         * bytes per "line".
                         * This is stupid and ugly, but very cheap...
                         */
 -                      damage = (damage + 63) / 64;
 +                      damage = DIV_ROUND_UP(damage, 64);
                ALLOC_GROW(dir.files, dir.nr + 1, dir.alloc);
                dir.files[dir.nr].name = file->name;
                dir.files[dir.nr].changed = damage;
@@@ -2905,8 -2225,8 +2905,8 @@@ static unsigned char *deflate_it(char *
        return deflated;
  }
  
 -static void emit_binary_diff_body(FILE *file, mmfile_t *one, mmfile_t *two,
 -                                const char *prefix)
 +static void emit_binary_diff_body(struct diff_options *o,
 +                                mmfile_t *one, mmfile_t *two)
  {
        void *cp;
        void *delta;
        }
  
        if (delta && delta_size < deflate_size) {
 -              fprintf(file, "%sdelta %lu\n", prefix, orig_size);
 +              char *s = xstrfmt("%lu", orig_size);
 +              emit_diff_symbol(o, DIFF_SYMBOL_BINARY_DIFF_HEADER_DELTA,
 +                               s, strlen(s), 0);
 +              free(s);
                free(deflated);
                data = delta;
                data_size = delta_size;
 -      }
 -      else {
 -              fprintf(file, "%sliteral %lu\n", prefix, two->size);
 +      } else {
 +              char *s = xstrfmt("%lu", two->size);
 +              emit_diff_symbol(o, DIFF_SYMBOL_BINARY_DIFF_HEADER_LITERAL,
 +                               s, strlen(s), 0);
 +              free(s);
                free(delta);
                data = deflated;
                data_size = deflate_size;
        /* emit data encoded in base85 */
        cp = data;
        while (data_size) {
 +              int len;
                int bytes = (52 < data_size) ? 52 : data_size;
 -              char line[70];
 +              char line[71];
                data_size -= bytes;
                if (bytes <= 26)
                        line[0] = bytes + 'A' - 1;
                        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);
 +
 +              len = strlen(line);
 +              line[len++] = '\n';
 +              line[len] = '\0';
 +
 +              emit_diff_symbol(o, DIFF_SYMBOL_BINARY_DIFF_BODY,
 +                               line, len, 0);
        }
 -      fprintf(file, "%s\n", prefix);
 +      emit_diff_symbol(o, DIFF_SYMBOL_BINARY_DIFF_FOOTER, NULL, 0, 0);
        free(data);
  }
  
 -static void emit_binary_diff(FILE *file, mmfile_t *one, mmfile_t *two,
 -                           const char *prefix)
 +static void emit_binary_diff(struct diff_options *o,
 +                           mmfile_t *one, mmfile_t *two)
  {
 -      fprintf(file, "%sGIT binary patch\n", prefix);
 -      emit_binary_diff_body(file, one, two, prefix);
 -      emit_binary_diff_body(file, two, one, prefix);
 +      emit_diff_symbol(o, DIFF_SYMBOL_BINARY_DIFF_HEADER, NULL, 0, 0);
 +      emit_binary_diff_body(o, one, two);
 +      emit_binary_diff_body(o, two, one);
  }
  
  int diff_filespec_is_binary(struct diff_filespec *one)
@@@ -3048,7 -2358,7 +3048,7 @@@ static void builtin_diff(const char *na
        const char *line_prefix = diff_line_prefix(o);
  
        diff_set_mnemonic_prefix(o, "a/", "b/");
 -      if (DIFF_OPT_TST(o, REVERSE_DIFF)) {
 +      if (o->flags.reverse_diff) {
                a_prefix = o->b_prefix;
                b_prefix = o->a_prefix;
        } else {
        if (o->submodule_format == DIFF_SUBMODULE_LOG &&
            (!one->mode || S_ISGITLINK(one->mode)) &&
            (!two->mode || S_ISGITLINK(two->mode))) {
 -              const char *del = diff_get_color_opt(o, DIFF_FILE_OLD);
 -              const char *add = diff_get_color_opt(o, DIFF_FILE_NEW);
 -              show_submodule_summary(o->file, one->path ? one->path : two->path,
 -                              line_prefix,
 +              show_submodule_summary(o, one->path ? one->path : two->path,
                                &one->oid, &two->oid,
 -                              two->dirty_submodule,
 -                              meta, del, add, reset);
 +                              two->dirty_submodule);
                return;
        } else if (o->submodule_format == DIFF_SUBMODULE_INLINE_DIFF &&
                   (!one->mode || S_ISGITLINK(one->mode)) &&
                   (!two->mode || S_ISGITLINK(two->mode))) {
 -              const char *del = diff_get_color_opt(o, DIFF_FILE_OLD);
 -              const char *add = diff_get_color_opt(o, DIFF_FILE_NEW);
 -              show_submodule_inline_diff(o->file, one->path ? one->path : two->path,
 -                              line_prefix,
 +              show_submodule_inline_diff(o, one->path ? one->path : two->path,
                                &one->oid, &two->oid,
 -                              two->dirty_submodule,
 -                              meta, del, add, reset, o);
 +                              two->dirty_submodule);
                return;
        }
  
 -      if (DIFF_OPT_TST(o, ALLOW_TEXTCONV)) {
 +      if (o->flags.allow_textconv) {
                textconv_one = get_textconv(one);
                textconv_two = get_textconv(two);
        }
                if (complete_rewrite &&
                    (textconv_one || !diff_filespec_is_binary(one)) &&
                    (textconv_two || !diff_filespec_is_binary(two))) {
 -                      fprintf(o->file, "%s", header.buf);
 +                      emit_diff_symbol(o, DIFF_SYMBOL_HEADER,
 +                                       header.buf, header.len, 0);
                        strbuf_reset(&header);
                        emit_rewrite_diff(name_a, name_b, one, two,
                                                textconv_one, textconv_two, o);
        }
  
        if (o->irreversible_delete && lbl[1][0] == '/') {
 -              fprintf(o->file, "%s", header.buf);
 +              emit_diff_symbol(o, DIFF_SYMBOL_HEADER, header.buf,
 +                               header.len, 0);
                strbuf_reset(&header);
                goto free_ab_and_return;
 -      } else if (!DIFF_OPT_TST(o, TEXT) &&
 +      } else if (!o->flags.text &&
            ( (!textconv_one && diff_filespec_is_binary(one)) ||
              (!textconv_two && diff_filespec_is_binary(two)) )) {
 +              struct strbuf sb = STRBUF_INIT;
                if (!one->data && !two->data &&
                    S_ISREG(one->mode) && S_ISREG(two->mode) &&
 -                  !DIFF_OPT_TST(o, BINARY)) {
 +                  !o->flags.binary) {
                        if (!oidcmp(&one->oid, &two->oid)) {
                                if (must_show_header)
 -                                      fprintf(o->file, "%s", header.buf);
 +                                      emit_diff_symbol(o, DIFF_SYMBOL_HEADER,
 +                                                       header.buf, header.len,
 +                                                       0);
                                goto free_ab_and_return;
                        }
 -                      fprintf(o->file, "%s", header.buf);
 -                      fprintf(o->file, "%sBinary files %s and %s differ\n",
 -                              line_prefix, lbl[0], lbl[1]);
 +                      emit_diff_symbol(o, DIFF_SYMBOL_HEADER,
 +                                       header.buf, header.len, 0);
 +                      strbuf_addf(&sb, "%sBinary files %s and %s differ\n",
 +                                  diff_line_prefix(o), lbl[0], lbl[1]);
 +                      emit_diff_symbol(o, DIFF_SYMBOL_BINARY_FILES,
 +                                       sb.buf, sb.len, 0);
 +                      strbuf_release(&sb);
                        goto free_ab_and_return;
                }
                if (fill_mmfile(&mf1, one) < 0 || fill_mmfile(&mf2, two) < 0)
                if (mf1.size == mf2.size &&
                    !memcmp(mf1.ptr, mf2.ptr, mf1.size)) {
                        if (must_show_header)
 -                              fprintf(o->file, "%s", header.buf);
 +                              emit_diff_symbol(o, DIFF_SYMBOL_HEADER,
 +                                               header.buf, header.len, 0);
                        goto free_ab_and_return;
                }
 -              fprintf(o->file, "%s", header.buf);
 +              emit_diff_symbol(o, DIFF_SYMBOL_HEADER, header.buf, header.len, 0);
                strbuf_reset(&header);
 -              if (DIFF_OPT_TST(o, BINARY))
 -                      emit_binary_diff(o->file, &mf1, &mf2, line_prefix);
 -              else
 -                      fprintf(o->file, "%sBinary files %s and %s differ\n",
 -                              line_prefix, lbl[0], lbl[1]);
 +              if (o->flags.binary)
 +                      emit_binary_diff(o, &mf1, &mf2);
 +              else {
 +                      strbuf_addf(&sb, "%sBinary files %s and %s differ\n",
 +                                  diff_line_prefix(o), lbl[0], lbl[1]);
 +                      emit_diff_symbol(o, DIFF_SYMBOL_BINARY_FILES,
 +                                       sb.buf, sb.len, 0);
 +                      strbuf_release(&sb);
 +              }
                o->found_changes = 1;
        } else {
                /* Crazy xdl interfaces.. */
                const struct userdiff_funcname *pe;
  
                if (must_show_header) {
 -                      fprintf(o->file, "%s", header.buf);
 +                      emit_diff_symbol(o, DIFF_SYMBOL_HEADER,
 +                                       header.buf, header.len, 0);
                        strbuf_reset(&header);
                }
  
                ecbdata.opt = o;
                ecbdata.header = header.len ? &header : NULL;
                xpp.flags = o->xdl_opts;
 +              xpp.anchors = o->anchors;
 +              xpp.anchors_nr = o->anchors_nr;
                xecfg.ctxlen = o->context;
                xecfg.interhunkctxlen = o->interhunkcontext;
                xecfg.flags = XDL_EMIT_FUNCNAMES;
 -              if (DIFF_OPT_TST(o, FUNCCONTEXT))
 +              if (o->flags.funccontext)
                        xecfg.flags |= XDL_EMIT_FUNCCONTEXT;
                if (pe)
                        xdiff_set_find_func(&xecfg, pe->pattern, pe->cflags);
@@@ -3304,8 -2605,6 +3304,8 @@@ static void builtin_diffstat(const cha
                memset(&xpp, 0, sizeof(xpp));
                memset(&xecfg, 0, sizeof(xecfg));
                xpp.flags = o->xdl_opts;
 +              xpp.anchors = o->anchors;
 +              xpp.anchors_nr = o->anchors_nr;
                xecfg.ctxlen = o->context;
                xecfg.interhunkctxlen = o->interhunkcontext;
                if (xdi_diff_outf(&mf1, &mf2, diffstat_consume, diffstat,
@@@ -3382,7 -2681,7 +3382,7 @@@ static void builtin_checkdiff(const cha
        diff_free_filespec_data(one);
        diff_free_filespec_data(two);
        if (data.status)
 -              DIFF_OPT_SET(o, CHECK_FAILED);
 +              o->flags.check_failed = 1;
  }
  
  struct diff_filespec *alloc_filespec(const char *path)
@@@ -3403,13 -2702,13 +3403,13 @@@ void free_filespec(struct diff_filespe
        }
  }
  
 -void fill_filespec(struct diff_filespec *spec, const unsigned char *sha1,
 -                 int sha1_valid, unsigned short mode)
 +void fill_filespec(struct diff_filespec *spec, const struct object_id *oid,
 +                 int oid_valid, unsigned short mode)
  {
        if (mode) {
                spec->mode = canon_mode(mode);
 -              hashcpy(spec->oid.hash, sha1);
 -              spec->oid_valid = sha1_valid;
 +              oidcpy(&spec->oid, oid);
 +              spec->oid_valid = oid_valid;
        }
  }
  
   * the work tree has that object contents, return true, so that
   * prepare_temp_file() does not have to inflate and extract.
   */
 -static int reuse_worktree_file(const char *name, const unsigned char *sha1, int want_file)
 +static int reuse_worktree_file(const char *name, const struct object_id *oid, int want_file)
  {
        const struct cache_entry *ce;
        struct stat st;
         * objects however would tend to be slower as they need
         * to be individually opened and inflated.
         */
 -      if (!FAST_WORKING_DIRECTORY && !want_file && has_sha1_pack(sha1))
 +      if (!FAST_WORKING_DIRECTORY && !want_file && has_sha1_pack(oid->hash))
                return 0;
  
        /*
         * Similarly, if we'd have to convert the file contents anyway, that
         * makes the optimization not worthwhile.
         */
 -      if (!want_file && would_convert_to_git(name))
 +      if (!want_file && would_convert_to_git(&the_index, name))
                return 0;
  
        len = strlen(name);
         * This is not the sha1 we are looking for, or
         * unreusable because it is not a regular file.
         */
 -      if (hashcmp(sha1, ce->oid.hash) || !S_ISREG(ce->ce_mode))
 +      if (oidcmp(oid, &ce->oid) || !S_ISREG(ce->ce_mode))
                return 0;
  
        /*
@@@ -3543,18 -2842,20 +3543,18 @@@ int diff_populate_filespec(struct diff_
                return diff_populate_gitlink(s, size_only);
  
        if (!s->oid_valid ||
 -          reuse_worktree_file(s->path, s->oid.hash, 0)) {
 +          reuse_worktree_file(s->path, &s->oid, 0)) {
                struct strbuf buf = STRBUF_INIT;
                struct stat st;
                int fd;
  
                if (lstat(s->path, &st) < 0) {
 -                      if (errno == ENOENT) {
 -                      err_empty:
 -                              err = -1;
 -                      empty:
 -                              s->data = (char *)"";
 -                              s->size = 0;
 -                              return err;
 -                      }
 +              err_empty:
 +                      err = -1;
 +              empty:
 +                      s->data = (char *)"";
 +                      s->size = 0;
 +                      return err;
                }
                s->size = xsize_t(st.st_size);
                if (!s->size)
                 * point if the path requires us to run the content
                 * conversion.
                 */
 -              if (size_only && !would_convert_to_git(s->path))
 +              if (size_only && !would_convert_to_git(&the_index, s->path))
                        return 0;
  
                /*
                /*
                 * Convert from working tree format to canonical git format
                 */
 -              if (convert_to_git(s->path, s->data, s->size, &buf, crlf_warn)) {
 +              if (convert_to_git(&the_index, s->path, s->data, s->size, &buf, crlf_warn)) {
                        size_t size = 0;
                        munmap(s->data, s->size);
                        s->should_munmap = 0;
@@@ -3650,7 -2951,8 +3650,7 @@@ void diff_free_filespec_blob(struct dif
  void diff_free_filespec_data(struct diff_filespec *s)
  {
        diff_free_filespec_blob(s);
 -      free(s->cnt_data);
 -      s->cnt_data = NULL;
 +      FREE_AND_NULL(s->cnt_data);
  }
  
  static void prep_temp_blob(const char *path, struct diff_tempfile *temp,
                           const struct object_id *oid,
                           int mode)
  {
 -      int fd;
        struct strbuf buf = STRBUF_INIT;
        struct strbuf template = STRBUF_INIT;
        char *path_dup = xstrdup(path);
        strbuf_addstr(&template, "XXXXXX_");
        strbuf_addstr(&template, base);
  
 -      fd = mks_tempfile_ts(&temp->tempfile, template.buf, strlen(base) + 1);
 -      if (fd < 0)
 +      temp->tempfile = mks_tempfile_ts(template.buf, strlen(base) + 1);
 +      if (!temp->tempfile)
                die_errno("unable to create temp-file");
        if (convert_to_working_tree(path,
                        (const char *)blob, (size_t)size, &buf)) {
                blob = buf.buf;
                size = buf.len;
        }
 -      if (write_in_full(fd, blob, size) != size)
 +      if (write_in_full(temp->tempfile->fd, blob, size) < 0 ||
 +          close_tempfile_gently(temp->tempfile))
                die_errno("unable to write temp-file");
 -      close_tempfile(&temp->tempfile);
 -      temp->name = get_tempfile_path(&temp->tempfile);
 +      temp->name = get_tempfile_path(temp->tempfile);
        oid_to_hex_r(temp->hex, oid);
        xsnprintf(temp->mode, sizeof(temp->mode), "%06o", mode);
        strbuf_release(&buf);
@@@ -3705,7 -3008,7 +3705,7 @@@ static struct diff_tempfile *prepare_te
  
        if (!S_ISGITLINK(one->mode) &&
            (!one->oid_valid ||
 -           reuse_worktree_file(name, one->oid.hash, 1))) {
 +           reuse_worktree_file(name, &one->oid, 1))) {
                struct stat st;
                if (lstat(name, &st) < 0) {
                        if (errno == ENOENT)
                        /* we can borrow from the file in the work tree */
                        temp->name = name;
                        if (!one->oid_valid)
 -                              sha1_to_hex_r(temp->hex, null_sha1);
 +                              oid_to_hex_r(temp->hex, &null_oid);
                        else
                                oid_to_hex_r(temp->hex, &one->oid);
                        /* Even though we may sometimes borrow the
                         * contents from the work tree, we always want
                         * one->mode.  mode is trustworthy even when
 -                       * !(one->sha1_valid), as long as
 +                       * !(one->oid_valid), as long as
                         * DIFF_FILE_VALID(one).
                         */
                        xsnprintf(temp->mode, sizeof(temp->mode), "%06o", one->mode);
@@@ -3874,9 -3177,9 +3874,9 @@@ static void fill_metainfo(struct strbu
                *must_show_header = 0;
        }
        if (one && two && oidcmp(&one->oid, &two->oid)) {
 -              int abbrev = DIFF_OPT_TST(o, FULL_INDEX) ? 40 : DEFAULT_ABBREV;
 +              int abbrev = o->flags.full_index ? 40 : DEFAULT_ABBREV;
  
 -              if (DIFF_OPT_TST(o, BINARY)) {
 +              if (o->flags.binary) {
                        mmfile_t mf;
                        if ((!fill_mmfile(&mf, one) && diff_filespec_is_binary(one)) ||
                            (!fill_mmfile(&mf, two) && diff_filespec_is_binary(two)))
@@@ -3906,7 -3209,7 +3906,7 @@@ static void run_diff_cmd(const char *pg
        int must_show_header = 0;
  
  
 -      if (DIFF_OPT_TST(o, ALLOW_EXTERNAL)) {
 +      if (o->flags.allow_external) {
                struct userdiff_driver *drv = userdiff_find_by_path(attr_path);
                if (drv && drv->external)
                        pgm = drv->external;
                fprintf(o->file, "* Unmerged path %s\n", name);
  }
  
 -static void diff_fill_sha1_info(struct diff_filespec *one)
 +static void diff_fill_oid_info(struct diff_filespec *one)
  {
        if (DIFF_FILE_VALID(one)) {
                if (!one->oid_valid) {
                        }
                        if (lstat(one->path, &st) < 0)
                                die_errno("stat '%s'", one->path);
 -                      if (index_path(one->oid.hash, one->path, &st, 0))
 +                      if (index_path(&one->oid, one->path, &st, 0))
                                die("cannot hash %s", one->path);
                }
        }
@@@ -3980,13 -3283,13 +3980,13 @@@ static void run_diff(struct diff_filepa
        const char *other;
        const char *attr_path;
  
 -      name  = p->one->path;
 -      other = (strcmp(name, p->two->path) ? p->two->path : NULL);
 +      name  = one->path;
 +      other = (strcmp(name, two->path) ? two->path : NULL);
        attr_path = name;
        if (o->prefix_length)
                strip_prefix(o->prefix_length, &name, &other);
  
 -      if (!DIFF_OPT_TST(o, ALLOW_EXTERNAL))
 +      if (!o->flags.allow_external)
                pgm = NULL;
  
        if (DIFF_PAIR_UNMERGED(p)) {
                return;
        }
  
 -      diff_fill_sha1_info(one);
 -      diff_fill_sha1_info(two);
 +      diff_fill_oid_info(one);
 +      diff_fill_oid_info(two);
  
        if (!pgm &&
            DIFF_FILE_VALID(one) && DIFF_FILE_VALID(two) &&
@@@ -4041,8 -3344,8 +4041,8 @@@ static void run_diffstat(struct diff_fi
        if (o->prefix_length)
                strip_prefix(o->prefix_length, &name, &other);
  
 -      diff_fill_sha1_info(p->one);
 -      diff_fill_sha1_info(p->two);
 +      diff_fill_oid_info(p->one);
 +      diff_fill_oid_info(p->two);
  
        builtin_diffstat(name, other, p->one, p->two, diffstat, o, p);
  }
@@@ -4065,8 -3368,8 +4065,8 @@@ static void run_checkdiff(struct diff_f
        if (o->prefix_length)
                strip_prefix(o->prefix_length, &name, &other);
  
 -      diff_fill_sha1_info(p->one);
 -      diff_fill_sha1_info(p->two);
 +      diff_fill_oid_info(p->one);
 +      diff_fill_oid_info(p->two);
  
        builtin_checkdiff(name, other, attr_path, p->one, p->two, o);
  }
@@@ -4085,7 -3388,7 +4085,7 @@@ void diff_setup(struct diff_options *op
        options->context = diff_context_default;
        options->interhunkcontext = diff_interhunk_context_default;
        options->ws_error_highlight = ws_error_highlight_default;
 -      DIFF_OPT_SET(options, RENAME_EMPTY);
 +      options->flags.rename_empty = 1;
  
        /* pathchange left =NULL by default */
        options->change = diff_change;
                options->a_prefix = "a/";
                options->b_prefix = "b/";
        }
 +
 +      options->color_moved = diff_color_moved_default;
  }
  
  void diff_setup_done(struct diff_options *options)
         * inside contents.
         */
  
 -      if (DIFF_XDL_TST(options, IGNORE_WHITESPACE) ||
 -          DIFF_XDL_TST(options, IGNORE_WHITESPACE_CHANGE) ||
 -          DIFF_XDL_TST(options, IGNORE_WHITESPACE_AT_EOL))
 -              DIFF_OPT_SET(options, DIFF_FROM_CONTENTS);
 +      if ((options->xdl_opts & XDF_WHITESPACE_FLAGS))
 +              options->flags.diff_from_contents = 1;
        else
 -              DIFF_OPT_CLR(options, DIFF_FROM_CONTENTS);
 +              options->flags.diff_from_contents = 0;
  
 -      if (DIFF_OPT_TST(options, FIND_COPIES_HARDER))
 +      if (options->flags.find_copies_harder)
                options->detect_rename = DIFF_DETECT_COPY;
  
 -      if (!DIFF_OPT_TST(options, RELATIVE_NAME))
 +      if (!options->flags.relative_name)
                options->prefix = NULL;
        if (options->prefix)
                options->prefix_length = strlen(options->prefix);
                                      DIFF_FORMAT_DIRSTAT |
                                      DIFF_FORMAT_SUMMARY |
                                      DIFF_FORMAT_CHECKDIFF))
 -              DIFF_OPT_SET(options, RECURSIVE);
 +              options->flags.recursive = 1;
        /*
         * Also pickaxe would not work very well if you do not say recursive
         */
        if (options->pickaxe)
 -              DIFF_OPT_SET(options, RECURSIVE);
 +              options->flags.recursive = 1;
        /*
         * When patches are generated, submodules diffed against the work tree
         * must be checked for dirtiness too so it can be shown in the output
         */
        if (options->output_format & DIFF_FORMAT_PATCH)
 -              DIFF_OPT_SET(options, DIRTY_SUBMODULES);
 +              options->flags.dirty_submodules = 1;
  
        if (options->detect_rename && options->rename_limit < 0)
                options->rename_limit = diff_rename_limit_default;
         * to have found.  It does not make sense not to return with
         * exit code in such a case either.
         */
 -      if (DIFF_OPT_TST(options, QUICK)) {
 +      if (options->flags.quick) {
                options->output_format = DIFF_FORMAT_NO_OUTPUT;
 -              DIFF_OPT_SET(options, EXIT_WITH_STATUS);
 +              options->flags.exit_with_status = 1;
        }
  
        options->diff_path_counter = 0;
  
 -      if (DIFF_OPT_TST(options, FOLLOW_RENAMES) && options->pathspec.nr != 1)
 +      if (options->flags.follow_renames && options->pathspec.nr != 1)
                die(_("--follow requires exactly one pathspec"));
 +
 +      if (!options->use_color || external_diff())
 +              options->color_moved = 0;
  }
  
  static int opt_arg(const char *arg, int arg_short, const char *arg_long, int *val)
@@@ -4512,12 -3812,17 +4512,12 @@@ int diff_opt_parse(struct diff_options 
                options->output_format |= DIFF_FORMAT_NUMSTAT;
        else if (!strcmp(arg, "--shortstat"))
                options->output_format |= DIFF_FORMAT_SHORTSTAT;
 -      else if (!strcmp(arg, "-X") || !strcmp(arg, "--dirstat"))
 -              return parse_dirstat_opt(options, "");
 -      else if (skip_prefix(arg, "-X", &arg))
 -              return parse_dirstat_opt(options, arg);
 -      else if (skip_prefix(arg, "--dirstat=", &arg))
 +      else if (skip_prefix(arg, "-X", &arg) ||
 +               skip_to_optional_arg(arg, "--dirstat", &arg))
                return parse_dirstat_opt(options, arg);
        else if (!strcmp(arg, "--cumulative"))
                return parse_dirstat_opt(options, "cumulative");
 -      else if (!strcmp(arg, "--dirstat-by-file"))
 -              return parse_dirstat_opt(options, "files");
 -      else if (skip_prefix(arg, "--dirstat-by-file=", &arg)) {
 +      else if (skip_to_optional_arg(arg, "--dirstat-by-file", &arg)) {
                parse_dirstat_opt(options, "files");
                return parse_dirstat_opt(options, arg);
        }
                return stat_opt(options, av);
  
        /* renames options */
 -      else if (starts_with(arg, "-B") || starts_with(arg, "--break-rewrites=") ||
 -               !strcmp(arg, "--break-rewrites")) {
 +      else if (starts_with(arg, "-B") ||
 +               skip_to_optional_arg(arg, "--break-rewrites", NULL)) {
                if ((options->break_opt = diff_scoreopt_parse(arg)) == -1)
                        return error("invalid argument to -B: %s", arg+2);
        }
 -      else if (starts_with(arg, "-M") || starts_with(arg, "--find-renames=") ||
 -               !strcmp(arg, "--find-renames")) {
 +      else if (starts_with(arg, "-M") ||
 +               skip_to_optional_arg(arg, "--find-renames", NULL)) {
                if ((options->rename_score = diff_scoreopt_parse(arg)) == -1)
                        return error("invalid argument to -M: %s", arg+2);
                options->detect_rename = DIFF_DETECT_RENAME;
        else if (!strcmp(arg, "-D") || !strcmp(arg, "--irreversible-delete")) {
                options->irreversible_delete = 1;
        }
 -      else if (starts_with(arg, "-C") || starts_with(arg, "--find-copies=") ||
 -               !strcmp(arg, "--find-copies")) {
 +      else if (starts_with(arg, "-C") ||
 +               skip_to_optional_arg(arg, "--find-copies", NULL)) {
                if (options->detect_rename == DIFF_DETECT_COPY)
 -                      DIFF_OPT_SET(options, FIND_COPIES_HARDER);
 +                      options->flags.find_copies_harder = 1;
                if ((options->rename_score = diff_scoreopt_parse(arg)) == -1)
                        return error("invalid argument to -C: %s", arg+2);
                options->detect_rename = DIFF_DETECT_COPY;
        else if (!strcmp(arg, "--no-renames"))
                options->detect_rename = 0;
        else if (!strcmp(arg, "--rename-empty"))
 -              DIFF_OPT_SET(options, RENAME_EMPTY);
 +              options->flags.rename_empty = 1;
        else if (!strcmp(arg, "--no-rename-empty"))
 -              DIFF_OPT_CLR(options, RENAME_EMPTY);
 -      else if (!strcmp(arg, "--relative"))
 -              DIFF_OPT_SET(options, RELATIVE_NAME);
 -      else if (skip_prefix(arg, "--relative=", &arg)) {
 -              DIFF_OPT_SET(options, RELATIVE_NAME);
 -              options->prefix = arg;
 +              options->flags.rename_empty = 0;
 +      else if (skip_to_optional_arg_default(arg, "--relative", &arg, NULL)) {
 +              options->flags.relative_name = 1;
 +              if (arg)
 +                      options->prefix = arg;
        }
  
        /* xdiff options */
                DIFF_XDL_SET(options, IGNORE_WHITESPACE_CHANGE);
        else if (!strcmp(arg, "--ignore-space-at-eol"))
                DIFF_XDL_SET(options, IGNORE_WHITESPACE_AT_EOL);
 +      else if (!strcmp(arg, "--ignore-cr-at-eol"))
 +              DIFF_XDL_SET(options, IGNORE_CR_AT_EOL);
        else if (!strcmp(arg, "--ignore-blank-lines"))
                DIFF_XDL_SET(options, IGNORE_BLANK_LINES);
        else if (!strcmp(arg, "--indent-heuristic"))
                DIFF_XDL_SET(options, INDENT_HEURISTIC);
        else if (!strcmp(arg, "--no-indent-heuristic"))
                DIFF_XDL_CLR(options, INDENT_HEURISTIC);
 -      else if (!strcmp(arg, "--patience"))
 +      else if (!strcmp(arg, "--patience")) {
 +              int i;
                options->xdl_opts = DIFF_WITH_ALG(options, PATIENCE_DIFF);
 -      else if (!strcmp(arg, "--histogram"))
 +              /*
 +               * Both --patience and --anchored use PATIENCE_DIFF
 +               * internally, so remove any anchors previously
 +               * specified.
 +               */
 +              for (i = 0; i < options->anchors_nr; i++)
 +                      free(options->anchors[i]);
 +              options->anchors_nr = 0;
 +      } else if (!strcmp(arg, "--histogram"))
                options->xdl_opts = DIFF_WITH_ALG(options, HISTOGRAM_DIFF);
        else if ((argcount = parse_long_opt("diff-algorithm", av, &optarg))) {
                long value = parse_algorithm_value(optarg);
                options->xdl_opts &= ~XDF_DIFF_ALGORITHM_MASK;
                options->xdl_opts |= value;
                return argcount;
 +      } else if (skip_prefix(arg, "--anchored=", &arg)) {
 +              options->xdl_opts = DIFF_WITH_ALG(options, PATIENCE_DIFF);
 +              ALLOC_GROW(options->anchors, options->anchors_nr + 1,
 +                         options->anchors_alloc);
 +              options->anchors[options->anchors_nr++] = xstrdup(arg);
        }
  
        /* flags options */
        else if (!strcmp(arg, "--binary")) {
                enable_patch_output(&options->output_format);
 -              DIFF_OPT_SET(options, BINARY);
 +              options->flags.binary = 1;
        }
        else if (!strcmp(arg, "--full-index"))
 -              DIFF_OPT_SET(options, FULL_INDEX);
 +              options->flags.full_index = 1;
        else if (!strcmp(arg, "-a") || !strcmp(arg, "--text"))
 -              DIFF_OPT_SET(options, TEXT);
 +              options->flags.text = 1;
        else if (!strcmp(arg, "-R"))
 -              DIFF_OPT_SET(options, REVERSE_DIFF);
 +              options->flags.reverse_diff = 1;
        else if (!strcmp(arg, "--find-copies-harder"))
 -              DIFF_OPT_SET(options, FIND_COPIES_HARDER);
 +              options->flags.find_copies_harder = 1;
        else if (!strcmp(arg, "--follow"))
 -              DIFF_OPT_SET(options, FOLLOW_RENAMES);
 +              options->flags.follow_renames = 1;
        else if (!strcmp(arg, "--no-follow")) {
 -              DIFF_OPT_CLR(options, FOLLOW_RENAMES);
 -              DIFF_OPT_CLR(options, DEFAULT_FOLLOW_RENAMES);
 -      } else if (!strcmp(arg, "--color"))
 -              options->use_color = 1;
 -      else if (skip_prefix(arg, "--color=", &arg)) {
 +              options->flags.follow_renames = 0;
 +              options->flags.default_follow_renames = 0;
 +      } else if (skip_to_optional_arg_default(arg, "--color", &arg, "always")) {
                int value = git_config_colorbool(NULL, arg);
                if (value < 0)
                        return error("option `color' expects \"always\", \"auto\", or \"never\"");
        }
        else if (!strcmp(arg, "--no-color"))
                options->use_color = 0;
 -      else if (!strcmp(arg, "--color-words")) {
 -              options->use_color = 1;
 -              options->word_diff = DIFF_WORDS_COLOR;
 -      }
 -      else if (skip_prefix(arg, "--color-words=", &arg)) {
 +      else if (!strcmp(arg, "--color-moved")) {
 +              if (diff_color_moved_default)
 +                      options->color_moved = diff_color_moved_default;
 +              if (options->color_moved == COLOR_MOVED_NO)
 +                      options->color_moved = COLOR_MOVED_DEFAULT;
 +      } else if (!strcmp(arg, "--no-color-moved"))
 +              options->color_moved = COLOR_MOVED_NO;
 +      else if (skip_prefix(arg, "--color-moved=", &arg)) {
 +              int cm = parse_color_moved(arg);
 +              if (cm < 0)
 +                      die("bad --color-moved argument: %s", arg);
 +              options->color_moved = cm;
 +      } else if (skip_to_optional_arg_default(arg, "--color-words", &options->word_regex, NULL)) {
                options->use_color = 1;
                options->word_diff = DIFF_WORDS_COLOR;
 -              options->word_regex = arg;
        }
        else if (!strcmp(arg, "--word-diff")) {
                if (options->word_diff == DIFF_WORDS_NONE)
                return argcount;
        }
        else if (!strcmp(arg, "--exit-code"))
 -              DIFF_OPT_SET(options, EXIT_WITH_STATUS);
 +              options->flags.exit_with_status = 1;
        else if (!strcmp(arg, "--quiet"))
 -              DIFF_OPT_SET(options, QUICK);
 +              options->flags.quick = 1;
        else if (!strcmp(arg, "--ext-diff"))
 -              DIFF_OPT_SET(options, ALLOW_EXTERNAL);
 +              options->flags.allow_external = 1;
        else if (!strcmp(arg, "--no-ext-diff"))
 -              DIFF_OPT_CLR(options, ALLOW_EXTERNAL);
 -      else if (!strcmp(arg, "--textconv"))
 -              DIFF_OPT_SET(options, ALLOW_TEXTCONV);
 -      else if (!strcmp(arg, "--no-textconv"))
 -              DIFF_OPT_CLR(options, ALLOW_TEXTCONV);
 -      else if (!strcmp(arg, "--ignore-submodules")) {
 -              DIFF_OPT_SET(options, OVERRIDE_SUBMODULE_CONFIG);
 -              handle_ignore_submodules_arg(options, "all");
 -      } else if (skip_prefix(arg, "--ignore-submodules=", &arg)) {
 -              DIFF_OPT_SET(options, OVERRIDE_SUBMODULE_CONFIG);
 +              options->flags.allow_external = 0;
 +      else if (!strcmp(arg, "--textconv")) {
 +              options->flags.allow_textconv = 1;
 +              options->flags.textconv_set_via_cmdline = 1;
 +      } else if (!strcmp(arg, "--no-textconv"))
 +              options->flags.allow_textconv = 0;
 +      else if (skip_to_optional_arg_default(arg, "--ignore-submodules", &arg, "all")) {
 +              options->flags.override_submodule_config = 1;
                handle_ignore_submodules_arg(options, arg);
 -      } else if (!strcmp(arg, "--submodule"))
 -              options->submodule_format = DIFF_SUBMODULE_LOG;
 -      else if (skip_prefix(arg, "--submodule=", &arg))
 +      } else if (skip_to_optional_arg_default(arg, "--submodule", &arg, "log"))
                return parse_submodule_opt(options, arg);
        else if (skip_prefix(arg, "--ws-error-highlight=", &arg))
                return parse_ws_error_highlight_opt(options, arg);
                         &options->interhunkcontext))
                ;
        else if (!strcmp(arg, "-W"))
 -              DIFF_OPT_SET(options, FUNCCONTEXT);
 +              options->flags.funccontext = 1;
        else if (!strcmp(arg, "--function-context"))
 -              DIFF_OPT_SET(options, FUNCCONTEXT);
 +              options->flags.funccontext = 1;
        else if (!strcmp(arg, "--no-function-context"))
 -              DIFF_OPT_CLR(options, FUNCCONTEXT);
 +              options->flags.funccontext = 0;
        else if ((argcount = parse_long_opt("output", av, &optarg))) {
                char *path = prefix_filename(prefix, optarg);
 -              options->file = fopen(path, "w");
 -              if (!options->file)
 -                      die_errno("Could not open '%s'", path);
 +              options->file = xfopen(path, "w");
                options->close_file = 1;
                if (options->use_color != GIT_COLOR_ALWAYS)
                        options->use_color = GIT_COLOR_NEVER;
@@@ -4902,20 -4193,14 +4902,20 @@@ const char *diff_aligned_abbrev(const s
        int abblen;
        const char *abbrev;
  
 +      /* Do we want all 40 hex characters? */
        if (len == GIT_SHA1_HEXSZ)
                return oid_to_hex(oid);
  
 +      /* An abbreviated value is fine, possibly followed by an ellipsis. */
        abbrev = diff_abbrev_oid(oid, len);
 +
 +      if (!print_sha1_ellipsis())
 +              return abbrev;
 +
        abblen = strlen(abbrev);
  
        /*
 -       * In well-behaved cases, where the abbbreviated result is the
 +       * In well-behaved cases, where the abbreviated result is the
         * same as the requested length, append three dots after the
         * abbreviation (hence the whole logic is limited to the case
         * where abblen < 37); when the actual abbreviated result is a
@@@ -5187,79 -4472,67 +5187,79 @@@ static void flush_one_pair(struct diff_
        }
  }
  
 -static void show_file_mode_name(FILE *file, const char *newdelete, struct diff_filespec *fs)
 +static void show_file_mode_name(struct diff_options *opt, const char *newdelete, struct diff_filespec *fs)
  {
 +      struct strbuf sb = STRBUF_INIT;
        if (fs->mode)
 -              fprintf(file, " %s mode %06o ", newdelete, fs->mode);
 +              strbuf_addf(&sb, " %s mode %06o ", newdelete, fs->mode);
        else
 -              fprintf(file, " %s ", newdelete);
 -      write_name_quoted(fs->path, file, '\n');
 -}
 +              strbuf_addf(&sb, " %s ", newdelete);
  
 +      quote_c_style(fs->path, &sb, NULL, 0);
 +      strbuf_addch(&sb, '\n');
 +      emit_diff_symbol(opt, DIFF_SYMBOL_SUMMARY,
 +                       sb.buf, sb.len, 0);
 +      strbuf_release(&sb);
 +}
  
 -static void show_mode_change(FILE *file, struct diff_filepair *p, int show_name,
 -              const char *line_prefix)
 +static void show_mode_change(struct diff_options *opt, struct diff_filepair *p,
 +              int show_name)
  {
        if (p->one->mode && p->two->mode && p->one->mode != p->two->mode) {
 -              fprintf(file, "%s mode change %06o => %06o%c", line_prefix, p->one->mode,
 -                      p->two->mode, show_name ? ' ' : '\n');
 +              struct strbuf sb = STRBUF_INIT;
 +              strbuf_addf(&sb, " mode change %06o => %06o",
 +                          p->one->mode, p->two->mode);
                if (show_name) {
 -                      write_name_quoted(p->two->path, file, '\n');
 +                      strbuf_addch(&sb, ' ');
 +                      quote_c_style(p->two->path, &sb, NULL, 0);
                }
 +              strbuf_addch(&sb, '\n');
 +              emit_diff_symbol(opt, DIFF_SYMBOL_SUMMARY,
 +                               sb.buf, sb.len, 0);
 +              strbuf_release(&sb);
        }
  }
  
 -static void show_rename_copy(FILE *file, const char *renamecopy, struct diff_filepair *p,
 -                      const char *line_prefix)
 +static void show_rename_copy(struct diff_options *opt, const char *renamecopy,
 +              struct diff_filepair *p)
  {
 +      struct strbuf sb = STRBUF_INIT;
        char *names = pprint_rename(p->one->path, p->two->path);
 -
 -      fprintf(file, " %s %s (%d%%)\n", renamecopy, names, similarity_index(p));
 +      strbuf_addf(&sb, " %s %s (%d%%)\n",
 +                      renamecopy, names, similarity_index(p));
        free(names);
 -      show_mode_change(file, p, 0, line_prefix);
 +      emit_diff_symbol(opt, DIFF_SYMBOL_SUMMARY,
 +                               sb.buf, sb.len, 0);
 +      show_mode_change(opt, p, 0);
 +      strbuf_release(&sb);
  }
  
  static void diff_summary(struct diff_options *opt, struct diff_filepair *p)
  {
 -      FILE *file = opt->file;
 -      const char *line_prefix = diff_line_prefix(opt);
 -
        switch(p->status) {
        case DIFF_STATUS_DELETED:
 -              fputs(line_prefix, file);
 -              show_file_mode_name(file, "delete", p->one);
 +              show_file_mode_name(opt, "delete", p->one);
                break;
        case DIFF_STATUS_ADDED:
 -              fputs(line_prefix, file);
 -              show_file_mode_name(file, "create", p->two);
 +              show_file_mode_name(opt, "create", p->two);
                break;
        case DIFF_STATUS_COPIED:
 -              fputs(line_prefix, file);
 -              show_rename_copy(file, "copy", p, line_prefix);
 +              show_rename_copy(opt, "copy", p);
                break;
        case DIFF_STATUS_RENAMED:
 -              fputs(line_prefix, file);
 -              show_rename_copy(file, "rename", p, line_prefix);
 +              show_rename_copy(opt, "rename", p);
                break;
        default:
                if (p->score) {
 -                      fprintf(file, "%s rewrite ", line_prefix);
 -                      write_name_quoted(p->two->path, file, ' ');
 -                      fprintf(file, "(%d%%)\n", similarity_index(p));
 +                      struct strbuf sb = STRBUF_INIT;
 +                      strbuf_addstr(&sb, " rewrite ");
 +                      quote_c_style(p->two->path, &sb, NULL, 0);
 +                      strbuf_addf(&sb, " (%d%%)\n", similarity_index(p));
 +                      emit_diff_symbol(opt, DIFF_SYMBOL_SUMMARY,
 +                                       sb.buf, sb.len, 0);
 +                      strbuf_release(&sb);
                }
 -              show_mode_change(file, p, !p->score, line_prefix);
 +              show_mode_change(opt, p, !p->score);
                break;
        }
  }
@@@ -5311,7 -4584,7 +5311,7 @@@ static void patch_id_add_mode(git_SHA_C
  }
  
  /* returns 0 upon success, and writes result into sha1 */
 -static int diff_get_patch_id(struct diff_options *options, unsigned char *sha1, int diff_header_only)
 +static int diff_get_patch_id(struct diff_options *options, struct object_id *oid, int diff_header_only)
  {
        struct diff_queue_struct *q = &diff_queued_diff;
        int i;
                if (DIFF_PAIR_UNMERGED(p))
                        continue;
  
 -              diff_fill_sha1_info(p->one);
 -              diff_fill_sha1_info(p->two);
 +              diff_fill_oid_info(p->one);
 +              diff_fill_oid_info(p->two);
  
                len1 = remove_space(p->one->path, strlen(p->one->path));
                len2 = remove_space(p->two->path, strlen(p->two->path));
                if (diff_filespec_is_binary(p->one) ||
                    diff_filespec_is_binary(p->two)) {
                        git_SHA1_Update(&ctx, oid_to_hex(&p->one->oid),
 -                                      40);
 +                                      GIT_SHA1_HEXSZ);
                        git_SHA1_Update(&ctx, oid_to_hex(&p->two->oid),
 -                                      40);
 +                                      GIT_SHA1_HEXSZ);
                        continue;
                }
  
                                     p->one->path);
        }
  
 -      git_SHA1_Final(sha1, &ctx);
 +      git_SHA1_Final(oid->hash, &ctx);
        return 0;
  }
  
 -int diff_flush_patch_id(struct diff_options *options, unsigned char *sha1, int diff_header_only)
 +int diff_flush_patch_id(struct diff_options *options, struct object_id *oid, int diff_header_only)
  {
        struct diff_queue_struct *q = &diff_queued_diff;
        int i;
 -      int result = diff_get_patch_id(options, sha1, diff_header_only);
 +      int result = diff_get_patch_id(options, oid, diff_header_only);
  
        for (i = 0; i < q->nr; i++)
                diff_free_filepair(q->queue[i]);
@@@ -5460,55 -4733,10 +5460,55 @@@ void diff_warn_rename_limit(const char 
                warning(_(rename_limit_warning));
        else
                return;
 -      if (0 < needed && needed < 32767)
 +      if (0 < needed)
                warning(_(rename_limit_advice), varname, needed);
  }
  
 +static void diff_flush_patch_all_file_pairs(struct diff_options *o)
 +{
 +      int i;
 +      static struct emitted_diff_symbols esm = EMITTED_DIFF_SYMBOLS_INIT;
 +      struct diff_queue_struct *q = &diff_queued_diff;
 +
 +      if (WSEH_NEW & WS_RULE_MASK)
 +              die("BUG: WS rules bit mask overlaps with diff symbol flags");
 +
 +      if (o->color_moved)
 +              o->emitted_symbols = &esm;
 +
 +      for (i = 0; i < q->nr; i++) {
 +              struct diff_filepair *p = q->queue[i];
 +              if (check_pair_status(p))
 +                      diff_flush_patch(p, o);
 +      }
 +
 +      if (o->emitted_symbols) {
 +              if (o->color_moved) {
 +                      struct hashmap add_lines, del_lines;
 +
 +                      hashmap_init(&del_lines,
 +                                   (hashmap_cmp_fn)moved_entry_cmp, o, 0);
 +                      hashmap_init(&add_lines,
 +                                   (hashmap_cmp_fn)moved_entry_cmp, o, 0);
 +
 +                      add_lines_to_move_detection(o, &add_lines, &del_lines);
 +                      mark_color_as_moved(o, &add_lines, &del_lines);
 +                      if (o->color_moved == COLOR_MOVED_ZEBRA_DIM)
 +                              dim_moved_lines(o);
 +
 +                      hashmap_free(&add_lines, 0);
 +                      hashmap_free(&del_lines, 0);
 +              }
 +
 +              for (i = 0; i < esm.nr; i++)
 +                      emit_diff_symbol_from_struct(o, &esm.buf[i]);
 +
 +              for (i = 0; i < esm.nr; i++)
 +                      free((void *)esm.buf[i].line);
 +      }
 +      esm.nr = 0;
 +}
 +
  void diff_flush(struct diff_options *options)
  {
        struct diff_queue_struct *q = &diff_queued_diff;
                separator++;
        }
  
 -      if (output_format & DIFF_FORMAT_DIRSTAT && DIFF_OPT_TST(options, DIRSTAT_BY_LINE))
 +      if (output_format & DIFF_FORMAT_DIRSTAT && options->flags.dirstat_by_line)
                dirstat_by_line = 1;
  
        if (output_format & (DIFF_FORMAT_DIFFSTAT|DIFF_FORMAT_SHORTSTAT|DIFF_FORMAT_NUMSTAT) ||
        }
  
        if (output_format & DIFF_FORMAT_NO_OUTPUT &&
 -          DIFF_OPT_TST(options, EXIT_WITH_STATUS) &&
 -          DIFF_OPT_TST(options, DIFF_FROM_CONTENTS)) {
 +          options->flags.exit_with_status &&
 +          options->flags.diff_from_contents) {
                /*
                 * run diff_flush_patch for the exit status. setting
                 * options->file to /dev/null should be safe, because we
                 */
                if (options->close_file)
                        fclose(options->file);
 -              options->file = fopen("/dev/null", "w");
 -              if (!options->file)
 -                      die_errno("Could not open /dev/null");
 +              options->file = xfopen("/dev/null", "w");
                options->close_file = 1;
 +              options->color_moved = 0;
                for (i = 0; i < q->nr; i++) {
                        struct diff_filepair *p = q->queue[i];
                        if (check_pair_status(p))
  
        if (output_format & DIFF_FORMAT_PATCH) {
                if (separator) {
 -                      fprintf(options->file, "%s%c",
 -                              diff_line_prefix(options),
 -                              options->line_termination);
 -                      if (options->stat_sep) {
 +                      emit_diff_symbol(options, DIFF_SYMBOL_SEPARATOR, NULL, 0, 0);
 +                      if (options->stat_sep)
                                /* attach patch instead of inline */
 -                              fputs(options->stat_sep, options->file);
 -                      }
 +                              emit_diff_symbol(options, DIFF_SYMBOL_STAT_SEP,
 +                                               NULL, 0, 0);
                }
  
 -              for (i = 0; i < q->nr; i++) {
 -                      struct diff_filepair *p = q->queue[i];
 -                      if (check_pair_status(p))
 -                              diff_flush_patch(p, options);
 -              }
 +              diff_flush_patch_all_file_pairs(options);
        }
  
        if (output_format & DIFF_FORMAT_CALLBACK)
@@@ -5619,11 -4854,11 +5619,11 @@@ free_queue
         * diff_addremove/diff_change does not set the bit when
         * DIFF_FROM_CONTENTS is in effect (e.g. with -w).
         */
 -      if (DIFF_OPT_TST(options, DIFF_FROM_CONTENTS)) {
 +      if (options->flags.diff_from_contents) {
                if (options->found_changes)
 -                      DIFF_OPT_SET(options, HAS_CHANGES);
 +                      options->flags.has_changes = 1;
                else
 -                      DIFF_OPT_CLR(options, HAS_CHANGES);
 +                      options->flags.has_changes = 0;
        }
  }
  
@@@ -5743,7 -4978,7 +5743,7 @@@ static void diffcore_skip_stat_unmatch(
                         * to determine how many paths were dirty only
                         * due to stat info mismatch.
                         */
 -                      if (!DIFF_OPT_TST(diffopt, NO_INDEX))
 +                      if (!diffopt->flags.no_index)
                                diffopt->skip_stat_unmatch++;
                        diff_free_filepair(p);
                }
@@@ -5792,10 -5027,10 +5792,10 @@@ void diffcore_std(struct diff_options *
                diff_resolve_rename_copy();
        diffcore_apply_filter(options);
  
 -      if (diff_queued_diff.nr && !DIFF_OPT_TST(options, DIFF_FROM_CONTENTS))
 -              DIFF_OPT_SET(options, HAS_CHANGES);
 +      if (diff_queued_diff.nr && !options->flags.diff_from_contents)
 +              options->flags.has_changes = 1;
        else
 -              DIFF_OPT_CLR(options, HAS_CHANGES);
 +              options->flags.has_changes = 0;
  
        options->found_follow = 0;
  }
@@@ -5807,23 -5042,23 +5807,23 @@@ int diff_result_code(struct diff_option
        diff_warn_rename_limit("diff.renameLimit",
                               opt->needed_rename_limit,
                               opt->degraded_cc_to_c);
 -      if (!DIFF_OPT_TST(opt, EXIT_WITH_STATUS) &&
 +      if (!opt->flags.exit_with_status &&
            !(opt->output_format & DIFF_FORMAT_CHECKDIFF))
                return status;
 -      if (DIFF_OPT_TST(opt, EXIT_WITH_STATUS) &&
 -          DIFF_OPT_TST(opt, HAS_CHANGES))
 +      if (opt->flags.exit_with_status &&
 +          opt->flags.has_changes)
                result |= 01;
        if ((opt->output_format & DIFF_FORMAT_CHECKDIFF) &&
 -          DIFF_OPT_TST(opt, CHECK_FAILED))
 +          opt->flags.check_failed)
                result |= 02;
        return result;
  }
  
  int diff_can_quit_early(struct diff_options *opt)
  {
 -      return (DIFF_OPT_TST(opt, QUICK) &&
 +      return (opt->flags.quick &&
                !opt->filter &&
 -              DIFF_OPT_TST(opt, HAS_CHANGES));
 +              opt->flags.has_changes);
  }
  
  /*
  static int is_submodule_ignored(const char *path, struct diff_options *options)
  {
        int ignored = 0;
 -      unsigned orig_flags = options->flags;
 -      if (!DIFF_OPT_TST(options, OVERRIDE_SUBMODULE_CONFIG))
 +      struct diff_flags orig_flags = options->flags;
 +      if (!options->flags.override_submodule_config)
                set_diffopt_flags_from_submodule_config(options, path);
 -      if (DIFF_OPT_TST(options, IGNORE_SUBMODULES))
 +      if (options->flags.ignore_submodules)
                ignored = 1;
        options->flags = orig_flags;
        return ignored;
  
  void diff_addremove(struct diff_options *options,
                    int addremove, unsigned mode,
 -                  const unsigned char *sha1,
 -                  int sha1_valid,
 +                  const struct object_id *oid,
 +                  int oid_valid,
                    const char *concatpath, unsigned dirty_submodule)
  {
        struct diff_filespec *one, *two;
         * Before the final output happens, they are pruned after
         * merged into rename/copy pairs as appropriate.
         */
 -      if (DIFF_OPT_TST(options, REVERSE_DIFF))
 +      if (options->flags.reverse_diff)
                addremove = (addremove == '+' ? '-' :
                             addremove == '-' ? '+' : addremove);
  
        two = alloc_filespec(concatpath);
  
        if (addremove != '+')
 -              fill_filespec(one, sha1, sha1_valid, mode);
 +              fill_filespec(one, oid, oid_valid, mode);
        if (addremove != '-') {
 -              fill_filespec(two, sha1, sha1_valid, mode);
 +              fill_filespec(two, oid, oid_valid, mode);
                two->dirty_submodule = dirty_submodule;
        }
  
        diff_queue(&diff_queued_diff, one, two);
 -      if (!DIFF_OPT_TST(options, DIFF_FROM_CONTENTS))
 -              DIFF_OPT_SET(options, HAS_CHANGES);
 +      if (!options->flags.diff_from_contents)
 +              options->flags.has_changes = 1;
  }
  
  void diff_change(struct diff_options *options,
                 unsigned old_mode, unsigned new_mode,
 -               const unsigned char *old_sha1,
 -               const unsigned char *new_sha1,
 -               int old_sha1_valid, int new_sha1_valid,
 +               const struct object_id *old_oid,
 +               const struct object_id *new_oid,
 +               int old_oid_valid, int new_oid_valid,
                 const char *concatpath,
                 unsigned old_dirty_submodule, unsigned new_dirty_submodule)
  {
            is_submodule_ignored(concatpath, options))
                return;
  
 -      if (DIFF_OPT_TST(options, REVERSE_DIFF)) {
 +      if (options->flags.reverse_diff) {
                SWAP(old_mode, new_mode);
 -              SWAP(old_sha1, new_sha1);
 -              SWAP(old_sha1_valid, new_sha1_valid);
 +              SWAP(old_oid, new_oid);
 +              SWAP(old_oid_valid, new_oid_valid);
                SWAP(old_dirty_submodule, new_dirty_submodule);
        }
  
  
        one = alloc_filespec(concatpath);
        two = alloc_filespec(concatpath);
 -      fill_filespec(one, old_sha1, old_sha1_valid, old_mode);
 -      fill_filespec(two, new_sha1, new_sha1_valid, new_mode);
 +      fill_filespec(one, old_oid, old_oid_valid, old_mode);
 +      fill_filespec(two, new_oid, new_oid_valid, new_mode);
        one->dirty_submodule = old_dirty_submodule;
        two->dirty_submodule = new_dirty_submodule;
        p = diff_queue(&diff_queued_diff, one, two);
  
 -      if (DIFF_OPT_TST(options, DIFF_FROM_CONTENTS))
 +      if (options->flags.diff_from_contents)
                return;
  
 -      if (DIFF_OPT_TST(options, QUICK) && options->skip_stat_unmatch &&
 +      if (options->flags.quick && options->skip_stat_unmatch &&
            !diff_filespec_check_stat_unmatch(p))
                return;
  
 -      DIFF_OPT_SET(options, HAS_CHANGES);
 +      options->flags.has_changes = 1;
  }
  
  struct diff_filepair *diff_unmerge(struct diff_options *options, const char *path)
@@@ -6009,7 -5244,7 +6009,7 @@@ size_t fill_textconv(struct userdiff_dr
  
        if (driver->textconv_cache && df->oid_valid) {
                *outbuf = notes_cache_get(driver->textconv_cache,
 -                                        df->oid.hash,
 +                                        &df->oid,
                                          &size);
                if (*outbuf)
                        return size;
  
        if (driver->textconv_cache && df->oid_valid) {
                /* ignore errors, as we might be in a readonly repository */
 -              notes_cache_put(driver->textconv_cache, df->oid.hash, *outbuf,
 +              notes_cache_put(driver->textconv_cache, &df->oid, *outbuf,
                                size);
                /*
                 * we could save up changes and flush them all at the end,
        return size;
  }
  
 +int textconv_object(const char *path,
 +                  unsigned mode,
 +                  const struct object_id *oid,
 +                  int oid_valid,
 +                  char **buf,
 +                  unsigned long *buf_size)
 +{
 +      struct diff_filespec *df;
 +      struct userdiff_driver *textconv;
 +
 +      df = alloc_filespec(path);
 +      fill_filespec(df, oid, oid_valid, mode);
 +      textconv = get_textconv(df);
 +      if (!textconv) {
 +              free_filespec(df);
 +              return 0;
 +      }
 +
 +      *buf_size = fill_textconv(textconv, df, buf);
 +      free_filespec(df);
 +      return 1;
 +}
 +
  void setup_diff_pager(struct diff_options *opt)
  {
        /*
         * and because it is easy to find people oneline advising "git diff
         * --exit-code" in hooks and other scripts, we do not do so.
         */
 -      if (!DIFF_OPT_TST(opt, EXIT_WITH_STATUS) &&
 +      if (!opt->flags.exit_with_status &&
            check_pager_config("diff") != 0)
                setup_pager();
  }
diff --combined t/t2203-add-intent.sh
@@@ -25,6 -25,18 +25,18 @@@ test_expect_success 'git status' 
        test_cmp expect actual
  '
  
+ test_expect_success 'git status with porcelain v2' '
+       git status --porcelain=v2 | grep -v "^?" >actual &&
+       nam1=d00491fd7e5bb6fa28c517a0bb32b8b506539d4d &&
+       nam2=ce013625030ba8dba906f756967f9e9ca394464a &&
+       cat >expect <<-EOF &&
+       1 DA N... 100644 000000 100644 $nam1 $_z40 1.t
+       1 A. N... 000000 100644 100644 $_z40 $nam2 elif
+       1 .A N... 000000 000000 100644 $_z40 $_z40 file
+       EOF
+       test_cmp expect actual
+ '
  test_expect_success 'check result of "add -N"' '
        git ls-files -s file >actual &&
        empty=$(git hash-object --stdin </dev/null) &&
@@@ -129,10 -141,10 +141,10 @@@ test_expect_success 'cache-tree does sk
        )
  '
  
 -test_expect_success 'commit: ita entries ignored in empty intial commit check' '
 -      git init empty-intial-commit &&
 +test_expect_success 'commit: ita entries ignored in empty initial commit check' '
 +      git init empty-initial-commit &&
        (
 -              cd empty-intial-commit &&
 +              cd empty-initial-commit &&
                : >one &&
                git add -N one &&
                test_must_fail git commit -m nothing-new-here
@@@ -150,5 -162,65 +162,65 @@@ test_expect_success 'commit: ita entrie
        )
  '
  
+ test_expect_success 'rename detection finds the right names' '
+       git init rename-detection &&
+       (
+               cd rename-detection &&
+               echo contents >first &&
+               git add first &&
+               git commit -m first &&
+               mv first third &&
+               git add -N third &&
+               git status | grep -v "^?" >actual.1 &&
+               test_i18ngrep "renamed: *first -> third" actual.1 &&
+               git status --porcelain | grep -v "^?" >actual.2 &&
+               cat >expected.2 <<-\EOF &&
+                R first -> third
+               EOF
+               test_cmp expected.2 actual.2 &&
+               hash=12f00e90b6ef79117ce6e650416b8cf517099b78 &&
+               git status --porcelain=v2 | grep -v "^?" >actual.3 &&
+               cat >expected.3 <<-EOF &&
+               2 .R N... 100644 100644 100644 $hash $hash R100 third   first
+               EOF
+               test_cmp expected.3 actual.3
+       )
+ '
+ test_expect_success 'double rename detection in status' '
+       git init rename-detection-2 &&
+       (
+               cd rename-detection-2 &&
+               echo contents >first &&
+               git add first &&
+               git commit -m first &&
+               git mv first second &&
+               mv second third &&
+               git add -N third &&
+               git status | grep -v "^?" >actual.1 &&
+               test_i18ngrep "renamed: *first -> second" actual.1 &&
+               test_i18ngrep "renamed: *second -> third" actual.1 &&
+               git status --porcelain | grep -v "^?" >actual.2 &&
+               cat >expected.2 <<-\EOF &&
+               R  first -> second
+                R second -> third
+               EOF
+               test_cmp expected.2 actual.2 &&
+               hash=12f00e90b6ef79117ce6e650416b8cf517099b78 &&
+               git status --porcelain=v2 | grep -v "^?" >actual.3 &&
+               cat >expected.3 <<-EOF &&
+               2 R. N... 100644 100644 100644 $hash $hash R100 second  first
+               2 .R N... 100644 100644 100644 $hash $hash R100 third   second
+               EOF
+               test_cmp expected.3 actual.3
+       )
+ '
  test_done
  
diff --combined wt-status.c
@@@ -121,13 -121,15 +121,13 @@@ static void status_printf_more(struct w
  
  void wt_status_prepare(struct wt_status *s)
  {
 -      struct object_id oid;
 -
        memset(s, 0, sizeof(*s));
        memcpy(s->color_palette, default_wt_status_colors,
               sizeof(default_wt_status_colors));
        s->show_untracked_files = SHOW_NORMAL_UNTRACKED_FILES;
        s->use_color = -1;
        s->relative_paths = 1;
 -      s->branch = resolve_refdup("HEAD", 0, oid.hash, NULL);
 +      s->branch = resolve_refdup("HEAD", 0, NULL, NULL);
        s->reference = "HEAD";
        s->fp = stdout;
        s->index_file = get_index_file();
        s->untracked.strdup_strings = 1;
        s->ignored.strdup_strings = 1;
        s->show_branch = -1;  /* unspecified */
 +      s->show_stash = 0;
        s->display_comment_prefix = 0;
  }
  
@@@ -360,8 -361,6 +360,6 @@@ static void wt_longstatus_print_change_
        switch (change_type) {
        case WT_STATUS_UPDATED:
                status = d->index_status;
-               if (d->head_path)
-                       one_name = d->head_path;
                break;
        case WT_STATUS_CHANGED:
                if (d->new_submodule_commits || d->dirty_submodule) {
                    change_type);
        }
  
+       /*
+        * Only pick up the rename it's relevant. If the rename is for
+        * the changed section and we're printing the updated section,
+        * ignore it.
+        */
+       if (d->rename_status == status)
+               one_name = d->rename_source;
        one = quote_path(one_name, s->prefix, &onebuf);
        two = quote_path(two_name, s->prefix, &twobuf);
  
                die("BUG: unhandled diff status %c", status);
        len = label_width - utf8_strwidth(what);
        assert(len >= 0);
-       if (status == DIFF_STATUS_COPIED || status == DIFF_STATUS_RENAMED)
+       if (one_name != two_name)
                status_printf_more(s, c, "%s%.*s%s -> %s",
                                   what, len, padding, one, two);
        else
        strbuf_release(&twobuf);
  }
  
- static char short_submodule_status(struct wt_status_change_data *d) {
+ static char short_submodule_status(struct wt_status_change_data *d)
+ {
        if (d->new_submodule_commits)
                return 'M';
        if (d->dirty_submodule & DIRTY_SUBMODULE_MODIFIED)
@@@ -432,7 -440,7 +439,7 @@@ static void wt_status_collect_changed_c
                struct wt_status_change_data *d;
  
                p = q->queue[i];
-               it = string_list_insert(&s->change, p->one->path);
+               it = string_list_insert(&s->change, p->two->path);
                d = it->util;
                if (!d) {
                        d = xcalloc(1, sizeof(*d));
                        /* mode_worktree is zero for a delete. */
                        break;
  
+               case DIFF_STATUS_COPIED:
+               case DIFF_STATUS_RENAMED:
+                       if (d->rename_status)
+                               die("BUG: multiple renames on the same target? how?");
+                       d->rename_source = xstrdup(p->one->path);
+                       d->rename_score = p->score * 100 / MAX_SCORE;
+                       d->rename_status = p->status;
+                       /* fallthru */
                case DIFF_STATUS_MODIFIED:
                case DIFF_STATUS_TYPE_CHANGED:
                case DIFF_STATUS_UNMERGED:
                        oidcpy(&d->oid_index, &p->one->oid);
                        break;
  
-               case DIFF_STATUS_UNKNOWN:
-                       die("BUG: worktree status unknown???");
+               default:
+                       die("BUG: unhandled diff-files status '%c'", p->status);
                        break;
                }
  
@@@ -530,8 -546,11 +545,11 @@@ static void wt_status_collect_updated_c
  
                case DIFF_STATUS_COPIED:
                case DIFF_STATUS_RENAMED:
-                       d->head_path = xstrdup(p->one->path);
-                       d->score = p->score * 100 / MAX_SCORE;
+                       if (d->rename_status)
+                               die("BUG: multiple renames on the same target? how?");
+                       d->rename_source = xstrdup(p->one->path);
+                       d->rename_score = p->score * 100 / MAX_SCORE;
+                       d->rename_status = p->status;
                        /* fallthru */
                case DIFF_STATUS_MODIFIED:
                case DIFF_STATUS_TYPE_CHANGED:
                         * values in these fields.
                         */
                        break;
+               default:
+                       die("BUG: unhandled diff-index status '%c'", p->status);
+                       break;
                }
        }
  }
@@@ -559,12 -582,12 +581,12 @@@ static void wt_status_collect_changes_w
        init_revisions(&rev, NULL);
        setup_revisions(0, NULL, &rev, NULL);
        rev.diffopt.output_format |= DIFF_FORMAT_CALLBACK;
 -      DIFF_OPT_SET(&rev.diffopt, DIRTY_SUBMODULES);
 +      rev.diffopt.flags.dirty_submodules = 1;
        rev.diffopt.ita_invisible_in_index = 1;
        if (!s->show_untracked_files)
 -              DIFF_OPT_SET(&rev.diffopt, IGNORE_UNTRACKED_IN_SUBMODULES);
 +              rev.diffopt.flags.ignore_untracked_in_submodules = 1;
        if (s->ignore_submodule_arg) {
 -              DIFF_OPT_SET(&rev.diffopt, OVERRIDE_SUBMODULE_CONFIG);
 +              rev.diffopt.flags.override_submodule_config = 1;
                handle_ignore_submodules_arg(&rev.diffopt, s->ignore_submodule_arg);
        }
        rev.diffopt.format_callback = wt_status_collect_changed_cb;
@@@ -583,7 -606,7 +605,7 @@@ static void wt_status_collect_changes_i
        opt.def = s->is_initial ? EMPTY_TREE_SHA1_HEX : s->reference;
        setup_revisions(0, NULL, &rev, &opt);
  
 -      DIFF_OPT_SET(&rev.diffopt, OVERRIDE_SUBMODULE_CONFIG);
 +      rev.diffopt.flags.override_submodule_config = 1;
        rev.diffopt.ita_invisible_in_index = 1;
        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;
-       rev.diffopt.detect_rename = 1;
+       rev.diffopt.detect_rename = DIFF_DETECT_RENAME;
        rev.diffopt.rename_limit = 200;
        rev.diffopt.break_opt = 0;
        copy_pathspec(&rev.prune_data, &s->pathspec);
@@@ -658,18 -681,13 +680,18 @@@ static void wt_status_collect_untracked
        if (s->show_untracked_files != SHOW_ALL_UNTRACKED_FILES)
                dir.flags |=
                        DIR_SHOW_OTHER_DIRECTORIES | DIR_HIDE_EMPTY_DIRECTORIES;
 -      if (s->show_ignored_files)
 +      if (s->show_ignored_mode) {
                dir.flags |= DIR_SHOW_IGNORED_TOO;
 -      else
 +
 +              if (s->show_ignored_mode == SHOW_MATCHING_IGNORED)
 +                      dir.flags |= DIR_SHOW_IGNORED_TOO_MODE_MATCHING;
 +      } else {
                dir.untracked = the_index.untracked;
 +      }
 +
        setup_standard_excludes(&dir);
  
 -      fill_directory(&dir, &s->pathspec);
 +      fill_directory(&dir, &the_index, &s->pathspec);
  
        for (i = 0; i < dir.nr; i++) {
                struct dir_entry *ent = dir.entries[i];
@@@ -805,27 -823,6 +827,27 @@@ static void wt_longstatus_print_changed
        wt_longstatus_print_trailer(s);
  }
  
 +static int stash_count_refs(struct object_id *ooid, struct object_id *noid,
 +                          const char *email, timestamp_t timestamp, int tz,
 +                          const char *message, void *cb_data)
 +{
 +      int *c = cb_data;
 +      (*c)++;
 +      return 0;
 +}
 +
 +static void wt_longstatus_print_stash_summary(struct wt_status *s)
 +{
 +      int stash_count = 0;
 +
 +      for_each_reflog_ent("refs/stash", stash_count_refs, &stash_count);
 +      if (stash_count > 0)
 +              status_printf_ln(s, GIT_COLOR_NORMAL,
 +                               Q_("Your stash currently has %d entry",
 +                                  "Your stash currently has %d entries", stash_count),
 +                               stash_count);
 +}
 +
  static void wt_longstatus_print_submodule_summary(struct wt_status *s, int uncommitted)
  {
        struct child_process sm_summary = CHILD_PROCESS_INIT;
@@@ -937,7 -934,7 +959,7 @@@ size_t wt_status_locate_end(const char 
  
  void wt_status_add_cut_line(FILE *fp)
  {
 -      const char *explanation = _("Do not touch the line above.\nEverything below will be removed.");
 +      const char *explanation = _("Do not modify or remove the line above.\nEverything below it will be ignored.");
        struct strbuf buf = STRBUF_INIT;
  
        fprintf(fp, "%c %s", comment_line_char, cut_line);
@@@ -954,7 -951,7 +976,7 @@@ static void wt_longstatus_print_verbose
        const char *c = color(WT_STATUS_HEADER, s);
  
        init_revisions(&rev, NULL);
 -      DIFF_OPT_SET(&rev.diffopt, ALLOW_TEXTCONV);
 +      rev.diffopt.flags.allow_textconv = 1;
        rev.diffopt.ita_invisible_in_index = 1;
  
        memset(&opt, 0, sizeof(opt));
        setup_revisions(0, NULL, &rev, &opt);
  
        rev.diffopt.output_format |= DIFF_FORMAT_PATCH;
-       rev.diffopt.detect_rename = 1;
+       rev.diffopt.detect_rename = DIFF_DETECT_RENAME;
        rev.diffopt.file = s->fp;
        rev.diffopt.close_file = 0;
        /*
@@@ -1028,8 -1025,7 +1050,8 @@@ static void wt_longstatus_print_trackin
                color_fprintf_ln(s->fp, color(WT_STATUS_HEADER, s), "%c",
                                 comment_line_char);
        else
 -              fputs("", s->fp);
 +              fputs("\n", s->fp);
 +      strbuf_release(&sb);
  }
  
  static int has_unmerged(struct wt_status *s)
@@@ -1092,8 -1088,7 +1114,8 @@@ static void show_am_in_progress(struct 
  static char *read_line_from_git_path(const char *filename)
  {
        struct strbuf buf = STRBUF_INIT;
 -      FILE *fp = fopen(git_path("%s", filename), "r");
 +      FILE *fp = fopen_or_warn(git_path("%s", filename), "r");
 +
        if (!fp) {
                strbuf_release(&buf);
                return NULL;
@@@ -1197,7 -1192,6 +1219,7 @@@ static int read_rebase_todolist(const c
                string_list_append(lines, line.buf);
        }
        fclose(f);
 +      strbuf_release(&line);
        return 0;
  }
  
@@@ -1417,7 -1411,7 +1439,7 @@@ struct grab_1st_switch_cbdata 
  };
  
  static int grab_1st_switch(struct object_id *ooid, struct object_id *noid,
 -                         const char *email, unsigned long timestamp, int tz,
 +                         const char *email, timestamp_t timestamp, int tz,
                           const char *message, void *cb_data)
  {
        struct grab_1st_switch_cbdata *cb = cb_data;
@@@ -1454,11 -1448,11 +1476,11 @@@ static void wt_status_get_detached_from
                return;
        }
  
 -      if (dwim_ref(cb.buf.buf, cb.buf.len, oid.hash, &ref) == 1 &&
 +      if (dwim_ref(cb.buf.buf, cb.buf.len, &oid, &ref) == 1 &&
            /* sha1 is a commit? match without further lookup */
            (!oidcmp(&cb.noid, &oid) ||
             /* perhaps sha1 is a tag, try to dereference to a commit */
 -           ((commit = lookup_commit_reference_gently(oid.hash, 1)) != NULL &&
 +           ((commit = lookup_commit_reference_gently(&oid, 1)) != NULL &&
              !oidcmp(&cb.noid, &commit->object.oid)))) {
                const char *from = ref;
                if (!skip_prefix(from, "refs/tags/", &from))
@@@ -1606,10 -1600,7 +1628,10 @@@ static void wt_longstatus_print(struct 
  
        if (s->is_initial) {
                status_printf_ln(s, color(WT_STATUS_HEADER, s), "%s", "");
 -              status_printf_ln(s, color(WT_STATUS_HEADER, s), _("Initial commit"));
 +              status_printf_ln(s, color(WT_STATUS_HEADER, s),
 +                               s->commit_template
 +                               ? _("Initial commit")
 +                               : _("No commits yet"));
                status_printf_ln(s, color(WT_STATUS_HEADER, s), "%s", "");
        }
  
        }
        if (s->show_untracked_files) {
                wt_longstatus_print_other(s, &s->untracked, _("Untracked files"), "add");
 -              if (s->show_ignored_files)
 +              if (s->show_ignored_mode)
                        wt_longstatus_print_other(s, &s->ignored, _("Ignored files"), "add -f");
                if (advice_status_u_option && 2000 < s->untracked_in_ms) {
                        status_printf_ln(s, GIT_COLOR_NORMAL, "%s", "");
                } else
                        printf(_("nothing to commit, working tree clean\n"));
        }
 +      if(s->show_stash)
 +              wt_longstatus_print_stash_summary(s);
  }
  
  static void wt_shortstatus_unmerged(struct string_list_item *it,
@@@ -1719,13 -1708,14 +1741,14 @@@ static void wt_shortstatus_status(struc
        putchar(' ');
        if (s->null_termination) {
                fprintf(stdout, "%s%c", it->string, 0);
-               if (d->head_path)
-                       fprintf(stdout, "%s%c", d->head_path, 0);
+               if (d->rename_source)
+                       fprintf(stdout, "%s%c", d->rename_source, 0);
        } else {
                struct strbuf onebuf = STRBUF_INIT;
                const char *one;
-               if (d->head_path) {
-                       one = quote_path(d->head_path, s->prefix, &onebuf);
+               if (d->rename_source) {
+                       one = quote_path(d->rename_source, s->prefix, &onebuf);
                        if (*one != '"' && strchr(one, ' ') != NULL) {
                                putchar('"');
                                strbuf_addch(&onebuf, '"');
@@@ -1768,7 -1758,6 +1791,7 @@@ static void wt_shortstatus_print_tracki
        const char *branch_color_remote = color(WT_STATUS_REMOTE_BRANCH, s);
  
        const char *base;
 +      char *short_base;
        const char *branch_name;
        int num_ours, num_theirs;
        int upstream_is_gone = 0;
  #define LABEL(string) (s->no_gettext ? (string) : _(string))
  
        if (s->is_initial)
 -              color_fprintf(s->fp, header_color, LABEL(N_("Initial commit on ")));
 +              color_fprintf(s->fp, header_color, LABEL(N_("No commits yet on ")));
  
        if (!strcmp(s->branch, "HEAD")) {
                color_fprintf(s->fp, color(WT_STATUS_NOBRANCH, s), "%s",
                upstream_is_gone = 1;
        }
  
 -      base = shorten_unambiguous_ref(base, 0);
 +      short_base = shorten_unambiguous_ref(base, 0);
        color_fprintf(s->fp, header_color, "...");
 -      color_fprintf(s->fp, branch_color_remote, "%s", base);
 -      free((char *)base);
 +      color_fprintf(s->fp, branch_color_remote, "%s", short_base);
 +      free(short_base);
  
        if (!upstream_is_gone && !num_ours && !num_theirs)
                goto conclude;
@@@ -2030,10 -2019,10 +2053,10 @@@ static void wt_porcelain_v2_print_chang
        struct wt_status *s)
  {
        struct wt_status_change_data *d = it->util;
-       struct strbuf buf_index = STRBUF_INIT;
-       struct strbuf buf_head = STRBUF_INIT;
-       const char *path_index = NULL;
-       const char *path_head = NULL;
+       struct strbuf buf = STRBUF_INIT;
+       struct strbuf buf_from = STRBUF_INIT;
+       const char *path = NULL;
+       const char *path_from = NULL;
        char key[3];
        char submodule_token[5];
        char sep_char, eol_char;
                 */
                sep_char = '\0';
                eol_char = '\0';
-               path_index = it->string;
-               path_head = d->head_path;
+               path = it->string;
+               path_from = d->rename_source;
        } else {
                /*
                 * Path(s) are C-quoted if necessary. Current path is ALWAYS first.
                 */
                sep_char = '\t';
                eol_char = '\n';
-               path_index = quote_path(it->string, s->prefix, &buf_index);
-               if (d->head_path)
-                       path_head = quote_path(d->head_path, s->prefix, &buf_head);
+               path = quote_path(it->string, s->prefix, &buf);
+               if (d->rename_source)
+                       path_from = quote_path(d->rename_source, s->prefix, &buf_from);
        }
  
-       if (path_head)
+       if (path_from)
                fprintf(s->fp, "2 %s %s %06o %06o %06o %s %s %c%d %s%c%s%c",
                                key, submodule_token,
                                d->mode_head, d->mode_index, d->mode_worktree,
                                oid_to_hex(&d->oid_head), oid_to_hex(&d->oid_index),
-                               key[0], d->score,
-                               path_index, sep_char, path_head, eol_char);
+                               d->rename_status, d->rename_score,
+                               path, sep_char, path_from, eol_char);
        else
                fprintf(s->fp, "1 %s %s %06o %06o %06o %s %s %s%c",
                                key, submodule_token,
                                d->mode_head, d->mode_index, d->mode_worktree,
                                oid_to_hex(&d->oid_head), oid_to_hex(&d->oid_index),
-                               path_index, eol_char);
+                               path, eol_char);
  
-       strbuf_release(&buf_index);
-       strbuf_release(&buf_head);
+       strbuf_release(&buf);
+       strbuf_release(&buf_from);
  }
  
  /*
@@@ -2267,11 -2256,9 +2290,11 @@@ int has_unstaged_changes(int ignore_sub
        int result;
  
        init_revisions(&rev_info, NULL);
 -      if (ignore_submodules)
 -              DIFF_OPT_SET(&rev_info.diffopt, IGNORE_SUBMODULES);
 -      DIFF_OPT_SET(&rev_info.diffopt, QUICK);
 +      if (ignore_submodules) {
 +              rev_info.diffopt.flags.ignore_submodules = 1;
 +              rev_info.diffopt.flags.override_submodule_config = 1;
 +      }
 +      rev_info.diffopt.flags.quick = 1;
        diff_setup_done(&rev_info.diffopt);
        result = run_diff_files(&rev_info, 0);
        return diff_result_code(&rev_info.diffopt, result);
@@@ -2290,8 -2277,8 +2313,8 @@@ int has_uncommitted_changes(int ignore_
  
        init_revisions(&rev_info, NULL);
        if (ignore_submodules)
 -              DIFF_OPT_SET(&rev_info.diffopt, IGNORE_SUBMODULES);
 -      DIFF_OPT_SET(&rev_info.diffopt, QUICK);
 +              rev_info.diffopt.flags.ignore_submodules = 1;
 +      rev_info.diffopt.flags.quick = 1;
        add_head_to_pending(&rev_info);
        diff_setup_done(&rev_info.diffopt);
        result = run_diff_index(&rev_info, 1);
   */
  int require_clean_work_tree(const char *action, const char *hint, int ignore_submodules, int gently)
  {
 -      struct lock_file *lock_file = xcalloc(1, sizeof(*lock_file));
 +      struct lock_file lock_file = LOCK_INIT;
        int err = 0, fd;
  
 -      fd = hold_locked_index(lock_file, 0);
 +      fd = hold_locked_index(&lock_file, 0);
        refresh_cache(REFRESH_QUIET);
        if (0 <= fd)
 -              update_index_if_able(&the_index, lock_file);
 -      rollback_lock_file(lock_file);
 +              update_index_if_able(&the_index, &lock_file);
 +      rollback_lock_file(&lock_file);
  
        if (has_unstaged_changes(ignore_submodules)) {
                /* TRANSLATORS: the action is e.g. "pull with rebase" */
diff --combined wt-status.h
@@@ -27,12 -27,6 +27,12 @@@ enum untracked_status_type 
        SHOW_ALL_UNTRACKED_FILES
  };
  
 +enum show_ignored_type {
 +      SHOW_NO_IGNORED,
 +      SHOW_TRADITIONAL_IGNORED,
 +      SHOW_MATCHING_IGNORED,
 +};
 +
  /* from where does this commit originate */
  enum commit_whence {
        FROM_COMMIT,     /* normal */
@@@ -44,10 -38,11 +44,11 @@@ struct wt_status_change_data 
        int worktree_status;
        int index_status;
        int stagemask;
-       int score;
        int mode_head, mode_index, mode_worktree;
        struct object_id oid_head, oid_index;
-       char *head_path;
+       int rename_status;
+       int rename_score;
+       char *rename_source;
        unsigned dirty_submodule       : 2;
        unsigned new_submodule_commits : 1;
  };
@@@ -76,15 -71,13 +77,15 @@@ struct wt_status 
        int display_comment_prefix;
        int relative_paths;
        int submodule_summary;
 -      int show_ignored_files;
 +      enum show_ignored_type show_ignored_mode;
        enum untracked_status_type show_untracked_files;
        const char *ignore_submodule_arg;
        char color_palette[WT_STATUS_MAXSLOT][COLOR_MAXLEN];
        unsigned colopts;
        int null_termination;
 +      int commit_template;
        int show_branch;
 +      int show_stash;
        int hints;
  
        enum wt_status_format status_format;