Skip to content

blame: ignore revs in HEAD:.git-blame-ignore-revs by default - #2224

Open
rmistry wants to merge 2 commits into
gitgitgadget:masterfrom
rmistry:blame-default-ignore-revs
Open

rmistry wants to merge 2 commits into
gitgitgadget:masterfrom
rmistry:blame-default-ignore-revs

Conversation

@rmistry

@rmistry rmistry commented Sep 11, 2026 •

Copy link
Copy Markdown

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 #1809
(https://lore.kernel.org/git/pull.1809.v2.git.1728707867.gitgitgadget@gmail.com/)
and addresses #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.

CC: Junio C Hamano gitster@pobox.com
CC: Abhijeetsingh Meena abhijeet040403@gmail.com
CC: Kristoffer Haugsbakk code@khaugsbakk.name
CC: Phillip Wood phillip.wood@dunelm.org.uk
CC: Eric Sunshine sunshine@sunshineco.com

@gitgitgadget

gitgitgadget Bot commented Sep 11, 2026

Copy link
Copy Markdown

Welcome to GitGitGadget

Hi @rmistry, and welcome to GitGitGadget, the GitHub App to send patch series to the Git mailing list from GitHub Pull Requests.

Please make sure that either:

  • Your Pull Request has a good description, if it consists of multiple commits, as it will be used as cover letter.
  • Your Pull Request description is empty, if it consists of a single commit, as the commit message should be descriptive enough by itself.

You can CC potential reviewers by adding a footer to the PR description with the following syntax:

CC: Revi Ewer <revi.ewer@example.com>, Ill Takalook <ill.takalook@example.net>

NOTE: DO NOT copy/paste your CC list from a previous GGG PR's description,
because it will result in a malformed CC list on the mailing list. See
example.

Also, it is a good idea to review the commit messages one last time, as the Git project expects them in a quite specific form:

  • the lines should not exceed 76 columns,
  • the first line should be like a header and typically start with a prefix like "tests:" or "revisions:" to state which subsystem the change is about, and
  • the commit messages' body should be describing the "why?" of the change.
  • Finally, the commit messages should end in a Signed-off-by: line matching the commits' author.

It is in general a good idea to await the automated test ("Checks") in this Pull Request before contributing the patches, e.g. to avoid trivial issues such as unportable code.

Contributing the patches

Before you can contribute the patches, your GitHub username needs to be added to the list of permitted users. Any already-permitted user can do that, by adding a comment to your PR of the form /allow. A good way to find other contributors is to locate recent pull requests where someone has been /allowed:

Both the person who commented /allow and the PR author are able to /allow you.

An alternative is the channel #git-devel on the Libera Chat IRC network:

<newcontributor> I've just created my first PR, could someone please /allow me? https://github.com/gitgitgadget/git/pull/12345
<veteran> newcontributor: it is done
<newcontributor> thanks!

Once on the list of permitted usernames, you can contribute the patches to the Git mailing list by adding a PR comment /submit.

If you want to see what email(s) would be sent for a /submit request, add a PR comment /preview to have the email(s) sent to you. You must have a public GitHub email address for this. Note that any reviewers CC'd via the list in the PR description will not actually be sent emails.

After you submit, GitGitGadget will respond with another comment that contains the link to the cover letter mail in the Git mailing list archive. Please make sure to monitor the discussion in that thread and to address comments and suggestions (while the comments and suggestions will be mirrored into the PR by GitGitGadget, you will still want to reply via mail).

If you do not want to subscribe to the Git mailing list just to be able to respond to a mail, you can download the mbox from the Git mailing list archive (click the (raw) link), then import it into your mail program. If you use GMail, you can do this via:

curl -g --user "<EMailAddress>:<Password>" \
    --url "imaps://imap.gmail.com/INBOX" -T /path/to/raw.txt

To iterate on your change, i.e. send a revised patch or patch series, you will first want to (force-)push to the same branch. You probably also want to modify your Pull Request description (or title). It is a good idea to summarize the revision by adding something like this to the cover letter (read: by editing the first comment on the PR, i.e. the PR description):

Changes since v1:
- Fixed a typo in the commit message (found by ...)
- Added a code comment to ... as suggested by ...
...

To send a new iteration, just add another PR comment with the contents: /submit.

Need help?

New contributors who want advice are encouraged to join git-mentoring@googlegroups.com, where volunteers who regularly contribute to Git are willing to answer newbie questions, give advice, or otherwise provide mentoring to interested contributors. You must join in order to post or view messages, but anyone can join.

You may also be able to find help in real time in the developer IRC channel, #git-devel on Libera Chat. Remember that IRC does not support offline messaging, so if you send someone a private message and log out, they cannot respond to you. The scrollback of #git-devel is archived, though.

@dscho

dscho commented Sep 11, 2026

Copy link
Copy Markdown
Member

/allow

@gitgitgadget

gitgitgadget Bot commented Sep 11, 2026

Copy link
Copy Markdown

User rmistry is now allowed to use GitGitGadget.

@rmistry

rmistry commented Sep 11, 2026

Copy link
Copy Markdown
Author

Thank you @dscho for allowing this pull request.

/submit

@rmistry

rmistry commented Sep 11, 2026

Copy link
Copy Markdown
Author

/submit

@gitgitgadget

gitgitgadget Bot commented Sep 11, 2026

Copy link
Copy Markdown

Submitted as pull.2224.git.1789169384240.gitgitgadget@gmail.com

To fetch this version into FETCH_HEAD:

git fetch https://github.com/gitgitgadget/git/ pr-2224/rmistry/blame-default-ignore-revs-v1

To fetch this version to local tag pr-2224/rmistry/blame-default-ignore-revs-v1:

git fetch --no-tags https://github.com/gitgitgadget/git/ tag pr-2224/rmistry/blame-default-ignore-revs-v1

@Ethan0456

Copy link
Copy Markdown

Thanks for picking it up @rmistry. I started working on it but got carried away in another direction, looking into detecting and considering .git-blame-ignore-revs from each project subdirectory, how they take precedence over each other, and how to override them (see #1809).

This implementation looks great! It seems to auto-detect the ignore revs file at the root only. I just glanced through the changes, though, and haven’t tested it yet.

@rmistry

rmistry commented Sep 12, 2026

Copy link
Copy Markdown
Author

Thank you @Ethan0456 for the kind words and for kicking off the initial work!

Keeping the scope focused strictly on the repository root helps keep things clean and aligned with how hosting platforms (GitHub, GitLab) and major repositories (Chromium, LLVM, Android) use the blame file.

@gitgitgadget

gitgitgadget Bot commented Sep 30, 2026

Copy link
Copy Markdown

Ravi Mistry wrote on the Git mailing list (how to reply to this email):

Hi all,

Gentle ping on this patch. Please let me know if you have any feedback or questions on this approach, or if there are other reviewers I should loop in.

TIA!

@gitgitgadget

gitgitgadget Bot commented Oct 6, 2026

Copy link
Copy Markdown

Junio C Hamano wrote on the Git mailing list (how to reply to this email):

"Ravi Mistry via GitGitGadget" <gitgitgadget@gmail.com> writes:

>  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 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`.

The proposed log message explains that '.git-blame-ignore-revs' is
used as the default for blame.ignoreRevsFile even when the user does
not ask to do so in order to match what hosting sites do, as it
would be confusing if the local repository behaved differently.
While wanting consistency is reasonable, the description above does
not quite match that goal.  If an untracked '.git-blame-ignore-revs'
file exists at the root of the working tree, or if a tracked one has
local changes relative to HEAD, the local repository behaves
differently from hosting sites that operate on the
'HEAD:.git-blame-ignore-revs' blob.  It may make more sense to say:
"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 rule should apply whether the repository is bare or not.

The proposed log message also talks about taking only a regular file
and ignoring everything else for "security" [*], but there is
another important thing we need to worry about security-wise.
Somebody has to audit the parser for these files (one unabbreviated
object name per line, ignoring whitespace and lines starting with
'#') and ensure that the implementation is truly secure.

This is a new threat vector introduced by this change.  Without this
patch, blame.ignoreRevsFile comes only from the configuration, which
cannot point to an attacker-controlled file under our threat model.
Now, however, the parser must read upstream-controlled content at a
known path, and it must be prepared to cope with attempts to use it
as an attack vector.

[Footnote]

 * By the way, "we do not read anything from the working tree.
   'HEAD:.git-blame-ignore-revs' is the only thing that is added
   to the picture" would make it unnecessary to lstat() and ignore
   non-regular files.

@gitgitgadget

gitgitgadget Bot commented Oct 6, 2026

Copy link
Copy Markdown

Ravi Mistry wrote on the Git mailing list (how to reply to this email):

"Junio C Hamano" <gitster@pobox.com> writes:

> While wanting consistency is reasonable, the description above does
> not quite match that goal.  If an untracked '.git-blame-ignore-revs'
> file exists at the root of the working tree, or if a tracked one has
> local changes relative to HEAD, the local repository behaves
> differently from hosting sites that operate on the
> 'HEAD:.git-blame-ignore-revs' blob.  It may make more sense to say:
> "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 rule should apply whether the repository is bare or not.

Thank you very much for the detailed feedback, Junio! Reading the
committed blob from HEAD instead of the working tree totally makes
sense.

> Somebody has to audit the parser for these files (one unabbreviated
> object name per line, ignoring whitespace and lines starting with
> '#') and ensure that the implementation is truly secure.

I looked through the parser in oidset.c (which we can share for
both the HEAD blob and configured files) and peel_to_commit_oid in
builtin/blame.c. Mostly looks good, IMHO, but there may be two edge
cases we can tighten up:

1. Rejecting lines with embedded NUL bytes via memchr in oidset.c
   (where strchr and the check after parse_oid_hex_algop currently
   stop at the first NUL byte and ignore trailing bytes on the
   line).

2. Passing OBJECT_INFO_SKIP_FETCH_OBJECT and OBJECT_INFO_QUICK in
   peel_to_commit_oid and peeling tags step by step so missing OIDs
   or tag targets do not trigger lazy promisor fetches in partial
   clones.

Does this plan sound good to you for v2?

Thanks,
Ravi

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 the
HEAD:.git-blame-ignore-revs blob 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 blob:

- 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 "<oid>\0garbage" or "<oid>\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 <rmistry@google.com>
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>
@rmistry
rmistry force-pushed the blame-default-ignore-revs branch from 22a100d to 35e303d Compare October 7, 2026 21:16
@gitgitgadget

gitgitgadget Bot commented Oct 7, 2026

Copy link
Copy Markdown

Junio C Hamano wrote on the Git mailing list (how to reply to this email):

Ravi Mistry <rmistry@google.com> writes:

> "Junio C Hamano" <gitster@pobox.com> writes:
>
>> While wanting consistency is reasonable, the description above does
>> not quite match that goal.  If an untracked '.git-blame-ignore-revs'
>> file exists at the root of the working tree, or if a tracked one has
>> local changes relative to HEAD, the local repository behaves
>> differently from hosting sites that operate on the
>> 'HEAD:.git-blame-ignore-revs' blob.  It may make more sense to say:
>> "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 rule should apply whether the repository is bare or not.
>
> Thank you very much for the detailed feedback, Junio! Reading the
> committed blob from HEAD instead of the working tree totally makes
> sense.
>
>> Somebody has to audit the parser for these files (one unabbreviated
>> object name per line, ignoring whitespace and lines starting with
>> '#') and ensure that the implementation is truly secure.
>
> I looked through the parser in oidset.c (which we can share for
> both the HEAD blob and configured files) and peel_to_commit_oid in
> builtin/blame.c. Mostly looks good, IMHO, but there may be two edge
> cases we can tighten up:
>
> 1. Rejecting lines with embedded NUL bytes via memchr in oidset.c
>    (where strchr and the check after parse_oid_hex_algop currently
>    stop at the first NUL byte and ignore trailing bytes on the
>    line).
>
> 2. Passing OBJECT_INFO_SKIP_FETCH_OBJECT and OBJECT_INFO_QUICK in
>    peel_to_commit_oid and peeling tags step by step so missing OIDs
>    or tag targets do not trigger lazy promisor fetches in partial
>    clones.
>
> Does this plan sound good to you for v2?

Are you presenting a different plan, or just adding details to what
you quoted from my message above?

I delegated because I did not want to spend time on the auditing
part, so if you are asking me that these two are the only things we
need to address, that defeats the point of me delegating it to
"somebody else" X-<.  Hopefully a v2 with some tightening the OID
parsing may entice folks (who are hopefully interested in security
related work) to chime in and they would help us decide if it is
good enough to cover these two points and nothing else.

Thanks.

@gitgitgadget

gitgitgadget Bot commented Oct 7, 2026

Copy link
Copy Markdown

Ravi Mistry wrote on the Git mailing list (how to reply to this email):

"Junio C Hamano" <gitster@pobox.com> writes:

> Are you presenting a different plan, or just adding details to what
> you quoted from my message above?
>
> I delegated because I did not want to spend time on the auditing
> part, so if you are asking me that these two are the only things we
> need to address, that defeats the point of me delegating it to
> "somebody else" X-<.  Hopefully a v2 with some tightening the OID
> parsing may entice folks (who are hopefully interested in security
> related work) to chime in and they would help us decide if it is
> good enough to cover these two points and nothing else.

Ah, sorry for my confusion, I was adding details to your plan
above (I thought the audit was being delegated to me so wanted to
make sure the approach was correct). Will put up a v2 for the
security reviewers to chime in.

Thanks again!
Ravi

@rmistry rmistry changed the title blame: default to ignoring revisions in .git-blame-ignore-revs blame: ignore revs in HEAD:.git-blame-ignore-revs by default Oct 8, 2026
@rmistry

rmistry commented Oct 8, 2026

Copy link
Copy Markdown
Author

/preview

@gitgitgadget

gitgitgadget Bot commented Oct 8, 2026

Copy link
Copy Markdown

Preview email sent as pull.2224.v2.git.1791491999.gitgitgadget@gmail.com

@rmistry

rmistry commented Oct 8, 2026

Copy link
Copy Markdown
Author

/submit

@gitgitgadget

gitgitgadget Bot commented Oct 8, 2026

Copy link
Copy Markdown

Submitted as pull.2224.v2.git.1791493644.gitgitgadget@gmail.com

To fetch this version into FETCH_HEAD:

git fetch https://github.com/gitgitgadget/git/ pr-2224/rmistry/blame-default-ignore-revs-v2

To fetch this version to local tag pr-2224/rmistry/blame-default-ignore-revs-v2:

git fetch --no-tags https://github.com/gitgitgadget/git/ tag pr-2224/rmistry/blame-default-ignore-revs-v2

Comment thread 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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Junio C Hamano wrote on the Git mailing list (how to reply to this email):

"Ravi Mistry via GitGitGadget" <gitgitgadget@gmail.com> writes:

> - 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 "<oid>\0garbage" or "<oid>\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.

Maybe I am slow, but I do not immediately see why ignoring
everything after the first NUL is a problem.  A call to
strbuf_trim() is ineffective at trimming whitespace that appears
immediately before such a NUL.  For example, while

    cf9bdb1...f6092d # comment LF

would feed the leading 'cf9bdb1...f6092d' part (after stripping
whitespace before '#') to parse_oid_hex_algop(), this

    cf9bdb1...f6092d NUL comment LF

would keep the whitespace after '92d' and cause the parsing to
fail.  I do not see any security implications here.

On the other hand ...

> - 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.

... this may be a very reasonable thing to do, I would think.  In a
shallow clone, if we are not auto-deepening the shallow boundary
during a "git blame" session, we have no reason to lazy fetch
entries in the ignore file that are older than the shallow boundary.

> diff --git a/oidset.c b/oidset.c
> index c8ff0b385c..90d39204d3 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);

A file with such an entry is rejected and the entire operation is
aborted as suspected attack attempt, which feels like striking the
balance between usability and security at a wrong place.

But a line with broken object name already is rejected with "die()"
with the existing code, so it may be OK.

> diff --git a/t/t8013-blame-ignore-revs.sh b/t/t8013-blame-ignore-revs.sh
> index cace00ae8d..70fe509a64 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 <t@example.com> 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

@@ -132,9 +132,11 @@ take effect.
`--ignore-revs-file <file>`::

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Junio C Hamano wrote on the Git mailing list (how to reply to this email):

"Ravi Mistry via GitGitGadget" <gitgitgadget@gmail.com> writes:

> From: Ravi Mistry <rmistry@google.com>
>
> 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 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 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>

These Helped-by: drew my attention as none of these folks commented
on v1 of this series.  I do see they have helped the original series
<pull.1809.v2.git.1728707867.gitgitgadget@gmail.com>, but it is not
clear how much their inputs have survivied to this version.

They are all CC'ed so they can give their Acked-by: or Reviewed-by: 
on this round if they want.


[...]

> diff --git a/builtin/blame.c b/builtin/blame.c
> index 6741a7b9df..a730ee87ea 100644
> --- a/builtin/blame.c
> +++ b/builtin/blame.c
> @@ -769,7 +769,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;
>  	}

Good, and the log message is clear why we make this change.

> @@ -946,17 +946,52 @@ 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);
> +}

OK.

>  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;
> +	}

OK, we make two passes, and during the first pass, we find where the
last "empty" entry that signals "forget everything you have seen" is.

>  	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++) {

And we scan starting from there.  Very clean.

> +		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,
> @@ -1036,6 +1071,8 @@ int cmd_blame(int argc,
>  	const char *const *opt_usage = cmd_is_annotate ? annotate_opt_usage : blame_opt_usage;
>  
>  	setup_default_color_by_age();
> +	string_list_append(&ignore_revs_file_list,
> +			   "HEAD:.git-blame-ignore-revs")->util = &sb;

Cute.  This takes advantage of the fact that everybody else just
appends to the string_list without populating the .util member.

> diff --git a/oidset.c b/oidset.c
> index 90d39204d3..8469d03b9b 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);
>  }

Shouldn't the above refactoring have been part of the previous step
instead?

Thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants