From: Ravi Mistry via GitGitGadget Date: Thu, 08 Oct 2026 21:07:22 GMT Subject: [PATCH v2 0/2] blame: ignore revs in HEAD:.git-blame-ignore-revs by default Message-ID: In-Reply-To: This series teaches git-blame(1) and git-annotate(1) to automatically use the HEAD:.git-blame-ignore-revs blob by default if it exists, so local runs match hosting platforms without requiring manual blame.ignoreRevsFile configuration in every clone. This restarts the stalled attempt in PR https://github.com/gitgitgadget/git/pull/1809 (https://lore.kernel.org/git/pull.1809.v2.git.1728707867.gitgitgadget@gmail.com/) and addresses https://github.com/gitgitgadget/git/issues/1494. Changes since v1: * Split the series into two commits. * Patch 1/2 hardens oidset_parse_file_carefully() in oidset.c and peel_to_commit_oid() in builtin/blame.c before exposing them to upstream-controlled content at a well-known path: * Reject lines containing embedded NUL bytes via memchr() so trailing bytes after a NUL cannot be silently ignored. * Pass OBJECT_INFO_LOOKUP_REPLACE | OBJECT_INFO_SKIP_FETCH_OBJECT | OBJECT_INFO_QUICK to odb_read_object_info_extended() and peel tags one layer per iteration so missing OIDs or tag targets do not trigger lazy promisor fetches or pack directory rescans in partial clones, and verify that each peeled target matches the tag's declared type. * Patch 2/2 reads the committed HEAD:.git-blame-ignore-revs blob (in both bare and non-bare repositories) instead of reading a file from the working tree, matching hosting platforms even when an untracked file is present or a tracked one has local modifications. The tree entry is checked with S_ISREG() so non-regular entries (such as committed symlinks) are skipped, parsed in memory via oidset_parse_buffer_carefully(), and bypassed without reading if cleared via blame.ignoreRevsFile="" or --no-ignore-revs-file. Ravi Mistry (2): blame: harden ignore-revs parser and tag peeling blame: ignore revs in HEAD:.git-blame-ignore-revs Documentation/blame-options.adoc | 8 +- Documentation/config/blame.adoc | 9 +- builtin/blame.c | 65 ++++++++- oidset.c | 81 ++++++++---- oidset.h | 9 ++ t/t8013-blame-ignore-revs.sh | 220 +++++++++++++++++++++++++++++++ 6 files changed, 357 insertions(+), 35 deletions(-) base-commit: fa7f9290efe2bd22dd736689597b474b93798e11 Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2224%2Frmistry%2Fblame-default-ignore-revs-v2 Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2224/rmistry/blame-default-ignore-revs-v2 Pull-Request: https://github.com/gitgitgadget/git/pull/2224 Range-diff vs v1: -: ---------- > 1: 2e12486c0d blame: harden ignore-revs parser and tag peeling 1: 22a100d00d ! 2: 35e303d65b blame: default to ignoring revisions in .git-blame-ignore-revs @@ Metadata Author: Ravi Mistry ## Commit message ## - blame: default to ignoring revisions in .git-blame-ignore-revs + 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. @@ Commit message 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. + 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, 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. + 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 file lookup, - subdirectory invocations, CLI and config overrides, symlink rejection, - comments and whitespace handling, and bare repositories. + 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 Helped-by: Kristoffer Haugsbakk @@ Commit message ## Documentation/blame-options.adoc ## @@ Documentation/blame-options.adoc: 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 +- 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. -+ option or the default `.git-blame-ignore-revs` file. An empty file name, -+ `""`, will clear the list of revs from previously processed files. -+ `--no-ignore-revs-file` will clear all previously specified ignore revs -+ files, including the default `.git-blame-ignore-revs` file. ++ processed after the default `HEAD:.git-blame-ignore-revs` blob (if it ++ exists) and any files specified with the `blame.ignoreRevsFile` config ++ option. An empty file name, `""`, or `--no-ignore-revs-file` will clear ++ the list of revs from previously processed files, including the default ++ `HEAD:.git-blame-ignore-revs` blob. `--color-lines`:: Color line annotations in the default format differently if they come from @@ Documentation/config/blame.adoc: blame.showRoot:: - `#` 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 in a non-bare repository, it is used by default. This option -+ may be repeated multiple times; files specified here are processed after -+ the default file. An empty file name will reset the list of ignored -+ revisions from previously processed files and disable the default file. -+ This option is handled before the command-line option `--ignore-revs-file`. ++ `#` are ignored. If the `HEAD:.git-blame-ignore-revs` blob exists, 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 ## builtin/blame.c ## -@@ - #include "hex.h" - #include "commit.h" - #include "diff.h" -+#include "path.h" - #include "revision.h" - #include "quote.h" - #include "string-list.h" -+#include "strmap.h" - #include "mailmap.h" - #include "parse-options.h" - #include "prio-queue.h" @@ builtin/blame.c: static int git_blame_config(const char *var, const char *value, - ret = git_config_pathname(&str, var, value); if (ret) return ret; -- if (str) + if (str) - string_list_insert(&ignore_revs_file_list, str); -+ if (str) { -+ if (!*str) -+ string_list_clear(&ignore_revs_file_list, 0); -+ else -+ string_list_append(&ignore_revs_file_list, str); -+ } ++ string_list_append(&ignore_revs_file_list, str); free(str); return 0; } -@@ builtin/blame.c: static void build_ignorelist(struct blame_scoreboard *sb, +@@ builtin/blame.c: static int peel_to_commit_oid(struct object_id *oid_ret, void *cbdata) + } + } + ++static void parse_default_ignore_revs_blob(struct blame_scoreboard *sb, ++ const char *name) ++{ ++ struct object_context oc; ++ struct object_id oid; ++ enum object_type type; ++ size_t size; ++ char *buf; ++ ++ if (get_oid_with_context(the_repository, name, GET_OID_QUIETLY, ++ &oid, &oc)) ++ goto out; ++ if (!S_ISREG(oc.mode)) ++ goto out; ++ ++ buf = odb_read_object(the_repository->objects, &oid, &type, &size); ++ if (!buf) ++ goto out; ++ if (type == OBJ_BLOB) ++ oidset_parse_buffer_carefully(&sb->ignore_list, buf, size, ++ the_repository->hash_algo, ++ peel_to_commit_oid, sb); ++ free(buf); ++ ++out: ++ object_context_release(&oc); ++} ++ + 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; -+ struct strset seen_files = STRSET_INIT; + size_t start_idx = 0, idx; + + for (idx = 0; idx < ignore_revs_file_list->nr; idx++) { @@ builtin/blame.c: static void build_ignorelist(struct blame_scoreboard *sb, - for_each_string_list_item(i, ignore_revs_file_list) { - if (!strcmp(i->string, "")) - oidset_clear(&sb->ignore_list); -- else -- oidset_parse_file_carefully(&sb->ignore_list, i->string, + for (idx = start_idx; idx < ignore_revs_file_list->nr; idx++) { -+ const char *path = ignore_revs_file_list->items[idx].string; -+ -+ if (strset_add(&seen_files, path)) -+ oidset_parse_file_carefully(&sb->ignore_list, path, ++ i = &ignore_revs_file_list->items[idx]; ++ if (i->util) ++ parse_default_ignore_revs_blob(sb, i->string); + else + oidset_parse_file_carefully(&sb->ignore_list, i->string, the_repository->hash_algo, - peel_to_commit_oid, sb); - } -+ strset_clear(&seen_files); - for_each_string_list_item(i, ignore_rev_list) { - if (repo_get_oid_committish(the_repository, i->string, &oid) || - peel_to_commit_oid(&oid, sb)) @@ builtin/blame.c: int cmd_blame(int argc, const char *const *opt_usage = cmd_is_annotate ? annotate_opt_usage : blame_opt_usage; setup_default_color_by_age(); -+ { -+ const char *work_tree = repo_get_work_tree(the_repository); -+ -+ if (work_tree) { -+ char *default_file = mkpathdup("%s/%s", work_tree, -+ ".git-blame-ignore-revs"); -+ struct stat st; -+ -+ if (!lstat(default_file, &st) && S_ISREG(st.st_mode) && -+ !access(default_file, R_OK)) -+ string_list_append(&ignore_revs_file_list, default_file); -+ free(default_file); -+ } -+ } ++ string_list_append(&ignore_revs_file_list, ++ "HEAD:.git-blame-ignore-revs")->util = &sb; repo_config(the_repository, git_blame_config, &output_option); repo_init_revisions(the_repository, &revs, NULL); revs.date_mode = blame_date_mode; + ## oidset.c ## +@@ oidset.c: 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); ++} + + ## oidset.h ## +@@ oidset.h: 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; + ## t/t8013-blame-ignore-revs.sh ## -@@ t/t8013-blame-ignore-revs.sh: test_expect_success ignore_merge ' +@@ t/t8013-blame-ignore-revs.sh: 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 file -+test_expect_success 'setup default .git-blame-ignore-revs' ' ++# Tests for default HEAD:.git-blame-ignore-revs blob ++test_expect_success 'setup default HEAD:.git-blame-ignore-revs' ' + git checkout -b default-file-branch && + test_write_lines line1 line2 >def-file && + git add def-file && @@ t/t8013-blame-ignore-revs.sh: test_expect_success ignore_merge ' + git commit -m "default mod" && + git tag DEF_B && + -+ git rev-parse DEF_B >.git-blame-ignore-revs ++ git rev-parse DEF_B >.git-blame-ignore-revs && ++ 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' ' ++test_expect_success 'default HEAD:.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 && @@ t/t8013-blame-ignore-revs.sh: test_expect_success ignore_merge ' + test_cmp expect actual +' + -+test_expect_success 'default .git-blame-ignore-revs respected by git annotate' ' ++test_expect_success 'default HEAD:.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_expect_success 'default HEAD:.git-blame-ignore-revs works from subdirectory' ' + mkdir -p sub && + ( + cd sub && @@ t/t8013-blame-ignore-revs.sh: test_expect_success ignore_merge ' + ) +' + -+test_expect_success 'disable default .git-blame-ignore-revs with --no-ignore-revs-file' ' ++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 ++' ++ ++test_expect_success 'uncommitted .git-blame-ignore-revs changes in working tree are ignored' ' ++ test_when_finished "git checkout -- .git-blame-ignore-revs" && ++ echo "invalid-oid-value" >.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 'disable default HEAD:.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 && @@ t/t8013-blame-ignore-revs.sh: test_expect_success ignore_merge ' + test_cmp expect actual +' + -+test_expect_success 'disable default .git-blame-ignore-revs with --ignore-revs-file ""' ' ++test_expect_success 'disable default HEAD:.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 && @@ t/t8013-blame-ignore-revs.sh: test_expect_success ignore_merge ' + test_cmp expect actual +' + -+test_expect_success 'disable default .git-blame-ignore-revs with blame.ignoreRevsFile=""' ' ++test_expect_success 'disable default HEAD:.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 && @@ t/t8013-blame-ignore-revs.sh: test_expect_success ignore_merge ' + test_cmp expect actual +' + -+test_expect_success 'default .git-blame-ignore-revs handles comments and whitespace' ' -+ test_when_finished "git rev-parse DEF_B >.git-blame-ignore-revs" && ++test_expect_success 'default HEAD:.git-blame-ignore-revs handles comments and whitespace' ' ++ rev_def_b=$(git rev-parse DEF_B) && + { + echo "# Leading comment" && + echo "" && -+ echo " $(git rev-parse DEF_B) " && ++ 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' ' -+ test_when_finished "git rev-parse DEF_B >.git-blame-ignore-revs" && ++test_expect_success 'empty default HEAD:.git-blame-ignore-revs is harmless' ' + : >.git-blame-ignore-revs && -+ git blame def-file ++ 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 is ignored' ' -+ test_when_finished "rm -f target_file .git-blame-ignore-revs && git rev-parse DEF_B >.git-blame-ignore-revs" && ++test_expect_success 'committed symlink .git-blame-ignore-revs in HEAD is ignored' ' ++ git rm -f .git-blame-ignore-revs && + git rev-parse DEF_B >target_file && -+ ln -sf target_file .git-blame-ignore-revs && ++ test_ln_s_add target_file .git-blame-ignore-revs && ++ test_tick && ++ git commit -m "symlink .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 'malformed default .git-blame-ignore-revs fails but can be bypassed' ' -+ test_when_finished "git rev-parse DEF_B >.git-blame-ignore-revs" && ++test_expect_success 'malformed default HEAD:.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 blame --ignore-revs-file "" def-file && ++ git -c blame.ignoreRevsFile="" blame def-file && + -+test_expect_success 'default .git-blame-ignore-revs deduplicated when also set in config' ' -+ test_config blame.ignoreRevsFile .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 ++ 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' ' ++test_expect_success 'default HEAD:.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 def-file && ++ git add .git-blame-ignore-revs def-file && + test_tick && + git commit -m C && + git tag DEF_C && @@ t/t8013-blame-ignore-revs.sh: test_expect_success ignore_merge ' + test_cmp expect actual +' + -+test_expect_success 'default .git-blame-ignore-revs ignored in bare repo' ' -+ 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]* 2/s/ .*//p" blame_raw >actual && -+ git rev-parse DEF_C >expect && -+ test_cmp expect actual -+' -+ -+test_expect_success 'blame works when .git-blame-ignore-revs does not exist' ' -+ rm -f .git-blame-ignore-revs && ++test_expect_success 'blame works when HEAD:.git-blame-ignore-revs does not exist and ignores untracked file' ' ++ git rm -f .git-blame-ignore-revs && ++ test_tick && ++ git commit -m "remove .git-blame-ignore-revs" && ++ 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_B >expect && -- gitgitgadget