← Back to Home

Git 2.55 Tested: blob:none, Sparse, LFS, 7 Errors

GitPartial Clonegit-lfs

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

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 sizevs fullfiles in worktree
git clone (full)50,408,484 B100%301
git clone --depth 18,450,792 B16.8%301
--filter=blob:none --no-checkout35,077 B0.070%0
--filter=blob:none (default checkout)8,453,742 B16.8%301
--filter=tree:0 --no-checkout30,365 B0.060%0
--filter=blob:none --depth 18,452,100 B16.8%301
git clone --sparse50,409,793 B100.0%0
--filter=blob:none --sparse plus set src/mod163,795 B0.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

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:

☁️ DigitalOcean Cloud ⚡ Vultr VPS 🤖 QoderWork CN (Refer & Earn) ☁️ Aliyun AI Products 📚 WordPress Books 🔍 WordPress SEO Books 🌐 Web Hosting Books 🐳 Docker Books 🐧 Linux Books 🐍 Python Books 💰 Affiliate Marketing 💵 Passive Income Books 🖥️ Server Books ☁️ Cloud Computing Books 🚀 DevOps Books 🤖 Xiaomi MiMo Platform
← Back to Home