pack-bitmap: fix bug with exact ref match in "pack.preferBitmapTips"

The "pack.preferBitmapTips" configuration allows the user to specify
which references should be preferred when generating bitmaps. This
option is typically expected to be set to a reference prefix, like for
example "refs/heads/".

It's not unreasonable though for a user to configure one specific
reference as preferred. But if they do, they'll hit a `BUG()`:

    $ git -c pack.preferBitmapTips=refs/heads/main repack -adb
    BUG: ../refs/iterator.c:366: attempt to trim too many characters
    error: pack-objects died of signal 6

The root cause for this bug is how we enumerate these references. We
call `refs_for_each_ref_in()`, which will:

  - Yield all references that have a user-specified prefix.

  - Trim each of these references so that the prefix is removed.

Typically, this function is called with a trailing slash, like
"refs/heads/", and in that case things work alright. But if the function
is called with the name of an existing reference then we'll try to trim
the full reference name, which would leave us with an empty name. And as
this would not really leave us with anything sensible, we call `BUG()`
instead of yielding this reference.

One could argue that this is a bug in `refs_for_each_ref_in()`. But the
question then becomes what the correct behaviour would be:

  - Do we want to skip exact matches? In our case we certainly don't
    want that, as the user has asked us to generate a bitmap for it.

  - Do we want to yield the reference with the empty refname? That would
    lead to a somewhat weird result.

Neither of these feel like viable options, so calling `BUG()` feels like
a sensible way out. The root cause ultimately is that we even try to
trim the whole refname in the first place. There are two possible ways
to fix this issue:

  - We can fix the bug by using `refs_for_each_fullref_in()` instead,
    which does not strip the prefix at all. Consequently, we would now
    start to accept all references that start with the configured
    prefix, including exact matches. So if we had "refs/heads/main", we
    would both match "refs/heads/main" and "refs/heads/main-branch".

  - Or we can fix the bug by appending a slash to the prefix if it
    doesn't already have one. This would mean that we only match
    ref hierarchies that start with this prefix.

While the first fix leaves the user with strictly _more_ configuration
options, we have already fixed a similar case in 10e8a9352b (refs.c:
stop matching non-directory prefixes in exclude patterns, 2025-03-06) by
using the second option. So for the sake of consistency, let's apply the
same fix here.

Clarify the documentation accordingly.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
This commit is contained in:
Patrick Steinhardt
2026-02-19 08:57:50 +01:00
committed by Junio C Hamano
parent ed693078e9
commit 7174098834
4 changed files with 101 additions and 5 deletions

View File

@@ -160,12 +160,13 @@ pack.usePathWalk::
processes. See linkgit:git-pack-objects[1] for full details.
pack.preferBitmapTips::
Specifies a ref hierarchy (e.g., "refs/heads/"); can be
given multiple times to specify more than one hierarchies.
When selecting which commits will receive bitmaps, prefer a
commit at the tip of any reference that is a suffix of any value
of this configuration over any other commits in the "selection
window".
commit at the tip of a reference that is contained in any of
the configured hierarchies.
+
Note that setting this configuration to `refs/foo` does not mean that
Note that setting this configuration to `refs/foo/` does not mean that
the commits at the tips of `refs/foo/bar` and `refs/foo/baz` will
necessarily be selected. This is because commits are selected for
bitmaps from within a series of windows of variable length.

View File

@@ -3328,15 +3328,26 @@ void for_each_preferred_bitmap_tip(struct repository *repo,
{
struct string_list_item *item;
const struct string_list *preferred_tips;
struct strbuf buf = STRBUF_INIT;
preferred_tips = bitmap_preferred_tips(repo);
if (!preferred_tips)
return;
for_each_string_list_item(item, preferred_tips) {
const char *pattern = item->string;
if (!ends_with(pattern, "/")) {
strbuf_reset(&buf);
strbuf_addf(&buf, "%s/", pattern);
pattern = buf.buf;
}
refs_for_each_ref_in(get_main_ref_store(repo),
item->string, cb, cb_data);
pattern, cb, cb_data);
}
strbuf_release(&buf);
}
int bitmap_is_preferred_refname(struct repository *r, const char *refname)

View File

@@ -466,6 +466,47 @@ test_bitmap_cases () {
)
'
test_expect_success 'pack.preferBitmapTips interprets patterns as hierarchy' '
git init repo &&
test_when_finished "rm -fr repo" &&
(
cd repo &&
# Create enough commits that not all will receive bitmap
# coverage even if they are all at the tip of some reference.
test_commit_bulk --message="%s" 103 &&
git log --format="create refs/tags/%s/tag %H" HEAD >refs &&
git update-ref --stdin <refs &&
# Create the bitmap.
git repack -adb &&
test-tool bitmap list-commits | sort >commits-with-bitmap &&
# Verify that we have at least one commit that did not
# receive a bitmap.
git rev-list HEAD >commits.raw &&
sort <commits.raw >commits &&
comm -13 commits-with-bitmap commits >commits-wo-bitmap &&
test_file_not_empty commits-wo-bitmap &&
commit_id=$(head commits-wo-bitmap) &&
ref_without_bitmap=$(git for-each-ref --points-at="$commit_id" --format="%(refname)") &&
# When passing the full refname we do not expect a
# bitmap to be generated, as it should be interpreted
# as if a slash was appended to the pattern.
git -c pack.preferBitmapTips="$ref_without_bitmap" repack -adb &&
test-tool bitmap list-commits >after &&
test_grep ! "$commit_id" after &&
# But if we pass the parent directory of the ref we
# should see a bitmap.
ref_namespace=$(dirname "$ref_without_bitmap") &&
git -c pack.preferBitmapTips="$ref_namespace" repack -adb &&
test-tool bitmap list-commits >after &&
test_grep "$commit_id" after
)
'
test_expect_success 'complains about multiple pack bitmaps' '
rm -fr repo &&
git init repo &&

View File

@@ -1345,4 +1345,47 @@ test_expect_success 'bitmapped packs are stored via the BTMP chunk' '
)
'
test_expect_success 'pack.preferBitmapTips interprets patterns as hierarchy' '
git init repo &&
test_when_finished "rm -fr repo" &&
(
cd repo &&
# Create enough commits that not all will receive bitmap
# coverage even if they are all at the tip of some reference.
test_commit_bulk --message="%s" 103 &&
git log --format="create refs/tags/%s %H" HEAD >refs &&
git update-ref --stdin <refs &&
# Create the bitmap via the MIDX.
git repack -adb --write-midx &&
test-tool bitmap list-commits | sort >commits-with-bitmap &&
# Verify that we have at least one commit that did not
# receive a bitmap.
git rev-list HEAD >commits.raw &&
sort <commits.raw >commits &&
comm -13 commits-with-bitmap commits >commits-wo-bitmap &&
test_file_not_empty commits-wo-bitmap &&
commit_id=$(head commits-wo-bitmap) &&
ref_without_bitmap=$(git for-each-ref --points-at="$commit_id" --format="%(refname)") &&
# When passing the full refname we do not expect a bitmap to be
# generated, as it should be interpreted as if a slash was
# appended to the pattern.
rm .git/objects/pack/multi-pack-index* &&
git -c pack.preferBitmapTips="$ref_without_bitmap" repack -adb --write-midx &&
test-tool bitmap list-commits >after &&
test_grep ! "$commit_id" after &&
# But if we pass the parent directory of the ref we should see
# a bitmap.
ref_namespace=$(dirname "$ref_without_bitmap") &&
rm .git/objects/pack/multi-pack-index* &&
git -c pack.preferBitmapTips="$ref_namespace" repack -adb --write-midx &&
test-tool bitmap list-commits >after &&
test_grep "$commit_id" after
)
'
test_done