| Tested on | Red Hat Enterprise Linux 10.2 (Coughlan) |
|---|---|
| Package | git 2.52.0git-filter-repo 2.47.0 |
| Applies to | Linux, macOS, and Windows with Git and git-filter-repo; GitHub, GitLab, Bitbucket, or self-hosted remotes |
| Privilege | Read or clone access for analysis; push access to the remote when you publish rewritten history |
| Scope | Shrink a bloated .git folder by removing unwanted paths with git-filter-repo or replacing old ancestry with a baseline plus recent commits; does not cover whole-repository migration or a full Git LFS migration guide |
| Related guides | Git tutorial git remove file from tracking |
Measure a bloated Git repository, find the large blobs still stored in history, and remove them with git-filter-repo on a disposable clone. When individual paths are not worth preserving, you can keep a recent commit window instead. Verify the smaller .git folder locally before you decide whether to publish rewritten history. To move selected paths into another repository while keeping their history, use move content from one Git repo to another instead of --invert-paths stripping.
A large working tree and a large Git history are different problems. Deleting a large file from the current version does not remove copies stored in older commits, so the repository can remain large and clones can stay slow. Replace REPO_URL and paths such as backups/ or path/to/large-file.zip with your own remote and file names.
Quick answer: why is my Git repository so large?
| Problem | What to do |
|---|---|
| Large blobs are still in history | git-filter-repo |
| Unreachable objects use disk space | git gc |
| Large binaries must remain in the project | Git LFS or object storage |
| You do not know what consumes space | git filter-repo --analyze |
| Keep only recent commits | Create a baseline and replay commits after a cutoff |
| Only the working tree is large | Remove or move current files; a history rewrite may not help |
| A secret or credential was committed | Revoke first, then clean history |
This tutorial focuses on the common case: large unwanted files remain in Git history and make clones and the .git object database larger than necessary.
Use this flow when you are not sure whether history rewriting is required:
┌──────────────────────────────┐
│ Git repository is too large │
└──────────────┬───────────────┘
▼
┌────────────────────────────────────────┐
│ Measure .git and run --analyze first │
└───────────────────┬────────────────────┘
▼
┌────────────────────────────────────────┐
│ Are the large objects still reachable? │
└────────────┬───────────────────┬───────┘
No│ │Yes
▼ ▼
┌─────────────────────┐ ┌─────────────────────────────┐
│ Repack with git gc │ │ Must the large files stay? │
└─────────────────────┘ └──────────┬───────────┬──────┘
Yes│ │No
▼ ▼
┌──────────────────┐ ┌──────────────────────────┐
│ Git LFS or object│ │ Know the unwanted paths? │
│ storage │ └──┬────────────────┬──────┘
└──────────────────┘ Yes│ │No
▼ ▼
┌─────────────────┐ ┌───────────────────────┐
│ git-filter-repo │ │ Keep recent commits? │
│ --invert-paths │ └───┬─────────────┬─────┘
└─────────────────┘ Yes│ │No
▼ ▼
┌────────────────────┐ ┌─────────────────┐
│ Baseline + recent │ │ Current snapshot│
│ commit window │ │ only │
└────────────────────┘ └─────────────────┘Check how large the repository is
Change into your repository, then measure the on-disk size of the object database:
du -sh .gitThe df and du command reports human-readable totals for directories such as .git.
116M .gitCheck how many Git objects are stored and how much space the packed objects use:
git count-objects -vHcount: 0
size: 0 bytes
in-pack: 32
packs: 1
size-pack: 115.04 MiB
prune-packable: 0
garbage: 0
size-garbage: 0 bytesdu -sh .git shows filesystem usage. git count-objects -vH reports object counts and packed size inside Git. Local .git size is not always identical to the storage number shown by GitHub or GitLab.
Will git gc reduce the repository size?
git gc reclaims unreachable objects and improves packing. It cannot delete a large file that is still reachable from a branch or tag.
If large unwanted blobs are still referenced by old commits, rewrite history. If you already made objects unreachable, git gc may reclaim local disk space.
To show why git gc is not enough when large files are still reachable, run a normal garbage collection and measure again:
git gcMeasure .git again after garbage collection:
du -sh .gitIn a test where .git was about 116M before git gc, it stayed at 116M because every large blob was still reachable. Repacking cannot remove reachable history. Do not treat git gc --aggressive or immediate pruning with --prune=now as the first fix for historical bloat. See the git prune command if you need to understand when immediate pruning is appropriate.
Find what is making the Git repository large
This cleanup uses git-filter-repo. Check that it is installed:
git filter-repo --versionIf the command is missing, install it. On RHEL and Fedora, use dnf with sudo:
sudo dnf install git-filter-repoOn Debian and Ubuntu, use apt:
sudo apt update && sudo apt install git-filter-repoOn macOS with Homebrew:
brew install git-filter-repoRun analysis on a fresh clone of REPO_URL. Analysis does not rewrite history:
git clone REPO_URL repo-cleanupChange into the new clone before you analyze it:
cd repo-cleanupRun analysis without rewriting history:
git filter-repo --analyzeGit writes reports under .git/filter-repo/analysis/. Useful files include:
blob-shas-and-paths.txt— blob IDs, unpacked/packed sizes, and paths associated with each blobpath-all-sizes.txt— paths ranked by accumulated sizedirectories-all-sizes.txtandextensions-all-sizes.txt— directory and extension totalsrenames.txt— rename hints when a blob appears under multiple paths
A trimmed excerpt from blob-shas-and-paths.txt might look like this:
64c2e34f... 31457280 31466895 builds/release.iso
9765d214... 26214400 26222415 backups/db-dump-v1.sql
29d136f5... 26214400 26222415 backups/db-dump-v2.sql
ee18d9fa... 20971520 20977935 [artifacts/archive.zip, builds/vendor-bundle.zip]
c02091db... 15728640 15733455 builds/application.binThe numeric columns show unpacked and packed size in bytes; the final column shows the path or paths associated with the blob.
Typical offenders are database dumps, ZIP archives, ISO images, generated binaries, and vendor artifacts committed by mistake.
Back up and coordinate before rewriting history
Rewriting history changes the commit IDs in the cleaned repository. Branches, tags, and commit or tag signatures can also be affected. After the rewrite is published, the safest option for collaborators is to delete their old clone and clone the repository again. Advanced users can rebase or reset existing work carefully, but they must not merge the old history into the rewritten history.
Work on a fresh disposable clone, not your everyday working copy. Keep a mirror backup until the cleanup is verified:
git clone --mirror REPO_URL repo-backup.gitgit filter-repo refuses to run, that usually means the clone is not fresh enough. Make a new clone instead of reaching for --force unless you understand the risk.
Keep only recent Git history
Path filtering works when you know which unwanted files or directories caused the bloat. If you no longer need any old commit-by-commit history, you can instead keep a recent time window. The cleaned branch starts with one baseline snapshot, followed by the ordinary commits made after your cutoff date.
This method removes every older version, including deleted files whose old paths you may not know. It also keeps the first recent commit readable as a normal diff. Making the first recent commit a root commit would preserve the final files, but that commit would appear to add the entire repository.
Run these steps only in the disposable clone. Record the current branch tree before changing any commits:
ORIGINAL_TREE=$(git rev-parse main^{tree})Choose the cutoff in ISO 8601 format. Include the offset so the boundary does not depend on the machine's local timezone:
CUTOFF='2026-01-01T00:00:00+00:00'Find the oldest first-parent commit after the cutoff:
FIRST=$(git rev-list --first-parent --reverse --since="$CUTOFF" main | sed -n '1p')Inspect that boundary before continuing:
git show --no-patch --format='%H%n%aI%n%s' "$FIRST"Stop if FIRST is empty or names a commit you did not intend to retain. Once the boundary is correct, record its parent and the parent's tree:
PARENT=$(git rev-parse "${FIRST}^")TREE=$(git rev-parse "${PARENT}^{tree}")Create a root commit from that tree. This is the baseline immediately before the retained commits begin:
BASE=$(printf '%s\n' 'Repository baseline before retained history' | GIT_AUTHOR_DATE="$CUTOFF" GIT_COMMITTER_DATE="$CUTOFF" git commit-tree "$TREE")Replay the cutoff window onto the new baseline:
git rebase --rebase-merges --committer-date-is-author-date --onto "$BASE" "$PARENT" mainRebase stops if a retained commit conflicts. Resolve the conflict only when you understand why it occurred; otherwise abort with git rebase --abort and return to the disposable clone's original state.
Compare the final tree with the value recorded before the rewrite:
test "$(git rev-parse main^{tree})" = "$ORIGINAL_TREE"A zero exit status means the current files, paths, modes, and blob contents are identical. Count what remains and inspect the rewritten boundary:
git rev-list --count maingit log --reverse --oneline main | sed -n '1,5p'Only after the tree comparison and project tests pass should you expire the disposable clone's reflogs and repack it:
git reflog expire --expire=now --allgit gc --prune=nowThis rewrites every retained commit ID because each commit ultimately points to the new baseline. Old commit links, signatures, pull-request diffs, and blame history before the cutoff no longer apply. Keep the mirror backup until the rewritten remote and a fresh clone have both passed verification.
Remove large files from Git history with git-filter-repo
Warning — this rewrites Git history
git filter-repo changes commits throughout the repository, which changes commit IDs and can remove files, commits, branches, tags, and signatures depending on the filter. Run it only on the disposable clone created earlier, and keep the backup until the cleanup is complete.
Do not push the rewritten history yet. First verify that the files you intended to remove are gone and that the project still contains everything it needs. Replacing the remote history later requires coordination with everyone using the repository.
All examples below assume you are inside a fresh disposable clone such as repo-cleanup.
Remove one file from every commit
--invert-paths means remove the listed paths instead of keeping only them:
git filter-repo --path path/to/large-file.zip --invert-pathsRemove a directory from history
Pass a trailing slash when you want every file under that directory removed:
git filter-repo --path backups/ --invert-pathsRemove multiple files or directories
List every path to drop in one command:
git filter-repo --path backups/ --path builds/release.iso --invert-pathsAfter removing backups/, builds/release.iso, and both historical names of a renamed vendor bundle, .git dropped from about 116M to about 16M in testing.
Remove every blob above a size limit
When many unrelated large files are present, you can remove blobs above a size threshold instead of listing every path:
git filter-repo --strip-blobs-bigger-than 50MThis is useful when many large files slipped into history, but it can remove blobs you did not review individually. Prefer explicit --path filters when you need precise control.
git-filter-repo removes the origin remote as a safety measure so you do not accidentally push rewritten history to the wrong place. That is expected.
If the large file was renamed or moved
git-filter-repo does not follow historical renames automatically. If analysis shows the same blob under two paths, include every historical name:
git filter-repo --path artifacts/archive.zip --path builds/vendor-bundle.zip --invert-pathsReview renames.txt and path-all-sizes.txt for older names or locations before filtering.
Verify the repository after filtering
Run analysis again after the rewrite:
git filter-repo --analyzeMeasure size again:
du -sh .git16M .gitCheck packed object totals after the rewrite:
git count-objects -vHcount: 0
size: 0 bytes
in-pack: 13
packs: 1
size-pack: 15.01 MiBConfirm the unwanted paths no longer appear in reachable history:
git log --all --oneline -- backups/db-dump-v1.sqlAn empty result confirms that the pathname no longer appears in the reachable history being checked. Also review the new --analyze reports to confirm the unwanted large blobs are no longer part of the rewritten history. For a path that had older names, check every historical pathname:
git log --all --oneline -- artifacts/archive.zip builds/vendor-bundle.zipConfirm the current tree still contains every file the project needs, then run your build or tests. A smaller .git folder is not success if you deleted required artifacts.
Publish the rewritten history safely
Consider merging or closing open pull or merge requests before replacing history. Rewritten commit IDs can invalidate their diffs and comments, and provider-specific PR or MR refs may continue referencing the old history.
git-filter-repo removes origin on purpose. After local verification, re-add the remote you intend to replace:
git remote add origin REPO_URLWhen you rewrote the branches and tags that should exist on the remote, push them back with force and prune refs that disappeared during filtering:
Danger — this replaces history on the remote repository
The command below can overwrite remote branches and tags and delete refs covered by the push when they no longer exist locally. Any new commits pushed by other users after you created the cleanup clone can be lost.
Before running it, stop writes to the repository, verify the rewritten clone and backup, resolve branch and tag protection, and make sure collaborators know they must switch to the new history.
git push --force --branches --tags --prune origin--force is required because existing remote refs still point at the old commit IDs. --prune removes destination refs covered by the push refspecs when their corresponding local refs no longer exist, so stale remote branches and tags are not left pointing to the old history.
If some protected refs reject the push while others succeed, the remote can be left with a mixture of old and rewritten history.
The required push strategy depends on which refs you rewrote. The command above fits the common case where you filtered the same branches and tags you plan to keep. It is not a universal recipe for every host or every cleanup.
GitHub
GitHub may take time to recalculate repository size after a force push. Forks, pull request refs, and cached objects can retain old data until GitHub garbage-collects them.
For comprehensive sensitive-data cleanup, GitHub's official procedure may use git push --force --mirror origin because that case aims to replace every writable ref. Protected or read-only refs can still reject the push and need provider-side handling. See GitHub: removing sensitive data from a repository.
GitLab
GitLab can retain objects behind internal refs even after you rewrite normal branches. Use GitLab's repository cleanup or purge workflows when storage metrics do not drop after a successful local rewrite. See GitLab: reducing repository size.
Hosting providers may keep internal refs that still reference old objects. A cleaned local clone and provider-reported repository storage are not always the same number immediately. Do not merge old history back into the rewritten default branch after the push.
When should you use Git LFS instead?
Use Git LFS when large binary files legitimately belong in the project but normal Git object storage is a poor fit. Examples include media assets, machine-learning models, and versioned binary deliverables.
Do not use LFS merely to hide accidental historical bloat. LFS storage is billed and managed separately from .git history. When migration to LFS is the actual goal, see git lfs migrate in the Git LFS documentation.
Removing secrets is a different problem
If you discover a password, token, or private key in history:
- Revoke or rotate the secret immediately.
- Remove or redact it from history.
- Coordinate cleanup of forks, clones, and provider caches.
Deleting the file from Git does not make an exposed secret safe again. Follow your provider's incident guidance, such as GitHub: removing sensitive data.
Prevent the repository from growing again
- Add generated artifacts, dumps, and build output to
.gitignore. - Store backups and release binaries in artifact or package repositories.
- Use Git LFS only where large binaries truly belong in version control.
- Reject oversized blobs in CI, pre-commit, or pre-receive hooks.
- Run
git filter-repo --analyzeperiodically on repositories that tend to accumulate binaries. - When old history has no operational value, retain a dated baseline and a recent commit window instead of carrying every historical blob indefinitely.
Common problems
| Problem | Reason | Fix |
|---|---|---|
| Repository did not shrink after deleting a file | Historical versions remain reachable | Remove the path from every commit with --invert-paths, not just the latest commit |
git gc did not help |
Large blobs are still reachable | Rewrite history or stop tracking the files going forward |
| Old versions of a renamed file remain | Historical path names were omitted | Include every old path from --analyze output |
| Local repo shrank but GitHub or GitLab did not | Server refs, forks, or delayed GC | Follow provider cleanup docs; wait for storage recalculation |
| Force push rejected | Protected branch or tag, or missing permission | Stop the rewrite push, resolve protection and permission requirements for all affected refs, then retry the coordinated update |
| Collaborator pushed old commits back | Mixed old and rewritten histories | Ask collaborators to re-clone; do not merge the old line |
| LFS quota did not shrink | LFS object storage is separate | Follow your hosting provider's LFS object-removal and storage-quota procedure |
git-filter-repo vs BFG vs filter-branch
| Tool | Recommendation |
|---|---|
git-filter-repo |
Recommended default for new history rewriting |
| BFG Repo-Cleaner | Specialized alternative, especially for large-file cleanup |
git filter-branch |
Legacy; do not use for new cleanup workflows |
git gc |
Maintenance only; not a replacement for removing reachable history |
Final verification checklist
Before you rewrite history:
- A mirror or other recovery copy exists
- Collaborators know a force push or re-clone may be required
- Large objects and paths are identified, including renamed paths
After filtering:
- Unwanted paths no longer appear in
git log --all -- <path> .gitsize andgit count-objects -vHlook reasonable- The project still builds or tests cleanly
- Required branches and tags are handled on the remote
- Provider storage has been rechecked when the host still reports old size
- Future large artifacts are excluded or moved to LFS or artifact storage
Summary
Measure .git with du and git count-objects -vH, then use git filter-repo --analyze to find large reachable blobs. Remove known paths with --invert-paths, strip reviewed oversized blobs, or replace old ancestry with a baseline plus a recent commit window. Verify the current tree and the smaller repository locally before you re-add origin and coordinate a force push.
git gc alone cannot remove blobs that commits still reference. Git LFS, provider cleanup tools, and prevention rules handle different parts of the problem.
References
- git-filter-repo documentation
- git-filter-repo installation notes
- git-filter-repo FAQ
- git count-objects documentation
- git gc documentation
- Git LFS
- GitHub: removing sensitive data from a repository
- GitLab: reducing repository size
Frequently Asked Questions
Why is my Git repository still large after deleting a large file?
Deleting the file from the latest commit leaves older blobs in history. Remove the path from every commit with git filter-repo --path <path> --invert-paths on a fresh clone, or stop tracking the file going forward if you do not need to rewrite history.
How do I find the largest files in Git history?
Run git filter-repo --analyze and read .git/filter-repo/analysis/blob-shas-and-paths.txt and path-all-sizes.txt.
Does git gc reduce repository size?
git gc reclaims unreachable objects and repacks data. It cannot remove large files that branches and tags still reference. Use history rewriting when old commits still store the blobs you want gone.
How do I remove a large file from all Git commits?
Clone REPO_URL into a disposable directory, then run git filter-repo --path path/to/large-file --invert-paths. Verify with git log --all -- path/to/large-file before you push rewritten history.
How do I reduce the size of the .git folder?
Find large reachable blobs, remove them with git-filter-repo on a fresh clone, then measure again with du -sh .git and git count-objects -vH.
Should I use git filter-repo or git filter-branch?
Use git-filter-repo for new work. Git documents filter-branch as slow, hard to use safely, and unsuitable for new history-rewriting tasks.
Does git-filter-repo change commit hashes?
Yes. Rewriting history creates new commit IDs (object IDs). Tags and signed commits are affected the same way.
Why did my local repository shrink but GitHub or GitLab still shows the old size?
Provider metrics, internal refs, forks, and delayed garbage collection can lag behind your local clone. Follow the host's repository cleanup guidance and allow time for storage recalculation.
Should I use Git LFS for large files?
Use Git LFS when large binaries legitimately belong in the project. Do not treat LFS as a substitute for cleaning accidental historical bloat.
Can I reduce repository size without rewriting Git history?
Only when wasted space comes from unreachable objects or packing, or when you only need to stop tracking large files going forward. Removing reachable old blobs requires changing the commits that reference them.
Can I keep only the last month of Git history?
Yes. Create a root baseline from the tree immediately before the cutoff, then rebase the commits after that date onto the baseline. Verify that the final tree ID matches the original branch before you publish the rewritten history.

