Re: [BUG] Lazy fetch in partial clone fails with "not our ref" since 5946a564 (odb/source: make read_object_info() pluggable)

From
Kristofer Karlsson <hidden>
Date
2026-11-04 10:11:37
Reply to
Patrick Ohly, 2026-11-03
On Tue, 3 Nov 2026 at 14:31, Patrick Ohly [off-list ref] wrote:
Hello,

"git merge" in a blobless partial clone fails with "not our ref" while
lazily fetching a missing object. This started to occur today in the
Kubernetes CI, completely breaking the merging of PRs. We should have
been using git 2.54.0, the affected version, for a few days at least
without issues, so it can't be ruled out that this is at least partly
caused by a change on the GitHub side. We are about to revert to git
2.52.0 to unblock the Kubernetes project.

[...]

I bisected v2.52.0 (good) to v2.54.0 (bad). The first bad commit is:

5946a564cddc0bf471f27ae4c3fe205441e3ef65
odb/source: make `read_object_info()` function pluggable

The parent, 3bc3177ad7 ( 2.53.0.185.g3bc3177ad7 ), is good.
Thanks for the detailed report.  I think I have found the cause: I
can reproduce the failure with your steps and make it go away with a
small change to the loose object cache.

The problem is in the semantics of the loose object cache, which
answers quick existence checks.  It is a two-layer cache: a bitmap
keyed by the first byte of the object ID, recording which
"objects/XX/" subdirectories have been listed, and an oidtree with
the objects found there.  The intended invariant is that once a
subdirectory has been listed, the oidtree holds every loose object
in it, so that "not in the oidtree" means "does not exist".

Writing a loose object does not update the cache, which breaks the
invariant if the subdirectory had already been listed.  In
practice, that means a quick lookup of any object in the same
subdirectory earlier in the same process.

In your example, the object the server refuses, 27b64cc8, is not in
either parent nor in the merge base.  It is the content-merged blob
that the merge itself just wrote as a loose object.  merge-ort's
prefetch had already listed "27/", so the checkout of the merge
result thinks the blob is missing and asks the server for an object
that only exists locally.  The server rejects the whole batch, and
we then die on the first object in it that really is missing,
3c804b88, which is why the two OIDs in the error differ.

The bug is older than 5946a564cd (odb/source: make
`read_object_info()` function pluggable), but that commit, which
matches the bisect result, is what exposed it.  Before it, any
non-quick lookup that missed called odb_reprepare(), and that also
threw away the whole loose cache.  A merge does such lookups between
writing the merged blob and checking out the result, so in practice
the stale cache was discarded before anything could use it.  Since
that commit, the retry only reprepares the packfile store, and the
stale cache survives until the checkout.

I think the fix is to maintain the invariant: after a successful
loose object write, if the subdirectory has already been listed,
insert the new OID into the oidtree.  (Clearing the subdirectory's
bit would also work, at the cost of listing it again.)

I have a local fix with a unit test for the loose source.  An
end-to-end test is awkward, since it needs a merged blob whose first
byte matches one of the prefetched objects.

I could polish this into a proper patch, but since Patrick
Steinhardt has been reworking this part of the code, I think his
thoughts on the approach would be useful first.

Thanks,
Kristofer
Thread (5 messages) 5 messages, 3 authors, last reply 4d ago
Keyboard shortcuts
hback out one level
jnext message in thread / next series-diff position
kprevious message in thread / previous series-diff position
ldrill in
Escclose help / fold thread tree
?toggle this help