status: add option to show ignored files differently
[gitweb.git] / diff.c
diff --git a/diff.c b/diff.c
index 9c382580306e340ed6333f96bc4919c4c507a7b9..6fd288420bc55b5213ad6dfea4a4f411543d6dbf 100644 (file)
--- a/diff.c
+++ b/diff.c
 #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
@@ -32,6 +34,7 @@ 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;
@@ -56,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)
@@ -81,6 +92,22 @@ static int parse_diff_color_slot(const char *var)
                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;
 }
 
@@ -229,12 +256,44 @@ 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)
@@ -299,6 +358,9 @@ int git_diff_ui_config(const char *var, const char *value, void *cb)
                return 0;
        }
 
+       if (git_color_config(var, value, cb) < 0)
+               return -1;
+
        return git_diff_basic_config(var, value, cb);
 }
 
@@ -343,9 +405,6 @@ int git_diff_basic_config(const char *var, const char *value, void *cb)
                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;
 
@@ -403,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;
@@ -415,7 +472,6 @@ struct emit_callback {
        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;
@@ -557,68 +613,737 @@ static void emit_line(struct diff_options *o, const char *set, const char *reset
        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 next_byte(const char **cp, const char **endp,
+                    const struct diff_options *diffopt)
+{
+       int retval;
+
+       if (*cp > *endp)
+               return -1;
+
+       if (isspace(**cp)) {
+               if (DIFF_XDL_TST(diffopt, IGNORE_WHITESPACE_CHANGE)) {
+                       while (*cp < *endp && isspace(**cp))
+                               (*cp)++;
+                       /*
+                        * After skipping a couple of whitespaces,
+                        * we still have to account for one space.
+                        */
+                       return (int)' ';
+               }
+
+               if (DIFF_XDL_TST(diffopt, IGNORE_WHITESPACE)) {
+                       while (*cp < *endp && isspace(**cp))
+                               (*cp)++;
+                       /* return the first non-ws character via the usual below */
+               }
+       }
+
+       retval = (unsigned char)(**cp);
+       (*cp)++;
+       return retval;
+}
+
+static int moved_entry_cmp(const struct diff_options *diffopt,
+                          const struct moved_entry *a,
+                          const struct moved_entry *b,
+                          const void *keydata)
+{
+       const char *ap = a->es->line, *ae = a->es->line + a->es->len;
+       const char *bp = b->es->line, *be = b->es->line + b->es->len;
+
+       if (!(diffopt->xdl_opts & XDF_WHITESPACE_FLAGS))
+               return a->es->len != b->es->len  || memcmp(ap, bp, a->es->len);
+
+       if (DIFF_XDL_TST(diffopt, IGNORE_WHITESPACE_AT_EOL)) {
+               while (ae > ap && isspace(*ae))
+                       ae--;
+               while (be > bp && isspace(*be))
+                       be--;
+       }
+
+       while (1) {
+               int ca, cb;
+               ca = next_byte(&ap, &ae, diffopt);
+               cb = next_byte(&bp, &be, diffopt);
+               if (ca != cb)
+                       return 1;
+               if (ca < 0)
+                       return 0;
+       }
+}
+
+static unsigned get_string_hash(struct emitted_diff_symbol *es, struct diff_options *o)
+{
+       if (o->xdl_opts & XDF_WHITESPACE_FLAGS) {
+               static struct strbuf sb = STRBUF_INIT;
+               const char *ap = es->line, *ae = es->line + es->len;
+               int c;
+
+               strbuf_reset(&sb);
+               while (ae > ap && isspace(*ae))
+                       ae--;
+               while ((c = next_byte(&ap, &ae, o)) > 0)
+                       strbuf_addch(&sb, c);
+
+               return memhash(sb.buf, sb.len);
+       } else {
+               return memhash(es->line, es->len);
+       }
+}
+
+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 = get_string_hash(l, o);
+       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,
@@ -641,7 +1366,8 @@ 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 @@ */
@@ -675,7 +1401,9 @@ static void emit_hunk_header(struct emit_callback *ecbdata,
        }
 
        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);
 }
 
@@ -691,23 +1419,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;
        }
 }
@@ -716,7 +1444,6 @@ static void emit_rewrite_lines(struct emit_callback *ecb,
                               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) {
@@ -734,13 +1461,8 @@ static void emit_rewrite_lines(struct emit_callback *ecb,
                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,
@@ -752,16 +1474,12 @@ 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)) {
                a_prefix = o->b_prefix;
@@ -773,8 +1491,6 @@ static void emit_rewrite_diff(const char *name_a,
 
        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);
@@ -801,18 +1517,23 @@ static void emit_rewrite_diff(const char *name_a,
 
        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)
@@ -825,7 +1546,7 @@ static void emit_rewrite_diff(const char *name_a,
 
 struct diff_words_buffer {
        mmfile_t text;
-       long alloc;
+       unsigned long alloc;
        struct diff_words_orig {
                const char *begin, *end;
        } *orig;
@@ -872,37 +1593,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;
 }
 
@@ -984,24 +1717,20 @@ static void fn_out_diff_words_aux(void *priv, char *line, unsigned long len)
                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;
@@ -1095,11 +1824,12 @@ static void diff_words_show(struct diff_words_data *diff_words)
 
        /* 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;
        }
@@ -1122,12 +1852,12 @@ static void diff_words_show(struct diff_words_data *diff_words)
        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;
 }
@@ -1135,9 +1865,29 @@ static void diff_words_show(struct diff_words_data *diff_words)
 /* 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)
@@ -1173,6 +1923,11 @@ static void init_diff_words_data(struct emit_callback *ecbdata,
                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)
@@ -1207,6 +1962,7 @@ static void free_diff_words_data(struct emit_callback *ecbdata)
 {
        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);
@@ -1243,8 +1999,6 @@ static unsigned long sane_truncate_line(struct emit_callback *ecb, char *line, u
        unsigned long allot;
        size_t l = len;
 
-       if (ecb->truncate)
-               return ecb->truncate(line, len);
        cp = line;
        allot = l;
        while (0 < l) {
@@ -1273,30 +2027,25 @@ static void find_lno(const char *line, struct emit_callback *ecbdata)
 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;
        }
 
@@ -1312,12 +2061,13 @@ static void fn_out_consume(void *priv, char *line, unsigned long len)
                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);
@@ -1337,21 +2087,7 @@ static void fn_out_consume(void *priv, char *line, unsigned long len)
                        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;
        }
 
@@ -1372,8 +2108,8 @@ static void fn_out_consume(void *priv, char *line, unsigned long len)
        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;
        }
 }
@@ -1518,20 +2254,14 @@ static int scale_linear(int it, int width, int max_change)
        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)
@@ -1555,14 +2285,16 @@ 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,
@@ -1589,9 +2321,19 @@ int print_stat_summary(FILE *fp, int files, int insertions, int deletions)
                            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)
@@ -1601,13 +2343,13 @@ 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);
@@ -1761,26 +2503,32 @@ static void show_stats(struct diffstat_t *data, struct diff_options *options)
                }
 
                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;
                }
 
@@ -1803,14 +2551,16 @@ static void show_stats(struct diffstat_t *data, struct diff_options *options)
                                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++) {
@@ -1831,11 +2581,14 @@ static void show_stats(struct diffstat_t *data, struct diff_options *options)
                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)
@@ -1847,7 +2600,7 @@ static void show_shortstats(struct diffstat_t *data, struct diff_options *option
 
        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))) {
@@ -1857,8 +2610,7 @@ static void show_shortstats(struct diffstat_t *data, struct diff_options *option
                        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)
@@ -2222,8 +2974,8 @@ static unsigned char *deflate_it(char *data,
        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;
@@ -2252,13 +3004,18 @@ static void emit_binary_diff_body(FILE *file, mmfile_t *one, mmfile_t *two,
        }
 
        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;
@@ -2267,8 +3024,9 @@ static void emit_binary_diff_body(FILE *file, mmfile_t *one, mmfile_t *two,
        /* 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;
@@ -2276,20 +3034,24 @@ static void emit_binary_diff_body(FILE *file, mmfile_t *one, mmfile_t *two,
                        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)
@@ -2366,24 +3128,16 @@ static void builtin_diff(const char *name_a,
        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;
        }
 
@@ -2432,7 +3186,8 @@ static void builtin_diff(const char *name_a,
                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);
@@ -2442,23 +3197,31 @@ static void builtin_diff(const char *name_a,
        }
 
        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) &&
            ( (!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)) {
                        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)
@@ -2467,16 +3230,21 @@ static void builtin_diff(const char *name_a,
                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]);
+                       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.. */
@@ -2488,7 +3256,8 @@ static void builtin_diff(const char *name_a,
                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);
                }
 
@@ -2957,7 +3726,6 @@ 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);
@@ -2967,18 +3735,18 @@ static void prep_temp_blob(const char *path, struct diff_tempfile *temp,
        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);
@@ -3246,7 +4014,7 @@ static void diff_fill_oid_info(struct diff_filespec *one)
                        }
                        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);
                }
        }
@@ -3279,8 +4047,8 @@ static void run_diff(struct diff_filepair *p, struct diff_options *o)
        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);
@@ -3403,6 +4171,8 @@ void diff_setup(struct diff_options *options)
                options->a_prefix = "a/";
                options->b_prefix = "b/";
        }
+
+       options->color_moved = diff_color_moved_default;
 }
 
 void diff_setup_done(struct diff_options *options)
@@ -3512,6 +4282,9 @@ void diff_setup_done(struct diff_options *options)
 
        if (DIFF_OPT_TST(options, 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)
@@ -3936,7 +4709,19 @@ int diff_opt_parse(struct diff_options *options,
        }
        else if (!strcmp(arg, "--no-color"))
                options->use_color = 0;
-       else if (!strcmp(arg, "--color-words")) {
+       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 (!strcmp(arg, "--color-words")) {
                options->use_color = 1;
                options->word_diff = DIFF_WORDS_COLOR;
        }
@@ -4466,67 +5251,79 @@ static void flush_one_pair(struct diff_filepair *p, struct diff_options *opt)
        }
 }
 
-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;
        }
 }
@@ -4731,6 +5528,51 @@ void diff_warn_rename_limit(const char *varname, int needed, int degraded_cc)
                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;
@@ -4803,6 +5645,7 @@ void diff_flush(struct diff_options *options)
                        fclose(options->file);
                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))
@@ -4814,20 +5657,14 @@ void diff_flush(struct diff_options *options)
 
        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)