add status config and command line options for rename detection

After performing a merge that has conflicts git status will, by default, attempt to detect renames which causes many objects to be examined. In a virtualized repo, those objects do not exist locally so the rename logic triggers them to be fetched from the server. This results in the status call taking hours to complete on very large repos vs seconds with this patch. Add a new config status.renames setting to enable turning off rename detection during status and commit. This setting will default to the value of diff.renames. Add a new config status.renamelimit setting to to enable bounding the time spent finding out inexact renames during status and commit. This setting will default to the value of diff.renamelimit. Add --no-renames command line option to status that enables overriding the config setting from the command line. Add --find-renames[=<n>] command line option to status that enables detecting renames and optionally setting the similarity index. Reviewed-by: Elijah Newren <newren@gmail.com> Original-Patch-by: Alejandro Pauly <alpauly@microsoft.com> Signed-off-by: Ben Peart <Ben.Peart@microsoft.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Ben Peart committed May 11, 2018 at 15:38 UTC e8b2dc2c2a8415bfd180ecf5cc237a54e69ac2e9
8 files changed +194 -2
Documentation/config.txt
+12
@@ -3119,6 +3119,18 @@ status.displayCommentPrefix::
3119 behavior of linkgit:git-status[1] in Git 1.8.4 and previous.
3120 Defaults to false.
3121
3122 +status.renameLimit::
3123 + The number of files to consider when performing rename detection
3124 + in linkgit:git-status[1] and linkgit:git-commit[1]. Defaults to
3125 + the value of diff.renameLimit.
3126 +
3127 +status.renames::
3128 + Whether and how Git detects renames in linkgit:git-status[1] and
3129 + linkgit:git-commit[1] . If set to "false", rename detection is
3130 + disabled. If set to "true", basic rename detection is enabled.
3131 + If set to "copies" or "copy", Git will detect copies, as well.
3132 + Defaults to the value of diff.renames.
3133 +
3134 status.showStash::
3135 If set to true, linkgit:git-status[1] will display the number of
3136 entries currently stashed away.
Documentation/git-status.txt
+10
@@ -135,6 +135,16 @@ ignored, then the directory is not shown, but all contents are shown.
135 Display or do not display detailed ahead/behind counts for the
136 branch relative to its upstream branch. Defaults to true.
137
138 +--renames::
139 +--no-renames::
140 + Turn on/off rename detection regardless of user configuration.
141 + See also linkgit:git-diff[1] `--no-renames`.
142 +
143 +--find-renames[=<n>]::
144 + Turn on rename detection, optionally setting the similarity
145 + threshold.
146 + See also linkgit:git-diff[1] `--find-renames`.
147 +
148 <pathspec>...::
149 See the 'pathspec' entry in linkgit:gitglossary[7].
150
builtin/commit.c
+42
@@ -143,6 +143,16 @@ static int opt_parse_m(const struct option *opt, const char *arg, int unset)
143 return 0;
144 }
145
146 +static int opt_parse_rename_score(const struct option *opt, const char *arg, int unset)
147 +{
148 + const char **value = opt->value;
149 + if (arg != NULL && *arg == '=')
150 + arg = arg + 1;
151 +
152 + *value = arg;
153 + return 0;
154 +}
155 +
156 static void determine_whence(struct wt_status *s)
157 {
158 if (file_exists(git_path_merge_head()))
@@ -1259,11 +1269,31 @@ static int git_status_config(const char *k, const char *v, void *cb)
1269 return error(_("Invalid untracked files mode '%s'"), v);
1270 return 0;
1271 }
1272 + if (!strcmp(k, "diff.renamelimit")) {
1273 + if (s->rename_limit == -1)
1274 + s->rename_limit = git_config_int(k, v);
1275 + return 0;
1276 + }
1277 + if (!strcmp(k, "status.renamelimit")) {
1278 + s->rename_limit = git_config_int(k, v);
1279 + return 0;
1280 + }
1281 + if (!strcmp(k, "diff.renames")) {
1282 + if (s->detect_rename == -1)
1283 + s->detect_rename = git_config_rename(k, v);
1284 + return 0;
1285 + }
1286 + if (!strcmp(k, "status.renames")) {
1287 + s->detect_rename = git_config_rename(k, v);
1288 + return 0;
1289 + }
1290 return git_diff_ui_config(k, v, NULL);
1291 }
1292
1293 int cmd_status(int argc, const char **argv, const char *prefix)
1294 {
1295 + static int no_renames = -1;
1296 + static const char *rename_score_arg = (const char *)-1;
1297 static struct wt_status s;
1298 int fd;
1299 struct object_id oid;
@@ -1297,6 +1327,10 @@ int cmd_status(int argc, const char **argv, const char *prefix)
1327 N_("ignore changes to submodules, optional when: all, dirty, untracked. (Default: all)"),
1328 PARSE_OPT_OPTARG, NULL, (intptr_t)"all" },
1329 OPT_COLUMN(0, "column", &s.colopts, N_("list untracked files in columns")),
1330 + OPT_BOOL(0, "no-renames", &no_renames, N_("do not detect renames")),
1331 + { OPTION_CALLBACK, 'M', "find-renames", &rename_score_arg,
1332 + N_("n"), N_("detect renames, optionally set similarity index"),
1333 + PARSE_OPT_OPTARG, opt_parse_rename_score },
1334 OPT_END(),
1335 };
1336
@@ -1336,6 +1370,14 @@ int cmd_status(int argc, const char **argv, const char *prefix)
1370 s.ignore_submodule_arg = ignore_submodule_arg;
1371 s.status_format = status_format;
1372 s.verbose = verbose;
1373 + if (no_renames != -1)
1374 + s.detect_rename = !no_renames;
1375 + if ((intptr_t)rename_score_arg != -1) {
1376 + if (s.detect_rename < DIFF_DETECT_RENAME)
1377 + s.detect_rename = DIFF_DETECT_RENAME;
1378 + if (rename_score_arg)
1379 + s.rename_score = parse_rename_score(&rename_score_arg);
1380 + }
1381
1382 wt_status_collect(&s);
1383
diff.c
+1 -1
@@ -177,7 +177,7 @@ static int parse_submodule_params(struct diff_options *options, const char *valu
177 return 0;
178 }
179
180 -static int git_config_rename(const char *var, const char *value)
180 +int git_config_rename(const char *var, const char *value)
181 {
182 if (!value)
183 return DIFF_DETECT_RENAME;
diff.h
+1
@@ -324,6 +324,7 @@ extern int git_diff_ui_config(const char *var, const char *value, void *cb);
324 extern void diff_setup(struct diff_options *);
325 extern int diff_opt_parse(struct diff_options *, const char **, int, const char *);
326 extern void diff_setup_done(struct diff_options *);
327 +extern int git_config_rename(const char *var, const char *value);
328
329 #define DIFF_DETECT_RENAME 1
330 #define DIFF_DETECT_COPY 2
t/t7525-status-rename.sh new
+113
@@ -0,0 +1,113 @@
1 +#!/bin/sh
2 +
3 +test_description='git status rename detection options'
4 +
5 +. ./test-lib.sh
6 +
7 +test_expect_success 'setup' '
8 + echo 1 >original &&
9 + git add . &&
10 + git commit -m"Adding original file." &&
11 + mv original renamed &&
12 + echo 2 >> renamed &&
13 + git add . &&
14 + cat >.gitignore <<-\EOF
15 + .gitignore
16 + expect*
17 + actual*
18 + EOF
19 +'
20 +
21 +test_expect_success 'status no-options' '
22 + git status >actual &&
23 + test_i18ngrep "renamed:" actual
24 +'
25 +
26 +test_expect_success 'status --no-renames' '
27 + git status --no-renames >actual &&
28 + test_i18ngrep "deleted:" actual &&
29 + test_i18ngrep "new file:" actual
30 +'
31 +
32 +test_expect_success 'status.renames inherits from diff.renames false' '
33 + git -c diff.renames=false status >actual &&
34 + test_i18ngrep "deleted:" actual &&
35 + test_i18ngrep "new file:" actual
36 +'
37 +
38 +test_expect_success 'status.renames inherits from diff.renames true' '
39 + git -c diff.renames=true status >actual &&
40 + test_i18ngrep "renamed:" actual
41 +'
42 +
43 +test_expect_success 'status.renames overrides diff.renames false' '
44 + git -c diff.renames=true -c status.renames=false status >actual &&
45 + test_i18ngrep "deleted:" actual &&
46 + test_i18ngrep "new file:" actual
47 +'
48 +
49 +test_expect_success 'status.renames overrides from diff.renames true' '
50 + git -c diff.renames=false -c status.renames=true status >actual &&
51 + test_i18ngrep "renamed:" actual
52 +'
53 +
54 +test_expect_success 'status status.renames=false' '
55 + git -c status.renames=false status >actual &&
56 + test_i18ngrep "deleted:" actual &&
57 + test_i18ngrep "new file:" actual
58 +'
59 +
60 +test_expect_success 'status status.renames=true' '
61 + git -c status.renames=true status >actual &&
62 + test_i18ngrep "renamed:" actual
63 +'
64 +
65 +test_expect_success 'commit honors status.renames=false' '
66 + git -c status.renames=false commit --dry-run >actual &&
67 + test_i18ngrep "deleted:" actual &&
68 + test_i18ngrep "new file:" actual
69 +'
70 +
71 +test_expect_success 'commit honors status.renames=true' '
72 + git -c status.renames=true commit --dry-run >actual &&
73 + test_i18ngrep "renamed:" actual
74 +'
75 +
76 +test_expect_success 'status config overridden' '
77 + git -c status.renames=true status --no-renames >actual &&
78 + test_i18ngrep "deleted:" actual &&
79 + test_i18ngrep "new file:" actual
80 +'
81 +
82 +test_expect_success 'status score=100%' '
83 + git status -M=100% >actual &&
84 + test_i18ngrep "deleted:" actual &&
85 + test_i18ngrep "new file:" actual &&
86 +
87 + git status --find-rename=100% >actual &&
88 + test_i18ngrep "deleted:" actual &&
89 + test_i18ngrep "new file:" actual
90 +'
91 +
92 +test_expect_success 'status score=01%' '
93 + git status -M=01% >actual &&
94 + test_i18ngrep "renamed:" actual &&
95 +
96 + git status --find-rename=01% >actual &&
97 + test_i18ngrep "renamed:" actual
98 +'
99 +
100 +test_expect_success 'copies not overridden by find-rename' '
101 + cp renamed copy &&
102 + git add copy &&
103 +
104 + git -c status.renames=copies status -M=01% >actual &&
105 + test_i18ngrep "copied:" actual &&
106 + test_i18ngrep "renamed:" actual &&
107 +
108 + git -c status.renames=copies status --find-rename=01% >actual &&
109 + test_i18ngrep "copied:" actual &&
110 + test_i18ngrep "renamed:" actual
111 +'
112 +
113 +test_done
wt-status.c
+12
@@ -138,6 +138,9 @@ void wt_status_prepare(struct wt_status *s)
138 s->show_stash = 0;
139 s->ahead_behind_flags = AHEAD_BEHIND_UNSPECIFIED;
140 s->display_comment_prefix = 0;
141 + s->detect_rename = -1;
142 + s->rename_score = -1;
143 + s->rename_limit = -1;
144 }
145
146 static void wt_longstatus_print_unmerged_header(struct wt_status *s)
@@ -592,6 +595,9 @@ static void wt_status_collect_changes_worktree(struct wt_status *s)
595 }
596 rev.diffopt.format_callback = wt_status_collect_changed_cb;
597 rev.diffopt.format_callback_data = s;
598 + rev.diffopt.detect_rename = s->detect_rename >= 0 ? s->detect_rename : rev.diffopt.detect_rename;
599 + rev.diffopt.rename_limit = s->rename_limit >= 0 ? s->rename_limit : rev.diffopt.rename_limit;
600 + rev.diffopt.rename_score = s->rename_score >= 0 ? s->rename_score : rev.diffopt.rename_score;
601 copy_pathspec(&rev.prune_data, &s->pathspec);
602 run_diff_files(&rev, 0);
603 }
@@ -625,6 +631,9 @@ static void wt_status_collect_changes_index(struct wt_status *s)
631 rev.diffopt.output_format |= DIFF_FORMAT_CALLBACK;
632 rev.diffopt.format_callback = wt_status_collect_updated_cb;
633 rev.diffopt.format_callback_data = s;
634 + rev.diffopt.detect_rename = s->detect_rename >= 0 ? s->detect_rename : rev.diffopt.detect_rename;
635 + rev.diffopt.rename_limit = s->rename_limit >= 0 ? s->rename_limit : rev.diffopt.rename_limit;
636 + rev.diffopt.rename_score = s->rename_score >= 0 ? s->rename_score : rev.diffopt.rename_score;
637 copy_pathspec(&rev.prune_data, &s->pathspec);
638 run_diff_index(&rev, 1);
639 }
@@ -982,6 +991,9 @@ static void wt_longstatus_print_verbose(struct wt_status *s)
991 setup_revisions(0, NULL, &rev, &opt);
992
993 rev.diffopt.output_format |= DIFF_FORMAT_PATCH;
994 + rev.diffopt.detect_rename = s->detect_rename >= 0 ? s->detect_rename : rev.diffopt.detect_rename;
995 + rev.diffopt.rename_limit = s->rename_limit >= 0 ? s->rename_limit : rev.diffopt.rename_limit;
996 + rev.diffopt.rename_score = s->rename_score >= 0 ? s->rename_score : rev.diffopt.rename_score;
997 rev.diffopt.file = s->fp;
998 rev.diffopt.close_file = 0;
999 /*
wt-status.h
+3 -1
@@ -89,7 +89,9 @@ struct wt_status {
89 int show_stash;
90 int hints;
91 enum ahead_behind_flags ahead_behind_flags;
92 -
92 + int detect_rename;
93 + int rename_score;
94 + int rename_limit;
95 enum wt_status_format status_format;
96 unsigned char sha1_commit[GIT_MAX_RAWSZ]; /* when not Initial */
97