Git LFS: Sensibly Versioning Large Files
AI generated
git
HEAD
Git · LFS · Binary Files · Repository Hygiene
Git LFS
Sensibly Versioning Large Files

Committing PSD files, videos, or archives straight into Git needlessly bloats repository size and clone time for the whole team, because Git's delta compression barely works on binary data. Git LFS replaces these files with lightweight pointers and stores the actual content externally, without changing the familiar commit and push workflow at all.

13 min. read git lfs track · pointer files · git lfs migrate Git 2.x · GitHub · GitLab

1. Why Git struggles with large binary files

Git's storage model is optimized for source text. On every commit, Git internally computes deltas between text lines and stores objects highly compressed, because textual changes compare well line by line. For a PSD file, a video, or a ZIP archive, this diffing logic barely applies: even a single changed pixel row or a freshly recalculated compression header causes the entire binary file to be written into history as a brand new, complete object. There is no meaningful line structure for Git to work with.

The consequences add up quickly: fifty versions of a 100-megabyte design file mean five gigabytes of pure history data, even though the working directory only shows the current version. Because Git history is immutable by default, every version ever committed stays in the repository forever, even after the file has been deleted from the current state. Every git clone downloads this entire history, which noticeably slows down onboarding new teammates, CI pipelines, and even simple backups, while needlessly straining the available storage on Git hosting platforms.

2. How Git LFS works under the hood: pointer files and smudge/clean filters

Git LFS (Large File Storage) solves this by no longer storing the actual binary content in Git's object database, replacing it instead with a tiny pointer file. This text file sits at the exact same place in Git history where the binary used to be, but is only a few bytes in size and contains just a version marker, the SHA-256 hash of the content (oid), and its file size. The actual binary content is stored on a separate LFS server, provided by GitHub, GitLab, or a self-hosted instance.

To keep this substitution transparent, Git LFS registers two Git filters: the clean filter, which replaces a binary file with its pointer representation on git add/git commit and uploads the content into a local LFS cache and, on push, to the server, and the smudge filter, which does the reverse on checkout, automatically swapping the pointer file back for the real binary content. For developers this mechanism is invisible: the working directory always shows the full file, while Git itself only ever tracks the lightweight pointer version.


# What actually gets stored in the Git history instead of the binary blob:
cat assets/hero-banner.psd

# version https://git-lfs.github.com/spec/v1
# oid sha256:4d7a2c8e9f1b3a6d5c8e0f2a1b4c7d9e6f3a2b5c8d1e4f7a0b3c6d9e2f5a8b1c
# size 157286400

# Locally, "git lfs" resolves the pointer back into the real file
git lfs pointer --file=assets/hero-banner.psd

3. Installing and setting up Git LFS

Git LFS is not part of Git itself; it is a separate client tool that must be installed additionally, for example via apt install git-lfs, brew install git-lfs, or the official installer package. After installation, git lfs install activates the necessary Git filters globally in the user profile, so that every newly cloned or initialized repository on that machine automatically recognizes and applies the filter=lfs directives from .gitattributes. This step only needs to run once per machine, not once per repository.

For cases where the global filters should deliberately not be set, for example on a CI runner with restricted permissions, git lfs install --local registers the filters only in the current repository's local .git/config. git lfs version confirms that both the LFS extension and the underlying Git version are compatible, which regularly causes hard-to-diagnose errors on older CI images when the extension is silently missing.


# Install the git-lfs client (once per operating system / package manager)
sudo apt-get install git-lfs        # Debian/Ubuntu
brew install git-lfs                # macOS

# Register the Git filters globally for this user account, once per machine
git lfs install

# Register the filters only for the current repository (e.g. restricted CI runner)
git lfs install --local

# Verify the installed client and Git compatibility
git lfs version

4. Tracking file types with git lfs track and .gitattributes

Which files get managed through LFS is defined by git lfs track. Running git lfs track "*.psd" writes a corresponding line with the attributes filter=lfs diff=lfs merge=lfs -text into the .gitattributes file at the repository root. These attributes tell Git to use the LFS filters for matching files and to treat them as binary, without normalizing line endings. Patterns can be combined freely, for example scoping to entire directories with assets/videos/**/*.mp4, or tracking several file types with separate track calls.

The critical, often overlooked step: the .gitattributes file itself must be versioned like any other file, with git add .gitattributes and git commit. Only then do the same tracking rules apply for every teammate who clones the repository. If the file is forgotten, the original author's files get correctly stored via LFS, but every other clone treats new binary files of the same type as ordinary blobs stored directly in history again.


# .gitattributes - generated and extended via "git lfs track"
*.psd filter=lfs diff=lfs merge=lfs -text
*.zip filter=lfs diff=lfs merge=lfs -text
*.mp4 filter=lfs diff=lfs merge=lfs -text
assets/videos/**/*.mov filter=lfs diff=lfs merge=lfs -text

# List every pattern currently tracked by Git LFS in this repository
# git lfs track

5. The everyday workflow: commit, push, clone with LFS

After the initial setup, the familiar routine with git add, git commit, and git push doesn't visibly change. Behind the scenes, Git automatically recognizes which files fall under LFS based on the .gitattributes rules, replaces them with pointers on commit, and uploads the actual content separately to the LFS endpoint on push. git lfs ls-files reliably shows which files in the current commit are actually managed as LFS objects, while git lfs status additionally shows whether local LFS files have already been uploaded or are still pending.

On clone, Git downloads only the pointer files immediately by default; the associated binary content is only fetched during checkout via the smudge filter, which noticeably speeds up the initial git clone. Important in practice: GitHub and GitLab meter LFS storage and bandwidth separately from the regular repository quota, with comparatively small free allowances that get exhausted quickly on active design or video repositories, at which point paid add-ons become necessary.


# Clone downloads pointers immediately, binary content follows on checkout
git clone https://git.example.com/team/shop.git

# List which files in the current commit are actually managed as LFS objects
git lfs ls-files
# 4d7a2c8e * assets/hero-banner.psd
# 9b3f1e0a * assets/videos/teaser.mp4

# Show which LFS files are staged, modified, or still pending upload
git lfs status

6. Migrating existing large files retroactively with git lfs migrate

Tracking rules set via git lfs track only apply to future commits. Binary files already checked into history before LFS was introduced remain untouched and keep bloating the repository. For that case, git lfs migrate import walks the entire commit history, extracts every matching blob, replaces it with a pointer file, and rewrites the affected commits. With --include="*.psd" --everything, the migration can be scoped to specific file types across every branch.

Because every affected commit ends up with a new checksum, this effectively changes the entire subsequent history, and a plain git push no longer works afterward, it requires git push --force-with-lease. A full backup of the repository is mandatory before running such a migration, since the operation is destructive and cannot easily be undone. Team coordination is just as important: after the force push, everyone must re-clone their local copies or explicitly rebase their branches onto the new history, otherwise duplicate, conflicting histories emerge.


# Always create a full backup/mirror before rewriting history
git clone --mirror https://git.example.com/team/shop.git shop-backup.git

# Rewrite history: replace already-committed .psd files with LFS pointers
git lfs migrate import --include="*.psd" --everything

# Push the rewritten history; regular push is rejected due to changed hashes
git push --force-with-lease origin --all
git push --force-with-lease origin --tags

# Every teammate must re-clone or reset onto the rewritten history
# git fetch origin && git reset --hard origin/main

7. Limits and pitfalls of Git LFS

Git LFS shifts the storage problem rather than fully resolving it: on common hosting platforms, LFS storage and bandwidth are billed separately and are often tight even in the base plan. For large, frequently changing asset libraries, these costs can climb quickly, especially because every new version of a binary file is stored as an entirely new LFS object with no delta compression between versions. On top of that, LFS only helps where it has been actively set up: without a retroactive migration, binary files already committed in the past remain sitting unchanged in the regular Git object store.

Teams that don't want to rely on a large hosting provider for LFS can run their own LFS server; many self-hosted Git platforms such as GitLab CE or Gitea already ship with built-in LFS support. As a lightweight complement when a full LFS setup isn't wanted, partial clone (git clone --filter=blob:none) is also worth considering, fetching large blobs only on actual access, without requiring a separate LFS server at all.

8. Alternatives and complements to Git LFS

git-annex follows a similar basic idea to Git LFS but is considerably more flexible and more complex: it supports arbitrary storage backends, encrypted remotes, and fine-grained control over which files are actually present locally. For teams primarily looking for a simple, widely adopted tool, the extra learning curve usually isn't justified. Another complement is Git submodules, which offload large asset collections into a dedicated repository that's only pulled in when needed, plus DVC (Data Version Control), purpose-built for ML datasets and pipelines, combining versioning with experiment tracking.

Just as important is recognizing when LFS is the wrong answer altogether. Generated or imported media, such as Magento product images under pub/media/, generally shouldn't live in a Git repository at all, regardless of whether LFS is involved. Such assets belong in database imports, S3 buckets, or CDN deployments, while Git stays reserved for source code and clearly scoped, genuinely version-worthy design assets. Git LFS is a tool for files that should actually be part of the development history, not a generic substitute for asset storage.

9. Git LFS compared: patterns applied right vs. wrong

Most Git LFS problems trace back to a handful of recurring scenarios that are fully avoidable with the right approach. The table below lines up typical missteps against the recommended strategy.

Task Wrong approach Correct pattern Benefit
Bringing already-committed binaries under LFS Only run git lfs track git lfs migrate import --everything History is actually shrunk
Sharing tracking rules across the team Leave .gitattributes uncommitted Commit and push .gitattributes Same rules on every clone
Distributing history after a migration Plain git push git push --force-with-lease + team notice No conflicting histories
Mixed assets in the same repo Some PSDs tracked, others not One pattern per file type, consistently Predictable repository behavior
Planning storage and bandwidth cost Only check quotas after a failed push Estimate LFS usage upfront in the hosting dashboard No blocked pushes mid-sprint

The table shows a consistent pattern: Git LFS works reliably once tracking rules, history, and team communication are all consistently aligned. Most problems don't come from the tool itself, but from a half-finished setup, such as a forgotten .gitattributes commit or a migration run without prior agreement across the team.

Mironsoft

Git workflows, repository hygiene, and deployment setup for PHP and Magento teams

Repository weighed down by large binary files?

We set up Git LFS for your team, safely migrate existing binary files out of history, and coordinate the force push so nobody loses in-progress work.

LFS setup

Clean tracking rules, .gitattributes, and team onboarding done right

History migration

Safe migration of existing binaries with backup and rollout plan

Cost check

Realistic estimate of LFS storage and bandwidth needs

10. Summary

Git LFS solves a specific structural problem: binary files don't fit Git's line-based delta compression and needlessly bloat both the repository and clone times. Pointer files with a version marker, SHA-256 hash, and file size replace the actual content in Git history, while smudge and clean filters handle the substitution transparently in the background on checkout and commit. git lfs track plus a committed .gitattributes define which file types are affected, and the familiar commit-push workflow stays unchanged for developers.

For repositories that already exist, git lfs migrate import is the only way to actually remove historical binary files from the past, though only with a full backup and a coordinated force push across the whole team. Teams that apply these steps consistently, while also knowing when assets shouldn't be in the repository at all, such as generated Magento media, keep their repository lean and performant over the long run.

Git LFS: Sensibly Versioning Large Files - The Essentials at a Glance

The core problem

Git's delta compression works on text, not binaries. Every version of a large file permanently bloats the history.

Pointer + filters

A small pointer file replaces the binary content in Git. Smudge and clean filters swap it transparently on checkout/commit.

Setting up tracking

git lfs track "*.psd" plus a committed .gitattributes so every teammate uses the same rules.

Migrating existing files

git lfs migrate import --everything rewrites history. Backup first, --force-with-lease after, notify the team.

11. FAQ: Git LFS

1What is Git LFS and what is it used for?
Replaces large binary files in Git history with small pointers and stores the content on a separate LFS server, without changing the familiar workflow.
2What does an LFS pointer file look like?
A few-byte text file with a version marker, SHA-256 hash (oid), and file size, replacing the binary content in history.
3What exactly do the smudge and clean filters do?
Clean replaces the binary with a pointer and uploads it on commit. Smudge does the opposite on checkout, restoring the real content.
4Do I need to reinstall Git LFS on every machine?
The client is installed once per machine, then git lfs install registers the filters globally for every repository on that machine.
5How do I decide which file types go through LFS?
With git lfs track "*.psd", which writes a rule into .gitattributes. That file must be committed and pushed to take effect for the team.
6What happens if I forget to commit .gitattributes?
Other clones won't know the rule and will treat new files as ordinary blobs again, causing inconsistent behavior across the team.
7How do I retroactively migrate already-committed binary files into LFS?
With git lfs migrate import --include="*.psd" --everything. Backup first, force push required afterward since commit hashes change.
8Why doesn't a plain git push work after a migration?
Migration creates new commit hashes, so Git sees diverging history. git push --force-with-lease plus a team heads-up is required.
9Does Git LFS cost extra on GitHub or GitLab?
Yes, LFS storage and bandwidth are usually billed separately with a small free allowance. Active repos often need paid add-ons.
10Should Magento product images be versioned via Git LFS?
Generally not. Generated/imported media shouldn't be in the repository at all regardless of LFS, better handled via database imports, S3, or CDN deployments.