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.allowFilterenabled. - If instead the pain is
git statuson 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 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 listbefore 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.
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.
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.
Related Jump to heading
- Large Repository Performance β the parent guide: how to tell which of the three slowness problems you actually have.
- Scheduling git maintenance for Background Repacking β keep a fast clone fast, instead of watching it decay over a quarter.
- Sparse Checkout for Large Monorepos β the complementary fix when the working tree, not the transfer, is what hurts.