Skip to content

Git Partial Clone Explained

Lesson 4 of 11Intermediate → Advanced9 min readModern Git Workflows · Modern Git ProductivityVerified: Git 2.43.0 on Ubuntu 24.04; every command and output in this lesson was run

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.

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.

Terminal window
git clone --filter=blob:none <url> project

What 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:

Terminal window
git config --get remote.origin.promisor
git config --get remote.origin.partialclonefilter
true
blob:none

promisor marks the remote as one that has promised to supply missing objects on request. partialclonefilter records the filter, so later fetches apply it too.

Objects arrive when needed. Reading a historical file version triggers a fetch:

Terminal window
git count-objects -v | grep in-pack
in-pack: 33
Terminal window
git cat-file -p HEAD~4:services/api/file.txt
change 1
Terminal window
git count-objects -v | grep in-pack
in-pack: 34

One 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.

FilterOmitsUse for
blob:noneAll file contentsThe common choice — history browsing stays fast
blob:limit=<n>Blobs larger than <n>Repositories with large binary assets
tree:0All trees and blobsExtremely aggressive; commit graph only
object:type=commitEverything except commitsSpecialised analysis

Sizes accept suffixes: blob:limit=1m, blob:limit=500k.

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.

Keeps small files, omits large ones. Good for repositories where a few large binaries dominate — design assets, test fixtures, compiled artefacts.

Terminal window
git clone --filter=blob:limit=1m <url> project

Text files come down as usual; the 400 MB of PSDs do not, unless someone checks them out.

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.

Partial clone requires the server to allow it. On the Git side that is the uploadpack.allowFilter configuration:

Terminal window
git config uploadpack.allowFilter true

Major 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:

Terminal window
git config --get remote.origin.promisor

Empty output means the filter did not take effect.

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:

Terminal window
git clone --filter=blob:none --no-checkout <url> project
cd project
git sparse-checkout set services/api
git checkout main

The 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 downloadedFiles on disk
Ordinary cloneEverythingEverything
Partial cloneCommits + trees, blobs on demandEverything
Sparse checkoutEverythingSubset
BothCommits + trees, blobs for your subsetSubset

The three-way distinction, stated as plainly as possible:

LimitsStill available?
Partial cloneWhich objects arrive initiallyYes — fetched on demand
Shallow cloneHow much commit history arrivesOnly by deepening explicitly
Sparse checkoutWhich paths are written to diskYes — 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.

Terminal window
# partial: works, fetches transparently
git log -p HEAD~500
# shallow with --depth 1: fails
git log HEAD~500
fatal: 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.

Four commands tell you what state a repository is in.

Is it partial, and with what filter?

Terminal window
git config --get remote.origin.promisor
git config --get remote.origin.partialclonefilter

How many objects are present?

Terminal window
git count-objects -v
count: 0
size: 0
in-pack: 33
packs: 1
size-pack: 28

Watching in-pack before and after an operation shows lazy fetching happening.

Which objects are missing?

Terminal window
git rev-list --objects --all --missing=print | grep '^?' | head

Lines 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.

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:

Terminal window
git fetch --refetch --filter=blob:none:none

More simply, if you decide partial clone was the wrong choice, re-clone without a filter.

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:

Terminal window
git config remote.origin.promisor true
git config remote.origin.partialclonefilter blob:none

Objects 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:

Terminal window
git fetch --refetch --filter=blob:none:none

blob: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:

Terminal window
git config --unset remote.origin.partialclonefilter
git config --unset remote.origin.promisor

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.

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.

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.

  • Partial clone omits objects matching a filter and fetches them on demand.
  • blob:none is the usual choice: all commits and trees, no file contents up front.
  • blob:limit=<n> targets large binaries; tree:0 is aggressive and rarely right for development.
  • The clone records remote.origin.promisor and the filter, and later fetches inherit it.
  • Servers must allow filtering (uploadpack.allowFilter); local path clones ignore --filter entirely.
  • 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.
  1. 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-repo
    mkdir -p services/api && echo start > services/api/file.txt
    git 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"; done
    git config uploadpack.allowFilter true
  2. Clone it partially — note the file:// URL:

    Terminal window
    cd ~/partial-lab
    git clone --filter=blob:none "file://$HOME/partial-lab/src-repo" blobless
    cd blobless
  3. Confirm the filter took effect:

    Terminal window
    git config --get remote.origin.promisor
    git config --get remote.origin.partialclonefilter
  4. Count the objects: git count-objects -v | grep in-pack. Note the number.

  5. Read a historical file version, which forces a lazy fetch:

    Terminal window
    git cat-file -p HEAD~4:services/api/file.txt
  6. Count again. Predict whether the number changed before you run it.

  7. Now try it wrong, with a local path instead of file://:

    Terminal window
    cd ~/partial-lab && git clone --filter=blob:none ./src-repo local-copy

    Read the warning carefully, then check git -C local-copy config --get remote.origin.promisor.

  8. Combine with sparse checkout:

    Terminal window
    git clone --filter=blob:none --no-checkout "file://$HOME/partial-lab/src-repo" combined
    cd 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.

The third “less” feature limits history rather than objects, and it does not degrade gracefully.