Skip to content

Commit c722648

Browse files
committed
diff: add renameThreshold configuration option (#878)
Add diff.renameThreshold, merge.renameThreshold, and status.renameThreshold configuration options to control the minimum similarity threshold for rename detection without requiring command-line flags. The cascade follows the existing pattern for renameLimit and renames: - merge.renameThreshold overrides diff.renameThreshold for merges - status.renameThreshold overrides diff.renameThreshold for status - CLI flags (-M, --find-renames) override all config values The value accepts the same format as -M: a percentage (e.g. 50%) or a fraction (e.g. 0.5). If unset, the default remains 50%. This also gives git-blame users control over the rename threshold for the first time, since blame has no -M threshold flag but inherits diff.renameThreshold via repo_diff_setup(). Assisted-by: Claude Opus 4.6 __________________ Thanks for taking the time to contribute to Git! This fork contains changes specific to monorepo scenarios. If you are an external contributor, then please detail your reason for submitting to this fork: * [x] This is an early version of work already under review upstream. * [ ] This change only applies to interactions with Azure DevOps and the GVFS Protocol. * [ ] This change only applies to the virtualization hook and VFS for Git.
2 parents a7af299 + bcb77db commit c722648

10 files changed

Lines changed: 164 additions & 3 deletions

File tree

Documentation/config/diff.adoc

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,17 @@ endif::git-diff[]
154154
`-l`. If not set, the default value is currently 1000. This
155155
setting has no effect if rename detection is turned off.
156156

157+
`diff.renameThreshold`::
158+
The minimum similarity threshold for rename detection;
159+
equivalent to the `git diff` option `-M`. The value is a
160+
percentage (e.g. `50%`), or a fraction between 0 and 1
161+
(e.g. `0.5`). If not set, the default is 50%. This setting
162+
has no effect if rename detection is turned off.
163+
+
164+
This config setting is also respected by linkgit:git-blame[1];
165+
to limit blame to only consider exact renames, for example, set
166+
`diff.renameThreshold = 100%`.
167+
157168
`diff.renames`::
158169
Whether and how Git detects renames. If set to `false`,
159170
rename detection is disabled. If set to `true`, basic rename

Documentation/config/merge.adoc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,13 @@ include::fmt-merge-msg.adoc[]
4747
currently defaults to 7000. This setting has no effect if
4848
rename detection is turned off.
4949

50+
`merge.renameThreshold`::
51+
The minimum similarity threshold for rename detection during
52+
a merge. If not specified, defaults to the value of
53+
`diff.renameThreshold`. See `diff.renameThreshold` for the
54+
accepted value format. This setting has no effect if rename
55+
detection is turned off.
56+
5057
`merge.renames`::
5158
Whether Git detects renames. If set to `false`, rename detection
5259
is disabled. If set to `true`, basic rename detection is enabled.

Documentation/config/status.adoc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ status.renameLimit::
5454
in linkgit:git-status[1] and linkgit:git-commit[1]. Defaults to
5555
the value of diff.renameLimit.
5656
57+
status.renameThreshold::
58+
The minimum similarity threshold for rename detection in
59+
linkgit:git-status[1] and linkgit:git-commit[1]. Defaults to
60+
the value of diff.renameThreshold. See `diff.renameThreshold`
61+
for the accepted value format.
62+
5763
status.renames::
5864
Whether and how Git detects renames in linkgit:git-status[1] and
5965
linkgit:git-commit[1] . If set to "false", rename detection is

Documentation/git-blame.adoc

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,10 @@ lines.
2525

2626
The origin of lines is automatically followed across whole-file
2727
renames (currently there is no option to turn the rename-following
28-
off). To follow lines moved from one file to another, or to follow
29-
lines that were copied and pasted from another file, etc., see the
30-
`-C` and `-M` options.
28+
off, but the minimum similarity threshold can be adjusted with
29+
`diff.renameThreshold`; see linkgit:git-diff[1]). To follow lines
30+
moved from one file to another, or to follow lines that were copied
31+
and pasted from another file, etc., see the `-C` and `-M` options.
3132

3233
The report does not tell you anything about lines which have been deleted or
3334
replaced; you need to use a tool such as `git diff` or the "pickaxe"

builtin/commit.c

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1672,6 +1672,26 @@ static int git_status_config(const char *k, const char *v,
16721672
s->detect_rename = git_config_rename(k, v);
16731673
return 0;
16741674
}
1675+
if (!strcmp(k, "diff.renamethreshold")) {
1676+
if (s->rename_score == -1) {
1677+
const char *arg = v;
1678+
if (!v)
1679+
return config_error_nonbool(k);
1680+
s->rename_score = parse_rename_score(&arg);
1681+
if (*arg)
1682+
return error(_("invalid value for '%s': '%s'"), k, v);
1683+
}
1684+
return 0;
1685+
}
1686+
if (!strcmp(k, "status.renamethreshold")) {
1687+
const char *arg = v;
1688+
if (!v)
1689+
return config_error_nonbool(k);
1690+
s->rename_score = parse_rename_score(&arg);
1691+
if (*arg)
1692+
return error(_("invalid value for '%s': '%s'"), k, v);
1693+
return 0;
1694+
}
16751695
return git_diff_ui_config(k, v, ctx, NULL);
16761696
}
16771697

diff.c

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
static int diff_detect_rename_default;
5757
static int diff_indent_heuristic = 1;
5858
static int diff_rename_limit_default = 1000;
59+
static int diff_rename_score_default;
5960
static int diff_suppress_blank_empty;
6061
static enum git_colorbool diff_use_color_default = GIT_COLOR_UNKNOWN;
6162
static int diff_color_moved_default;
@@ -398,6 +399,15 @@ int git_diff_ui_config(const char *var, const char *value,
398399
diff_detect_rename_default = git_config_rename(var, value);
399400
return 0;
400401
}
402+
if (!strcmp(var, "diff.renamethreshold")) {
403+
const char *arg = value;
404+
if (!value)
405+
return config_error_nonbool(var);
406+
diff_rename_score_default = parse_rename_score(&arg);
407+
if (*arg)
408+
return error(_("invalid value for '%s': '%s'"), var, value);
409+
return 0;
410+
}
401411
if (!strcmp(var, "diff.autorefreshindex")) {
402412
diff_auto_refresh_index = git_config_bool(var, value);
403413
return 0;
@@ -5140,6 +5150,7 @@ void repo_diff_setup(struct repository *r, struct diff_options *options)
51405150
options->add_remove = diff_addremove;
51415151
options->use_color = diff_use_color_default;
51425152
options->detect_rename = diff_detect_rename_default;
5153+
options->rename_score = diff_rename_score_default;
51435154
options->xdl_opts |= diff_algorithm;
51445155
if (diff_indent_heuristic)
51455156
DIFF_XDL_SET(options, INDENT_HEURISTIC);

merge-ort.c

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5452,6 +5452,22 @@ static void merge_recursive_config(struct merge_options *opt, int ui)
54525452
repo_config_get_int(opt->repo, "merge.verbosity", &opt->verbosity);
54535453
repo_config_get_int(opt->repo, "diff.renamelimit", &opt->rename_limit);
54545454
repo_config_get_int(opt->repo, "merge.renamelimit", &opt->rename_limit);
5455+
if (!repo_config_get_string(opt->repo, "diff.renamethreshold", &value)) {
5456+
const char *arg = value;
5457+
opt->rename_score = parse_rename_score(&arg);
5458+
if (*arg)
5459+
die(_("invalid value for '%s': '%s'"),
5460+
"diff.renameThreshold", value);
5461+
free(value);
5462+
}
5463+
if (!repo_config_get_string(opt->repo, "merge.renamethreshold", &value)) {
5464+
const char *arg = value;
5465+
opt->rename_score = parse_rename_score(&arg);
5466+
if (*arg)
5467+
die(_("invalid value for '%s': '%s'"),
5468+
"merge.renameThreshold", value);
5469+
free(value);
5470+
}
54555471
repo_config_get_bool(opt->repo, "merge.renormalize", &renormalize);
54565472
opt->renormalize = renormalize;
54575473
if (!repo_config_get_string(opt->repo, "diff.renames", &value)) {

t/t4001-diff-rename.sh

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,21 @@ test_expect_success 'test diff.renames unset' '
126126
compare_diff_patch current expected
127127
'
128128

129+
test_expect_success 'diff.renameThreshold=100% suppresses inexact rename in diff' '
130+
git -c diff.renameThreshold=100% diff --cached $tree >current &&
131+
compare_diff_patch current no-rename
132+
'
133+
134+
test_expect_success 'diff.renameThreshold=1% detects rename in diff' '
135+
git -c diff.renameThreshold=1% diff --cached $tree >current &&
136+
compare_diff_patch current expected
137+
'
138+
139+
test_expect_success '-M overrides diff.renameThreshold' '
140+
git -c diff.renameThreshold=100% diff -M --cached $tree >current &&
141+
compare_diff_patch current expected
142+
'
143+
129144
test_expect_success 'favour same basenames over different ones' '
130145
cp path1 another-path &&
131146
git add another-path &&
@@ -155,6 +170,16 @@ test_expect_success 'favour same basenames even with minor differences' '
155170
test_grep "renamed: .*path1 -> subdir/path1" out
156171
'
157172

173+
test_expect_success 'diff.renameThreshold with modified rename in status' '
174+
git show HEAD:path1 | sed -e "s/Line 1/Changed 1/" \
175+
-e "s/Line 2/Changed 2/" -e "s/Line 3/Changed 3/" >subdir/path1 &&
176+
git add subdir/path1 &&
177+
git -c diff.renameThreshold=100% status >out &&
178+
test_grep ! "renamed:" out &&
179+
git -c diff.renameThreshold=1% status >out &&
180+
test_grep "renamed:" out
181+
'
182+
158183
test_expect_success 'two files with same basename and same content' '
159184
git reset --hard &&
160185
mkdir -p dir/A dir/B &&

t/t6434-merge-recursive-rename-options.sh

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,4 +332,36 @@ test_expect_success 'merge.renames overrides diff.renames' '
332332
$check_50
333333
'
334334

335+
test_expect_success 'diff.renameThreshold sets merge threshold' '
336+
git read-tree --reset -u HEAD &&
337+
test_must_fail git -c diff.renameThreshold=$th0 merge-recursive $tail &&
338+
check_threshold_0
339+
'
340+
341+
test_expect_success 'diff.renameThreshold=100% limits to exact renames in merge' '
342+
git read-tree --reset -u HEAD &&
343+
test_must_fail git -c diff.renameThreshold=100% merge-recursive $tail &&
344+
check_exact_renames
345+
'
346+
347+
test_expect_success 'merge.renameThreshold overrides diff.renameThreshold' '
348+
git read-tree --reset -u HEAD &&
349+
test_must_fail git -c diff.renameThreshold=100% \
350+
-c merge.renameThreshold=$th0 merge-recursive $tail &&
351+
check_threshold_0
352+
'
353+
354+
test_expect_success 'merge.renameThreshold defaults to diff.renameThreshold' '
355+
git read-tree --reset -u HEAD &&
356+
test_must_fail git -c diff.renameThreshold=$th2 merge-recursive $tail &&
357+
check_threshold_2
358+
'
359+
360+
test_expect_success '--find-renames overrides merge.renameThreshold' '
361+
git read-tree --reset -u HEAD &&
362+
test_must_fail git -c merge.renameThreshold=100% \
363+
merge-recursive --find-renames=$th0 $tail &&
364+
check_threshold_0
365+
'
366+
335367
test_done

t/t7525-status-rename.sh

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,38 @@ test_expect_success 'status score=01%' '
9797
test_grep "renamed:" actual
9898
'
9999

100+
test_expect_success 'diff.renameThreshold sets default threshold' '
101+
git -c diff.renameThreshold=100% status >actual &&
102+
test_grep "deleted:" actual &&
103+
test_grep "new file:" actual
104+
'
105+
106+
test_expect_success 'status.renameThreshold overrides diff.renameThreshold' '
107+
git -c diff.renameThreshold=100% -c status.renameThreshold=01% status >actual &&
108+
test_grep "renamed:" actual
109+
'
110+
111+
test_expect_success 'diff.renameThreshold=01% detects rename in status' '
112+
git -c diff.renameThreshold=01% status >actual &&
113+
test_grep "renamed:" actual
114+
'
115+
116+
test_expect_success 'commit honors diff.renameThreshold' '
117+
git -c diff.renameThreshold=100% commit --dry-run >actual &&
118+
test_grep "deleted:" actual &&
119+
test_grep "new file:" actual
120+
'
121+
122+
test_expect_success 'commit honors status.renameThreshold' '
123+
git -c status.renameThreshold=01% commit --dry-run >actual &&
124+
test_grep "renamed:" actual
125+
'
126+
127+
test_expect_success '-M overrides status.renameThreshold' '
128+
git -c status.renameThreshold=100% status -M=01% >actual &&
129+
test_grep "renamed:" actual
130+
'
131+
100132
test_expect_success 'copies not overridden by find-renames' '
101133
cp renamed copy &&
102134
git add copy &&

0 commit comments

Comments
 (0)