Repository health, LFS and recovery
Keep a repository fast with fsck, gc and repack, move large assets into Git LFS, purge a file from history, and recover from a damaged object store.
Health checks and housekeeping
git count-objects -vH # loose objects, packs and total size
git fsck --full # verify every object and link
git fsck --unreachable # objects nothing points at
git gc # pack loose objects, prune what is safe
git gc --aggressive # slower and better compressed, rarely worth it
git prune --expire=now # remove unreachable objects immediately
git repack -ad # rebuild packs without redundant ones
git reflog expire --expire=now --all| Command | Does | Risk |
|---|---|---|
git count-objects -vH | Reports size and object counts | None |
git fsck --full | Verifies object integrity and links | None |
git gc | Packs objects and prunes unreachable ones past the grace period | Low |
git prune --expire=now | Deletes unreachable objects immediately | Removes anything you might still recover |
git repack -ad | Rebuilds all packs from scratch | Interrupting it leaves the repository in an awkward state |
git reflog expire --expire=now --all | Drops reflog entries | Destroys your local recovery path |
Git runs git gc automatically when the number of loose objects grows, so manual runs are mostly for recovery work, size audits, or a repository that has just been through a history rewrite.
Git LFS for large assets
# once per machine
git lfs install
# track a file type: the blob goes elsewhere, a small pointer goes in Git
git lfs track "*.psd"
git add .gitattributes
git add design/mockup.psd
git commit -m "Add design source (LFS)"
git lfs ls-files
git lfs status
git lfs pull # fetch missing objects
git lfs migrate import --include="*.zip" --everything # move existing history to LFS- LFS stores the bytes on a separate server and commits a text pointer, so a clone without LFS installed gives you pointers instead of files.
- The
.gitattributesentry must be committed, or the next clone cannot tell which paths are LFS-tracked. - Storage and bandwidth quotas apply on most hosts; a repository full of video becomes an ongoing bill rather than a one-off cost.
- Converting existing history requires a rewrite (
git lfs migrate), so do it early, before many people have cloned.
⚠️
Rewriting history to move files into LFS changes every affected commit id. Treat it like any other rewrite: freeze pushes, announce it, and expect everyone to delete and re-clone rather than pull — an old clone will happily push the old blobs back.
Removing a leaked file and recovering damage
# a file was committed by mistake: it lives in history, not only in the tip
git log --all --oneline -- path/to/secret.env
git filter-repo --path path/to/secret.env --invert-paths
# git filter-branch is the legacy equivalent; prefer filter-repo
# find the largest objects in your packs
git verify-pack -v .git/objects/pack/*.idx | sort -k3 -n -r | head -5
# recover from a damaged or partially fetched repository
git fsck --full # identify what is missing or corrupt
git remote -v
git fetch --all # re-download objects from a known-good remote
git fetch --refetch # force a fresh download of the packfiles
git reflog # a local safety net while it still exists- Once a secret has been pushed, treat it as compromised: rotate the credential first, then clean the history. Cleaning alone does not un-leak anything.
- After any history rewrite, every clone must be replaced — old clones re-push the deleted objects on their next push.
- A corrupt object usually means a bad disk or an interrupted transfer. Re-fetch from a known-good remote before attempting anything more creative.
- Back up important repositories by mirroring them (
git clone --mirror), not by copying a working directory, which holds no history.
FAQ
How big is too big for a Git repository?
There is no hard limit, but clone time and everyday commands degrade past roughly a gigabyte of packfile, and hosts escalate warnings after that. Keep binaries in LFS or outside Git entirely.
Does git gc delete work I still need?
Not while something still points at it:
gc only prunes unreachable objects older than the grace period, two weeks for loose objects by default. --expire=now bypasses that protection, so use it only when you are certain.Related
Worktrees, submodules and large repositories Hooks, aliases and automation
Last refreshed 2026-09-18.