Git 2.55 Tested: blob:none, Sparse, LFS, 7 Errors
There is 48 MiB in that repository, and you probably need to open ten files out of three hundred. Git hands you six switches to fetch less: --depth, --filter=blob:none, --filter=tree:0, --sparse, combinations of them, and git lfs migrate. The trouble is that they do not shrink the same thing. One only trims the working tree, one only trims historical file contents, and one trims both at the cost of every future git log -p reaching back out to the network.
I tested all six combinations against the same sample repository and measured the result in bytes. Every number below came out of a real run on this Windows machine (Git 2.55.0.windows.3 and git-lfs 3.7.1), and the commands are reproduced so you can redo it.
The decision tree: which switch is actually yours
- You only want the latest snapshot, not the history →
git clone --depth 1 - You want the full history but not the historical file contents yet →
git clone --filter=blob:none --no-checkout - You do not even want the tree structure, just the commit graph →
git clone --filter=tree:0 --no-checkout - You want a handful of subdirectories out of a big repo →
git clone --filter=blob:none --sparse, thengit sparse-checkout set - You just want a clean working tree and do not care about .git →
git clone --sparse - You want big files out of the packfile →
git lfs migrate import(but read the LFS section first)
When in doubt, pick --filter=blob:none --sparse. It is the only combination that compresses both .git and the working tree, and the price is that you have to stay online.
The sample repo: where 48 MiB comes from
To make the differences measurable I built a repository with a deliberately lopsided shape: 300 text files of 500 bytes spread across 30 directories, plus one 8 MiB binary asset rewritten six times.
git init -q -b main .
for i in $(seq 0 299); do
d=src/mod$((i % 30)); mkdir -p "$d"
head -c 500 /dev/zero | tr '\0' 'y' > "$d/f$i.txt"
done
mkdir -p assets
for v in 0 1 2 3 4 5; do
head -c 8388608 /dev/urandom > assets/model.bin
git add -A && git commit -q -m "v$v"
done
git gc --aggressive -q --prune=now
The resulting .git directory is 50,410,860 bytes, and git count-objects -vH reports size-pack: 48.02 MiB with in-pack: 56. These blobs are random bytes, so Git cannot compress them. Swap in compressible text assets and the absolute numbers shrink a lot, but the ratios between the six methods do not move.
Six methods, measured in bytes
Same source repository, six clone shapes, all over the file:/// protocol (the reason is errors one and two below):
| Method | .git size | vs full | files in worktree |
|---|---|---|---|
git clone (full) | 50,408,484 B | 100% | 301 |
git clone --depth 1 | 8,450,792 B | 16.8% | 301 |
--filter=blob:none --no-checkout | 35,077 B | 0.070% | 0 |
--filter=blob:none (default checkout) | 8,453,742 B | 16.8% | 301 |
--filter=tree:0 --no-checkout | 30,365 B | 0.060% | 0 |
--filter=blob:none --depth 1 | 8,452,100 B | 16.8% | 301 |
git clone --sparse | 50,409,793 B | 100.0% | 0 |
--filter=blob:none --sparse plus set src/mod1 | 63,795 B | 0.127% | 10 |
That is a 790x spread between the first and the last row. But look at row four first: unless you pass --no-checkout, Git checks out HEAD the moment the clone finishes, and checking out HEAD needs every blob in the HEAD tree. Everything blob:none just saved comes straight back during a 2.9 second checkout.
Three counterintuitive results
Result one: --sparse saves nothing in .git. The measured .git for git clone --sparse is 50,409,793 B, which is 1,309 B *more* than the full clone at 50,408,484 B (the extra is the sparse-checkout config). What it saves is the working tree: file count after checkout is 0, and after git sparse-checkout set src/mod1 src/mod2 it becomes 20. The official git-sparse-checkout page (fetched 2026-10-22, page labelled 2.55.0) says it changes "the working tree from having all tracked files present to only having a subset of those files" — working tree, start to finish, nothing about the object database. The same page carries an all-caps warning: THIS COMMAND IS EXPERIMENTAL.
Result two: blob:none defers, it does not forgive. I tested the price directly. In a blobless repo whose .git was 35,090 B, a single git cat-file -s pushed .git to 8,427,456 B — an increase of 8,392,366 B, or 240x, from one command. Blobs that come back stay local forever; deleting the remote does not evict them. Run git log -p --all once in CI and the entire saving is gone.
Result three: --filter=blob:none --depth 1 is mostly decoration. Measured at 8,452,100 B, which is 1,308 B *larger* than plain --depth 1 at 8,450,792 B. The reason is plain: after --depth 1 a single commit remains, and its contents must be checked out, so there is nothing left for a blob filter to filter. These two switches are not additive.
The git-lfs migration: the repo does not get smaller
I ran the whole procedure: full clone first (.git = 50,408,497 B, size-pack: 48.02 MiB), then git lfs migrate import --include="*.bin" --everything, then git reflog expire --expire=now --all and git gc --prune=now -q.
The result was .git growing from 50,408,497 B to 100,746,552 B, close to double. git count-objects -vH showed in-pack moving from 56 to 81 while size-pack stayed at 48.02 MiB — not one of those six 8 MiB blobs had been collected.
The cause is a ref. The migration rewrote local branches only. Measured: refs/heads/main points at 552b69a14f2f18e2... (new history) while refs/remotes/origin/main still points at c6f0c8982585a61e... (old history). The old commits are still referenced by a remote-tracking ref, so the old blobs remain reachable, and no amount of --prune=now will let git gc drop them.
After deleting both remote-tracking refs, expiring the reflog again and re-running gc, size-pack fell from 48.02 MiB to 7.65 KiB, in-pack went from 81 back to 57, and total .git returned to 50,396,692 B.
And now the sentence that matters: after a fully cleaned migration .git is 50,396,692 B, and before the migration it was 50,408,497 B. The total saving is 11,805 bytes, or 0.023%.
Because .git/lfs holds 50,331,648 B — six historical versions times 8,388,608 B, every one of them, just relocated from the packfile into the LFS object store. What LFS buys you is not downloading those files on future clones, and keeping giant blobs out of the packfile. It does not buy you a smaller repository. If shrinking an existing repo is the actual goal, you want a tool that deletes the files out of history outright, not LFS.
Note: this section uses incompressible random bytes. Real images, audio and model weights are similarly hard to compress, so the direction holds. If your large files are compressible text-like assets, the packfile was already folding some of it away and the LFS before/after gap will look bigger.
Troubleshooting: 8 real errors and how to trigger them
All eight were reproduced locally; exit codes and output are copied verbatim.
Error one: --depth is silently ignored on local paths
$ git clone -q --depth 1 "D:/tmp/src" "D:/tmp/shallow"
warning: --depth is ignored in local clones; use file:// instead.
Exit code 0, the clone "succeeds", .git is 50,396,153 B (versus 50,396,147 B for a full clone), and git rev-list --count HEAD still returns 6. Git takes a hardlink/copy fast path for local paths and never invokes upload-pack, so the server-side truncation never happens. To get a real shallow clone, write file:///D:/tmp/src.
Error two: --filter has exactly the same trap
$ git clone --filter=blob:none "D:/tmp/src" "D:/tmp/blobless"
warning: --filter is ignored in local clones; use file:// instead.
Also exit code 0, also a full clone.
Error three: an unsupported filter degrades with a warning only (the dangerous one)
$ git clone --filter=blob:none "file:///D:/tmp/src-nofilter" "D:/tmp/nb"
warning: filtering not recognized by server, ignoring
Exit code 0, clone completes, .git = 50,408,679 B, indistinguishable from a full clone. And then the part that hurts:
$ git config --get remote.origin.partialclonefilter
blob:none
This repository declares itself a partial clone in its config while having skipped zero blobs. Anything downstream that reasons about promisor remotes reasons from a false premise. The server side needs an explicit switch; the Git v2.55.0 source file Documentation/config/uploadpack.adoc says "If this option is set, upload-pack will support partial clone and partial fetch object filtering":
git config uploadpack.allowFilter true
Check this before pointing partial clone at a self-hosted bare repo. And do not verify success by watching for a warning — measure the size of .git.
Error four: git lfs migrate refuses to run on a dirty worktree
$ git lfs track "*.bin"
$ git lfs migrate import --include="*.bin" --everything
override changes in your working copy? All uncommitted changes will be lost! [y/N]
working copy must not be dirty
Exit code 2, and there is a self-locking trap inside it. The moment git lfs track writes *.bin filter=lfs diff=lfs merge=lfs -text into .gitattributes, the clean filter goes live and git status starts reporting that large file as modified. The worktree is now dirty and migrate refuses. I committed .gitattributes and it still reported dirty. The correct order is migrate first, track second — do not touch .gitattributes before the migration.
Error five: remote-tracking refs still point at the old history, so gc cannot collect
The test is to compare two refs:
$ git rev-parse refs/heads/main
552b69a14f2f18e262538edb933f72b25a481b49
$ git rev-parse refs/remotes/origin/main
c6f0c8982585a61e63bc100b1dd65179d3d759fe
Different values mean the old history is still attached. Remove them and gc again:
git update-ref -d refs/remotes/origin/main
git update-ref -d refs/remotes/origin/HEAD
git reflog expire --expire=now --all
git gc --prune=now
Error six: a blobless repo after its promisor remote disappears
Three commands, three messages, all exit code 128:
$ git cat-file -s b023e6741d61ac1e47b030f5c389a45358a8c9ce
fatal: git cat-file: could not get object info
$ git show b023e6741d61ac1e47b030f5c389a45358a8c9ce
fatal: bad object b023e6741d61ac1e47b030f5c389a45358a8c9ce
$ git log -p -1
fatal: unable to read c5b4eef6e79bff515a54f17b3f372a9370f4ea6a
git log -p is the one that gets misread most often. The commit message and the commit line print normally, and only then does Git blow up while reading an object, which looks like a corrupt repository. It is not: Git simply needed an object it had assumed was always fetchable. Run git fetch --refetch before going offline, or do not use blobless at all.
Error seven: cone mode does not accept files
$ git sparse-checkout set assets/model.bin
fatal: 'assets/model.bin' is not a directory; to treat it as a directory anyway, rerun with --skip-checks
Exit code 128. Cone mode takes directories only, by design. The official git-sparse-checkout page puts it as: By default, the input list is considered a list of directories.
Error eight: sparse-checkout accepts a directory that does not exist, silently
$ git sparse-checkout set src/nope
(exit code 0, no output at all)
$ git sparse-checkout list
src/mod5
src/nope
A nonexistent directory is written into the config with exit code 0 and zero output. You believe you added src/nope to your working tree; nothing happened. Always read it back with git sparse-checkout list together with git ls-files.
Where this stops
- Everything came from one Windows machine over the
file:///protocol. No real Git server was involved, so wall-clock timings (11.8 s full versus 1.4 s blobless) will be dominated by latency on a real network. The byte counts do not depend on the network. - The sample has the shape "one large file rewritten six times". Real repositories look more like "many medium files, each edited a few times", and under that shape
--filter=blob:nonedoes better than it does here, because it filters per blob regardless of file size. - I did not test
--filter=blob:limit=,--filter=sparse:oid,combine, or the sparse index. - I did not test LFS against a real LFS server (none available here), so the "LFS makes clones faster" benefit is explained as a mechanism in this article, not measured.
- Versions: Git 2.55.0, released 2026-06-29 per git-scm.com and the GitHub blog post Highlights from Git 2.55; git-lfs 3.7.1, released 2025-10-17, with v3.8.0 already out upstream since 2026-08-28, so this machine is one major version behind (source: the git-lfs/git-lfs Releases API).
- No affiliate links in this article. Every number came out of a command on this machine.
Where to go next
If the "what is actually inside the file, and what is each knob worth in bytes" approach suits you, the Parquet and Arrow columnar storage test uses the identical method: build data with a known shape, then switch each default off one at a time and measure.
Dependency and version constraints fail in a different direction. The PEP 440 version constraint post-mortem found that pip's own error text is simply wrong — the same species of problem as Git degrading a filter with a warning: a tool's description of itself is not the same as the facts.
The DuckDB 1.5.6 benchmark walks the same "measurable on one machine" path, which is worth copying if you have no GPU and no servers to test against.
👉 Join Xiaomi MiMo Platform: Leading AI model platform with cost-effective inference
👉 Join Aliyun AI: Top AI products with exclusive coupons for business innovation
📌 This article was AI-assisted generated and human-reviewed | TechPassive — An AI-driven content testing site focused on real tool reviews
🔗 Recommended Tools
These are carefully selected tools. Using our affiliate links supports us to keep producing quality content: