tests: move test_cmp_rev to test-lib-functions
[gitweb.git] / diff.c
diff --git a/diff.c b/diff.c
index f1b044780ffb0e590f8dd1d609ae2b2c3b54b473..732d4c227595873bb94224da618ce5612a7415af 100644 (file)
--- a/diff.c
+++ b/diff.c
@@ -15,6 +15,7 @@
 #include "sigchain.h"
 #include "submodule.h"
 #include "ll-merge.h"
+#include "string-list.h"
 
 #ifdef NO_FAST_WORKING_DIRECTORY
 #define FAST_WORKING_DIRECTORY 0
@@ -25,7 +26,8 @@
 static int diff_detect_rename_default;
 static int diff_rename_limit_default = 400;
 static int diff_suppress_blank_empty;
-int diff_use_color_default = -1;
+static int diff_use_color_default = -1;
+static int diff_context_default = 3;
 static const char *diff_word_regex_cfg;
 static const char *external_diff_cmd_cfg;
 int diff_auto_refresh_index = 1;
@@ -68,26 +70,30 @@ static int parse_diff_color_slot(const char *var, int ofs)
        return -1;
 }
 
-static int parse_dirstat_params(struct diff_options *options, const char *params,
+static int parse_dirstat_params(struct diff_options *options, const char *params_string,
                                struct strbuf *errmsg)
 {
-       const char *p = params;
-       int p_len, ret = 0;
+       char *params_copy = xstrdup(params_string);
+       struct string_list params = STRING_LIST_INIT_NODUP;
+       int ret = 0;
+       int i;
 
-       while (*p) {
-               p_len = strchrnul(p, ',') - p;
-               if (!memcmp(p, "changes", p_len)) {
+       if (*params_copy)
+               string_list_split_in_place(&params, params_copy, ',', -1);
+       for (i = 0; i < params.nr; i++) {
+               const char *p = params.items[i].string;
+               if (!strcmp(p, "changes")) {
                        DIFF_OPT_CLR(options, DIRSTAT_BY_LINE);
                        DIFF_OPT_CLR(options, DIRSTAT_BY_FILE);
-               } else if (!memcmp(p, "lines", p_len)) {
+               } else if (!strcmp(p, "lines")) {
                        DIFF_OPT_SET(options, DIRSTAT_BY_LINE);
                        DIFF_OPT_CLR(options, DIRSTAT_BY_FILE);
-               } else if (!memcmp(p, "files", p_len)) {
+               } else if (!strcmp(p, "files")) {
                        DIFF_OPT_CLR(options, DIRSTAT_BY_LINE);
                        DIFF_OPT_SET(options, DIRSTAT_BY_FILE);
-               } else if (!memcmp(p, "noncumulative", p_len)) {
+               } else if (!strcmp(p, "noncumulative")) {
                        DIFF_OPT_CLR(options, DIRSTAT_CUMULATIVE);
-               } else if (!memcmp(p, "cumulative", p_len)) {
+               } else if (!strcmp(p, "cumulative")) {
                        DIFF_OPT_SET(options, DIRSTAT_CUMULATIVE);
                } else if (isdigit(*p)) {
                        char *end;
@@ -99,27 +105,35 @@ static int parse_dirstat_params(struct diff_options *options, const char *params
                                while (isdigit(*++end))
                                        ; /* nothing */
                        }
-                       if (end - p == p_len)
+                       if (!*end)
                                options->dirstat_permille = permille;
                        else {
-                               strbuf_addf(errmsg, _("  Failed to parse dirstat cut-off percentage '%.*s'\n"),
-                                           p_len, p);
+                               strbuf_addf(errmsg, _("  Failed to parse dirstat cut-off percentage '%s'\n"),
+                                           p);
                                ret++;
                        }
                } else {
-                       strbuf_addf(errmsg, _("  Unknown dirstat parameter '%.*s'\n"),
-                                   p_len, p);
+                       strbuf_addf(errmsg, _("  Unknown dirstat parameter '%s'\n"), p);
                        ret++;
                }
 
-               p += p_len;
-
-               if (*p)
-                       p++; /* more parameters, swallow separator */
        }
+       string_list_clear(&params, 0);
+       free(params_copy);
        return ret;
 }
 
+static int parse_submodule_params(struct diff_options *options, const char *value)
+{
+       if (!strcmp(value, "log"))
+               DIFF_OPT_SET(options, SUBMODULE_LOG);
+       else if (!strcmp(value, "short"))
+               DIFF_OPT_CLR(options, SUBMODULE_LOG);
+       else
+               return -1;
+       return 0;
+}
+
 static int git_config_rename(const char *var, const char *value)
 {
        if (!value)
@@ -141,6 +155,12 @@ int git_diff_ui_config(const char *var, const char *value, void *cb)
                diff_use_color_default = git_config_colorbool(var, value);
                return 0;
        }
+       if (!strcmp(var, "diff.context")) {
+               diff_context_default = git_config_int(var, value);
+               if (diff_context_default < 0)
+                       return -1;
+               return 0;
+       }
        if (!strcmp(var, "diff.renames")) {
                diff_detect_rename_default = git_config_rename(var, value);
                return 0;
@@ -169,6 +189,13 @@ int git_diff_ui_config(const char *var, const char *value, void *cb)
        if (!strcmp(var, "diff.ignoresubmodules"))
                handle_ignore_submodules_arg(&default_diff_options, value);
 
+       if (!strcmp(var, "diff.submodule")) {
+               if (parse_submodule_params(&default_diff_options, value))
+                       warning(_("Unknown value for 'diff.submodule' config variable: '%s'"),
+                               value);
+               return 0;
+       }
+
        if (git_color_config(var, value, cb) < 0)
                return -1;
 
@@ -1300,6 +1327,7 @@ struct diffstat_t {
                unsigned is_unmerged:1;
                unsigned is_binary:1;
                unsigned is_renamed:1;
+               unsigned is_interesting:1;
                uintmax_t added, deleted;
        } **files;
 };
@@ -1398,11 +1426,11 @@ int print_stat_summary(FILE *fp, int files, int insertions, int deletions)
 
        if (!files) {
                assert(insertions == 0 && deletions == 0);
-               return fputs(_(" 0 files changed\n"), fp);
+               return fprintf(fp, "%s\n", " 0 files changed");
        }
 
        strbuf_addf(&sb,
-                   Q_(" %d file changed", " %d files changed", files),
+                   (files == 1) ? " %d file changed" : " %d files changed",
                    files);
 
        /*
@@ -1419,8 +1447,7 @@ int print_stat_summary(FILE *fp, int files, int insertions, int deletions)
                 * do not translate it.
                 */
                strbuf_addf(&sb,
-                           Q_(", %d insertion(+)", ", %d insertions(+)",
-                              insertions),
+                           (insertions == 1) ? ", %d insertion(+)" : ", %d insertions(+)",
                            insertions);
        }
 
@@ -1430,8 +1457,7 @@ int print_stat_summary(FILE *fp, int files, int insertions, int deletions)
                 * do not translate it.
                 */
                strbuf_addf(&sb,
-                           Q_(", %d deletion(-)", ", %d deletions(-)",
-                              deletions),
+                           (deletions == 1) ? ", %d deletion(-)" : ", %d deletions(-)",
                            deletions);
        }
        strbuf_addch(&sb, '\n');
@@ -1471,8 +1497,8 @@ static void show_stats(struct diffstat_t *data, struct diff_options *options)
        for (i = 0; (i < count) && (i < data->nr); i++) {
                struct diffstat_file *file = data->files[i];
                uintmax_t change = file->added + file->deleted;
-               if (!data->files[i]->is_renamed &&
-                        (change == 0)) {
+
+               if (!file->is_interesting && (change == 0)) {
                        count++; /* not shown == room for one more */
                        continue;
                }
@@ -1499,7 +1525,7 @@ static void show_stats(struct diffstat_t *data, struct diff_options *options)
                if (max_change < change)
                        max_change = change;
        }
-       count = i; /* min(count, data->nr) */
+       count = i; /* where we can stop scanning in data->files[] */
 
        /*
         * We have width = stat_width or term_columns() columns total.
@@ -1587,16 +1613,15 @@ static void show_stats(struct diffstat_t *data, struct diff_options *options)
         */
        for (i = 0; i < count; i++) {
                const char *prefix = "";
-               char *name = data->files[i]->print_name;
-               uintmax_t added = data->files[i]->added;
-               uintmax_t deleted = data->files[i]->deleted;
+               struct diffstat_file *file = data->files[i];
+               char *name = file->print_name;
+               uintmax_t added = file->added;
+               uintmax_t deleted = file->deleted;
                int name_len;
 
-               if (!data->files[i]->is_renamed &&
-                        (added + deleted == 0)) {
-                       total_files--;
+               if (!file->is_interesting && (added + deleted == 0))
                        continue;
-               }
+
                /*
                 * "scale" the filename
                 */
@@ -1612,7 +1637,7 @@ static void show_stats(struct diffstat_t *data, struct diff_options *options)
                                name = slash;
                }
 
-               if (data->files[i]->is_binary) {
+               if (file->is_binary) {
                        fprintf(options->file, "%s", line_prefix);
                        show_name(options->file, prefix, name, len);
                        fprintf(options->file, " %*s", number_width, "Bin");
@@ -1629,7 +1654,7 @@ static void show_stats(struct diffstat_t *data, struct diff_options *options)
                        fprintf(options->file, "\n");
                        continue;
                }
-               else if (data->files[i]->is_unmerged) {
+               else if (file->is_unmerged) {
                        fprintf(options->file, "%s", line_prefix);
                        show_name(options->file, prefix, name, len);
                        fprintf(options->file, " Unmerged\n");
@@ -1641,8 +1666,6 @@ static void show_stats(struct diffstat_t *data, struct diff_options *options)
                 */
                add = added;
                del = deleted;
-               adds += add;
-               dels += del;
 
                if (graph_width <= max_change) {
                        int total = add + del;
@@ -1668,16 +1691,24 @@ static void show_stats(struct diffstat_t *data, struct diff_options *options)
                show_graph(options->file, '-', del, del_c, reset);
                fprintf(options->file, "\n");
        }
-       for (i = count; i < data->nr; i++) {
-               uintmax_t added = data->files[i]->added;
-               uintmax_t deleted = data->files[i]->deleted;
-               if (!data->files[i]->is_renamed &&
-                        (added + deleted == 0)) {
+
+       for (i = 0; i < data->nr; i++) {
+               struct diffstat_file *file = data->files[i];
+               uintmax_t added = file->added;
+               uintmax_t deleted = file->deleted;
+
+               if (file->is_unmerged ||
+                   (!file->is_interesting && (added + deleted == 0))) {
                        total_files--;
                        continue;
                }
-               adds += added;
-               dels += deleted;
+
+               if (!file->is_binary) {
+                       adds += added;
+                       dels += deleted;
+               }
+               if (i < count)
+                       continue;
                if (!extra_shown)
                        fprintf(options->file, "%s ...\n", line_prefix);
                extra_shown = 1;
@@ -1697,9 +1728,8 @@ static void show_shortstats(struct diffstat_t *data, struct diff_options *option
                int added = data->files[i]->added;
                int deleted= data->files[i]->deleted;
 
-               if (data->files[i]->is_unmerged)
-                       continue;
-               if (!data->files[i]->is_renamed && (added + deleted == 0)) {
+               if (data->files[i]->is_unmerged ||
+                   (!data->files[i]->is_interesting && (added + deleted == 0))) {
                        total_files--;
                } else if (!data->files[i]->is_binary) { /* don't count bytes */
                        adds += added;
@@ -2215,7 +2245,7 @@ static void builtin_diff(const char *name_a,
        mmfile_t mf1, mf2;
        const char *lbl[2];
        char *a_one, *b_two;
-       const char *set = diff_get_color_opt(o, DIFF_METAINFO);
+       const char *meta = diff_get_color_opt(o, DIFF_METAINFO);
        const char *reset = diff_get_color_opt(o, DIFF_RESET);
        const char *a_prefix, *b_prefix;
        struct userdiff_driver *textconv_one = NULL;
@@ -2236,7 +2266,7 @@ static void builtin_diff(const char *name_a,
                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, two->dirty_submodule,
-                               del, add, reset);
+                               meta, del, add, reset);
                return;
        }
 
@@ -2262,24 +2292,24 @@ 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";
-       strbuf_addf(&header, "%s%sdiff --git %s %s%s\n", line_prefix, set, a_one, b_two, reset);
+       strbuf_addf(&header, "%s%sdiff --git %s %s%s\n", line_prefix, meta, a_one, b_two, reset);
        if (lbl[0][0] == '/') {
                /* /dev/null */
-               strbuf_addf(&header, "%s%snew file mode %06o%s\n", line_prefix, set, two->mode, reset);
+               strbuf_addf(&header, "%s%snew file mode %06o%s\n", line_prefix, meta, two->mode, reset);
                if (xfrm_msg)
                        strbuf_addstr(&header, xfrm_msg);
                must_show_header = 1;
        }
        else if (lbl[1][0] == '/') {
-               strbuf_addf(&header, "%s%sdeleted file mode %06o%s\n", line_prefix, set, one->mode, reset);
+               strbuf_addf(&header, "%s%sdeleted file mode %06o%s\n", line_prefix, meta, one->mode, reset);
                if (xfrm_msg)
                        strbuf_addstr(&header, xfrm_msg);
                must_show_header = 1;
        }
        else {
                if (one->mode != two->mode) {
-                       strbuf_addf(&header, "%s%sold mode %06o%s\n", line_prefix, set, one->mode, reset);
-                       strbuf_addf(&header, "%s%snew mode %06o%s\n", line_prefix, set, two->mode, reset);
+                       strbuf_addf(&header, "%s%sold mode %06o%s\n", line_prefix, meta, one->mode, reset);
+                       strbuf_addf(&header, "%s%snew mode %06o%s\n", line_prefix, meta, two->mode, reset);
                        must_show_header = 1;
                }
                if (xfrm_msg)
@@ -2399,13 +2429,20 @@ static void builtin_diffstat(const char *name_a, const char *name_b,
                             struct diff_filespec *two,
                             struct diffstat_t *diffstat,
                             struct diff_options *o,
-                            int complete_rewrite)
+                            struct diff_filepair *p)
 {
        mmfile_t mf1, mf2;
        struct diffstat_file *data;
        int same_contents;
+       int complete_rewrite = 0;
+
+       if (!DIFF_PAIR_UNMERGED(p)) {
+               if (p->status == DIFF_STATUS_MODIFIED && p->score)
+                       complete_rewrite = 1;
+       }
 
        data = diffstat_add(diffstat, name_a, name_b);
+       data->is_interesting = p->status != DIFF_STATUS_UNKNOWN;
 
        if (!one || !two) {
                data->is_unmerged = 1;
@@ -3116,11 +3153,10 @@ static void run_diffstat(struct diff_filepair *p, struct diff_options *o,
 {
        const char *name;
        const char *other;
-       int complete_rewrite = 0;
 
        if (DIFF_PAIR_UNMERGED(p)) {
                /* unmerged */
-               builtin_diffstat(p->one->path, NULL, NULL, NULL, diffstat, o, 0);
+               builtin_diffstat(p->one->path, NULL, NULL, NULL, diffstat, o, p);
                return;
        }
 
@@ -3133,9 +3169,7 @@ static void run_diffstat(struct diff_filepair *p, struct diff_options *o,
        diff_fill_sha1_info(p->one);
        diff_fill_sha1_info(p->two);
 
-       if (p->status == DIFF_STATUS_MODIFIED && p->score)
-               complete_rewrite = 1;
-       builtin_diffstat(name, other, p->one, p->two, diffstat, o, complete_rewrite);
+       builtin_diffstat(name, other, p->one, p->two, diffstat, o, p);
 }
 
 static void run_checkdiff(struct diff_filepair *p, struct diff_options *o)
@@ -3172,7 +3206,7 @@ void diff_setup(struct diff_options *options)
        options->break_opt = -1;
        options->rename_limit = -1;
        options->dirstat_permille = diff_dirstat_permille_default;
-       options->context = 3;
+       options->context = diff_context_default;
        DIFF_OPT_SET(options, RENAME_EMPTY);
 
        options->change = diff_change;
@@ -3468,6 +3502,14 @@ static int parse_dirstat_opt(struct diff_options *options, const char *params)
        return 1;
 }
 
+static int parse_submodule_opt(struct diff_options *options, const char *value)
+{
+       if (parse_submodule_params(options, value))
+               die(_("Failed to parse --submodule option parameter: '%s'"),
+                       value);
+       return 1;
+}
+
 int diff_opt_parse(struct diff_options *options, const char **av, int ac)
 {
        const char *arg = av[0];
@@ -3648,10 +3690,8 @@ int diff_opt_parse(struct diff_options *options, const char **av, int ac)
                handle_ignore_submodules_arg(options, arg + 20);
        } else if (!strcmp(arg, "--submodule"))
                DIFF_OPT_SET(options, SUBMODULE_LOG);
-       else if (!prefixcmp(arg, "--submodule=")) {
-               if (!strcmp(arg + 12, "log"))
-                       DIFF_OPT_SET(options, SUBMODULE_LOG);
-       }
+       else if (!prefixcmp(arg, "--submodule="))
+               return parse_submodule_opt(options, arg + 12);
 
        /* misc options */
        else if (!strcmp(arg, "-z"))
@@ -4873,3 +4913,19 @@ size_t fill_textconv(struct userdiff_driver *driver,
 
        return size;
 }
+
+void setup_diff_pager(struct diff_options *opt)
+{
+       /*
+        * If the user asked for our exit code, then either they want --quiet
+        * or --exit-code. We should definitely not bother with a pager in the
+        * former case, as we will generate no output. Since we still properly
+        * report our exit code even when a pager is run, we _could_ run a
+        * pager with --exit-code. But since we have not done so historically,
+        * and because it is easy to find people oneline advising "git diff
+        * --exit-code" in hooks and other scripts, we do not do so.
+        */
+       if (!DIFF_OPT_TST(opt, EXIT_WITH_STATUS) &&
+           check_pager_config("diff") != 0)
+               setup_pager();
+}