Hi,
I'm not sure if this is a bug or if I'm misunderstanding the intended semantics of `--shallow-since`, but I'm seeing behavior that surprised me and I'd appreciate another pair of eyes on it.
When fetching with `--shallow-since` into an empty repository, I can end up with entries in `.git/shallow` that point to commit objects which were never sent in the pack. The resulting repo looks healthy for read-only operations, but a later attempt to deepen the clone fails with a confusing error.
Here's a minimal reproducer:
git init --bare server.git
git clone server.git work
(
cd work
GIT_COMMITTER_DATE="100000000 +0000" git commit --allow-empty -m V
V=$(git rev-parse HEAD)
GIT_COMMITTER_DATE="200000000 +0000" git commit --allow-empty -m T
GIT_COMMITTER_DATE="300000000 +0000" git commit --allow-empty -m S
git checkout -b feature "$V"
GIT_COMMITTER_DATE="100000001 +0000" git commit --allow-empty -m Fold
git checkout -
GIT_COMMITTER_DATE="400000000 +0000" git merge --no-ff -m M feature
GIT_COMMITTER_DATE="500000000 +0000" git commit --allow-empty -m want
git push origin HEAD
) git init --bare client
git -C client fetch --shallow-since="150000000 +0000" \
"file://$PWD/server.git" main # Check each shallow entry actually exists
while read oid; do
git -C client cat-file -e "$oid" \
&& echo "$oid OK" \
|| echo "$oid MISSING"
done < client/shallowOn my machine (git 2.51.0, and also against recent `next`) this prints:
79f92113... OK
bc6015ef... MISSINGThe history looks like this (newest at top):
want (time=500M)
|
M (time=400M, merge)
/ \
Fold S (time=100M+1 / time=300M)
\ |
\ T (time=200M)
\ |
V (time=100M)With `--shallow-since=150M`, I'd naively expect the cutoff to exclude `V` and `Fold`, so the shallow boundary would be whichever commits have those as parents. The server does seem to compute both `M` (parent `Fold` is too old) and `T` (parent `V` is too old) as boundaries — both show up in `.git/shallow`.
But the pack only contains `want` and `M`. My guess is that once `M` is treated as a graft point (no parents), the path `M -> S -> T` disappears, so `T` never gets enumerated for the pack — but it was already promised to the client as a shallow boundary.
I noticed the same thing with `--shallow-exclude=feature`, which I think goes through the same codepath. `--depth=N` doesn't exhibit this.
The repo looks healthy for read-only operations, and a plain incremental fetch against the same server also works (the server has the object, so it just silently ignores the shallow line). But if you then try to *deepen* the clone, the server sends back `unshallow <T>` and the client dies:
# after the reproducer above
git -C client fetch --shallow-since="100000000 +0000" \
"file://$PWD/server.git" main
# -> fatal: error in object: unshallow bc6015ef...I believe this is fetch-pack.c:receive_shallow_info hitting parse_object() on an object the client never actually received.
Is there an invariant here that I'm missing? Should `.git/shallow` always point at objects the client actually has, or is the client expected to tolerate missing shallow objects?
Happy to provide more detail or a larger real-world reproducer if helpful — I originally hit this on a repo with heavy merge-queue history where about a third of the shallow entries were missing.
Thanks, Nick