git/list[1] front-page[2] threads[3] people[4] search[5] about
 

bug: fetch --shallow-since can produce .git/shallow entries with no backing objects

From
NGNick Gavalas <njg@anthropic.com>
Date
Feb 27, 2026, 19:00 UTC
Message-ID
<CAHPsMLNvvneszHtBfHwuADss=_rtbi3jYkXd8gYFrxSs8A1X-Q@mail.gmail.com>
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/shallow
On my machine (git 2.51.0, and also against recent `next`) this prints:
    79f92113... OK
    bc6015ef... MISSING
The 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

Message 1 of 1 in “bug: fetch --shallow-since can produce .git/shallow entries with no backing objects”
  1. Nick GavalasFeb 27, 2026

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.