Don't. Use git filter-repo. Listed for completeness.
git filter-branch rewrites history by replaying every commit through user-supplied filters. It is deprecated as of Git 2.24: slow, riddled with sharp edges, and the man page itself tells you to use git filter-repo instead. It is documented here so you recognize it in old scripts; do not write new ones.
Reads from refs and objects, writes a parallel set of rewritten objects, and updates refs to point at them. Backs up originals under refs/original/. The working tree and index are clobbered repeatedly during --tree-filter runs because it checks out every single commit.
git filter-branch [--setup <command>] [--subdirectory-filter <directory>]
[--env-filter <command>] [--tree-filter <command>]
[--index-filter <command>] [--parent-filter <command>]
[--msg-filter <command>] [--commit-filter <command>]
[--tag-name-filter <command>] [--prune-empty]
[--original <namespace>] [-d <directory>] [-f | --force]
[--state-branch <branch>] [--] [<rev-list options>...]| Flag | What it does |
|---|---|
--tree-filter <cmd> | ⚠️ Check out every commit, run <cmd> against the working tree, then re-stage. Catastrophically slow on real repos; this is the option that gives filter-branch its bad reputation. |
--index-filter <cmd> | Run <cmd> against the index without checking out files. Orders of magnitude faster than --tree-filter for path removals. |
--commit-filter <cmd> | ⚠️ Replace the default git commit-tree with <cmd>. Powerful and dangerous: any bug here corrupts every commit on the rewrite. |
--env-filter <cmd> | Modify environment variables (author/committer name, email, date) per commit. The standard way to fix bad identities. |
--subdirectory-filter <dir> | Treat <dir> as the new repo root, dropping everything else. Common for splitting a subproject out. |
--prune-empty | Drop commits that become empty after filtering. Almost always what you want when removing files from history. |
--tag-name-filter <cmd> | Rewrite tag names through <cmd>. Without this, tags keep pointing at the original commits and look ghostly. |
# Don't actually do this. Use `git filter-repo` (see recommendations).
$ git filter-branch --force --index-filter \
'git rm --cached --ignore-unmatch path/to/secret' \
--prune-empty --tag-name-filter cat -- --all$ git filter-branch --prune-empty --subdirectory-filter src/lib HEAD$ git filter-branch --env-filter '
if [ "$GIT_AUTHOR_EMAIL" = "old@example.com" ]; then
export GIT_AUTHOR_EMAIL="new@example.com"
export GIT_COMMITTER_EMAIL="new@example.com"
fi
' --tag-name-filter cat -- --branches --tags$ pip install git-filter-repo
$ git filter-repo --path path/to/secret --invert-paths
$ git filter-repo --subdirectory-filter src/lib
$ git filter-repo --mailmap mailmap.txtgit filter-repo (https://github.com/newren/git-filter-repo). It is faster, safer, has saner defaults, and is the tool the Git project itself recommends.--index-filter over --tree-filter. The latter is hours-vs-seconds slower because it checks out every single commit.refs/original/. Until you delete that namespace and run git gc, the "deleted" content is still in the repo and pushable.--tree-filter on a repo with thousands of commits can take literal hours and burn through SSDs. Reach for --index-filter or, better, filter-repo.Hit each option, then Check answers. Score is recorded; Next is always open.