Git
filter-branch
empty changeset
commits
version control
Git - remove commits with empty changeset using filter-branch
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
## Introduction
Git is a powerful version control system that tracks changes in files and facilitates collaboration among developers. Sometimes, however, commits in a Git repository include no actual file changes (empty changesets). These commits clutter the history and can cause confusion during code review, bisecting, or log analysis. This article explains how to remove such commits using `git filter-branch` and the more modern `git-filter-repo`, providing technical context and practical examples.
## Understanding Commits with Empty Changesets
In Git, a commit represents a set of changes applied to a repository. Each commit is uniquely identified by a SHA-1 hash and contains metadata such as the author, a timestamp, and a commit message. An empty changeset occurs when a commit does not introduce any changes to the content of files within the repository. These can arise from several scenarios:
* A mistaken commit after resolving merge conflicts where the resolution matches one parent exactly.
* Automated commits from CI/CD pipelines that ran without meaningful changes.
* Commits created with `git commit --allow-empty` for tagging or documentation purposes.
* Squash merges or cherry-picks that result in no diff when the changes were already present.
## Identifying Empty Commits
Before removing empty commits, you should identify them. Use this command to list commits that introduce no file changes:
```bash
git log --oneline --all --diff-filter=d --name-only | \
grep -B1 "^$" | grep -v "^$" | grep -v "^--$"
```
A more reliable approach uses `git diff-tree`:
```bash
git rev-list --all | while read commit; do
if [ -z "$(git diff-tree --no-commit-id --name-only -r $commit)" ]; then
echo "Empty: $commit $(git log --format='%s' -1 $commit)"
fi
done
```
This iterates over every commit and checks whether `git diff-tree` produces any file output. If it produces nothing, the commit has an empty changeset.
## Using `git filter-branch` to Remove Empty Commits
`git filter-branch` rewrites commit history by applying a filter to each commit. To remove empty commits, use the `--commit-filter` option:
```bash
git filter-branch --commit-filter '
if [ -z "$(git diff-tree --no-commit-id --name-only -r $GIT_COMMIT)" ]; then
skip_commit "$@"
else
git commit-tree "$@"
fi
' -- --all
```
### How This Works
* `--commit-filter`: Runs a shell script for each commit being rewritten.
* `git diff-tree --no-commit-id --name-only -r $GIT_COMMIT`: Checks whether the commit identified by `$GIT_COMMIT` has any file changes compared to its parent.
* `skip_commit "$@"`: Tells Git to exclude this commit from the rewritten history, connecting its parent(s) directly to its child commits.
* `git commit-tree "$@"`: Keeps the commit in the history if it does have changes.
* `-- --all`: Applies the filter to all branches.
### Important Caveats
* **Backup first**: Create a backup branch or clone before running `filter-branch`. The operation is destructive and rewrites history.
```bash
git branch backup-before-filter
```
* **Clean up refs**: After running `filter-branch`, Git stores the original refs under `refs/original/`. Remove them when you are satisfied with the result:
```bash
git for-each-ref --format='%(refname)' refs/original/ | \
xargs -n1 git update-ref -d
```
* **Garbage collect**: Run `git gc --prune=now` to reclaim space from the removed commits.
## Using `git-filter-repo` (Recommended Alternative)
Git officially recommends `git-filter-repo` over `filter-branch` for new work. It is faster, safer, and more user-friendly.
Install it:
```bash
pip install git-filter-repo
```
Remove empty commits:
```bash
git filter-repo --prune-empty always
```
The `--prune-empty` flag with the `always` option removes any commit that has no file changes after filtering. This single command replaces the entire `filter-branch` script above.
### Advantages of `git-filter-repo`
| Feature | `filter-branch` | `git-filter-repo` |
| --------------- | -------------------------------- | -------------------------------- |
| Speed | Slow (processes each commit sequentially) | Fast (optimized rewriting engine) |
| Safety | No built-in backup | Refuses to run on dirty repos, warns about remote state |
| Simplicity | Requires shell scripting | Declarative flags |
| Maintenance | Deprecated in Git docs | Actively maintained |
## Impact on Collaboration
Rewriting history changes commit hashes for every commit that comes after a removed commit. This means:
* **Force push required**: You must use `git push --force` (or `--force-with-lease` for safety) to update remote branches.
* **Collaborators must re-sync**: Existing clones and forks will have divergent history. Collaborators need to either re-clone or carefully rebase their local work onto the new history.
* **Notify your team**: Always inform collaborators before rewriting shared history.
## When to Keep Empty Commits
Not all empty commits should be removed. Some legitimate uses include:
* **Merge commits**: Merge commits may have empty diffs when the merge was a fast-forward. These are part of the branch topology and should typically be preserved.
* **Intentional markers**: Some workflows use `--allow-empty` commits to mark releases, trigger CI pipelines, or record decisions.
* **Revert-of-revert**: A commit that reverts a revert may appear empty relative to the grandparent but carries important intent.
## Summary
Empty commits clutter Git history and can confuse developers during code review and debugging. The traditional approach uses `git filter-branch --commit-filter` with `git diff-tree` to identify and skip empty commits during history rewriting. The modern and recommended approach uses `git-filter-repo --prune-empty always`, which is faster and safer. Always back up your repository before rewriting history, and coordinate with collaborators since all downstream commit hashes will change.

