Git Partial Clone Explained
A partial clone downloads the repository without some of its objects, and fetches the missing ones on demand. The clone is faster and smaller; the cost is that some later operations need the network.
It is the answer to “cloning this repository takes twenty minutes” — a different problem from “my working tree is too big”, which is sparse checkout, and from “I do not need old history”, which is shallow clone.
What gets left out
Section titled “What gets left out”An ordinary clone downloads every object reachable from every ref: every commit, every tree, every version of every file, for the whole history.
Most of that is history you will never look at. A repository with ten years of development contains thousands of versions of each file; you need the current ones.
A partial clone omits objects matching a filter, records that the remote can supply them later, and fetches them when something actually needs them.
git clone --filter=blob:none <url> projectWhat it doesClones the repository while omitting all file contents, downloading them on demand as they are needed.
Why we run itCommit and tree objects are small; file contents dominate a repository's size. Omitting them makes the initial clone dramatically faster on a large repository.
Expected resultA normal clone that completes faster than usual. The working tree is fully checked out — blobs for the checked-out commit are fetched during checkout.
The clone records what it did:
git config --get remote.origin.promisorgit config --get remote.origin.partialclonefiltertrueblob:nonepromisor marks the remote as one that has promised to supply missing objects on request. partialclonefilter
records the filter, so later fetches apply it too.
Lazy fetching
Section titled “Lazy fetching”Objects arrive when needed. Reading a historical file version triggers a fetch:
git count-objects -v | grep in-packin-pack: 33git cat-file -p HEAD~4:services/api/file.txtchange 1git count-objects -v | grep in-packin-pack: 34One more object, fetched automatically because something asked for it. The command worked exactly as it would in a full clone — the only difference is that it required the network.
The filters
Section titled “The filters”| Filter | Omits | Use for |
|---|---|---|
blob:none | All file contents | The common choice — history browsing stays fast |
blob:limit=<n> | Blobs larger than <n> | Repositories with large binary assets |
tree:0 | All trees and blobs | Extremely aggressive; commit graph only |
object:type=commit | Everything except commits | Specialised analysis |
Sizes accept suffixes: blob:limit=1m, blob:limit=500k.
blob:none
Section titled “blob:none”The default recommendation. You get every commit and every tree, so git log, git log --stat, branch
operations and history navigation all work offline. Only file contents are fetched on demand.
Because trees are present, Git knows the shape of every commit without asking the server. That is what makes this filter feel normal in daily use.
blob:limit=<n>
Section titled “blob:limit=<n>”Keeps small files, omits large ones. Good for repositories where a few large binaries dominate — design assets, test fixtures, compiled artefacts.
git clone --filter=blob:limit=1m <url> projectText files come down as usual; the 400 MB of PSDs do not, unless someone checks them out.
tree:0
Section titled “tree:0”Omits trees as well as blobs, so the clone contains little more than commits. Any operation needing to know
what a commit contained requires a fetch — including git log --stat and git diff.
Appropriate for automation that only reads commit metadata. Painful as a development clone.
Server support
Section titled “Server support”Partial clone requires the server to allow it. On the Git side that is the uploadpack.allowFilter
configuration:
git config uploadpack.allowFilter trueMajor hosting providers support filtered clones. A self-hosted server may not, and the symptom is that
--filter is silently ignored — the clone succeeds and is simply not partial.
Check afterwards:
git config --get remote.origin.promisorEmpty output means the filter did not take effect.
Combining with sparse checkout
Section titled “Combining with sparse checkout”This pairing is the standard approach for large monorepos, and it is worth understanding why neither alone is sufficient.
Partial clone alone: the clone is fast, but every file is written to disk. On a repository with a million files, checkout is slow and your working tree is enormous.
Sparse checkout alone: the working tree is small, but the clone downloaded everything anyway.
Together:
git clone --filter=blob:none --no-checkout <url> projectcd projectgit sparse-checkout set services/apigit checkout mainThe clone transfers commits and trees but no file contents. Sparse checkout limits which paths are written. Checkout fetches blobs only for those paths. Blobs for the rest of the repository are never downloaded.
| Objects downloaded | Files on disk | |
|---|---|---|
| Ordinary clone | Everything | Everything |
| Partial clone | Commits + trees, blobs on demand | Everything |
| Sparse checkout | Everything | Subset |
| Both | Commits + trees, blobs for your subset | Subset |
Partial, shallow and sparse
Section titled “Partial, shallow and sparse”The three-way distinction, stated as plainly as possible:
| Limits | Still available? | |
|---|---|---|
| Partial clone | Which objects arrive initially | Yes — fetched on demand |
| Shallow clone | How much commit history arrives | Only by deepening explicitly |
| Sparse checkout | Which paths are written to disk | Yes — objects are all present |
The important asymmetry: partial clone degrades gracefully, shallow clone does not. A partial clone behaves like a full one, just with occasional network access. A shallow clone genuinely lacks the history — commands referring to truncated commits fail rather than fetching.
# partial: works, fetches transparentlygit log -p HEAD~500
# shallow with --depth 1: failsgit log HEAD~500fatal: ambiguous argument 'HEAD~500': unknown revision or path not in the working tree.That is why partial clone is generally the better choice for a developer workstation, and shallow clone is better suited to CI.
Inspecting a partial clone
Section titled “Inspecting a partial clone”Four commands tell you what state a repository is in.
Is it partial, and with what filter?
git config --get remote.origin.promisorgit config --get remote.origin.partialclonefilterHow many objects are present?
git count-objects -vcount: 0size: 0in-pack: 33packs: 1size-pack: 28Watching in-pack before and after an operation shows lazy fetching happening.
Which objects are missing?
git rev-list --objects --all --missing=print | grep '^?' | headLines beginning ? are objects the repository knows about but does not have. On a blob:none clone
immediately after cloning, that is most blobs in history.
Am I about to trigger a large fetch? There is no direct answer, but the shape of the command tells you.
Anything that reads file content across many commits — git log -p, git blame on an old file,
git grep over history — will fetch. Anything that reads only commits and trees will not.
Performance
Section titled “Performance”Faster initial clone. The saving scales with how much of the repository is historical file content — often the large majority.
Slower first access to old objects. Each lazy fetch is a network round trip.
Some commands become expensive. git log -p over deep history, git blame on an old file, or checking
out a very old commit may fetch many objects at once. On a slow connection this is noticeable.
Later fetches inherit the filter, because it is recorded in the remote’s configuration. You do not have to repeat it.
To materialise everything and stop lazy fetching:
git fetch --refetch --filter=blob:none:noneMore simply, if you decide partial clone was the wrong choice, re-clone without a filter.
Converting an existing clone
Section titled “Converting an existing clone”You do not have to re-clone to adopt or abandon a filter.
Make an existing full clone partial — this does not delete anything you already have, it only applies the filter to future fetches:
git config remote.origin.promisor truegit config remote.origin.partialclonefilter blob:noneObjects already present stay present. The benefit is on subsequent fetches, which is modest — the usual reason to do this is a repository that has grown since you cloned it.
Make a partial clone complete:
git fetch --refetch --filter=blob:none:noneblob:none:none is the “no filter” filter. Everything previously omitted is downloaded, and the repository
behaves like an ordinary clone. Useful before going offline for a while.
To also stop future fetches filtering:
git config --unset remote.origin.partialclonefiltergit config --unset remote.origin.promisorWhen to use it
Section titled “When to use it”Good fit:
- Repositories where cloning is measured in minutes.
- Monorepos, especially combined with sparse checkout.
- Repositories with a long history of large binary files.
- Developer workstations with reliable connectivity.
Poor fit:
- Small repositories — the complexity buys nothing.
- Offline or intermittent connectivity.
- CI jobs that need history — a shallow clone is usually the better tool there.
- Servers that do not support filtering.
Common mistakes
Section titled “Common mistakes”Confusing it with sparse checkout. Partial clone limits objects; sparse checkout limits paths on disk.
Expecting a smaller working tree. Every file is still checked out unless you add sparse checkout.
Using --filter on a local path clone. Silently ignored. Use file://.
Assuming the filter applied. Check remote.origin.promisor.
Using tree:0 for a development clone. Too aggressive; ordinary operations become network-bound.
Using it where a shallow clone fits better. For CI needing recent history only, --depth is simpler
and needs no ongoing connectivity.
Forgetting the offline implications. Some previously instant operations now require the network.
Mental Model
Section titled “Mental Model”A partial clone is a repository with an account at the warehouse.
You take the catalogue and the items you need now. Everything else stays in the warehouse, and when you ask for it, it is delivered automatically. You never notice the difference — except that it takes a moment and needs the road to be open.
A shallow clone, by contrast, means the warehouse only ever sent you the recent stock, and asking for older items returns an error rather than a delivery.
What You Learned
Section titled “What You Learned”- Partial clone omits objects matching a filter and fetches them on demand.
blob:noneis the usual choice: all commits and trees, no file contents up front.blob:limit=<n>targets large binaries;tree:0is aggressive and rarely right for development.- The clone records
remote.origin.promisorand the filter, and later fetches inherit it. - Servers must allow filtering (
uploadpack.allowFilter); local path clones ignore--filterentirely. - Combined with sparse checkout, it gives a fast clone and a small working tree.
- It degrades gracefully — operations work but may need the network. Shallow clone does not.
Try It Yourself
Section titled “Try It Yourself”-
Create a repository with some history, to act as the remote:
Terminal window mkdir ~/partial-lab && cd ~/partial-lab && git init src-repo && cd src-repomkdir -p services/api && echo start > services/api/file.txtgit add . && git commit -m "Initial"for i in 1 2 3 4 5; do echo "change $i" >> services/api/file.txt; git commit -am "change $i"; donegit config uploadpack.allowFilter true -
Clone it partially — note the
file://URL:Terminal window cd ~/partial-labgit clone --filter=blob:none "file://$HOME/partial-lab/src-repo" bloblesscd blobless -
Confirm the filter took effect:
Terminal window git config --get remote.origin.promisorgit config --get remote.origin.partialclonefilter -
Count the objects:
git count-objects -v | grep in-pack. Note the number. -
Read a historical file version, which forces a lazy fetch:
Terminal window git cat-file -p HEAD~4:services/api/file.txt -
Count again. Predict whether the number changed before you run it.
-
Now try it wrong, with a local path instead of
file://:Terminal window cd ~/partial-lab && git clone --filter=blob:none ./src-repo local-copyRead the warning carefully, then check
git -C local-copy config --get remote.origin.promisor. -
Combine with sparse checkout:
Terminal window git clone --filter=blob:none --no-checkout "file://$HOME/partial-lab/src-repo" combinedcd combined && git sparse-checkout set services/api && git checkout main
Step 6 shows lazy fetching happening. Step 7 shows the trap — a clone that looks partial and is not.
Next Lesson
Section titled “Next Lesson”The third “less” feature limits history rather than objects, and it does not degrade gracefully.