How do you use git filter-repo to rewrite repository history at scale, and how does it compare to the deprecated git filter-branch?
Quick Answer
git filter-repo rewrites history by exporting the repo as a fast-import stream, applying transformations (path removal, text replacement, author rewriting), and reimporting into a clean repository. It is 10-100x faster than filter-branch, handles edge cases correctly, and is the officially recommended replacement. All downstream commit hashes change, requiring team-wide re-cloning and credential rotation for removed secrets.
Detailed Answer
Think of git filter-repo as performing surgery on a patient's entire medical record. You can remove references to a specific medication (delete files), redact social security numbers (replace text), correct misspelled doctor names (fix author info), or extract one department's records into a standalone file (subdirectory extraction). But every page that changes gets a new certification number (SHA hash), and every page that references a changed page also gets a new number. By the end, you have a parallel medical history where everything is consistent but nothing matches the original numbering.
git filter-repo (installed via pip install git-filter-repo) processes the entire repository through Git's fast-export/fast-import protocol. Fast-export serializes every blob, commit, tag, and ref into a text stream. Filter-repo applies user-specified transformations to this stream (path filtering, content replacement, author mapping, message editing), then feeds the modified stream into fast-import to create a new repository. This architecture is why it is dramatically faster than filter-branch, which checked out each commit individually, applied transformations in the working directory, and re-committed. A repository with 50,000 commits that takes 12 hours with filter-branch can complete in under 5 minutes with filter-repo.
Internally, filter-repo processes the fast-export stream in Python, providing both built-in operations (--path, --path-rename, --replace-text, --email-callback) and a full callback API for custom transformations. The --path <path> --invert-paths combination removes a specific path from all history. The --replace-text flag takes a file of expressions mapping old strings to replacements, scanning every blob. The --subdirectory-filter extracts a subdirectory into the repository root, useful for splitting monorepos. After rewriting, filter-repo stores the old-to-new commit hash mapping in .git/filter-repo/commit-map, which is essential for updating external references (JIRA tickets, CI logs, wiki links). It also removes the remote origin by default to prevent accidental pushes of the original unrewritten refs.
The most common production use cases are: removing accidentally committed secrets (API keys, passwords, certificates), excising large binary files that bloated the repository, correcting author identities after corporate email migrations, and extracting services from a monorepo into standalone repositories. After rewriting, the coordination burden is significant: all team members must delete local clones and re-clone (or run git fetch origin followed by git reset --hard origin/main for each branch), CI/CD pipelines must be updated, branch protection rules may need reconfiguration, and any external references to old commit SHAs become invalid. Most critically, any secrets that were removed must still be rotated because the old objects remain accessible via SHA on GitHub/GitLab until the platform runs garbage collection, and anyone who previously cloned the repository retains the old objects locally.
A critical gotcha: filter-repo refuses to run on a repository that is not a fresh clone, to prevent accidental data loss. Always work on a fresh clone: git clone --mirror then process. Create a backup before starting. Another trap: GitHub and GitLab cache repository data, so even after force-pushing the rewritten history, old objects may be accessible via direct SHA URLs for hours or days. Contact platform support for immediate purging if the removed content is highly sensitive. Also, filter-branch is not just slow but genuinely buggy: it mishandles edge cases involving merge commits with file renames, signed tags, and grafts. The Git project officially recommends filter-repo and warns against filter-branch in its man page. Never use filter-branch for new work.