A Git repository can look small from the outside while hiding a massive .git directory underneath. If you want to keep Git repositories small, you need to look beyond the files in your working directory. Git also stores previous versions, deleted files, branches, tags, and recovery data.
I recently ran into exactly this situation. The working tree looked fine, but the .git directory had grown far beyond what I expected. Basic Git commands became slower and the repository consumed several gigabytes of disk space.
After inspecting it, the real issue was not the checked-out files. It was the Git object database.
Why keeping Git repositories small matters
A large repository affects more than local disk space. It can make cloning and fetching slower, increase CI build times, and waste storage on every developer machine and build agent.
The important detail is that deleting a large file in a new commit does not remove it from older commits. Git is designed to preserve history, so the object can remain in the repository until that history is rewritten and the unreachable data is eventually cleaned up.
Why Git repositories become huge
Most oversized repositories are caused by one or more of the following issues.
Large files were committed at some point
This is the most common cause. Even if a large file was later deleted, Git can still keep it in history.
Common examples include:
- database dumps
- ZIP archives
- video files
- generated build output
node_modules- compiled binaries
- design exports
- backup files
Generated files ended up in version control
Build output should almost never be committed. If your repository tracks folders such as dist, build, bin, obj, or package directories, it can grow quickly.
Loose objects have not been packed yet
Git initially stores many objects as individual files. Maintenance tasks later combine them into pack files, which are usually more space-efficient and faster for Git to process.
This is not always a sign that something is wrong. However, thousands of large loose objects can consume a surprising amount of space.
Reflogs and unreachable objects retain old states
Git intentionally keeps references to previous states so you can recover commits after operations such as a reset or rebase. This recovery data is useful, but it can temporarily retain objects that are no longer part of a branch.
The project contains many changing binary files
Git works best with source code and other text-based files. It is less efficient when large binary files change frequently, because it cannot always store their differences as efficiently as it can with text.
Inspect the repository before cleaning it
Do not start by deleting files from .git. First find out where the space is being used.
Check the total size of the Git directory:
du -sh .git
On PowerShell, you can use:
(Get-ChildItem .git -Recurse -Force | Measure-Object -Property Length -Sum).Sum / 1GB
Next, inspect the Git object database:
git count-objects -vH
You might see output similar to this:
count: 3786
size: 5.72 GiB
in-pack: 3228
packs: 1
size-pack: 6.22 MiB
In this example, count is the number of loose objects and size is the disk space they consume. size-pack shows how much space is used by existing pack files. The large difference tells you that loose objects deserve further investigation.
This command shows where the space is used, but not which files caused it.
Find the largest objects in Git history
My preferred way to investigate a suspicious repository is git filter-repo. Install it first, then run:
git filter-repo --analyze
This does not rewrite your repository. It creates reports in:
.git/filter-repo/analysis/
The reports show the largest blobs, paths, directories, and extensions in the repository history. Start with files such as:
blob-shas-and-paths.txt
path-all-sizes.txt
extensions-all-sizes.txt
This gives you evidence before deciding whether normal maintenance is enough or whether history needs to be rewritten.
How to keep Git repositories small
1. Never commit generated output
Your repository should contain source files, not artifacts that can be recreated.
Good candidates for .gitignore include:
bin/
obj/
dist/
build/
node_modules/
coverage/
.env
*.log
*.zip
*.tar
*.gz
*.mp4
*.psd
*.bak
The exact list depends on your stack. GitHub maintains a useful collection of gitignore templates for common platforms and development tools.
Adding a path to .gitignore does not stop tracking a file that has already been committed. To keep the local file but remove it from the Git index, use:
git rm --cached path/to/file
For a tracked directory:
git rm -r --cached dist/
git commit -m "Stop tracking generated output"
This removes the file from future commits, but it does not remove earlier versions from Git history.
2. Review staged files before committing
Many oversized repositories start with one accidental commit. A backup archive is copied into the project, or a generated export is included without anyone noticing.
Before committing, check what Git is about to store:
git status
git diff --cached --stat
I find the second command especially useful when a commit contains more files than expected. It provides a quick overview without showing the complete diff.
3. Use Git LFS for large binary assets
If a project genuinely needs large binary files, consider Git Large File Storage. Git LFS keeps small pointer files in Git and stores the binary content separately.
Typical examples include:
- media files
- large design assets
- machine learning models
- large exported documents
- game assets
For example:
git lfs install
git lfs track "*.psd"
git add .gitattributes
Git LFS is most effective when configured before large files enter normal Git history. Tracking an extension today does not automatically migrate older files. Existing history may need to be migrated separately, which is also a history rewrite.
Check the storage, bandwidth, and pricing limits of your Git hosting provider before adopting Git LFS.
4. Run normal Git maintenance first
If the repository contains many loose objects, start with standard garbage collection:
git gc
You can also run Git’s maintenance tasks:
git maintenance run
These commands can pack objects more efficiently, but they do not remove large files that are still reachable from commits, branches, or tags. They optimize the existing repository rather than rewriting its history.
After maintenance, measure again:
du -sh .git
git count-objects -vH
5. Treat aggressive cleanup as a last resort
You may find commands online such as:
git reflog expire --expire=now --all
git gc --aggressive --prune=now
Do not use these as routine maintenance. Expiring all reflogs removes useful recovery options. According to the Git documentation, --prune=now also increases the risk of corruption if another process is writing to the repository at the same time.
Only consider this type of cleanup when you understand what will be removed, have a verified backup, and have closed IDEs, CI jobs, and other processes that could write to the repository. In most cases, regular git gc is the better first step.
6. Store artifacts outside Git
Git is version control, not a general backup drive. Store database dumps, screenshots, installer packages, and generated exports in a system designed for them, such as:
- cloud storage
- artifact storage
- package feeds
- release attachments
- backup systems
This keeps clones smaller and prevents every contributor and build agent from downloading files they do not need.
7. Monitor repository growth
A repository rarely becomes gigantic overnight unless a very large file is committed. More often, it grows gradually until cloning or fetching becomes painful.
Run these commands occasionally:
du -sh .git
git count-objects -vH
If the .git directory grows much faster than the working tree, investigate it early.
Remove a large file from Git history
If a large file was committed in the past, deleting it from the latest commit is not enough. To permanently remove it from Git history, you need to rewrite that history.
First, create a fresh backup or mirror clone. Then you can remove a specific path with git filter-repo:
git filter-repo --path path/to/large-file.zip --invert-paths
Before doing this in a shared repository, keep the following consequences in mind:
- commit hashes will change
- open pull requests may be affected
- signed commits and tags may no longer validate
- you will normally need to force-push the rewritten branches and tags
- everyone using the repository should coordinate the change
- existing clones should preferably be replaced with fresh clones
Do not force-push a rewritten repository until you have checked the result locally and agreed on a migration moment with the team.
Also be aware that the repository may not become smaller on the hosting platform immediately. GitHub, Azure DevOps, and other platforms run their own server-side cleanup and may temporarily retain old objects.
Practical checklist
This is the checklist I now use:
- commit source code, not generated artifacts
- keep
.gitignoreup to date - remember that
.gitignoredoes not remove tracked files or history - review staged files before committing
- use Git LFS when large binary files genuinely belong with the project
- use
git filter-repo --analyzebefore rewriting anything - run normal Git maintenance before aggressive cleanup
- create a backup before rewriting history
- coordinate history rewrites with the entire team
- measure the repository again after every cleanup step
End Note
Git repositories usually become gigantic for understandable reasons. Git is doing exactly what it was designed to do: remembering previous versions of your project.
The best solution is prevention. Ignore generated output, review what you commit, and keep large binaries outside normal Git history. If a repository is already suspiciously large, measure it first and identify the largest objects before deciding what to remove.
That approach worked for me because it separated two very different problems: inefficiently stored loose objects and large files that were genuinely part of the history. The first can often be improved with maintenance. The second usually requires a carefully planned history rewrite.
You may also find these articles useful:
- From Hugo to Astro: How I Rebuilt My Blog and Automated Publishing with Azure Pipelines
- Publish an Azure Web App from JetBrains with a Simple Script
- JetBrains Rider Cheatsheet for Windows and macOS
Try the inspection commands on one of your own repositories first. If you find an unexpected cause, let me know what it was.

