Skip to content

Commit 10c0f35

Browse files
authored
blame: add blame.rename* configuration (#894)
blame's config callback did not load diff.renameThreshold because it chained directly to git_default_config, skipping git_diff_basic_config (and git_diff_ui_config) where that setting is handled. As a result, repo_diff_setup() always saw a zero rename_score, and blame fell back to DEFAULT_RENAME_SCORE (50%) regardless of the configured threshold. This PR moves diff.renameThreshold from git_diff_ui_config to git_diff_basic_config, alongside the related diff.renameLimit setting, so that plumbing commands that use git_diff_basic_config can also pick it up - but instead of trying to import that for blame, this PR adds separate configuration for blame.renames, blame.renameThreshold, and blame.renameLimit Assisted-by: Claude Opus 4.6
2 parents 6bcaab8 + 92725e7 commit 10c0f35

7 files changed

Lines changed: 111 additions & 19 deletions

File tree

Documentation/config/blame.adoc

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,21 @@ blame.markUnblamableLines::
3535
blame.markIgnoredLines::
3636
Mark lines that were changed by an ignored revision that we attributed to
3737
another commit with a '?' in the output of linkgit:git-blame[1].
38+
39+
blame.renames::
40+
If set to `false`, disable rename following in
41+
linkgit:git-blame[1]. This option defaults to `true`.
42+
43+
blame.renameThreshold::
44+
The minimum similarity threshold for rename detection in
45+
linkgit:git-blame[1]; equivalent to the `-M` option of
46+
linkgit:git-diff[1]. The value is a percentage (e.g. `50%`),
47+
or a fraction between 0 and 1 (e.g. `0.5`). If not set, the
48+
default is 50%. To limit blame to only follow exact renames,
49+
set `blame.renameThreshold = 100%`.
50+
51+
blame.renameLimit::
52+
The number of files to consider when performing rename
53+
detection in linkgit:git-blame[1]; equivalent to the `-l`
54+
option of linkgit:git-diff[1]. If not set, the default
55+
value is currently 1000.

Documentation/config/diff.adoc

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -160,10 +160,6 @@ endif::git-diff[]
160160
percentage (e.g. `50%`), or a fraction between 0 and 1
161161
(e.g. `0.5`). If not set, the default is 50%. This setting
162162
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%`.
167163

168164
`diff.renames`::
169165
Whether and how Git detects renames. If set to `false`,

Documentation/git-blame.adoc

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@ When specified one or more times, `-L` restricts annotation to the requested
2424
lines.
2525

2626
The origin of lines is automatically followed across whole-file
27-
renames (currently there is no option to turn the rename-following
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.
27+
renames. This can be disabled with `blame.renames`, and the minimum
28+
similarity threshold can be adjusted with `blame.renameThreshold`
29+
(see linkgit:git-config[1]). To follow lines moved from one file to
30+
another, or to follow lines that were copied and pasted from another
31+
file, etc., see the `-C` and `-M` options.
3232

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

builtin/blame.c

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ static int incremental;
6565
static int xdl_opts;
6666
static int abbrev = -1;
6767
static int no_whole_file_rename;
68+
static int blame_detect_rename = -1;
6869
static int show_progress;
6970
static char repeated_meta_color[COLOR_MAXLEN];
7071
static int coloring_mode;
@@ -780,6 +781,27 @@ static int git_blame_config(const char *var, const char *value,
780781
}
781782
}
782783

784+
if (!strcmp(var, "blame.renames")) {
785+
blame_detect_rename = git_config_bool(var, value);
786+
return 0;
787+
}
788+
789+
/*
790+
* Blame does not use git_diff_basic_config in its config
791+
* chain, so diff_rename_score_default is not normally loaded.
792+
* Forward blame.renameThreshold as diff.renameThreshold to
793+
* set the global that repo_diff_setup() copies into
794+
* diff_options.rename_score.
795+
*/
796+
if (!strcmp(var, "blame.renamethreshold"))
797+
return git_diff_basic_config("diff.renamethreshold",
798+
value, ctx, cb);
799+
800+
/* Same approach for blame.renameLimit; see above. */
801+
if (!strcmp(var, "blame.renamelimit"))
802+
return git_diff_basic_config("diff.renamelimit",
803+
value, ctx, cb);
804+
783805
if (!strcmp(var, "diff.algorithm")) {
784806
long diff_algorithm;
785807
if (!value)
@@ -1028,7 +1050,10 @@ int cmd_blame(int argc,
10281050
}
10291051
parse_done:
10301052
revision_opts_finish(&revs);
1031-
no_whole_file_rename = !revs.diffopt.flags.follow_renames;
1053+
if (blame_detect_rename >= 0)
1054+
no_whole_file_rename = !blame_detect_rename;
1055+
if (!revs.diffopt.flags.follow_renames)
1056+
no_whole_file_rename = 1;
10321057
xdl_opts |= revs.diffopt.xdl_opts & XDF_INDENT_HEURISTIC;
10331058
revs.diffopt.flags.follow_renames = 0;
10341059
argc = parse_options_end(&ctx);

diff.c

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -399,15 +399,6 @@ int git_diff_ui_config(const char *var, const char *value,
399399
diff_detect_rename_default = git_config_rename(var, value);
400400
return 0;
401401
}
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-
}
411402
if (!strcmp(var, "diff.autorefreshindex")) {
412403
diff_auto_refresh_index = git_config_bool(var, value);
413404
return 0;
@@ -494,6 +485,16 @@ int git_diff_basic_config(const char *var, const char *value,
494485
return 0;
495486
}
496487

488+
if (!strcmp(var, "diff.renamethreshold")) {
489+
const char *arg = value;
490+
if (!value)
491+
return config_error_nonbool(var);
492+
diff_rename_score_default = parse_rename_score(&arg);
493+
if (*arg)
494+
return error(_("invalid value for '%s': '%s'"), var, value);
495+
return 0;
496+
}
497+
497498
if (userdiff_config(var, value) < 0)
498499
return -1;
499500

t/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -982,6 +982,7 @@ integration_tests = [
982982
't8013-blame-ignore-revs.sh',
983983
't8014-blame-ignore-fuzzy.sh',
984984
't8015-blame-diff-algorithm.sh',
985+
't8016-blame-rename-config.sh',
985986
't8020-last-modified.sh',
986987
't8100-git-survey.sh',
987988
't9001-send-email.sh',

t/t8016-blame-rename-config.sh

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#!/bin/sh
2+
3+
test_description='git blame rename configuration options'
4+
5+
. ./test-lib.sh
6+
7+
test_expect_success 'setup' '
8+
test_write_lines line1 line2 line3 >v1-before-inexact.txt &&
9+
test_write_lines other1 other2 other3 >unrelated.txt &&
10+
git add v1-before-inexact.txt unrelated.txt &&
11+
GIT_AUTHOR_NAME=Original git commit -m "add files" &&
12+
13+
test_write_lines changed1 line2 line3 >v2-before-exact.txt &&
14+
git rm v1-before-inexact.txt &&
15+
git rm unrelated.txt &&
16+
git add v2-before-exact.txt &&
17+
GIT_AUTHOR_NAME=Inexact git commit -m "inexact rename with content change" &&
18+
19+
git mv v2-before-exact.txt v3.txt &&
20+
GIT_AUTHOR_NAME=Exact git commit -m "exact rename"
21+
'
22+
23+
test_expect_success 'blame follows renames by default' '
24+
git blame --porcelain v3.txt >output &&
25+
grep "^filename v1-before-inexact.txt" output
26+
'
27+
28+
test_expect_success 'blame.renames=false disables rename following' '
29+
git -c blame.renames=false blame --porcelain v3.txt >output &&
30+
! grep "^filename v1-before-inexact.txt" output &&
31+
! grep "^filename v2-before-exact.txt" output
32+
'
33+
34+
test_expect_success 'blame.renameThreshold=100% allows exact but skips inexact renames' '
35+
git -c blame.renameThreshold=100% blame --porcelain v3.txt >output &&
36+
grep "^filename v2-before-exact.txt" output &&
37+
! grep "^filename v1-before-inexact.txt" output
38+
'
39+
40+
test_expect_success 'blame.renameLimit=1 skips when sources*destinations exceeds limit' '
41+
git -c blame.renameLimit=1 blame --porcelain v3.txt >output &&
42+
grep "^filename v2-before-exact.txt" output &&
43+
! grep "^filename v1-before-inexact.txt" output
44+
'
45+
46+
test_expect_success 'blame.renameLimit=2 detects with two sources' '
47+
git -c blame.renameLimit=2 blame --porcelain v3.txt >output &&
48+
grep "^filename v1-before-inexact.txt" output
49+
'
50+
51+
test_done

0 commit comments

Comments
 (0)