Skip to content

Commit 22a100d

Browse files
committed
blame: default to ignoring revisions in .git-blame-ignore-revs
git-blame(1) can ignore a list of commits specified via --ignore-revs-file or the blame.ignoreRevsFile configuration option. This is useful for skipping uninteresting revisions such as tree-wide formatting changes, large-scale refactors, and code modernizations that would otherwise obscure genuine historical authorship. When revision-ignoring was introduced in commit ae3f36d ("blame: add blame.ignoreRevsFile config option", 2019-10-18), it intentionally avoided adopting a default ignore file. At the time, the capability was new and unproven, so avoiding unrequested filesystem I/O or unexpected attribution shifts took priority over a project-wide default. Requiring explicit opt-in per clone was therefore the prudent design. Since then, maintaining a .git-blame-ignore-revs file in the repository root has become the de facto standard across the Git ecosystem, adopted by major hosting platforms (GitHub, GitLab, Gerrit) and prominent open source projects (such as Chromium and LLVM). As a consequence, developers frequently encounter a jarring mismatch: web interfaces seamlessly ignore formatting commits, but local git-blame(1) and git-annotate(1) runs do not, unless each user manually configures blame.ignoreRevsFile for every local checkout. Teach git-blame(1) and git-annotate(1) to automatically check for a regular .git-blame-ignore-revs file at the root of the working tree when operating in a non-bare repository. To ensure consistent precedence, security, and override semantics: - Loading the default file occurs before reading configuration and CLI options, preserving user and repository config overrides. - Path resolution is anchored to repo_get_work_tree() and verified via lstat() to ensure it is a regular file. Symbolic links, directories, FIFOs, and sockets are safely skipped, preventing local information disclosure and denial-of-service hangs. - In build_ignorelist(), ignore-rev files are parsed starting after the last empty string entry. This ensures setting blame.ignoreRevsFile to "" or passing --ignore-revs-file "" or --no-ignore-revs-file cleanly discards the default file without attempting to open or parse it, allowing users to bypass corrupted default files. - Duplicate parsing is prevented by tracking seen files in a strset. Update documentation in blame-options.adoc and config/blame.adoc, and add comprehensive test coverage in t8013 for the default file lookup, subdirectory invocations, CLI and config overrides, symlink rejection, comments and whitespace handling, and bare repositories. Based-on-patch-by: Abhijeetsingh Meena <abhijeet040403@gmail.com> Helped-by: Kristoffer Haugsbakk <code@khaugsbakk.name> Helped-by: Phillip Wood <phillip.wood@dunelm.org.uk> Helped-by: Eric Sunshine <sunshine@sunshineco.com> Signed-off-by: Ravi Mistry <rmistry@google.com>
1 parent fa7f929 commit 22a100d

4 files changed

Lines changed: 196 additions & 12 deletions

File tree

‎Documentation/blame-options.adoc‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,8 +133,10 @@ take effect.
133133
Ignore revisions listed in _<file>_, which must be in the same format as an
134134
`fsck.skipList`. This option may be repeated, and these files will be
135135
processed after any files specified with the `blame.ignoreRevsFile` config
136-
option. An empty file name, `""`, will clear the list of revs from
137-
previously processed files.
136+
option or the default `.git-blame-ignore-revs` file. An empty file name,
137+
`""`, will clear the list of revs from previously processed files.
138+
`--no-ignore-revs-file` will clear all previously specified ignore revs
139+
files, including the default `.git-blame-ignore-revs` file.
138140

139141
`--color-lines`::
140142
Color line annotations in the default format differently if they come from

‎Documentation/config/blame.adoc‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,12 @@ blame.showRoot::
2323
blame.ignoreRevsFile::
2424
Ignore revisions listed in the file, one unabbreviated object name per
2525
line, in linkgit:git-blame[1]. Whitespace and comments beginning with
26-
`#` are ignored. This option may be repeated multiple times. Empty
27-
file names will reset the list of ignored revisions. This option will
28-
be handled before the command line option `--ignore-revs-file`.
26+
`#` are ignored. If `.git-blame-ignore-revs` exists at the root of the
27+
working tree in a non-bare repository, it is used by default. This option
28+
may be repeated multiple times; files specified here are processed after
29+
the default file. An empty file name will reset the list of ignored
30+
revisions from previously processed files and disable the default file.
31+
This option is handled before the command-line option `--ignore-revs-file`.
2932

3033
blame.markUnblamableLines::
3134
Mark lines that were changed by an ignored revision that we could not

‎builtin/blame.c‎

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@
1515
#include "hex.h"
1616
#include "commit.h"
1717
#include "diff.h"
18+
#include "path.h"
1819
#include "revision.h"
1920
#include "quote.h"
2021
#include "string-list.h"
22+
#include "strmap.h"
2123
#include "mailmap.h"
2224
#include "parse-options.h"
2325
#include "prio-queue.h"
@@ -768,8 +770,12 @@ static int git_blame_config(const char *var, const char *value,
768770
ret = git_config_pathname(&str, var, value);
769771
if (ret)
770772
return ret;
771-
if (str)
772-
string_list_insert(&ignore_revs_file_list, str);
773+
if (str) {
774+
if (!*str)
775+
string_list_clear(&ignore_revs_file_list, 0);
776+
else
777+
string_list_append(&ignore_revs_file_list, str);
778+
}
773779
free(str);
774780
return 0;
775781
}
@@ -936,16 +942,24 @@ static void build_ignorelist(struct blame_scoreboard *sb,
936942
{
937943
struct string_list_item *i;
938944
struct object_id oid;
945+
struct strset seen_files = STRSET_INIT;
946+
size_t start_idx = 0, idx;
947+
948+
for (idx = 0; idx < ignore_revs_file_list->nr; idx++) {
949+
if (!*ignore_revs_file_list->items[idx].string)
950+
start_idx = idx + 1;
951+
}
939952

940953
oidset_init(&sb->ignore_list, 0);
941-
for_each_string_list_item(i, ignore_revs_file_list) {
942-
if (!strcmp(i->string, ""))
943-
oidset_clear(&sb->ignore_list);
944-
else
945-
oidset_parse_file_carefully(&sb->ignore_list, i->string,
954+
for (idx = start_idx; idx < ignore_revs_file_list->nr; idx++) {
955+
const char *path = ignore_revs_file_list->items[idx].string;
956+
957+
if (strset_add(&seen_files, path))
958+
oidset_parse_file_carefully(&sb->ignore_list, path,
946959
the_repository->hash_algo,
947960
peel_to_commit_oid, sb);
948961
}
962+
strset_clear(&seen_files);
949963
for_each_string_list_item(i, ignore_rev_list) {
950964
if (repo_get_oid_committish(the_repository, i->string, &oid) ||
951965
peel_to_commit_oid(&oid, sb))
@@ -1020,6 +1034,20 @@ int cmd_blame(int argc,
10201034
const char *const *opt_usage = cmd_is_annotate ? annotate_opt_usage : blame_opt_usage;
10211035

10221036
setup_default_color_by_age();
1037+
{
1038+
const char *work_tree = repo_get_work_tree(the_repository);
1039+
1040+
if (work_tree) {
1041+
char *default_file = mkpathdup("%s/%s", work_tree,
1042+
".git-blame-ignore-revs");
1043+
struct stat st;
1044+
1045+
if (!lstat(default_file, &st) && S_ISREG(st.st_mode) &&
1046+
!access(default_file, R_OK))
1047+
string_list_append(&ignore_revs_file_list, default_file);
1048+
free(default_file);
1049+
}
1050+
}
10231051
repo_config(the_repository, git_blame_config, &output_option);
10241052
repo_init_revisions(the_repository, &revs, NULL);
10251053
revs.date_mode = blame_date_mode;

‎t/t8013-blame-ignore-revs.sh‎

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,4 +327,155 @@ test_expect_success ignore_merge '
327327
test_cmp expect actual
328328
'
329329

330+
# Tests for default .git-blame-ignore-revs file
331+
test_expect_success 'setup default .git-blame-ignore-revs' '
332+
git checkout -b default-file-branch &&
333+
test_write_lines line1 line2 >def-file &&
334+
git add def-file &&
335+
test_tick &&
336+
git commit -m "default base" &&
337+
git tag DEF_A &&
338+
339+
test_write_lines line1-modified line2-modified >def-file &&
340+
git add def-file &&
341+
test_tick &&
342+
git commit -m "default mod" &&
343+
git tag DEF_B &&
344+
345+
git rev-parse DEF_B >.git-blame-ignore-revs
346+
'
347+
348+
test_expect_success 'default .git-blame-ignore-revs is used by default' '
349+
git blame --line-porcelain def-file >blame_raw &&
350+
sed -ne "/^[0-9a-f][0-9a-f]* [0-9][0-9]* 1/s/ .*//p" blame_raw >actual &&
351+
git rev-parse DEF_A >expect &&
352+
test_cmp expect actual &&
353+
sed -ne "/^[0-9a-f][0-9a-f]* [0-9][0-9]* 2/s/ .*//p" blame_raw >actual &&
354+
test_cmp expect actual
355+
'
356+
357+
test_expect_success 'default .git-blame-ignore-revs respected by git annotate' '
358+
git rev-parse --short DEF_A >expect_sha &&
359+
git annotate def-file >actual &&
360+
test_grep "^$(cat expect_sha)" actual
361+
'
362+
363+
test_expect_success 'default .git-blame-ignore-revs works from subdirectory' '
364+
mkdir -p sub &&
365+
(
366+
cd sub &&
367+
git blame --line-porcelain ../def-file >blame_raw &&
368+
sed -ne "/^[0-9a-f][0-9a-f]* [0-9][0-9]* 1/s/ .*//p" blame_raw >actual &&
369+
git rev-parse DEF_A >expect &&
370+
test_cmp expect actual
371+
)
372+
'
373+
374+
test_expect_success 'disable default .git-blame-ignore-revs with --no-ignore-revs-file' '
375+
git blame --line-porcelain --no-ignore-revs-file def-file >blame_raw &&
376+
sed -ne "/^[0-9a-f][0-9a-f]* [0-9][0-9]* 1/s/ .*//p" blame_raw >actual &&
377+
git rev-parse DEF_B >expect &&
378+
test_cmp expect actual &&
379+
sed -ne "/^[0-9a-f][0-9a-f]* [0-9][0-9]* 2/s/ .*//p" blame_raw >actual &&
380+
test_cmp expect actual
381+
'
382+
383+
test_expect_success 'disable default .git-blame-ignore-revs with --ignore-revs-file ""' '
384+
git blame --line-porcelain --ignore-revs-file "" def-file >blame_raw &&
385+
sed -ne "/^[0-9a-f][0-9a-f]* [0-9][0-9]* 1/s/ .*//p" blame_raw >actual &&
386+
git rev-parse DEF_B >expect &&
387+
test_cmp expect actual &&
388+
sed -ne "/^[0-9a-f][0-9a-f]* [0-9][0-9]* 2/s/ .*//p" blame_raw >actual &&
389+
test_cmp expect actual
390+
'
391+
392+
test_expect_success 'disable default .git-blame-ignore-revs with blame.ignoreRevsFile=""' '
393+
test_config blame.ignoreRevsFile "" &&
394+
git blame --line-porcelain def-file >blame_raw &&
395+
sed -ne "/^[0-9a-f][0-9a-f]* [0-9][0-9]* 1/s/ .*//p" blame_raw >actual &&
396+
git rev-parse DEF_B >expect &&
397+
test_cmp expect actual &&
398+
sed -ne "/^[0-9a-f][0-9a-f]* [0-9][0-9]* 2/s/ .*//p" blame_raw >actual &&
399+
test_cmp expect actual
400+
'
401+
402+
test_expect_success 'default .git-blame-ignore-revs handles comments and whitespace' '
403+
test_when_finished "git rev-parse DEF_B >.git-blame-ignore-revs" &&
404+
{
405+
echo "# Leading comment" &&
406+
echo "" &&
407+
echo " $(git rev-parse DEF_B) " &&
408+
echo "# Trailing comment"
409+
} >.git-blame-ignore-revs &&
410+
git blame --line-porcelain def-file >blame_raw &&
411+
sed -ne "/^[0-9a-f][0-9a-f]* [0-9][0-9]* 1/s/ .*//p" blame_raw >actual &&
412+
git rev-parse DEF_A >expect &&
413+
test_cmp expect actual
414+
'
415+
416+
test_expect_success 'empty default .git-blame-ignore-revs is harmless' '
417+
test_when_finished "git rev-parse DEF_B >.git-blame-ignore-revs" &&
418+
: >.git-blame-ignore-revs &&
419+
git blame def-file
420+
'
421+
422+
test_expect_success SYMLINKS 'symlink .git-blame-ignore-revs is ignored' '
423+
test_when_finished "rm -f target_file .git-blame-ignore-revs && git rev-parse DEF_B >.git-blame-ignore-revs" &&
424+
git rev-parse DEF_B >target_file &&
425+
ln -sf target_file .git-blame-ignore-revs &&
426+
git blame --line-porcelain def-file >blame_raw &&
427+
sed -ne "/^[0-9a-f][0-9a-f]* [0-9][0-9]* 1/s/ .*//p" blame_raw >actual &&
428+
git rev-parse DEF_B >expect &&
429+
test_cmp expect actual
430+
'
431+
432+
test_expect_success 'malformed default .git-blame-ignore-revs fails but can be bypassed' '
433+
test_when_finished "git rev-parse DEF_B >.git-blame-ignore-revs" &&
434+
echo "invalid-oid-value" >.git-blame-ignore-revs &&
435+
test_must_fail git blame def-file &&
436+
git blame --no-ignore-revs-file def-file &&
437+
git blame --ignore-revs-file "" def-file
438+
'
439+
440+
test_expect_success 'default .git-blame-ignore-revs deduplicated when also set in config' '
441+
test_config blame.ignoreRevsFile .git-blame-ignore-revs &&
442+
git blame --line-porcelain def-file >blame_raw &&
443+
sed -ne "/^[0-9a-f][0-9a-f]* [0-9][0-9]* 1/s/ .*//p" blame_raw >actual &&
444+
git rev-parse DEF_A >expect &&
445+
test_cmp expect actual
446+
'
447+
448+
test_expect_success 'default .git-blame-ignore-revs combined with config blame.ignoreRevsFile' '
449+
test_write_lines line1-modified line2-c >def-file &&
450+
git add def-file &&
451+
test_tick &&
452+
git commit -m C &&
453+
git tag DEF_C &&
454+
git rev-parse DEF_C >custom_ignore &&
455+
test_config blame.ignoreRevsFile custom_ignore &&
456+
git blame --line-porcelain def-file >blame_raw &&
457+
sed -ne "/^[0-9a-f][0-9a-f]* [0-9][0-9]* 1/s/ .*//p" blame_raw >actual &&
458+
git rev-parse DEF_A >expect &&
459+
test_cmp expect actual &&
460+
sed -ne "/^[0-9a-f][0-9a-f]* [0-9][0-9]* 2/s/ .*//p" blame_raw >actual &&
461+
git rev-parse DEF_A >expect &&
462+
test_cmp expect actual
463+
'
464+
465+
test_expect_success 'default .git-blame-ignore-revs ignored in bare repo' '
466+
git clone --bare . bare.git &&
467+
git -C bare.git blame --line-porcelain def-file >blame_raw &&
468+
sed -ne "/^[0-9a-f][0-9a-f]* [0-9][0-9]* 2/s/ .*//p" blame_raw >actual &&
469+
git rev-parse DEF_C >expect &&
470+
test_cmp expect actual
471+
'
472+
473+
test_expect_success 'blame works when .git-blame-ignore-revs does not exist' '
474+
rm -f .git-blame-ignore-revs &&
475+
git blame --line-porcelain def-file >blame_raw &&
476+
sed -ne "/^[0-9a-f][0-9a-f]* [0-9][0-9]* 1/s/ .*//p" blame_raw >actual &&
477+
git rev-parse DEF_B >expect &&
478+
test_cmp expect actual
479+
'
480+
330481
test_done

0 commit comments

Comments
 (0)