refs/files: keep root refs out of loose ref cache

With the files backend, root refs such as HEAD or ORIG_HEAD are only
added to the loose ref cache if REFS_FOR_EACH_INCLUDE_ROOT_REFS is
passed when the cache is created. If the cache has already been created
for an earlier ref iterator, root refs are silently omitted. Conversely,
once they have been added, any later iterator also yields them, whether
or not they were requested. This has been the case since d0f00c1ac1
(refs: introduce `refs_for_each_include_root_refs()`, 2024-02-23).

For example, `git for-each-ref --include-root-refs --merged=main` omits
root refs that are merged into main, because looking up the given commit
loads the replace refs, which populates the loose ref cache first. The
reftable backend is not affected, as it filters root refs for each
iterator rather than caching them.

Fix this by keeping the root refs out of the loose ref cache. Instead,
read them into a separate, temporary ref cache whenever an iterator is
requested with REFS_FOR_EACH_INCLUDE_ROOT_REFS, and overlay an iterator
over that cache on the loose ref iterator. The cache is freed along with
the iterator.

Alternatively, the root refs could always be added to the loose ref
cache and skipped during iteration unless requested. But that would mean
reading them in every command that uses the loose ref cache, even though
only two call sites actually request them. Also, keeping the root ref
cache for reuse by later iterators is not worthwhile, because those two
call sites only request the root refs once per command.

Signed-off-by: Andy Koppe <andy.koppe@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
This commit is contained in:
Andy Koppe authored and Junio C Hamano committed 2026-10-19 16:06:08 -07:00
1 parent a018953688
commit 7f4e8cd956
2 files changed
+41 -8

No files matched your search

+29 -8
View File
@@ -481,8 +481,7 @@ static void add_root_refs(struct files_ref_store *refs,
for_each_root_ref(refs, fill_root_ref, &data);
}
static struct ref_cache *get_loose_ref_cache(struct files_ref_store *refs,
unsigned int flags)
static struct ref_cache *get_loose_ref_cache(struct files_ref_store *refs)
{
if (!refs->loose) {
struct ref_dir *dir;
@@ -499,9 +498,6 @@ static struct ref_cache *get_loose_ref_cache(struct files_ref_store *refs,
dir = get_ref_dir(refs->loose->root);
if (flags & REFS_FOR_EACH_INCLUDE_ROOT_REFS)
add_root_refs(refs, dir);
/*
* Add an incomplete entry for "refs/" (to be filled
* lazily):
@@ -511,6 +507,19 @@ static struct ref_cache *get_loose_ref_cache(struct files_ref_store *refs,
return refs->loose;
}
/*
* Root refs are kept out of the loose ref cache because ref iterators must
* only include them when requested with REFS_FOR_EACH_INCLUDE_ROOT_REFS.
*/
static struct ref_cache *create_root_ref_cache(struct files_ref_store *refs)
{
struct ref_cache *cache = create_ref_cache(&refs->base, NULL);
cache->root->flag &= ~REF_INCOMPLETE;
add_root_refs(refs, get_ref_dir(cache->root));
return cache;
}
static int read_ref_internal(struct ref_store *ref_store, const char *refname,
struct object_id *oid, struct strbuf *referent,
unsigned int *type, int *failure_errno, int skip_packed_refs)
@@ -1004,6 +1013,7 @@ struct files_ref_iterator {
struct ref_iterator base;
struct ref_iterator *iter0;
struct ref_cache *root_refs;
struct repository *repo;
unsigned int flags;
};
@@ -1053,6 +1063,7 @@ static void files_ref_iterator_release(struct ref_iterator *ref_iterator)
struct files_ref_iterator *iter =
(struct files_ref_iterator *)ref_iterator;
ref_iterator_free(iter->iter0);
free_ref_cache(iter->root_refs);
}
static struct ref_iterator_vtable files_ref_iterator_vtable = {
@@ -1068,6 +1079,7 @@ static struct ref_iterator *files_ref_iterator_begin(
{
struct files_ref_store *refs;
struct ref_iterator *loose_iter, *packed_iter, *overlay_iter;
struct ref_cache *root_refs = NULL;
struct files_ref_iterator *iter;
struct ref_iterator *ref_iterator;
unsigned int required_flags = REF_STORE_READ;
@@ -1094,9 +1106,17 @@ static struct ref_iterator *files_ref_iterator_begin(
* disk, and re-reads it if not.
*/
loose_iter = cache_ref_iterator_begin(get_loose_ref_cache(refs, flags),
loose_iter = cache_ref_iterator_begin(get_loose_ref_cache(refs),
prefix, ref_store->repo, 1);
if (flags & REFS_FOR_EACH_INCLUDE_ROOT_REFS) {
root_refs = create_root_ref_cache(refs);
loose_iter = overlay_ref_iterator_begin(
cache_ref_iterator_begin(root_refs, prefix,
ref_store->repo, 0),
loose_iter);
}
/*
* The packed-refs file might contain broken references, for
* example an old version of a reference that points at an
@@ -1118,6 +1138,7 @@ static struct ref_iterator *files_ref_iterator_begin(
ref_iterator = &iter->base;
base_ref_iterator_init(ref_iterator, &files_ref_iterator_vtable);
iter->iter0 = overlay_iter;
iter->root_refs = root_refs;
iter->repo = ref_store->repo;
iter->flags = flags;
@@ -1490,7 +1511,7 @@ static int should_pack_refs(struct files_ref_store *refs,
if (limit < 16)
limit = 16;
iter = cache_ref_iterator_begin(get_loose_ref_cache(refs, 0), NULL,
iter = cache_ref_iterator_begin(get_loose_ref_cache(refs), NULL,
refs->base.repo, 0);
while ((ret = ref_iterator_advance(iter)) == ITER_OK) {
if (should_pack_ref(refs, &iter->ref, opts))
@@ -1530,7 +1551,7 @@ static int files_optimize(struct ref_store *ref_store,
packed_refs_lock(refs->packed_ref_store, LOCK_DIE_ON_ERROR, &err);
iter = cache_ref_iterator_begin(get_loose_ref_cache(refs, 0), NULL,
iter = cache_ref_iterator_begin(get_loose_ref_cache(refs), NULL,
refs->base.repo, 0);
while ((ok = ref_iterator_advance(iter)) == ITER_OK) {
/*
+12
View File
@@ -96,6 +96,18 @@ test_expect_success '--include-root-refs omits dangling symrefs' '
)
'
test_expect_success '--include-root-refs with --merged' '
cat >expect <<-\EOF &&
HEAD
ORIG_HEAD
EOF
test_when_finished "git update-ref -d ORIG_HEAD" &&
git update-ref ORIG_HEAD main &&
git for-each-ref --format="%(refname)" --include-root-refs \
--merged=main "*HEAD" >actual &&
test_cmp expect actual
'
test_expect_success 'filtering with --points-at' '
cat >expect <<-\EOF &&
refs/heads/main