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 48d5251c6df700..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; } @@ -911,36 +912,139 @@ 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); } } +#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, @@ -1020,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 c8ff0b385c58ac..8469d03b9b270f 100644 --- a/oidset.c +++ b/oidset.c @@ -70,41 +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; - - /* - * 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 cace00ae8d6ae7..92d8296c7bf1e1 100755 --- a/t/t8013-blame-ignore-revs.sh +++ b/t/t8013-blame-ignore-revs.sh @@ -327,4 +327,275 @@ 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 +' + +# 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