diff: add --word-diff option that generalizes --color-words
[gitweb.git] / diff.c
diff --git a/diff.c b/diff.c
index 7a321f4254cf25101f9c9ed5df1621e3bf4b2e29..d81a57a3c415ec7191ef0c686f8e2134d5b7ee0c 100644 (file)
--- a/diff.c
+++ b/diff.c
@@ -194,6 +194,7 @@ struct emit_callback {
        struct diff_words_data *diff_words;
        int *found_changesp;
        FILE *file;
+       struct strbuf *header;
 };
 
 static int count_lines(const char *data, int size)
@@ -571,16 +572,68 @@ static void diff_words_append(char *line, unsigned long len,
        buffer->text.ptr[buffer->text.size] = '\0';
 }
 
+struct diff_words_style_elem
+{
+       const char *prefix;
+       const char *suffix;
+       const char *color; /* NULL; filled in by the setup code if
+                           * color is enabled */
+};
+
+struct diff_words_style
+{
+       enum diff_words_type type;
+       struct diff_words_style_elem new, old, ctx;
+       const char *newline;
+};
+
+struct diff_words_style diff_words_styles[] = {
+       { DIFF_WORDS_PORCELAIN, {"+", "\n"}, {"-", "\n"}, {" ", "\n"}, "~\n" },
+       { DIFF_WORDS_PLAIN, {"{+", "+}"}, {"[-", "-]"}, {"", ""}, "\n" },
+       { DIFF_WORDS_COLOR, {"", ""}, {"", ""}, {"", ""}, "\n" }
+};
+
 struct diff_words_data {
        struct diff_words_buffer minus, plus;
        const char *current_plus;
        FILE *file;
        regex_t *word_regex;
+       enum diff_words_type type;
+       struct diff_words_style *style;
 };
 
+static int fn_out_diff_words_write_helper(FILE *fp,
+                                         struct diff_words_style_elem *st_el,
+                                         const char *newline,
+                                         size_t count, const char *buf)
+{
+       while (count) {
+               char *p = memchr(buf, '\n', count);
+               if (p != buf) {
+                       if (st_el->color && fputs(st_el->color, fp) < 0)
+                               return -1;
+                       if (fputs(st_el->prefix, fp) < 0 ||
+                           fwrite(buf, p ? p - buf : count, 1, fp) != 1 ||
+                           fputs(st_el->suffix, fp) < 0)
+                               return -1;
+                       if (st_el->color && *st_el->color
+                           && fputs(GIT_COLOR_RESET, fp) < 0)
+                               return -1;
+               }
+               if (!p)
+                       return 0;
+               if (fputs(newline, fp) < 0)
+                       return -1;
+               count -= p + 1 - buf;
+               buf = p + 1;
+       }
+       return 0;
+}
+
 static void fn_out_diff_words_aux(void *priv, char *line, unsigned long len)
 {
        struct diff_words_data *diff_words = priv;
+       struct diff_words_style *style = diff_words->style;
        int minus_first, minus_len, plus_first, plus_len;
        const char *minus_begin, *minus_end, *plus_begin, *plus_end;
 
@@ -604,16 +657,17 @@ static void fn_out_diff_words_aux(void *priv, char *line, unsigned long len)
                plus_begin = plus_end = diff_words->plus.orig[plus_first].end;
 
        if (diff_words->current_plus != plus_begin)
-               fwrite(diff_words->current_plus,
-                               plus_begin - diff_words->current_plus, 1,
-                               diff_words->file);
+               fn_out_diff_words_write_helper(diff_words->file,
+                               &style->ctx, style->newline,
+                               plus_begin - diff_words->current_plus,
+                               diff_words->current_plus);
        if (minus_begin != minus_end)
-               color_fwrite_lines(diff_words->file,
-                               diff_get_color(1, DIFF_FILE_OLD),
+               fn_out_diff_words_write_helper(diff_words->file,
+                               &style->old, style->newline,
                                minus_end - minus_begin, minus_begin);
        if (plus_begin != plus_end)
-               color_fwrite_lines(diff_words->file,
-                               diff_get_color(1, DIFF_FILE_NEW),
+               fn_out_diff_words_write_helper(diff_words->file,
+                               &style->new, style->newline,
                                plus_end - plus_begin, plus_begin);
 
        diff_words->current_plus = plus_end;
@@ -696,11 +750,12 @@ static void diff_words_show(struct diff_words_data *diff_words)
        xdemitconf_t xecfg;
        xdemitcb_t ecb;
        mmfile_t minus, plus;
+       struct diff_words_style *style = diff_words->style;
 
        /* special case: only removal */
        if (!diff_words->plus.text.size) {
-               color_fwrite_lines(diff_words->file,
-                       diff_get_color(1, DIFF_FILE_OLD),
+               fn_out_diff_words_write_helper(diff_words->file,
+                       &style->old, style->newline,
                        diff_words->minus.text.size, diff_words->minus.text.ptr);
                diff_words->minus.text.size = 0;
                return;
@@ -721,10 +776,10 @@ static void diff_words_show(struct diff_words_data *diff_words)
        free(plus.ptr);
        if (diff_words->current_plus != diff_words->plus.text.ptr +
                        diff_words->plus.text.size)
-               fwrite(diff_words->current_plus,
+               fn_out_diff_words_write_helper(diff_words->file,
+                       &style->ctx, style->newline,
                        diff_words->plus.text.ptr + diff_words->plus.text.size
-                       - diff_words->current_plus, 1,
-                       diff_words->file);
+                       - diff_words->current_plus, diff_words->current_plus);
        diff_words->minus.text.size = diff_words->plus.text.size = 0;
 }
 
@@ -797,6 +852,11 @@ static void fn_out_consume(void *priv, char *line, unsigned long len)
        const char *plain = diff_get_color(ecbdata->color_diff, DIFF_PLAIN);
        const char *reset = diff_get_color(ecbdata->color_diff, DIFF_RESET);
 
+       if (ecbdata->header) {
+               fprintf(ecbdata->file, "%s", ecbdata->header->buf);
+               strbuf_reset(ecbdata->header);
+               ecbdata->header = NULL;
+       }
        *(ecbdata->found_changesp) = 1;
 
        if (ecbdata->label_path[0]) {
@@ -831,6 +891,9 @@ static void fn_out_consume(void *priv, char *line, unsigned long len)
 
        if (len < 1) {
                emit_line(ecbdata->file, reset, reset, line, len);
+               if (ecbdata->diff_words
+                   && ecbdata->diff_words->type == DIFF_WORDS_PORCELAIN)
+                       fputs("~\n", ecbdata->file);
                return;
        }
 
@@ -845,9 +908,13 @@ static void fn_out_consume(void *priv, char *line, unsigned long len)
                        return;
                }
                diff_words_flush(ecbdata);
-               line++;
-               len--;
-               emit_line(ecbdata->file, plain, reset, line, len);
+               if (ecbdata->diff_words->type == DIFF_WORDS_PORCELAIN) {
+                       emit_line(ecbdata->file, plain, reset, line, len);
+                       fputs("~\n", ecbdata->file);
+               } else {
+                       /* don't print the prefix character */
+                       emit_line(ecbdata->file, plain, reset, line+1, len-1);
+               }
                return;
        }
 
@@ -1601,6 +1668,7 @@ static void builtin_diff(const char *name_a,
        const char *reset = diff_get_color_opt(o, DIFF_RESET);
        const char *a_prefix, *b_prefix;
        const char *textconv_one = NULL, *textconv_two = NULL;
+       struct strbuf header = STRBUF_INIT;
 
        if (DIFF_OPT_TST(o, SUBMODULE_LOG) &&
                        (!one->mode || S_ISGITLINK(one->mode)) &&
@@ -1608,7 +1676,7 @@ static void builtin_diff(const char *name_a,
                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 ? one->path : two->path,
-                               one->sha1, two->sha1,
+                               one->sha1, two->sha1, two->dirty_submodule,
                                del, add, reset);
                return;
        }
@@ -1635,25 +1703,26 @@ static void builtin_diff(const char *name_a,
        b_two = quote_two(b_prefix, name_b + (*name_b == '/'));
        lbl[0] = DIFF_FILE_VALID(one) ? a_one : "/dev/null";
        lbl[1] = DIFF_FILE_VALID(two) ? b_two : "/dev/null";
-       fprintf(o->file, "%sdiff --git %s %s%s\n", set, a_one, b_two, reset);
+       strbuf_addf(&header, "%sdiff --git %s %s%s\n", set, a_one, b_two, reset);
        if (lbl[0][0] == '/') {
                /* /dev/null */
-               fprintf(o->file, "%snew file mode %06o%s\n", set, two->mode, reset);
+               strbuf_addf(&header, "%snew file mode %06o%s\n", set, two->mode, reset);
                if (xfrm_msg && xfrm_msg[0])
-                       fprintf(o->file, "%s%s%s\n", set, xfrm_msg, reset);
+                       strbuf_addf(&header, "%s%s%s\n", set, xfrm_msg, reset);
        }
        else if (lbl[1][0] == '/') {
-               fprintf(o->file, "%sdeleted file mode %06o%s\n", set, one->mode, reset);
+               strbuf_addf(&header, "%sdeleted file mode %06o%s\n", set, one->mode, reset);
                if (xfrm_msg && xfrm_msg[0])
-                       fprintf(o->file, "%s%s%s\n", set, xfrm_msg, reset);
+                       strbuf_addf(&header, "%s%s%s\n", set, xfrm_msg, reset);
        }
        else {
                if (one->mode != two->mode) {
-                       fprintf(o->file, "%sold mode %06o%s\n", set, one->mode, reset);
-                       fprintf(o->file, "%snew mode %06o%s\n", set, two->mode, reset);
+                       strbuf_addf(&header, "%sold mode %06o%s\n", set, one->mode, reset);
+                       strbuf_addf(&header, "%snew mode %06o%s\n", set, two->mode, reset);
                }
                if (xfrm_msg && xfrm_msg[0])
-                       fprintf(o->file, "%s%s%s\n", set, xfrm_msg, reset);
+                       strbuf_addf(&header, "%s%s%s\n", set, xfrm_msg, reset);
+
                /*
                 * we do not run diff between different kind
                 * of objects.
@@ -1663,6 +1732,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);
+                       strbuf_reset(&header);
                        emit_rewrite_diff(name_a, name_b, one, two,
                                                textconv_one, textconv_two, o);
                        o->found_changes = 1;
@@ -1680,6 +1751,8 @@ static void builtin_diff(const char *name_a,
                if (mf1.size == mf2.size &&
                    !memcmp(mf1.ptr, mf2.ptr, mf1.size))
                        goto free_ab_and_return;
+               fprintf(o->file, "%s", header.buf);
+               strbuf_reset(&header);
                if (DIFF_OPT_TST(o, BINARY))
                        emit_binary_diff(o->file, &mf1, &mf2);
                else
@@ -1696,6 +1769,11 @@ static void builtin_diff(const char *name_a,
                struct emit_callback ecbdata;
                const struct userdiff_funcname *pe;
 
+               if (!DIFF_XDL_TST(o, WHITESPACE_FLAGS)) {
+                       fprintf(o->file, "%s", header.buf);
+                       strbuf_reset(&header);
+               }
+
                if (textconv_one) {
                        size_t size;
                        mf1.ptr = run_textconv(textconv_one, one, &size);
@@ -1725,6 +1803,7 @@ static void builtin_diff(const char *name_a,
                if (ecbdata.ws_rule & WS_BLANK_AT_EOF)
                        check_blank_at_eof(&mf1, &mf2, &ecbdata);
                ecbdata.file = o->file;
+               ecbdata.header = header.len ? &header : NULL;
                xpp.flags = XDF_NEED_MINIMAL | o->xdl_opts;
                xecfg.ctxlen = o->context;
                xecfg.interhunkctxlen = o->interhunkcontext;
@@ -1737,10 +1816,13 @@ static void builtin_diff(const char *name_a,
                        xecfg.ctxlen = strtoul(diffopts + 10, NULL, 10);
                else if (!prefixcmp(diffopts, "-u"))
                        xecfg.ctxlen = strtoul(diffopts + 2, NULL, 10);
-               if (DIFF_OPT_TST(o, COLOR_DIFF_WORDS)) {
+               if (o->word_diff) {
+                       int i;
+
                        ecbdata.diff_words =
                                xcalloc(1, sizeof(struct diff_words_data));
                        ecbdata.diff_words->file = o->file;
+                       ecbdata.diff_words->type = o->word_diff;
                        if (!o->word_regex)
                                o->word_regex = userdiff_word_regex(one);
                        if (!o->word_regex)
@@ -1756,10 +1838,23 @@ static void builtin_diff(const char *name_a,
                                        die ("Invalid regular expression: %s",
                                                        o->word_regex);
                        }
+                       for (i = 0; i < ARRAY_SIZE(diff_words_styles); i++) {
+                               if (o->word_diff == diff_words_styles[i].type) {
+                                       ecbdata.diff_words->style =
+                                               &diff_words_styles[i];
+                                       break;
+                               }
+                       }
+                       if (DIFF_OPT_TST(o, COLOR_DIFF)) {
+                               struct diff_words_style *st = ecbdata.diff_words->style;
+                               st->old.color = diff_get_color_opt(o, DIFF_FILE_OLD);
+                               st->new.color = diff_get_color_opt(o, DIFF_FILE_NEW);
+                               st->ctx.color = diff_get_color_opt(o, DIFF_PLAIN);
+                       }
                }
                xdi_diff_outf(&mf1, &mf2, fn_out_consume, &ecbdata,
                              &xpp, &xecfg, &ecb);
-               if (DIFF_OPT_TST(o, COLOR_DIFF_WORDS))
+               if (o->word_diff)
                        free_diff_words_data(&ecbdata);
                if (textconv_one)
                        free(mf1.ptr);
@@ -1769,6 +1864,7 @@ static void builtin_diff(const char *name_a,
        }
 
  free_ab_and_return:
+       strbuf_release(&header);
        diff_free_filespec_data(one);
        diff_free_filespec_data(two);
        free(a_one);
@@ -1978,7 +2074,7 @@ static int reuse_worktree_file(const char *name, const unsigned char *sha1, int
         * If ce is marked as "assume unchanged", there is no
         * guarantee that work tree matches what we are looking for.
         */
-       if (ce->ce_flags & CE_VALID)
+       if ((ce->ce_flags & CE_VALID) || ce_skip_worktree(ce))
                return 0;
 
        /*
@@ -2010,9 +2106,14 @@ static int populate_from_stdin(struct diff_filespec *s)
 static int diff_populate_gitlink(struct diff_filespec *s, int size_only)
 {
        int len;
-       char *data = xmalloc(100);
+       char *data = xmalloc(100), *dirty = "";
+
+       /* Are we looking at the work tree? */
+       if (s->dirty_submodule)
+               dirty = "-dirty";
+
        len = snprintf(data, 100,
-               "Subproject commit %s\n", sha1_to_hex(s->sha1));
+                      "Subproject commit %s%s\n", sha1_to_hex(s->sha1), dirty);
        s->data = data;
        s->size = len;
        s->should_free = 1;
@@ -2275,7 +2376,7 @@ static void run_external_diff(const char *pgm,
        }
        *arg = NULL;
        fflush(NULL);
-       retval = run_command_v_opt(spawn_arg, 0);
+       retval = run_command_v_opt(spawn_arg, RUN_USING_SHELL);
        remove_tempfile();
        if (retval) {
                fprintf(stderr, "external diff died, stopping at %s.\n", name);
@@ -2551,6 +2652,20 @@ int diff_setup_done(struct diff_options *options)
        if (count > 1)
                die("--name-only, --name-status, --check and -s are mutually exclusive");
 
+       /*
+        * Most of the time we can say "there are changes"
+        * only by checking if there are changed paths, but
+        * --ignore-whitespace* options force us to look
+        * inside contents.
+        */
+
+       if (DIFF_XDL_TST(options, IGNORE_WHITESPACE) ||
+           DIFF_XDL_TST(options, IGNORE_WHITESPACE_CHANGE) ||
+           DIFF_XDL_TST(options, IGNORE_WHITESPACE_AT_EOL))
+               DIFF_OPT_SET(options, DIFF_FROM_CONTENTS);
+       else
+               DIFF_OPT_CLR(options, DIFF_FROM_CONTENTS);
+
        if (DIFF_OPT_TST(options, FIND_COPIES_HARDER))
                options->detect_rename = DIFF_DETECT_COPY;
 
@@ -2590,6 +2705,12 @@ int diff_setup_done(struct diff_options *options)
         */
        if (options->pickaxe)
                DIFF_OPT_SET(options, RECURSIVE);
+       /*
+        * When patches are generated, submodules diffed against the work tree
+        * must be checked for dirtiness too so it can be shown in the output
+        */
+       if (options->output_format & DIFF_FORMAT_PATCH)
+               DIFF_OPT_SET(options, DIRTY_SUBMODULES);
 
        if (options->detect_rename && options->rename_limit < 0)
                options->rename_limit = diff_rename_limit_default;
@@ -2611,7 +2732,7 @@ int diff_setup_done(struct diff_options *options)
         * to have found.  It does not make sense not to return with
         * exit code in such a case either.
         */
-       if (DIFF_OPT_TST(options, QUIET)) {
+       if (DIFF_OPT_TST(options, QUICK)) {
                options->output_format = DIFF_FORMAT_NO_OUTPUT;
                DIFF_OPT_SET(options, EXIT_WITH_STATUS);
        }
@@ -2788,21 +2909,54 @@ int diff_opt_parse(struct diff_options *options, const char **av, int ac)
                DIFF_OPT_SET(options, FOLLOW_RENAMES);
        else if (!strcmp(arg, "--color"))
                DIFF_OPT_SET(options, COLOR_DIFF);
+       else if (!prefixcmp(arg, "--color=")) {
+               int value = git_config_colorbool(NULL, arg+8, -1);
+               if (value == 0)
+                       DIFF_OPT_CLR(options, COLOR_DIFF);
+               else if (value > 0)
+                       DIFF_OPT_SET(options, COLOR_DIFF);
+               else
+                       return error("option `color' expects \"always\", \"auto\", or \"never\"");
+       }
        else if (!strcmp(arg, "--no-color"))
                DIFF_OPT_CLR(options, COLOR_DIFF);
        else if (!strcmp(arg, "--color-words")) {
                DIFF_OPT_SET(options, COLOR_DIFF);
-               DIFF_OPT_SET(options, COLOR_DIFF_WORDS);
+               options->word_diff = DIFF_WORDS_COLOR;
        }
        else if (!prefixcmp(arg, "--color-words=")) {
                DIFF_OPT_SET(options, COLOR_DIFF);
-               DIFF_OPT_SET(options, COLOR_DIFF_WORDS);
+               options->word_diff = DIFF_WORDS_COLOR;
                options->word_regex = arg + 14;
        }
+       else if (!strcmp(arg, "--word-diff")) {
+               if (options->word_diff == DIFF_WORDS_NONE)
+                       options->word_diff = DIFF_WORDS_PLAIN;
+       }
+       else if (!prefixcmp(arg, "--word-diff=")) {
+               const char *type = arg + 12;
+               if (!strcmp(type, "plain"))
+                       options->word_diff = DIFF_WORDS_PLAIN;
+               else if (!strcmp(type, "color")) {
+                       DIFF_OPT_SET(options, COLOR_DIFF);
+                       options->word_diff = DIFF_WORDS_COLOR;
+               }
+               else if (!strcmp(type, "porcelain"))
+                       options->word_diff = DIFF_WORDS_PORCELAIN;
+               else if (!strcmp(type, "none"))
+                       options->word_diff = DIFF_WORDS_NONE;
+               else
+                       die("bad --word-diff argument: %s", type);
+       }
+       else if (!prefixcmp(arg, "--word-diff-regex=")) {
+               if (options->word_diff == DIFF_WORDS_NONE)
+                       options->word_diff = DIFF_WORDS_PLAIN;
+               options->word_regex = arg + 18;
+       }
        else if (!strcmp(arg, "--exit-code"))
                DIFF_OPT_SET(options, EXIT_WITH_STATUS);
        else if (!strcmp(arg, "--quiet"))
-               DIFF_OPT_SET(options, QUIET);
+               DIFF_OPT_SET(options, QUICK);
        else if (!strcmp(arg, "--ext-diff"))
                DIFF_OPT_SET(options, ALLOW_EXTERNAL);
        else if (!strcmp(arg, "--no-ext-diff"))
@@ -3039,7 +3193,8 @@ int diff_unmodified_pair(struct diff_filepair *p)
         * dealing with a change.
         */
        if (one->sha1_valid && two->sha1_valid &&
-           !hashcmp(one->sha1, two->sha1))
+           !hashcmp(one->sha1, two->sha1) &&
+           !one->dirty_submodule && !two->dirty_submodule)
                return 1; /* no change */
        if (!one->sha1_valid && !two->sha1_valid)
                return 1; /* both look at the same file on the filesystem. */
@@ -3174,6 +3329,8 @@ static void diff_resolve_rename_copy(void)
                }
                else if (hashcmp(p->one->sha1, p->two->sha1) ||
                         p->one->mode != p->two->mode ||
+                        p->one->dirty_submodule ||
+                        p->two->dirty_submodule ||
                         is_null_sha1(p->one->sha1))
                        p->status = DIFF_STATUS_MODIFIED;
                else {
@@ -3484,6 +3641,29 @@ void diff_flush(struct diff_options *options)
                separator++;
        }
 
+       if (output_format & DIFF_FORMAT_NO_OUTPUT &&
+           DIFF_OPT_TST(options, EXIT_WITH_STATUS) &&
+           DIFF_OPT_TST(options, DIFF_FROM_CONTENTS)) {
+               /*
+                * run diff_flush_patch for the exit status. setting
+                * options->file to /dev/null should be safe, becaue we
+                * aren't supposed to produce any output anyway.
+                */
+               if (options->close_file)
+                       fclose(options->file);
+               options->file = fopen("/dev/null", "w");
+               if (!options->file)
+                       die_errno("Could not open /dev/null");
+               options->close_file = 1;
+               for (i = 0; i < q->nr; i++) {
+                       struct diff_filepair *p = q->queue[i];
+                       if (check_pair_status(p))
+                               diff_flush_patch(p, options);
+                       if (options->found_changes)
+                               break;
+               }
+       }
+
        if (output_format & DIFF_FORMAT_PATCH) {
                if (separator) {
                        putc(options->line_termination, options->file);
@@ -3511,6 +3691,18 @@ void diff_flush(struct diff_options *options)
        q->nr = q->alloc = 0;
        if (options->close_file)
                fclose(options->file);
+
+       /*
+        * Report the content-level differences with HAS_CHANGES;
+        * diff_addremove/diff_change does not set the bit when
+        * DIFF_FROM_CONTENTS is in effect (e.g. with -w).
+        */
+       if (DIFF_OPT_TST(options, DIFF_FROM_CONTENTS)) {
+               if (options->found_changes)
+                       DIFF_OPT_SET(options, HAS_CHANGES);
+               else
+                       DIFF_OPT_CLR(options, HAS_CHANGES);
+       }
 }
 
 static void diffcore_apply_filter(const char *filter)
@@ -3594,7 +3786,7 @@ static void diffcore_skip_stat_unmatch(struct diff_options *diffopt)
                struct diff_filepair *p = q->queue[i];
 
                /*
-                * 1. Entries that come from stat info dirtyness
+                * 1. Entries that come from stat info dirtiness
                 *    always have both sides (iow, not create/delete),
                 *    one side of the object name is unknown, with
                 *    the same mode and size.  Keep the ones that
@@ -3630,6 +3822,23 @@ static void diffcore_skip_stat_unmatch(struct diff_options *diffopt)
        *q = outq;
 }
 
+static int diffnamecmp(const void *a_, const void *b_)
+{
+       const struct diff_filepair *a = *((const struct diff_filepair **)a_);
+       const struct diff_filepair *b = *((const struct diff_filepair **)b_);
+       const char *name_a, *name_b;
+
+       name_a = a->one ? a->one->path : a->two->path;
+       name_b = b->one ? b->one->path : b->two->path;
+       return strcmp(name_a, name_b);
+}
+
+void diffcore_fix_diff_index(struct diff_options *options)
+{
+       struct diff_queue_struct *q = &diff_queued_diff;
+       qsort(q->queue, q->nr, sizeof(q->queue[0]), diffnamecmp);
+}
+
 void diffcore_std(struct diff_options *options)
 {
        if (options->skip_stat_unmatch)
@@ -3647,7 +3856,7 @@ void diffcore_std(struct diff_options *options)
        diff_resolve_rename_copy();
        diffcore_apply_filter(options->filter);
 
-       if (diff_queued_diff.nr)
+       if (diff_queued_diff.nr && !DIFF_OPT_TST(options, DIFF_FROM_CONTENTS))
                DIFF_OPT_SET(options, HAS_CHANGES);
        else
                DIFF_OPT_CLR(options, HAS_CHANGES);
@@ -3671,7 +3880,7 @@ int diff_result_code(struct diff_options *opt, int status)
 void diff_addremove(struct diff_options *options,
                    int addremove, unsigned mode,
                    const unsigned char *sha1,
-                   const char *concatpath)
+                   const char *concatpath, unsigned dirty_submodule)
 {
        struct diff_filespec *one, *two;
 
@@ -3703,18 +3912,22 @@ void diff_addremove(struct diff_options *options,
 
        if (addremove != '+')
                fill_filespec(one, sha1, mode);
-       if (addremove != '-')
+       if (addremove != '-') {
                fill_filespec(two, sha1, mode);
+               two->dirty_submodule = dirty_submodule;
+       }
 
        diff_queue(&diff_queued_diff, one, two);
-       DIFF_OPT_SET(options, HAS_CHANGES);
+       if (!DIFF_OPT_TST(options, DIFF_FROM_CONTENTS))
+               DIFF_OPT_SET(options, HAS_CHANGES);
 }
 
 void diff_change(struct diff_options *options,
                 unsigned old_mode, unsigned new_mode,
                 const unsigned char *old_sha1,
                 const unsigned char *new_sha1,
-                const char *concatpath)
+                const char *concatpath,
+                unsigned old_dirty_submodule, unsigned new_dirty_submodule)
 {
        struct diff_filespec *one, *two;
 
@@ -3727,6 +3940,8 @@ void diff_change(struct diff_options *options,
                const unsigned char *tmp_c;
                tmp = old_mode; old_mode = new_mode; new_mode = tmp;
                tmp_c = old_sha1; old_sha1 = new_sha1; new_sha1 = tmp_c;
+               tmp = old_dirty_submodule; old_dirty_submodule = new_dirty_submodule;
+                       new_dirty_submodule = tmp;
        }
 
        if (options->prefix &&
@@ -3737,9 +3952,12 @@ void diff_change(struct diff_options *options,
        two = alloc_filespec(concatpath);
        fill_filespec(one, old_sha1, old_mode);
        fill_filespec(two, new_sha1, new_mode);
+       one->dirty_submodule = old_dirty_submodule;
+       two->dirty_submodule = new_dirty_submodule;
 
        diff_queue(&diff_queued_diff, one, two);
-       DIFF_OPT_SET(options, HAS_CHANGES);
+       if (!DIFF_OPT_TST(options, DIFF_FROM_CONTENTS))
+               DIFF_OPT_SET(options, HAS_CHANGES);
 }
 
 void diff_unmerge(struct diff_options *options,
@@ -3766,6 +3984,7 @@ static char *run_textconv(const char *pgm, struct diff_filespec *spec,
        const char **arg = argv;
        struct child_process child;
        struct strbuf buf = STRBUF_INIT;
+       int err = 0;
 
        temp = prepare_temp_file(spec->path, spec);
        *arg++ = pgm;
@@ -3773,18 +3992,23 @@ static char *run_textconv(const char *pgm, struct diff_filespec *spec,
        *arg = NULL;
 
        memset(&child, 0, sizeof(child));
+       child.use_shell = 1;
        child.argv = argv;
        child.out = -1;
-       if (start_command(&child) != 0 ||
-           strbuf_read(&buf, child.out, 0) < 0 ||
-           finish_command(&child) != 0) {
-               close(child.out);
-               strbuf_release(&buf);
+       if (start_command(&child)) {
                remove_tempfile();
-               error("error running textconv command '%s'", pgm);
                return NULL;
        }
+
+       if (strbuf_read(&buf, child.out, 0) < 0)
+               err = error("error reading from textconv command '%s'", pgm);
        close(child.out);
+
+       if (finish_command(&child) || err) {
+               strbuf_release(&buf);
+               remove_tempfile();
+               return NULL;
+       }
        remove_tempfile();
 
        return strbuf_detach(&buf, outsize);