Skip to content

Commit da97bbe

Browse files
committed
diff: add renameThreshold configuration option
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 Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
1 parent b5f9211 commit da97bbe

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
@@ -29,6 +29,12 @@ status.renameLimit::
2929
in linkgit:git-status[1] and linkgit:git-commit[1]. Defaults to
3030
the value of diff.renameLimit.
3131

32+
status.renameThreshold::
33+
The minimum similarity threshold for rename detection in
34+
linkgit:git-status[1] and linkgit:git-commit[1]. Defaults to
35+
the value of diff.renameThreshold. See `diff.renameThreshold`
36+
for the accepted value format.
37+
3238
status.renames::
3339
Whether and how Git detects renames in linkgit:git-status[1] and
3440
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;
@@ -4856,6 +4866,7 @@ void repo_diff_setup(struct repository *r, struct diff_options *options)
48564866
options->add_remove = diff_addremove;
48574867
options->use_color = diff_use_color_default;
48584868
options->detect_rename = diff_detect_rename_default;
4869+
options->rename_score = diff_rename_score_default;
48594870
options->xdl_opts |= diff_algorithm;
48604871
if (diff_indent_heuristic)
48614872
DIFF_XDL_SET(options, INDENT_HEURISTIC);

merge-ort.c

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5444,6 +5444,22 @@ static void merge_recursive_config(struct merge_options *opt, int ui)
54445444
repo_config_get_int(the_repository, "merge.verbosity", &opt->verbosity);
54455445
repo_config_get_int(the_repository, "diff.renamelimit", &opt->rename_limit);
54465446
repo_config_get_int(the_repository, "merge.renamelimit", &opt->rename_limit);
5447+
if (!repo_config_get_string(the_repository, "diff.renamethreshold", &value)) {
5448+
const char *arg = value;
5449+
opt->rename_score = parse_rename_score(&arg);
5450+
if (*arg)
5451+
die(_("invalid value for '%s': '%s'"),
5452+
"diff.renameThreshold", value);
5453+
free(value);
5454+
}
5455+
if (!repo_config_get_string(the_repository, "merge.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+
"merge.renameThreshold", value);
5461+
free(value);
5462+
}
54475463
repo_config_get_bool(the_repository, "merge.renormalize", &renormalize);
54485464
opt->renormalize = renormalize;
54495465
if (!repo_config_get_string(the_repository, "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)