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

1 messages from 2026-02-27 to 2026-02-27. Participants: Nick Gavalas.
Thread: https://gitlist.dev/t/65092

## Nick Gavalas, 2026-02-27 19:00

Subject: bug: fetch --shallow-since can produce .git/shallow entries with no backing objects
Message-ID: <CAHPsMLNvvneszHtBfHwuADss=_rtbi3jYkXd8gYFrxSs8A1X-Q@mail.gmail.com>
URL: https://gitlist.dev/e/CAHPsMLNvvneszHtBfHwuADss%3D_rtbi3jYkXd8gYFrxSs8A1X-Q%40mail.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

```
