diff: document the new --color-moved setting
[gitweb.git] / diff.c
diff --git a/diff.c b/diff.c
index e0d39d04da5c863b3156b4c1a801b0ca7d8e9abe..4af73a7e0c4da09f21b0819f2f7d32ad74f861e4 100644 (file)
--- a/diff.c
+++ b/diff.c
@@ -16,6 +16,7 @@
 #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"
@@ -32,6 +33,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 +58,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 +91,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 +255,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)
@@ -571,6 +629,8 @@ enum diff_symbol {
        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,
@@ -600,9 +660,355 @@ enum diff_symbol {
  * 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_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)
+{
+       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;
+}
+
+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 (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;
+}
+
+/* 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) {
+                       if (block_length < COLOR_MOVED_MIN_BLOCK_LENGTH &&
+                           o->color_moved != COLOR_MOVED_PLAIN) {
+                               for (i = 0; i < block_length + 1; i++) {
+                                       l = &o->emitted_symbols->buf[n - i];
+                                       l->flags &= ~DIFF_SYMBOL_MOVED_LINE;
+                               }
+                       }
+                       pmb_nr = 0;
+                       block_length = 0;
+                       continue;
+               }
+
+               l->flags |= DIFF_SYMBOL_MOVED_LINE;
+               block_length++;
+
+               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;
+               }
+
+               if (flipped_block)
+                       l->flags |= DIFF_SYMBOL_MOVED_LINE_ALT;
+       }
+
+       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,
@@ -629,12 +1035,18 @@ static void emit_line_ws_markup(struct diff_options *o,
        }
 }
 
-static void emit_diff_symbol(struct diff_options *o, enum diff_symbol s,
-                            const char *line, int len, unsigned flags)
+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);
@@ -647,6 +1059,7 @@ static void emit_diff_symbol(struct diff_options *o, enum diff_symbol s,
        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:
@@ -670,14 +1083,56 @@ static void emit_diff_symbol(struct diff_options *o, enum diff_symbol s,
                                    flags & (DIFF_SYMBOL_CONTENT_WS_MASK), 0);
                break;
        case DIFF_SYMBOL_PLUS:
-               set = diff_get_color_opt(o, DIFF_FILE_NEW);
+               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:
-               set = diff_get_color_opt(o, DIFF_FILE_OLD);
+               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);
@@ -766,12 +1221,26 @@ static void emit_diff_symbol(struct diff_options *o, enum diff_symbol s,
        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);
@@ -1368,9 +1837,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)
@@ -1406,6 +1895,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)
@@ -1440,6 +1934,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);
@@ -3650,6 +4145,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)
@@ -3759,6 +4256,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)
@@ -4183,7 +4683,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;
        }
@@ -4713,67 +5225,76 @@ 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);
                }
+               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);
 }
 
 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);
                }
-               show_mode_change(file, p, !p->score, line_prefix);
+               show_mode_change(opt, p, !p->score);
                break;
        }
 }
@@ -4981,16 +5502,46 @@ void diff_warn_rename_limit(const char *varname, int needed, int degraded_cc)
 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)
@@ -5065,6 +5616,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))
@@ -5077,10 +5629,10 @@ void diff_flush(struct diff_options *options)
        if (output_format & DIFF_FORMAT_PATCH) {
                if (separator) {
                        emit_diff_symbol(options, DIFF_SYMBOL_SEPARATOR, NULL, 0, 0);
-                       if (options->stat_sep) {
+                       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);
                }
 
                diff_flush_patch_all_file_pairs(options);