Committed a Large File to Git? History Cleanup and Migrating to LFS
Sometimes a push gets rejected after you commit a video or a design master file — GitHub blocks pushes containing files over 100 MB. There is also a quieter version of this incident. No single file trips the limit, but binaries accumulate version after version until one day the repository has swollen to several GB and a clone takes ten minutes. Both cases share the same cause: once a file has been committed, every version of it stays in history even after you delete it — that is Git’s storage model. This post covers the diagnosis that identifies the offending files, the history cleanup, and the migration to LFS for files you still need.
Diagnosis — find the largest files in the repository #
First, check how heavy the repository actually is.
git count-objects -vH
# count: 1324
# size-pack: 2.31 GiB
# ...size-pack is the actual size the full history occupies. If this value is large regardless of what the files in your working directory add up to, a heavy blob is sitting somewhere in history. One pipeline finds the offenders.
git rev-list --objects --all |
git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' |
awk '/^blob/ {print $3, $4}' |
sort -rn |
head -10
# 734003200 assets/demo-video.mp4
# 209715200 design/main.psd
# ...This sorts every object in the entire history by size. The paths listed here become your cleanup targets. Files deleted long ago still appear in this list as long as they remain in history.
Just committed — clean up before push #
If you have not pushed yet, the problem is confined to your machine and the cleanup is simple. If it went into the last commit, untrack it and replace the commit.
git rm --cached assets/demo-video.mp4 # untrack; the file itself stays
echo "*.mp4" >> .gitignore
git add .gitignore
git commit --amend --no-editIf it entered a few commits back, stop at that commit with an interactive rebase and do the same.
git rebase -i <commit just before the one with the file>
# change that commit to "edit" in the todo list
git rm --cached assets/demo-video.mp4
git commit --amend --no-edit
git rebase --continueCaught before push, it ends here. None of the coordination below is needed, which is why the habit of checking git diff --staged --stat for oversized files right before pushing pays for itself.
Deep in history — filter-repo #
If it has been pushed and has accumulated across many commits, the entire history needs rewriting. The tool is git filter-repo. filter-repo expects to run in a freshly cloned repository, so take a dedicated cleanup clone first.
git clone https://github.com/example/my-service.git cleanup
cd cleanup
git filter-repo --strip-blobs-bigger-than 50M--strip-blobs-bigger-than removes every blob over the given size from the entire history. To remove only specific files or directories, use path-based rules.
git filter-repo --invert-paths --path assets/demo-video.mp4
git filter-repo --invert-paths --path design/raw/The rewritten history is a new history in which every commit hash has changed, so reconnect the remote, push by force, and announce that the whole team must re-clone. A push from an old clone resurrects the deleted blobs. This coordination is exactly the same as in the secret-leak incident — the details and cautions are covered in Committed a Secret to Git? Rotate the Key First, Then Clean History with filter-repo.
git remote add origin https://github.com/example/my-service.git
git push --force --all
git push --force --tagsAfter cleanup — move still-needed files to LFS #
Some files end with removal; others still need version control going forward. For large assets you will keep working with — design masters, trained models — move them to Git LFS. LFS is an extension that keeps only a small pointer file in the repository and stores the actual content in separate storage; the overview is in Practical Git Workflows #7: Monorepos and Git — sparse-checkout, Submodules, and LFS.
There is a dedicated command for converting files already in history to LFS.
git lfs migrate import --include="*.psd" --everythingmigrate import converts files matching the pattern into LFS pointers across the entire history (--everything covers all branches). This is also a history rewrite, so commit hashes change and the same coordination — force push and a re-clone announcement — follows. If you are rewriting history with filter-repo anyway, bundling the LFS migration into the same rewrite cycle spares the team a second round of disruption.
After the migration, commit the tracking rule so new files flow into LFS naturally.
git lfs track "*.psd"
git add .gitattributes
git commit -m "Track PSD files with LFS"Prevention #
- Know the thresholds. GitHub warns at 50 MB and blocks the push at 100 MB. If you saw the warning, the time to act has already arrived.
- Start with LFS from day one. If a project is expected to carry large assets, set up
git lfs trackrules before the first commit. The cost is not comparable to migrating after the files are in history. - Block generated artifacts with .gitignore. Build outputs, compressed archives, rendered results — anything you can regenerate was never a candidate for version control.
Wrap-up #
The response order, once more.
- Diagnose — identify the offending files with
git count-objects -vHand the blob-size pipeline. - Clean — before push, use amend or rebase; after push, rewrite history with filter-repo in a fresh clone.
- Announce — after the force push, have the whole team re-clone.
- Relocate — move still-needed assets to LFS with
git lfs migrate importand commit the tracking rules.
Unlike a leaked secret, a large-file incident is not a race against the clock. But the longer it sits, the more versions pile into history and the larger the cleanup grows — so the cheapest response is to handle it the moment the 50 MB warning appears.