Skip to content

Commit a56420d

Browse files
rmistrygitster
authored andcommitted
blame: ignore revs in HEAD:.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 add the HEAD:.git-blame-ignore-revs blob, if it exists, as the initial element in the list of ignore-revs files in both bare and non-bare repositories. Reading the committed blob from HEAD rather than the working tree ensures that local runs match hosting platforms even when an untracked .git-blame-ignore-revs file is present or a tracked one has uncommitted local changes. To ensure consistent precedence and override semantics: - The default HEAD:.git-blame-ignore-revs entry is added before reading configuration and CLI options, preserving user and repository config overrides. - In git_blame_config(), blame.ignoreRevsFile entries are appended via string_list_append() rather than inserted in sorted order via string_list_insert() so that configuration entries preserve their order relative to the initial default entry. - The HEAD:.git-blame-ignore-revs tree entry is resolved quietly via get_oid_with_context(). Its mode is checked with S_ISREG() before reading the object so that non-regular tree entries (such as a committed symbolic link whose blob stores a target path rather than revision IDs, a subdirectory, or a gitlink) are skipped instead of being read and rejected as malformed object names. The blob is parsed in memory via a new oidset_parse_buffer_carefully() helper in oidset.c that shares line parsing with oidset_parse_file_carefully(). - In build_ignorelist(), ignore-revs entries are processed 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 blob without attempting to read or parse it, allowing users to bypass a malformed default blob. Update documentation in blame-options.adoc and config/blame.adoc, and add comprehensive test coverage in t8013 for the default blob lookup, subdirectory invocations, bare repositories, uncommitted and untracked working-tree files, CLI and config overrides, committed symlink entries, and comments and whitespace handling. 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> Signed-off-by: Junio C Hamano <gitster@pobox.com>
1 parent cea766a commit a56420d

6 files changed

Lines changed: 301 additions & 36 deletions

File tree

‎Documentation/blame-options.adoc‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -132,9 +132,11 @@ take effect.
132132
`--ignore-revs-file <file>`::
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
135-
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.
135+
processed after the default `HEAD:.git-blame-ignore-revs` blob (if it
136+
exists) and any files specified with the `blame.ignoreRevsFile` config
137+
option. An empty file name, `""`, or `--no-ignore-revs-file` will clear
138+
the list of revs from previously processed files, including the default
139+
`HEAD:.git-blame-ignore-revs` blob.
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 the `HEAD:.git-blame-ignore-revs` blob exists, it
27+
is added as the initial element in the list of ignore-revs files.
28+
Other files listed in the configuration are also used, but an empty
29+
element makes all elements that appeared before in the list forgotten.
30+
This option will be handled before the command line option
31+
`--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: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -769,7 +769,7 @@ static int git_blame_config(const char *var, const char *value,
769769
if (ret)
770770
return ret;
771771
if (str)
772-
string_list_insert(&ignore_revs_file_list, str);
772+
string_list_append(&ignore_revs_file_list, str);
773773
free(str);
774774
return 0;
775775
}
@@ -946,17 +946,52 @@ static int peel_to_commit_oid(struct object_id *oid_ret, void *cbdata)
946946
}
947947
}
948948

949+
static void parse_default_ignore_revs_blob(struct blame_scoreboard *sb,
950+
const char *name)
951+
{
952+
struct object_context oc;
953+
struct object_id oid;
954+
enum object_type type;
955+
size_t size;
956+
char *buf;
957+
958+
if (get_oid_with_context(the_repository, name, GET_OID_QUIETLY,
959+
&oid, &oc))
960+
goto out;
961+
if (!S_ISREG(oc.mode))
962+
goto out;
963+
964+
buf = odb_read_object(the_repository->objects, &oid, &type, &size);
965+
if (!buf)
966+
goto out;
967+
if (type == OBJ_BLOB)
968+
oidset_parse_buffer_carefully(&sb->ignore_list, buf, size,
969+
the_repository->hash_algo,
970+
peel_to_commit_oid, sb);
971+
free(buf);
972+
973+
out:
974+
object_context_release(&oc);
975+
}
976+
949977
static void build_ignorelist(struct blame_scoreboard *sb,
950978
struct string_list *ignore_revs_file_list,
951979
struct string_list *ignore_rev_list)
952980
{
953981
struct string_list_item *i;
954982
struct object_id oid;
983+
size_t start_idx = 0, idx;
984+
985+
for (idx = 0; idx < ignore_revs_file_list->nr; idx++) {
986+
if (!*ignore_revs_file_list->items[idx].string)
987+
start_idx = idx + 1;
988+
}
955989

956990
oidset_init(&sb->ignore_list, 0);
957-
for_each_string_list_item(i, ignore_revs_file_list) {
958-
if (!strcmp(i->string, ""))
959-
oidset_clear(&sb->ignore_list);
991+
for (idx = start_idx; idx < ignore_revs_file_list->nr; idx++) {
992+
i = &ignore_revs_file_list->items[idx];
993+
if (i->util)
994+
parse_default_ignore_revs_blob(sb, i->string);
960995
else
961996
oidset_parse_file_carefully(&sb->ignore_list, i->string,
962997
the_repository->hash_algo,
@@ -1036,6 +1071,8 @@ int cmd_blame(int argc,
10361071
const char *const *opt_usage = cmd_is_annotate ? annotate_opt_usage : blame_opt_usage;
10371072

10381073
setup_default_color_by_age();
1074+
string_list_append(&ignore_revs_file_list,
1075+
"HEAD:.git-blame-ignore-revs")->util = &sb;
10391076
repo_config(the_repository, git_blame_config, &output_option);
10401077
repo_init_revisions(the_repository, &revs, NULL);
10411078
revs.date_mode = blame_date_mode;

‎oidset.c‎

Lines changed: 58 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -70,44 +70,76 @@ void oidset_parse_file(struct oidset *set, const char *path,
7070
oidset_parse_file_carefully(set, path, algop, NULL, NULL);
7171
}
7272

73+
static void parse_oidset_line(struct oidset *set, struct strbuf *sb,
74+
const struct git_hash_algo *algop,
75+
oidset_parse_tweak_fn fn, void *cbdata)
76+
{
77+
const char *p;
78+
const char *name;
79+
struct object_id oid;
80+
81+
if (memchr(sb->buf, '\0', sb->len))
82+
die("invalid object name: %s", sb->buf);
83+
84+
/*
85+
* Allow trailing comments, leading whitespace
86+
* (including before commits), and empty or whitespace
87+
* only lines.
88+
*/
89+
name = strchr(sb->buf, '#');
90+
if (name)
91+
strbuf_setlen(sb, name - sb->buf);
92+
strbuf_trim(sb);
93+
if (!sb->len)
94+
return;
95+
96+
if (parse_oid_hex_algop(sb->buf, &oid, &p, algop) || *p != '\0')
97+
die("invalid object name: %s", sb->buf);
98+
if (fn && fn(&oid, cbdata))
99+
return;
100+
oidset_insert(set, &oid);
101+
}
102+
73103
void oidset_parse_file_carefully(struct oidset *set, const char *path,
74104
const struct git_hash_algo *algop,
75105
oidset_parse_tweak_fn fn, void *cbdata)
76106
{
77107
FILE *fp;
78108
struct strbuf sb = STRBUF_INIT;
79-
struct object_id oid;
80109

81110
fp = fopen(path, "r");
82111
if (!fp)
83112
die("could not open object name list: %s", path);
84-
while (!strbuf_getline(&sb, fp)) {
85-
const char *p;
86-
const char *name;
87-
88-
if (memchr(sb.buf, '\0', sb.len))
89-
die("invalid object name: %s", sb.buf);
90-
91-
/*
92-
* Allow trailing comments, leading whitespace
93-
* (including before commits), and empty or whitespace
94-
* only lines.
95-
*/
96-
name = strchr(sb.buf, '#');
97-
if (name)
98-
strbuf_setlen(&sb, name - sb.buf);
99-
strbuf_trim(&sb);
100-
if (!sb.len)
101-
continue;
102-
103-
if (parse_oid_hex_algop(sb.buf, &oid, &p, algop) || *p != '\0')
104-
die("invalid object name: %s", sb.buf);
105-
if (fn && fn(&oid, cbdata))
106-
continue;
107-
oidset_insert(set, &oid);
108-
}
113+
while (!strbuf_getline(&sb, fp))
114+
parse_oidset_line(set, &sb, algop, fn, cbdata);
109115
if (ferror(fp))
110116
die_errno("Could not read '%s'", path);
111117
fclose(fp);
112118
strbuf_release(&sb);
113119
}
120+
121+
void oidset_parse_buffer_carefully(struct oidset *set, const char *buf,
122+
size_t size,
123+
const struct git_hash_algo *algop,
124+
oidset_parse_tweak_fn fn, void *cbdata)
125+
{
126+
struct strbuf sb = STRBUF_INIT;
127+
const char *p = buf, *end;
128+
129+
if (!size)
130+
return;
131+
end = buf + size;
132+
133+
while (p < end) {
134+
const char *nl = memchr(p, '\n', end - p);
135+
size_t len = (nl ? nl : end) - p;
136+
137+
strbuf_reset(&sb);
138+
if (len && p[len - 1] == '\r')
139+
len--;
140+
strbuf_add(&sb, p, len);
141+
parse_oidset_line(set, &sb, algop, fn, cbdata);
142+
p = nl ? nl + 1 : end;
143+
}
144+
strbuf_release(&sb);
145+
}

‎oidset.h‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,15 @@ void oidset_parse_file_carefully(struct oidset *set, const char *path,
9898
const struct git_hash_algo *algop,
9999
oidset_parse_tweak_fn fn, void *cbdata);
100100

101+
/*
102+
* Similar to oidset_parse_file_carefully(), but parses lines from an
103+
* in-memory buffer of 'size' bytes.
104+
*/
105+
void oidset_parse_buffer_carefully(struct oidset *set, const char *buf,
106+
size_t size,
107+
const struct git_hash_algo *algop,
108+
oidset_parse_tweak_fn fn, void *cbdata);
109+
101110
struct oidset_iter {
102111
const kh_oid_set_t *set;
103112
khiter_t iter;

0 commit comments

Comments
 (0)