Committed a Large File to Git? History Cleanup and Migrating to LFS

6 min read

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.

Check repository size
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.

Top 10 largest blobs
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.

Remove it from the last commit
git rm --cached assets/demo-video.mp4   # untrack; the file itself stays
echo "*.mp4" >> .gitignore
git add .gitignore
git commit --amend --no-edit

If it entered a few commits back, stop at that commit with an interactive rebase and do the same.

Remove it from an earlier commit
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 --continue

Caught 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.

Bulk removal by size
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.

Removal by path
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.

Push the rewritten history
git remote add origin https://github.com/example/my-service.git
git push --force --all
git push --force --tags
Note
GitHub’s repository size display may not shrink immediately after the history cleanup. Server-side garbage collection takes time to run. Do not conclude the cleanup failed based on the size figure right after the force push.

After 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.

Migrate files in history to LFS
git lfs migrate import --include="*.psd" --everything

migrate 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.

Tracking rule for future commits
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 track rules 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.
Tip
If you decide on LFS, check the hosting bill as well. GitHub’s free allowances for LFS storage and bandwidth are on the small side, so teams with many assets should look into the paid tiers or an external storage integration.

Wrap-up #

The response order, once more.

  1. Diagnose — identify the offending files with git count-objects -vH and the blob-size pipeline.
  2. Clean — before push, use amend or rebase; after push, rewrite history with filter-repo in a fresh clone.
  3. Announce — after the force push, have the whole team re-clone.
  4. Relocate — move still-needed assets to LFS with git lfs migrate import and 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.

X