Git LFS Migration: Converting an Existing Repository Retroactively
AI generated
git
HEAD
Git · Git LFS
Git LFS Migration
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.

9 min read Git Git LFS Repository Migration

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

11. FAQ: LFS Migration

1Does git lfs migrate import change the commit hashes?
Yes, every commit that contains a migrated file gets rewritten, which changes its hash and the hashes of every following commit. That is unavoidable, since the file content itself is part of the commit object and gets swapped out.
2Do I need to enable Git LFS separately with my hosting provider?
On GitHub, GitLab, and Bitbucket, Git LFS is available server side by default, though often with a limited free quota for storage and bandwidth. A self hosted server additionally needs an LFS capable server component.
3Can I limit the migration to specific directories?
Yes, the --include option accepts glob patterns scoped to specific paths or file extensions, complemented by --exclude for targeted exceptions within those patterns.
4What happens to already open pull requests during the migration?
Since the commit hashes of the target branch change, open pull requests against the old history become invalid or show massive, unexpected diffs. They should either be merged before the migration or rebased onto the new history afterward.
5How long does a migration take on a very large repository?
It depends heavily on the number of affected commits and the total size of the files being migrated, but it can easily take several hours on repositories with tens of thousands of commits. A dry run gives a realistic time estimate up front.
6Can git lfs migrate import be reversed, moving files back from LFS into normal blobs?
Yes, git lfs migrate export is the reverse command, replacing LFS pointers with the actual file content again. This process also rewrites the history and requires the same care as the import.
7Does git lfs migrate import automatically detect all large files?
No, the command only follows the patterns defined in .gitattributes or explicitly passed via --include. A prior analysis of actual file sizes is therefore necessary to define the right patterns.
8Do I need to adjust CI pipelines after the migration?
Usually yes, at least to verify that git-lfs is installed on the build agents and that jobs which do not need binary files avoid unnecessary LFS traffic using GIT_LFS_SKIP_SMUDGE.
9What is the difference between git lfs migrate and the BFG Repo-Cleaner?
BFG Repo-Cleaner removes files and history objects entirely without replacing them with LFS pointers, while git lfs migrate import specifically swaps files for LFS references and keeps them retrievable. For an actual LFS migration, git lfs migrate is the tool built for that purpose.
10Should I announce a maintenance window before the migration?
For an actively used team repository, absolutely, since every push during the migration causes conflicts with the rewritten history. A clearly communicated window during which nobody pushes significantly reduces the risk of rework.