Converting an existing repository retroactively
A repository that has grown over years usually contains a fair number of large binary files that would have been better off in Git LFS from the start. git lfs migrate import can fix that retroactively, but only if history rewriting, team coordination, and the force push are planned carefully.
Table of Contents
- 1. When a retroactive LFS migration pays off
- 2. Identifying large files in the existing history
- 3. Defining .gitattributes and tracking rules
- 4. git lfs migrate import: rewriting the history
- 5. Migrating only the current branch versus --everything
- 6. Force push and team coordination
- 7. Verification after the migration
- 8. Keeping an eye on storage and bandwidth quotas
- 9. Rollback strategy if something goes wrong
- 10. Summary
- 11. FAQ
1. When a retroactive LFS migration pays off
Git was designed for text files and their line-by-line versioning, not for large binary files such as images, videos, PDFs, or compiled artifacts. Every change to such a file creates a full new copy in the object store, since Git cannot meaningfully delta-compress binary content, causing the repository to grow noticeably over the years even if the current files themselves are not particularly large anymore.
A clear signal that action is needed is a clone that takes noticeably longer than the current file set would justify, or a .git directory that dwarfs the working directory many times over. In that case, a migration is worth it, one that retroactively removes large binary files from the Git object database and references them through Git LFS instead, keeping the actual history lean and leaving only small pointer files in the regular repository.
2. Identifying large files in the existing history
Before a migration begins, it has to be clear which file types and paths are actually responsible for the repository's size. A simple but effective approach scans every object in the repository for its compressed size and lists the largest entries together with the path under which they were last known.
This analysis frequently shows that a handful of file types account for the bulk of the repository size, for example PNG screenshots in a documentation folder or compiled DLL files that were accidentally checked in. That insight directly determines which patterns get registered for Git LFS in the .gitattributes file later on.
# List the largest objects in the history sorted by size
git rev-list --objects --all |
git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' |
awk '/^blob/ {print substr($0,6)}' |
sort -k2 -n -r |
head -n 20
3. Defining .gitattributes and tracking rules
Git LFS decides which files are managed through the LFS mechanism based on the .gitattributes file. The git lfs track command automatically appends matching entries to this file, typically by file extension, but it can also be scoped to specific directories if only part of the files of a given type should be affected.
It is important to finalize the rules before the actual migration, since git lfs migrate import uses exactly these patterns to decide which historical files get rewritten. A rule added afterward does not automatically apply retroactively to commits that were already migrated.
git lfs install
git lfs track "*.psd" "*.zip" "assets/videos/**"
git add .gitattributes
git commit -m "chore: define LFS tracking rules"
4. git lfs migrate import: rewriting the history
The actual migration step runs with git lfs migrate import, which walks through every commit in the selected history, replaces matching files with LFS pointers, and moves the actual content into LFS storage. Since every affected commit is rewritten in the process, all commit hashes from the first affected commit onward inevitably change, much like with a large scale interactive rebase.
The --include parameter controls which patterns are considered when no .gitattributes exists beforehand; otherwise the command automatically picks up the patterns defined there. A dry run in a freshly cloned, isolated directory before the actual migration is strongly recommended, to verify the result before applying the change to the shared repository.
# Test in an isolated copy
git clone --no-local /path/to/original test-clone
cd test-clone
# Migrate based on the already defined .gitattributes
git lfs migrate import --include-ref=refs/heads/main
# Verify the result
git lfs ls-files
du -sh .git
5. Migrating only the current branch versus --everything
By default, git lfs migrate import only processes the currently checked out branch, which is useful for an initial validation but does not fully clean up a repository with many active feature branches or tags. The --everything option covers all references, meaning every local branch, tag, and, if desired, remote tracking branch, and ensures that no old, uncleaned objects remain in the repository through a forgotten branch.
The downside of a full migration is the higher effort, since every affected branch and tag gets rewritten, which potentially invalidates every open pull request built on the old history. In practice it is worth running the migration in a maintenance window during which no open feature branches are running against the affected history.
git lfs migrate import --everything
6. Force push and team coordination
Since the migration rewrites history, the result then has to be pushed to the remote with a force push, for which git push --force-with-lease is preferable to a plain --force, since it prevents accidentally overwriting commits that others pushed in the meantime. Every developer with an existing local clone then has to discard their own state and either re-clone or deliberately switch to the new history.
Clear communication before the migration is essential: all open branches should be merged or saved as a patch beforehand, and a fixed cutoff time should be communicated after which nobody pushes to the old repository anymore. Without this coordination, conflicts between the old and new history are practically guaranteed after the migration.
git push --force-with-lease origin --all
git push --force-with-lease origin --tags
# Every developer, after the migration
git fetch origin
git reset --hard origin/main
7. Verification after the migration
After the migration, it should be checked whether all expected files are actually managed through LFS and whether the repository size shrank as expected. The git lfs ls-files command lists all files currently tracked through LFS in the working directory, while comparing the .git directory size before and after the migration confirms the quantitative effect.
Equally important is a spot check that historical commits still check out correctly, for example by checking out an older tag and confirming that the binary files referenced there resolve correctly through LFS. A broken LFS pointer would otherwise only become noticeable once someone actually accesses the affected commit.
8. Keeping an eye on storage and bandwidth quotas
Git LFS storage and bandwidth are metered by most hosting providers and billed separately from regular Git storage, which can cause a sudden quota overrun during a large scale retroactive migration if nobody calculated the actual data volume beforehand. Before the migration, it is worth estimating the total size of all files to be migrated and comparing it against the current quota of the hosting plan.
Since every clone and every CI checkout will consume LFS bandwidth going forward, the CI configuration should also be reviewed, in particular whether jobs that do not need the affected binary files can be configured with GIT_LFS_SKIP_SMUDGE=1 to avoid unnecessary downloads.
GIT_LFS_SKIP_SMUDGE=1 git clone https://example.com/repo.git
9. Rollback strategy if something goes wrong
A full backup of the repository should be taken before any migration, ideally a plain git clone --mirror into a separate directory or an additional remote that exists independently of the actual migration process. If it turns out after the migration that important references are missing or the LFS configuration is broken, the old state can be fully restored from this mirror.
A rollback after a force push has already happened is considerably more effort, since every developer who already pulled the new history has to be reset back to the old state. That is why the extra time for a careful dry run in an isolated clone before the actual migration is almost always worth it, since it significantly reduces the risk of a costly rollback in the shared repository.
# Full backup before the migration
git clone --mirror /path/to/original backup-before-lfs-migration.git
| Step | Tool | History affected | Recommended timing |
|---|---|---|---|
| Identify large files | git rev-list + cat-file | No | Before planning |
| Define tracking rules | .gitattributes + git lfs track | No | Before migration |
| Rewrite history | git lfs migrate import | Yes, from first match | Maintenance window |
| Publish the result | git push --force-with-lease | Yes, remote | Right after testing |
| Save a backup | git clone --mirror | No, copy only | Before every step |
Mironsoft
Git workflows, branching strategies, and CI hooks
Chaotic Git history and unclear branching rules across the team?
We set up clean Git workflows, clarify branching strategies for the team, and automate quality checks via Git hooks and CI pipelines so the history stays traceable.
Workflow Audit
Review the existing branching strategy and merge practice for weak spots.
Hook Automation
Set up pre-commit and pre-push hooks for linting, tests, and commit conventions.
Team Training
Teach rebase, cherry-pick, and conflict resolution hands-on across the team.
10. Summary
LFS Migration
Core command
git lfs migrate import replaces files with LFS pointers
Scope
--everything covers all branches, tags, and references
Publishing
git push --force-with-lease after a successful dry run
Safety net
git clone --mirror as a full backup before starting