{"thread":{"id":"65092","subject":"bug: fetch --shallow-since can produce .git/shallow entries with no backing objects","startedAt":"2026-02-27T19:00:20Z","lastAt":"2026-02-27T19:00:20Z","messageCount":1,"participants":["Nick Gavalas"],"isPatch":false,"patchVersion":null,"patchTotal":null},"messages":[{"id":"537319","messageId":"CAHPsMLNvvneszHtBfHwuADss=_rtbi3jYkXd8gYFrxSs8A1X-Q@mail.gmail.com","threadId":"65092","inReplyTo":null,"subject":"bug: fetch --shallow-since can produce .git/shallow entries with no backing objects","fromName":"Nick Gavalas","fromEmail":"njg@anthropic.com","sentAt":"2026-02-27T19:00:07Z","receivedAt":"2026-02-27T19:00:20Z","isPatch":false,"sender":{"key":"njg@anthropic.com","avatar":null},"body":"Hi,\n\nI'm not sure if this is a bug or if I'm misunderstanding the intended\nsemantics of `--shallow-since`, but I'm seeing behavior that surprised me\nand I'd appreciate another pair of eyes on it.\n\nWhen fetching with `--shallow-since` into an empty repository, I can end up\nwith entries in `.git/shallow` that point to commit objects which were\nnever sent in the pack. The resulting repo looks healthy for read-only\noperations, but a later attempt to deepen the clone fails with a confusing\nerror.\n\nHere's a minimal reproducer:\n\n    git init --bare server.git\n    git clone server.git work\n    (\n        cd work\n        GIT_COMMITTER_DATE=\"100000000 +0000\" git commit --allow-empty -m V\n        V=$(git rev-parse HEAD)\n        GIT_COMMITTER_DATE=\"200000000 +0000\" git commit --allow-empty -m T\n        GIT_COMMITTER_DATE=\"300000000 +0000\" git commit --allow-empty -m S\n        git checkout -b feature \"$V\"\n        GIT_COMMITTER_DATE=\"100000001 +0000\" git commit --allow-empty -m Fold\n        git checkout -\n        GIT_COMMITTER_DATE=\"400000000 +0000\" git merge --no-ff -m M feature\n        GIT_COMMITTER_DATE=\"500000000 +0000\" git commit --allow-empty -m want\n        git push origin HEAD\n    )\n\n    git init --bare client\n    git -C client fetch --shallow-since=\"150000000 +0000\" \\\n        \"file://$PWD/server.git\" main\n\n    # Check each shallow entry actually exists\n    while read oid; do\n        git -C client cat-file -e \"$oid\" \\\n            && echo \"$oid OK\" \\\n            || echo \"$oid MISSING\"\n    done < client/shallow\n\nOn my machine (git 2.51.0, and also against recent `next`) this prints:\n\n    79f92113... OK\n    bc6015ef... MISSING\n\nThe history looks like this (newest at top):\n\n    want     (time=500M)\n      |\n      M      (time=400M, merge)\n     / \\\n   Fold  S   (time=100M+1 / time=300M)\n     \\   |\n      \\  T   (time=200M)\n       \\ |\n        V    (time=100M)\n\nWith `--shallow-since=150M`, I'd naively expect the cutoff to exclude `V`\nand `Fold`, so the shallow boundary would be whichever commits have those\nas parents. The server does seem to compute both `M` (parent `Fold` is too\nold) and `T` (parent `V` is too old) as boundaries — both show up in\n`.git/shallow`.\n\nBut the pack only contains `want` and `M`. My guess is that once `M` is\ntreated as a graft point (no parents), the path `M -> S -> T` disappears,\nso `T` never gets enumerated for the pack — but it was already promised\nto the client as a shallow boundary.\n\nI noticed the same thing with `--shallow-exclude=feature`, which I think\ngoes through the same codepath. `--depth=N` doesn't exhibit this.\n\nThe repo looks healthy for read-only operations, and a plain incremental\nfetch against the same server also works (the server has the object, so it\njust silently ignores the shallow line). But if you then try to *deepen* the\nclone, the server sends back `unshallow <T>` and the client dies:\n\n    # after the reproducer above\n    git -C client fetch --shallow-since=\"100000000 +0000\" \\\n        \"file://$PWD/server.git\" main\n    # -> fatal: error in object: unshallow bc6015ef...\n\nI believe this is fetch-pack.c:receive_shallow_info hitting parse_object()\non an object the client never actually received.\n\nIs there an invariant here that I'm missing? Should `.git/shallow` always\npoint at objects the client actually has, or is the client expected to\ntolerate missing shallow objects?\n\nHappy to provide more detail or a larger real-world reproducer if helpful —\nI originally hit this on a repo with heavy merge-queue history where about\na third of the shallow entries were missing.\n\nThanks,\nNick\n"}]}