From e9b2c1dae2cd11783a66f0afdce53dfad78b43d8 Mon Sep 17 00:00:00 2001 From: Ravi Mistry Date: Wed, 7 Oct 2026 18:58:36 +0000 Subject: [PATCH 1/2] blame: harden ignore-revs parser and tag peeling Currently, blame.ignoreRevsFile and --ignore-revs-file only read paths explicitly configured by the user, which cannot point to an attacker-controlled file under Git's threat model. An upcoming commit will teach git-blame(1) and git-annotate(1) to automatically read .git-blame-ignore-revs by default if it exists, exposing the ignore-revs parser (oidset_parse_file_carefully() in oidset.c) and its tag-peeling callback (peel_to_commit_oid() in builtin/blame.c) to upstream-controlled content at a well-known path. Harden both code paths before enabling the default ignore-revs file: - In oidset_parse_file_carefully(), strbuf_getline() reads up to the next newline and records the full line length in sb.len, including any embedded NUL bytes. However, strchr(sb.buf, '#') and parse_oid_hex_algop(sb.buf, &oid, &p, algop) treat sb.buf as a NUL-terminated string. If a line contains an embedded NUL byte after a valid object name (such as "\0garbage" or "\0# comment"), *p is '\0' and trailing bytes on the line are silently ignored. Reject any line containing an embedded NUL byte via memchr() before stripping comments and whitespace. - In peel_to_commit_oid(), odb_read_object_info() is called without OBJECT_INFO_SKIP_FETCH_OBJECT or OBJECT_INFO_QUICK, and deref_tag() calls parse_object() on tag targets without checking whether the target object exists locally first. In a partial clone, any missing commit OID or tag target listed in the ignore-revs file would trigger lazy promisor fetches and pack directory rescans during git-blame(1). Use odb_read_object_info_extended() with OBJECT_INFO_LOOKUP_REPLACE | OBJECT_INFO_SKIP_FETCH_OBJECT | OBJECT_INFO_QUICK and peel OBJ_TAG objects one layer per iteration, verifying that each target object exists locally and matches the tag's declared type before parsing it. Signed-off-by: Ravi Mistry --- builtin/blame.c | 20 +++++++++++++++++-- oidset.c | 3 +++ t/t8013-blame-ignore-revs.sh | 38 ++++++++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 2 deletions(-) diff --git a/builtin/blame.c b/builtin/blame.c index 48d5251c6df700..6741a7b9df4671 100644 --- a/builtin/blame.c +++ b/builtin/blame.c @@ -911,21 +911,37 @@ static int is_a_rev(const char *name) static int peel_to_commit_oid(struct object_id *oid_ret, void *cbdata) { struct repository *r = ((struct blame_scoreboard *)cbdata)->repo; + enum object_type expected_type = OBJ_ANY; struct object_id oid; oidcpy(&oid, oid_ret); while (1) { + unsigned flags = OBJECT_INFO_LOOKUP_REPLACE | + OBJECT_INFO_SKIP_FETCH_OBJECT | + OBJECT_INFO_QUICK; + struct object_info oi = OBJECT_INFO_INIT; + enum object_type kind; struct object *obj; - int kind = odb_read_object_info(r->objects, &oid, NULL); + + oi.typep = &kind; + if (odb_read_object_info_extended(r->objects, &oid, &oi, + flags) < 0) + return -1; + if (expected_type != OBJ_ANY && kind != expected_type) + return -1; if (kind == OBJ_COMMIT) { oidcpy(oid_ret, &oid); return 0; } if (kind != OBJ_TAG) return -1; - obj = deref_tag(r, parse_object(r, &oid), NULL, 0); + obj = parse_object(r, &oid); + if (!obj || obj->type != OBJ_TAG) + return -1; + obj = ((struct tag *)obj)->tagged; if (!obj) return -1; + expected_type = obj->type; oidcpy(&oid, &obj->oid); } } diff --git a/oidset.c b/oidset.c index c8ff0b385c58ac..90d39204d3b7c3 100644 --- a/oidset.c +++ b/oidset.c @@ -85,6 +85,9 @@ void oidset_parse_file_carefully(struct oidset *set, const char *path, const char *p; const char *name; + if (memchr(sb.buf, '\0', sb.len)) + die("invalid object name: %s", sb.buf); + /* * Allow trailing comments, leading whitespace * (including before commits), and empty or whitespace diff --git a/t/t8013-blame-ignore-revs.sh b/t/t8013-blame-ignore-revs.sh index cace00ae8d6ae7..70fe509a6406fb 100755 --- a/t/t8013-blame-ignore-revs.sh +++ b/t/t8013-blame-ignore-revs.sh @@ -327,4 +327,42 @@ test_expect_success ignore_merge ' test_cmp expect actual ' +test_expect_success 'ignore-revs-file rejects lines with embedded NUL bytes' ' + rev_b=$(git rev-parse B) && + printf "%sQgarbage\n" "$rev_b" | q_to_nul >ignore_nul && + test_must_fail git blame file --ignore-revs-file ignore_nul 2>err && + test_grep "invalid object name:" err && + + printf "%sQ# comment\n" "$rev_b" | q_to_nul >ignore_nul_comment && + test_must_fail git blame file --ignore-revs-file ignore_nul_comment 2>err && + test_grep "invalid object name:" err +' + +test_expect_success 'ignore-revs-file peels chained tags and skips missing tag targets' ' + test_write_lines BB L2-modified L3 L4 L5 L6 L7 L8 CC >file && + git add file && + test_tick && + git commit -m D && + git tag -a -m "tag 1" D_TAG1 HEAD && + git tag -a -m "tag 2" D_TAG2 D_TAG1 && + git rev-parse D_TAG2 >ignore_tag_chain && + git blame --line-porcelain file --ignore-revs-file ignore_tag_chain >blame_raw && + sed -ne "/^[0-9a-f][0-9a-f]* [0-9][0-9]* 2/s/ .*//p" blame_raw >actual && + git rev-parse A >expect && + test_cmp expect actual && + + test_config extensions.partialClone origin && + test_config remote.origin.promisor true && + test_config remote.origin.url /nonexistent && + missing_oid=$(test_oid deadbeef) && + bad_tag=$(printf "object %s\ntype commit\ntag bad-tag\ntagger T 0 +0000\n\nmsg\n" "$missing_oid" | + git hash-object -t tag -w --stdin) && + test_write_lines "$missing_oid" "$bad_tag" >ignore_bad_tag && + git blame --line-porcelain file --ignore-revs-file ignore_bad_tag >blame_raw 2>err && + test_must_be_empty err && + sed -ne "/^[0-9a-f][0-9a-f]* [0-9][0-9]* 2/s/ .*//p" blame_raw >actual && + git rev-parse HEAD >expect && + test_cmp expect actual +' + test_done From 6039d4bc2d8b22254406b9dec79c7fdb1a4c499c Mon Sep 17 00:00:00 2001 From: Ravi Mistry Date: Wed, 7 Oct 2026 20:18:50 +0000 Subject: [PATCH 2/2] blame: ignore revs in .git-blame-ignore-revs by default git-blame(1) can ignore a list of commits specified via --ignore-revs-file or the blame.ignoreRevsFile configuration variable. 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 ae3f36dea1 ("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 at the top level of the repository has become the standard convention across the Git ecosystem, adopted by many open source projects (such as Chromium and LLVM) as well as hosting platforms (GitHub, GitLab, Gerrit). However, git-blame(1) and git-annotate(1) still ignore this file unless each user remembers to manually configure blame.ignoreRevsFile in every local clone. Teach git-blame(1) and git-annotate(1) to automatically add .git-blame-ignore-revs, if it exists, as the initial element in the list of ignore-revs files. Similar to how read_mailmap() handles .mailmap, consult .git-blame-ignore-revs at the root of the working tree when a working tree is available, and otherwise consult the HEAD:.git-blame-ignore-revs blob in bare repositories. Reading only from the working tree in non-bare repositories makes it ergonomic to prepare a new .git-blame-ignore-revs file, edit an existing one, or remove a committed one and immediately test the effect with git-blame(1) before committing, while consulting HEAD:.git-blame-ignore-revs in bare repositories allows them to respect the committed ignore list as well. To ensure consistent precedence, override semantics, and safety when reading upstream-controlled content: - The default .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. - When a working tree is present, the working-tree .git-blame-ignore-revs path is checked with lstat() and S_ISREG(), opened with open_nofollow(), and verified with fstat() and S_ISREG() so symbolic links and non-regular files are silently skipped. In bare repositories, HEAD:.git-blame-ignore-revs is resolved quietly via get_oid_with_context() and its tree entry mode is checked with S_ISREG() before checking the object. - In both modes, the file or blob size is capped at IGNORE_REVS_MAX_FILE_SIZE (100MB, matching ATTR_MAX_FILE_SIZE from 3c50032ff5 ("attr: ignore overly large gitattributes files", 2022-12-01)) before reading its contents, skipping an oversized file or blob with a warning without loading it into memory. The contents are parsed 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 entry without attempting to read or parse it, allowing users to bypass a malformed default ignore-revs file. Update documentation in blame-options.adoc and config/blame.adoc, and add test coverage in t8013 for the default working-tree lookup, uncommitted and untracked .git-blame-ignore-revs files, subdirectory invocations, bare repositories, CLI and config overrides, symbolic links, overly large files and blobs, and comments and whitespace handling. Based-on-patch-by: Abhijeetsingh Meena Helped-by: Kristoffer Haugsbakk Helped-by: Phillip Wood Helped-by: Eric Sunshine Signed-off-by: Ravi Mistry --- Documentation/blame-options.adoc | 9 +- Documentation/config/blame.adoc | 10 +- builtin/blame.c | 102 +++++++++++++- oidset.c | 84 +++++++---- oidset.h | 9 ++ t/t8013-blame-ignore-revs.sh | 233 +++++++++++++++++++++++++++++++ 6 files changed, 411 insertions(+), 36 deletions(-) diff --git a/Documentation/blame-options.adoc b/Documentation/blame-options.adoc index 1ae1222b6b5ffa..7e910373bd3d0f 100644 --- a/Documentation/blame-options.adoc +++ b/Documentation/blame-options.adoc @@ -132,9 +132,12 @@ take effect. `--ignore-revs-file `:: Ignore revisions listed in __, which must be in the same format as an `fsck.skipList`. This option may be repeated, and these files will be - processed after any files specified with the `blame.ignoreRevsFile` config - option. An empty file name, `""`, will clear the list of revs from - previously processed files. + processed after the default `.git-blame-ignore-revs` file at the root of + the working tree (or `HEAD:.git-blame-ignore-revs` in a bare repository), + if it exists, and any files specified with the `blame.ignoreRevsFile` + configuration variable. An empty file name, `""`, or + `--no-ignore-revs-file` will clear the list of revs from previously + processed files, including the default ignore-revs file. `--color-lines`:: Color line annotations in the default format differently if they come from diff --git a/Documentation/config/blame.adoc b/Documentation/config/blame.adoc index 4d047c17908cd6..a46aea8d987c60 100644 --- a/Documentation/config/blame.adoc +++ b/Documentation/config/blame.adoc @@ -23,9 +23,13 @@ blame.showRoot:: blame.ignoreRevsFile:: Ignore revisions listed in the file, one unabbreviated object name per line, in linkgit:git-blame[1]. Whitespace and comments beginning with - `#` are ignored. This option may be repeated multiple times. Empty - file names will reset the list of ignored revisions. This option will - be handled before the command line option `--ignore-revs-file`. + `#` are ignored. If `.git-blame-ignore-revs` exists at the root of the + working tree (or `HEAD:.git-blame-ignore-revs` in a bare repository), it + is added as the initial element in the list of ignore-revs files. + Other files listed in the configuration are also used, but an empty + element makes all elements that appeared before in the list forgotten. + This option will be handled before the command line option + `--ignore-revs-file`. blame.markUnblamableLines:: Mark lines that were changed by an ignored revision that we could not diff --git a/builtin/blame.c b/builtin/blame.c index 6741a7b9df4671..1e753a0b605227 100644 --- a/builtin/blame.c +++ b/builtin/blame.c @@ -29,6 +29,7 @@ #include "object-name.h" #include "odb.h" #include "pager.h" +#include "path.h" #include "blame.h" #include "refs.h" #include "setup.h" @@ -769,7 +770,7 @@ static int git_blame_config(const char *var, const char *value, if (ret) return ret; if (str) - string_list_insert(&ignore_revs_file_list, str); + string_list_append(&ignore_revs_file_list, str); free(str); return 0; } @@ -946,17 +947,104 @@ static int peel_to_commit_oid(struct object_id *oid_ret, void *cbdata) } } +#define IGNORE_REVS_MAX_FILE_SIZE (100 * 1024 * 1024) + +static void parse_default_ignore_revs_blob(struct blame_scoreboard *sb, + const char *path) +{ + char *name = xstrfmt("HEAD:%s", path); + struct object_context oc; + struct object_id oid; + enum object_type type; + size_t size; + char *buf; + + if (get_oid_with_context(sb->repo, name, GET_OID_QUIETLY, + &oid, &oc)) + goto out; + if (!S_ISREG(oc.mode)) + goto out; + + if (odb_read_object_info(sb->repo->objects, &oid, &size) != OBJ_BLOB) + goto out; + if (size >= IGNORE_REVS_MAX_FILE_SIZE) { + warning(_("ignoring overly large ignore-revs blob '%s'"), name); + goto out; + } + + buf = odb_read_object(sb->repo->objects, &oid, &type, &size); + if (!buf) + goto out; + if (type == OBJ_BLOB) + oidset_parse_buffer_carefully(&sb->ignore_list, buf, size, + sb->repo->hash_algo, + peel_to_commit_oid, sb); + free(buf); + +out: + object_context_release(&oc); + free(name); +} + +static void parse_default_ignore_revs_file(struct blame_scoreboard *sb, + const char *name) +{ + char *path = repo_worktree_path(sb->repo, "%s", name); + struct stat st; + int fd = -1; + size_t size; + char *buf = NULL; + + if (lstat(path, &st) || !S_ISREG(st.st_mode)) + goto out; + fd = open_nofollow(path, O_RDONLY); + if (fd < 0 || fstat(fd, &st) || !S_ISREG(st.st_mode)) + goto out; + if (st.st_size >= IGNORE_REVS_MAX_FILE_SIZE) { + warning(_("ignoring overly large ignore-revs file '%s'"), name); + goto out; + } + size = xsize_t(st.st_size); + buf = xmalloc(size); + if (read_in_full(fd, buf, size) == (ssize_t)size) + oidset_parse_buffer_carefully(&sb->ignore_list, buf, size, + sb->repo->hash_algo, + peel_to_commit_oid, sb); + +out: + if (fd >= 0) + close(fd); + free(buf); + free(path); +} + +static void parse_default_ignore_revs(struct blame_scoreboard *sb, + const char *name) +{ + if (repo_get_work_tree(sb->repo)) + parse_default_ignore_revs_file(sb, name); + else + parse_default_ignore_revs_blob(sb, name); +} + static void build_ignorelist(struct blame_scoreboard *sb, struct string_list *ignore_revs_file_list, struct string_list *ignore_rev_list) { struct string_list_item *i; struct object_id oid; + size_t start_idx = 0, idx; + + for (idx = 0; idx < ignore_revs_file_list->nr; idx++) { + if (!*ignore_revs_file_list->items[idx].string) + start_idx = idx + 1; + } oidset_init(&sb->ignore_list, 0); - for_each_string_list_item(i, ignore_revs_file_list) { - if (!strcmp(i->string, "")) - oidset_clear(&sb->ignore_list); + for (idx = start_idx; idx < ignore_revs_file_list->nr; idx++) { + i = &ignore_revs_file_list->items[idx]; + if (i->util) + parse_default_ignore_revs(sb, i->string); else oidset_parse_file_carefully(&sb->ignore_list, i->string, the_repository->hash_algo, @@ -1036,6 +1124,12 @@ int cmd_blame(int argc, const char *const *opt_usage = cmd_is_annotate ? annotate_opt_usage : blame_opt_usage; setup_default_color_by_age(); + /* + * An entry with a non-NULL .util member names the default + * ignore-revs file at the project root; see build_ignorelist(). + */ + string_list_append(&ignore_revs_file_list, + ".git-blame-ignore-revs")->util = (void *)1; repo_config(the_repository, git_blame_config, &output_option); repo_init_revisions(the_repository, &revs, NULL); revs.date_mode = blame_date_mode; diff --git a/oidset.c b/oidset.c index 90d39204d3b7c3..8469d03b9b270f 100644 --- a/oidset.c +++ b/oidset.c @@ -70,44 +70,76 @@ void oidset_parse_file(struct oidset *set, const char *path, oidset_parse_file_carefully(set, path, algop, NULL, NULL); } +static void parse_oidset_line(struct oidset *set, struct strbuf *sb, + const struct git_hash_algo *algop, + oidset_parse_tweak_fn fn, void *cbdata) +{ + const char *p; + const char *name; + struct object_id oid; + + if (memchr(sb->buf, '\0', sb->len)) + die("invalid object name: %s", sb->buf); + + /* + * Allow trailing comments, leading whitespace + * (including before commits), and empty or whitespace + * only lines. + */ + name = strchr(sb->buf, '#'); + if (name) + strbuf_setlen(sb, name - sb->buf); + strbuf_trim(sb); + if (!sb->len) + return; + + if (parse_oid_hex_algop(sb->buf, &oid, &p, algop) || *p != '\0') + die("invalid object name: %s", sb->buf); + if (fn && fn(&oid, cbdata)) + return; + oidset_insert(set, &oid); +} + void oidset_parse_file_carefully(struct oidset *set, const char *path, const struct git_hash_algo *algop, oidset_parse_tweak_fn fn, void *cbdata) { FILE *fp; struct strbuf sb = STRBUF_INIT; - struct object_id oid; fp = fopen(path, "r"); if (!fp) die("could not open object name list: %s", path); - while (!strbuf_getline(&sb, fp)) { - const char *p; - const char *name; - - if (memchr(sb.buf, '\0', sb.len)) - die("invalid object name: %s", sb.buf); - - /* - * Allow trailing comments, leading whitespace - * (including before commits), and empty or whitespace - * only lines. - */ - name = strchr(sb.buf, '#'); - if (name) - strbuf_setlen(&sb, name - sb.buf); - strbuf_trim(&sb); - if (!sb.len) - continue; - - if (parse_oid_hex_algop(sb.buf, &oid, &p, algop) || *p != '\0') - die("invalid object name: %s", sb.buf); - if (fn && fn(&oid, cbdata)) - continue; - oidset_insert(set, &oid); - } + while (!strbuf_getline(&sb, fp)) + parse_oidset_line(set, &sb, algop, fn, cbdata); if (ferror(fp)) die_errno("Could not read '%s'", path); fclose(fp); strbuf_release(&sb); } + +void oidset_parse_buffer_carefully(struct oidset *set, const char *buf, + size_t size, + const struct git_hash_algo *algop, + oidset_parse_tweak_fn fn, void *cbdata) +{ + struct strbuf sb = STRBUF_INIT; + const char *p = buf, *end; + + if (!size) + return; + end = buf + size; + + while (p < end) { + const char *nl = memchr(p, '\n', end - p); + size_t len = (nl ? nl : end) - p; + + strbuf_reset(&sb); + if (len && p[len - 1] == '\r') + len--; + strbuf_add(&sb, p, len); + parse_oidset_line(set, &sb, algop, fn, cbdata); + p = nl ? nl + 1 : end; + } + strbuf_release(&sb); +} diff --git a/oidset.h b/oidset.h index e0f1a6ff4ff203..667c3908429ebd 100644 --- a/oidset.h +++ b/oidset.h @@ -98,6 +98,15 @@ void oidset_parse_file_carefully(struct oidset *set, const char *path, const struct git_hash_algo *algop, oidset_parse_tweak_fn fn, void *cbdata); +/* + * Similar to oidset_parse_file_carefully(), but parses lines from an + * in-memory buffer of 'size' bytes. + */ +void oidset_parse_buffer_carefully(struct oidset *set, const char *buf, + size_t size, + const struct git_hash_algo *algop, + oidset_parse_tweak_fn fn, void *cbdata); + struct oidset_iter { const kh_oid_set_t *set; khiter_t iter; diff --git a/t/t8013-blame-ignore-revs.sh b/t/t8013-blame-ignore-revs.sh index 70fe509a6406fb..92d8296c7bf1e1 100755 --- a/t/t8013-blame-ignore-revs.sh +++ b/t/t8013-blame-ignore-revs.sh @@ -365,4 +365,237 @@ test_expect_success 'ignore-revs-file peels chained tags and skips missing tag t test_cmp expect actual ' +# Tests for default .git-blame-ignore-revs +test_expect_success 'setup default .git-blame-ignore-revs' ' + git checkout -b default-file-branch && + test_write_lines line1 line2 >def-file && + git add def-file && + test_tick && + git commit -m "default base" && + git tag DEF_A && + + test_write_lines line1-modified line2-modified >def-file && + git add def-file && + test_tick && + git commit -m "default mod" && + git tag DEF_B && + + git rev-parse DEF_B >.git-blame-ignore-revs && + git blame --line-porcelain def-file >blame_raw && + sed -ne "/^[0-9a-f][0-9a-f]* [0-9][0-9]* 1/s/ .*//p" blame_raw >actual && + git rev-parse DEF_A >expect && + test_cmp expect actual && + + git add .git-blame-ignore-revs && + test_tick && + git commit -m "add .git-blame-ignore-revs" +' + +test_expect_success 'default .git-blame-ignore-revs is used by default' ' + git blame --line-porcelain def-file >blame_raw && + sed -ne "/^[0-9a-f][0-9a-f]* [0-9][0-9]* 1/s/ .*//p" blame_raw >actual && + git rev-parse DEF_A >expect && + test_cmp expect actual && + sed -ne "/^[0-9a-f][0-9a-f]* [0-9][0-9]* 2/s/ .*//p" blame_raw >actual && + test_cmp expect actual +' + +test_expect_success 'default .git-blame-ignore-revs respected by git annotate' ' + git rev-parse --short DEF_A >expect_sha && + git annotate def-file >actual && + test_grep "^$(cat expect_sha)" actual +' + +test_expect_success 'default .git-blame-ignore-revs works from subdirectory' ' + test_when_finished "rm -rf sub" && + mkdir -p sub && + echo "invalid-oid-value" >sub/.git-blame-ignore-revs && + ( + cd sub && + git blame --line-porcelain ../def-file >blame_raw && + sed -ne "/^[0-9a-f][0-9a-f]* [0-9][0-9]* 1/s/ .*//p" blame_raw >actual && + git rev-parse DEF_A >expect && + test_cmp expect actual + ) +' + +test_expect_success 'default HEAD:.git-blame-ignore-revs respected in bare repo' ' + test_when_finished "rm -rf bare.git" && + git clone --bare . bare.git && + git -C bare.git blame --line-porcelain def-file >blame_raw && + sed -ne "/^[0-9a-f][0-9a-f]* [0-9][0-9]* 1/s/ .*//p" blame_raw >actual && + git rev-parse DEF_A >expect && + test_cmp expect actual && + + git -C .git blame --line-porcelain HEAD -- def-file >blame_raw && + sed -ne "/^[0-9a-f][0-9a-f]* [0-9][0-9]* 1/s/ .*//p" blame_raw >actual && + test_cmp expect actual +' + +test_expect_success 'uncommitted .git-blame-ignore-revs changes in working tree are respected' ' + test_when_finished "git checkout -- .git-blame-ignore-revs" && + : >.git-blame-ignore-revs && + git blame --line-porcelain def-file >blame_raw && + sed -ne "/^[0-9a-f][0-9a-f]* [0-9][0-9]* 1/s/ .*//p" blame_raw >actual && + git rev-parse DEF_B >expect && + test_cmp expect actual && + + rm .git-blame-ignore-revs && + git blame --line-porcelain def-file >blame_raw && + sed -ne "/^[0-9a-f][0-9a-f]* [0-9][0-9]* 1/s/ .*//p" blame_raw >actual && + test_cmp expect actual && + + echo "invalid-oid-value" >.git-blame-ignore-revs && + test_must_fail git blame def-file +' + +test_expect_success 'disable default .git-blame-ignore-revs with --no-ignore-revs-file' ' + git blame --line-porcelain --no-ignore-revs-file def-file >blame_raw && + sed -ne "/^[0-9a-f][0-9a-f]* [0-9][0-9]* 1/s/ .*//p" blame_raw >actual && + git rev-parse DEF_B >expect && + test_cmp expect actual && + sed -ne "/^[0-9a-f][0-9a-f]* [0-9][0-9]* 2/s/ .*//p" blame_raw >actual && + test_cmp expect actual +' + +test_expect_success 'disable default .git-blame-ignore-revs with --ignore-revs-file ""' ' + git blame --line-porcelain --ignore-revs-file "" def-file >blame_raw && + sed -ne "/^[0-9a-f][0-9a-f]* [0-9][0-9]* 1/s/ .*//p" blame_raw >actual && + git rev-parse DEF_B >expect && + test_cmp expect actual && + sed -ne "/^[0-9a-f][0-9a-f]* [0-9][0-9]* 2/s/ .*//p" blame_raw >actual && + test_cmp expect actual +' + +test_expect_success 'disable default .git-blame-ignore-revs with blame.ignoreRevsFile=""' ' + test_config blame.ignoreRevsFile "" && + git blame --line-porcelain def-file >blame_raw && + sed -ne "/^[0-9a-f][0-9a-f]* [0-9][0-9]* 1/s/ .*//p" blame_raw >actual && + git rev-parse DEF_B >expect && + test_cmp expect actual && + sed -ne "/^[0-9a-f][0-9a-f]* [0-9][0-9]* 2/s/ .*//p" blame_raw >actual && + test_cmp expect actual +' + +test_expect_success 'default .git-blame-ignore-revs handles comments and whitespace' ' + rev_def_b=$(git rev-parse DEF_B) && + { + echo "# Leading comment" && + echo "" && + echo " $rev_def_b # inline comment" && + echo "# Trailing comment" + } >.git-blame-ignore-revs && + git add .git-blame-ignore-revs && + test_tick && + git commit -m "comments and whitespace in .git-blame-ignore-revs" && + git blame --line-porcelain def-file >blame_raw && + sed -ne "/^[0-9a-f][0-9a-f]* [0-9][0-9]* 1/s/ .*//p" blame_raw >actual && + git rev-parse DEF_A >expect && + test_cmp expect actual +' + +test_expect_success 'empty default .git-blame-ignore-revs is harmless' ' + : >.git-blame-ignore-revs && + git add .git-blame-ignore-revs && + test_tick && + git commit -m "empty .git-blame-ignore-revs" && + git blame --line-porcelain def-file >blame_raw && + sed -ne "/^[0-9a-f][0-9a-f]* [0-9][0-9]* 1/s/ .*//p" blame_raw >actual && + git rev-parse DEF_B >expect && + test_cmp expect actual +' + +test_expect_success SYMLINKS 'symlink .git-blame-ignore-revs in working tree is ignored' ' + test_when_finished "git checkout -- .git-blame-ignore-revs && rm -f target_file" && + git rev-parse DEF_B >target_file && + rm -f .git-blame-ignore-revs && + ln -s target_file .git-blame-ignore-revs && + git blame --line-porcelain def-file >blame_raw && + sed -ne "/^[0-9a-f][0-9a-f]* [0-9][0-9]* 1/s/ .*//p" blame_raw >actual && + git rev-parse DEF_B >expect && + test_cmp expect actual +' + +test_expect_success 'committed symlink .git-blame-ignore-revs in bare repo HEAD is ignored' ' + test_when_finished "git reset --hard HEAD~ && rm -rf bare-symlink.git" && + blob=$(printf "%s" "target_file" | git hash-object -w --stdin) && + git update-index --replace --cacheinfo 120000,$blob,.git-blame-ignore-revs && + test_tick && + git commit -m "symlink .git-blame-ignore-revs" && + git clone --bare . bare-symlink.git && + git -C bare-symlink.git blame --line-porcelain def-file >blame_raw && + sed -ne "/^[0-9a-f][0-9a-f]* [0-9][0-9]* 1/s/ .*//p" blame_raw >actual && + git rev-parse DEF_B >expect && + test_cmp expect actual +' + +test_expect_success 'malformed default .git-blame-ignore-revs fails but can be bypassed' ' + git rm -f .git-blame-ignore-revs && + echo "invalid-oid-value" >.git-blame-ignore-revs && + git add .git-blame-ignore-revs && + test_tick && + git commit -m "malformed .git-blame-ignore-revs" && + test_must_fail git blame def-file && + git blame --no-ignore-revs-file def-file && + git blame --ignore-revs-file "" def-file && + git -c blame.ignoreRevsFile="" blame def-file && + + rev_def_b=$(git rev-parse DEF_B) && + printf "%sQgarbage\n" "$rev_def_b" | q_to_nul >.git-blame-ignore-revs && + git add .git-blame-ignore-revs && + test_tick && + git commit -m "NUL in .git-blame-ignore-revs" && + test_must_fail git blame def-file 2>err && + test_grep "invalid object name:" err +' + +test_expect_success 'default .git-blame-ignore-revs combined with config blame.ignoreRevsFile' ' + git rev-parse DEF_B >.git-blame-ignore-revs && + test_write_lines line1-modified line2-c >def-file && + git add .git-blame-ignore-revs def-file && + test_tick && + git commit -m C && + git tag DEF_C && + git rev-parse DEF_C >custom_ignore && + test_config blame.ignoreRevsFile custom_ignore && + git blame --line-porcelain def-file >blame_raw && + sed -ne "/^[0-9a-f][0-9a-f]* [0-9][0-9]* 1/s/ .*//p" blame_raw >actual && + git rev-parse DEF_A >expect && + test_cmp expect actual && + sed -ne "/^[0-9a-f][0-9a-f]* [0-9][0-9]* 2/s/ .*//p" blame_raw >actual && + git rev-parse DEF_A >expect && + test_cmp expect actual +' + +test_expect_success 'blame works when .git-blame-ignore-revs does not exist' ' + git rm -f .git-blame-ignore-revs && + test_tick && + git commit -m "remove .git-blame-ignore-revs" && + git blame --line-porcelain def-file >blame_raw && + sed -ne "/^[0-9a-f][0-9a-f]* [0-9][0-9]* 1/s/ .*//p" blame_raw >actual && + git rev-parse DEF_B >expect && + test_cmp expect actual +' + +test_expect_success EXPENSIVE 'overly large default .git-blame-ignore-revs is ignored' ' + test_when_finished "rm -f .git-blame-ignore-revs" && + dd if=/dev/zero of=.git-blame-ignore-revs bs=1048576 count=101 2>/dev/null && + git blame --line-porcelain def-file >blame_raw 2>err && + sed -ne "/^[0-9a-f][0-9a-f]* [0-9][0-9]* 1/s/ .*//p" blame_raw >actual && + git rev-parse DEF_B >expect && + test_cmp expect actual && + test_grep "warning: ignoring overly large ignore-revs file '\''.git-blame-ignore-revs'\''" err && + + blob=$(git hash-object -w .git-blame-ignore-revs) && + rm -f .git-blame-ignore-revs && + git update-index --add --cacheinfo 100644,$blob,.git-blame-ignore-revs && + test_tick && + git commit -m "overly large .git-blame-ignore-revs" && + test_when_finished "git reset --hard HEAD~" && + git -C .git blame --line-porcelain HEAD -- def-file >blame_raw 2>err && + sed -ne "/^[0-9a-f][0-9a-f]* [0-9][0-9]* 1/s/ .*//p" blame_raw >actual && + test_cmp expect actual && + test_grep "warning: ignoring overly large ignore-revs blob '\''HEAD:.git-blame-ignore-revs'\''" err +' + test_done