Git Partial Clone Missing Files: How to Fix Sparse Checkout and Promisor Remote Issues

Git partial clone missing files: how to fix sparse checkout and promisor remote issues

When files appear to be missing after a Git partial clone, Git usually has not lost your source tree. The fix is to separate three cases: files hidden by sparse checkout, blobs intentionally missing because of --filter=blob:none, and a broken or unreachable promisor remote. Here’s what was actually wrong and how I fixed it.

I hit this while cloning a large monorepo into a Linux dev container with Git 2.43. The clone looked successful, the branch was correct, and git status was clean. Then half the files I expected were not in the working tree, and a build step failed as if someone had deleted a package directory.

The confusing part is that partial clone and sparse checkout both make files “missing” on purpose, but for different reasons. Sparse checkout controls what appears in the working tree. Partial clone controls which Git objects are downloaded up front. When those two features overlap, the first job is not to reclone harder, but to prove which layer is hiding the file or failing to fetch it.

The setup I was using

The repository was cloned like this:

git clone --filter=blob:none --sparse [email protected]:example/big-monorepo.git repo

cd repo

git sparse-checkout set services/api packages/shared

That command creates a blobless partial clone and a sparse working tree.

In plain English:

  • --filter=blob:none downloads commits and trees, but not file contents until Git needs them.
  • --sparse starts with a reduced working tree instead of checking out everything.
  • git sparse-checkout set ... tells Git which paths should appear locally.

This setup works well for a monorepo. It can also trick you at 1:12 a.m., because “file not on disk” can mean any of these:

The path is outside the sparse-checkout rules.

The path is inside the sparse rules, but its blob has not been fetched yet.

The blob should be fetched on demand, but the promisor remote is not configured or reachable.

The local object cache is damaged.

The server does not support the filter request the way the client expects.

Git’s own partial clone docs describe this model as Git functioning without a complete copy of the repository. A promisor remote is the remote that promises to provide missing objects later. That is technically clear. It is less helpful when your build says No such file or directory.

Git partial clone missing files: first check sparse checkout

The first wrong path I took was trying to fetch more history:

git fetch --all --prune

git pull

It felt reasonable: files are missing, so fetch more of the repository. Classic developer superstition.

It did not help because my missing path was not included in sparse checkout. Fetching updates the object database and refs. It does not automatically widen the working tree.

Check sparse mode first:

git sparse-checkout list

Example output:

services/api

packages/shared

If the file you need lives under tools/build-scripts, Git is doing exactly what you asked. It is excluding that directory from the working tree.

Fix it by adding the missing path:

git sparse-checkout add tools/build-scripts

This works because sparse checkout is a working-tree filter. Adding the path tells Git to materialize matching tracked files locally. In a partial clone, this may also trigger lazy fetching of the blobs needed for those files.

If you want to remove sparse checkout completely:

git sparse-checkout disable

This restores the full working tree for the current checkout. On a huge repository, expect Git to download a lot of blobs. Do not run this in a tiny CI container unless you enjoy watching disk space turn into smoke.

You can also confirm whether Git knows about the path at all:

git ls-tree -r --name-only HEAD -- tools/build-scripts | head

If this prints paths, the files exist in the commit. They are just not necessarily present in your working tree.

Then check whether the missing file is an expected lazy blob

Once the sparse rules include the path, test whether Git can read the file from the object database or fetch it on demand:

git cat-file -e HEAD:tools/build-scripts/build.sh

No output means success. That is Git’s charming way of saying, “Yes, I can resolve this object.”

If the blob is missing and Git cannot fetch it, you may see errors like:

fatal: could not fetch 9f3c0a... from promisor remote

or:

fatal: unable to read 9f3c0a...

That points away from sparse checkout and toward the partial clone promise being broken.

Now inspect the partial clone config:

git config --local --get extensions.partialClone

git config --local --get remote.origin.promisor

git config --local --get remote.origin.partialclonefilter

For a healthy blobless clone from origin, I expect something like:

origin

true

blob:none

The exact remote name can vary, but this is the part that matters: Git must know which remote is allowed to provide promised objects, and that remote must still exist.

I have seen this break after people rewrite .git/config, rename remotes, copy .git directories into Docker layers, or use a CI cache restored from a different repository URL. The working tree still looks like a Git repo, but the “go ask origin for that missing blob” path is gone.

Fix the promisor remote config

If extensions.partialClone points to origin, make sure origin exists:

git remote -v

If origin is missing or has the wrong URL, repair it:

git remote set-url origin [email protected]:example/big-monorepo.git

If the promisor flags are missing, put them back:

git config remote.origin.promisor true

git config remote.origin.partialclonefilter blob:none

git config extensions.partialClone origin

A partial clone can have missing objects only because Git knows a promisor remote can provide them later. Without that config, Git no longer treats the missing blob as an intentional deferred download. It starts treating it like corruption.

Then fetch with the same filter:

git fetch origin --filter=blob:none

This keeps the repository in blobless mode while refreshing refs and making sure future object negotiation still uses the partial clone filter.

Now force Git to reapply sparse checkout:

git sparse-checkout reapply

After config or fetch repairs, reapply makes Git revisit the sparse working tree and materialize files that should be present under the current rules.

If the file is inside the sparse set but still not on disk, check it out directly:

git checkout HEAD -- tools/build-scripts/build.sh

Checkout asks Git to populate that path from HEAD. In a partial clone, that read can trigger the missing blob fetch.

If the cache is poisoned, reclone smaller instead of debugging forever

At some point, fixing the local repository costs more time than replacing it. My rule: if promisor config is correct, the remote is reachable, sparse rules include the path, and git cat-file still fails on multiple files, I stop trying to massage the cache.

Save local work first:

git status --short

git diff > /tmp/partial-clone-recovery.patch

Then make a fresh clone:

cd ..

mv repo repo.broken

git clone --filter=blob:none --sparse [email protected]:example/big-monorepo.git repo

cd repo

git sparse-checkout set services/api packages/shared tools/build-scripts

This rebuilds .git/objects, pack metadata, sparse checkout files, and promisor config from a clean negotiation with the server.

Apply your patch only after the checkout is healthy:

git apply /tmp/partial-clone-recovery.patch

I prefer this over running random garbage collection commands on a partial clone. git gc, git repack, and multi-pack-index maintenance are valid Git tools, but they are not the first lever to pull when the problem is “my promisor remote cannot supply a missing blob.” Recent Git versions have improved promisor pack handling, but an old CI image plus a restored cache can still create a very weird afternoon.

Verification: prove the fix held

After the repair, I run these checks:

git sparse-checkout list

git config --local --get remote.origin.promisor

git config --local --get remote.origin.partialclonefilter

git ls-files tools/build-scripts/build.sh

test -f tools/build-scripts/build.sh && echo "file is in the working tree"

git fsck --connectivity-only

Expected output should look like:

services/api

packages/shared

tools/build-scripts

true

blob:none

tools/build-scripts/build.sh

file is in the working tree

For git fsck --connectivity-only, a partial clone may still know about promised objects it has not downloaded yet. What I care about here is no fatal missing-object error for the paths I need. If fsck reports promised objects in a way your Git version treats as normal, do not panic. If checkout or cat-file cannot fetch a required blob from the promisor remote, that is the real failure.

One more useful debugging command:

GIT_TRACE_PACKET=1 GIT_TRACE=1 git checkout HEAD -- tools/build-scripts/build.sh

Tracing shows whether Git is contacting the remote during the lazy fetch. If there is no network attempt, your config or path classification is probably wrong. If there is a network attempt and the server rejects the filter or object request, you are looking at a remote capability or auth problem.

CI and Docker gotchas

  • Do not cache partial clone .git directories across unrelated remotes. A promisor cache is tied to the remote that promised the missing objects. Reusing it with a different URL can fail in deeply unfun ways.
  • Do not assume git fetch --unshallow fixes partial clone issues. Shallow clone limits commit history. Partial clone filters objects. They are different axes.
  • If your CI job runs offline after checkout, partial clone can break later build steps. Either prefetch the needed paths before going offline or avoid --filter=blob:none for that job.
  • If sparse checkout is in non-cone mode, path patterns can be less obvious. Run git sparse-checkout list and keep the rules boring unless you truly need pattern matching.
  • If the host does not support partial clone filters, use a normal clone or a sparse checkout without --filter. Sparse checkout alone still reduces the working tree, but it does not reduce the object database in the same way.

The mental model that finally fixed this for me was simple: missing from disk is not the same as missing from Git. First check whether sparse checkout excluded the path. Then check whether the object is intentionally deferred. Only after that should you blame the promisor remote.

TL;DR fix

git sparse-checkout add  && git config remote.origin.promisor true && git config remote.origin.partialclonefilter blob:none && git fetch origin --filter=blob:none && git sparse-checkout reapply

Leave a Comment