Speeding up clones with partial clone Jump to heading

A repository whose history contains a decade of assets makes every new clone pay for every version of every file that has ever existed β€” including the ones deleted years ago. A blobless partial clone changes the deal: Git downloads the complete commit and tree graph, which is small, and fetches file contents only when a command actually needs them. This recipe applies the first of the client-side fixes catalogued in Large Repository Performance, and it is the one with the best ratio of improvement to disruption.

When to use this approach Jump to heading

  • Cloning takes long enough that people avoid making fresh clones, which is itself a source of bugs.
  • The repository’s history contains large or numerous assets, even if the current checkout is modest.
  • Developers mostly work near the tip of history β€” the normal case β€” so lazily fetched blobs are rarely needed.
  • Your remote supports partial clone filters; every major host does, and self-managed instances need uploadpack.allowFilter enabled.
  • If instead the pain is git status on a huge working tree, this is the wrong fix β€” use sparse checkout, which addresses a different bottleneck.

Step 1 β€” Measure the baseline clone Jump to heading

# Time a conventional clone into a scratch directory
time git clone https://example.com/org/repo.git /tmp/baseline-clone

# What did it actually download?
git -C /tmp/baseline-clone count-objects -vH

What changed: nothing yet β€” but β€œthe clone is slow” is now a number you can hold the fix accountable to.

Step 2 β€” Clone with a blobless filter Jump to heading

# --filter=blob:none omits file contents; commits and trees still arrive in full
time git clone --filter=blob:none https://example.com/org/repo.git /tmp/partial-clone

What changed: the transfer is now dominated by commit and tree objects, which compress extremely well. A repository that took twenty minutes typically lands in under a minute, and the working tree that gets checked out is identical byte for byte.

# Prove the checkout is the same, and that history is intact
diff -r --exclude=.git /tmp/baseline-clone /tmp/partial-clone && echo "identical working trees"
git -C /tmp/partial-clone rev-list --count HEAD    # same commit count as the baseline
git -C /tmp/partial-clone config remote.origin.partialclonefilter   # expect: blob:none
What arrives at clone time and what arrives laterAt clone time the complete commit graph and all trees are transferred, which is a small fraction of the repository. Blobs for the checked-out commit arrive immediately after. Blobs for older commits arrive only if a command such as checkout or diff of that commit needs them, and most never do.at clone time β€” one fast transferevery commit + every treeHEAD blobsβ‰ˆ 4% of a full clonelater, only if something asksc1c2c3c4c5HEADgit checkout c3 β†’ fetches c3's blobs on demand (a second or two)commits nobody visits never cost anything β€” which is most of them

Step 3 β€” Convert an existing clone in place Jump to heading

Nobody has to re-clone. Two configuration keys turn an existing full clone into one that behaves partially from now on.

# Mark the remote as a promisor: it can supply objects on demand
git config remote.origin.promisor true
git config remote.origin.partialclonefilter blob:none

What changed: future fetches omit blobs, so the clone stops growing at the old rate. Objects already present stay present β€” this reduces future cost rather than reclaiming past cost.

git config --get-regexp '^remote\.origin\.(promisor|partialclonefilter)$'
# Expect both keys; then verify a fetch is smaller than it used to be
git fetch --dry-run

To reclaim the space as well, re-clone with the filter and delete the old copy β€” but only after confirming there is no unpushed work:

git status --porcelain --branch | head -1      # look for [ahead N] before deleting anything
git stash list                                  # stashes are local-only and easy to forget

SAFETY WARNING β€” deleting a clone destroys anything that exists only there: unpushed commits, stashes, and unreferenced work recoverable via the reflog. Push every branch and check git stash list before removing a directory. If in doubt, rename the old clone instead of deleting it, and remove it a week later.

Step 4 β€” Learn which commands now fetch lazily Jump to heading

The trade is specific and worth internalising, because the surprise otherwise arrives during an incident.

Which commands stay fast and which now pauseOperations that only need commits and trees β€” log, branch, merge-base, bisect navigation, status and diff of the working tree β€” stay fast. Operations that need historical file contents β€” checking out an old commit, log with patches, a full-history search, and blame of an old file β€” trigger a fetch.unchanged β€” no blobs neededgit log --onelinegit branch -agit merge-base a bgit statusgit diff (working tree)git bisect (navigation)often measurably faster than beforepauses to fetch on first usegit checkout <old-commit>git log -pgit log -S <string>git blame <old file>git bisect (each checkout)git show <old-commit>:filecached after the first fetch β€” the pause happens once

The right-hand column is not a warning against partial clone; it is a list of the moments to expect a short pause. If one of those workflows is central to your day β€” a code archaeologist running git log -S across a decade β€” keep a full clone for it and use the partial one for everything else.

# Batch-fetch ahead of a history-heavy session instead of paying per command
git fetch origin --refetch --filter=blob:none    # keeps the filter, refreshes what is present

Step 5 β€” Pick a different filter for CI Jump to heading

A CI runner has the opposite profile from a developer: one commit, no history, nothing reused.


# .github/workflows/build.yml β€” a checkout tuned for a throwaway runner
- uses: actions/checkout@v4
  with:
    fetch-depth: 1          # only the commit under test
    filter: blob:none       # and only the blobs it needs
# The equivalent by hand, for runners not using a checkout action
git clone --depth=1 --filter=blob:none --single-branch \
  --branch "$BRANCH" https://example.com/org/repo.git .

What changed: the runner downloads the smallest set of objects that can build the commit. Note that --depth=1 is safe here precisely because the job never runs blame or merge-base β€” and if a job does need those, as a changelog generator does, it must fetch more history or it will silently produce wrong output.

Different consumers deserve different clonesA developer machine wants a blobless partial clone with full history so blame and bisect work. A build runner wants depth one and blobless because it never inspects history. A release job needs full history and tags to compute versions, so it must not use a shallow clone.developer machine--filter=blob:nonefull history keptblame, bisect, merge-baseall behave normallyfast clone, no capability lostbuild runner--depth=1 --filter=blob:noneone commit, one branchdiscarded after the jobnever inspects historysmallest possible downloadrelease jobfetch-depth: 0needs tags and full historyto compute the next versionand the changelog rangea shallow clone breaks it silently

Validation checklist Jump to heading

Frequently Asked Questions Jump to heading

Does a partial clone lose any history? Jump to heading

No. Every commit and every tree is present, so the shape of history is complete and merge-base, bisect and blame all work. Only file contents are deferred, and any command that needs one fetches it transparently. That is the essential difference from a shallow clone, which genuinely does not have the commits and therefore cannot answer questions about them.

What happens if the remote is unreachable? Jump to heading

Anything already fetched keeps working β€” your current checkout, recent history, commits you have made. Commands that need an absent blob fail with a clear error until connectivity returns. If you routinely work offline, either keep a full clone or pre-fetch the range you will need with git fetch before disconnecting.

Can I go back to a full clone without re-cloning? Jump to heading

Yes. Remove the filter configuration and run git fetch --refetch, which downloads the objects that were previously omitted. It takes about as long as the original full clone would have, but it happens once, in the background of your choosing, rather than blocking the first checkout.