gitreforge rewrites Git history in bare or mirrored repositories. Use it when you
need to clean sensitive files out of history, fix author or committer identities,
or remove commits that became empty after a rewrite.
The tool works directly on Git objects and refs. Because every rewrite creates new commit hashes, run it on a mirror clone or another disposable copy first, inspect the result, and then force-push the rewritten refs when you are ready.
- List every author and committer identity found in the repository.
- Rewrite author and committer names and email addresses in existing commits.
- Remove files, directories, or path patterns from every commit.
- Prune non-merge commits whose tree is identical to their parent.
- Re-sign selected rewritten commits with SSH commit signatures.
- Preview changes with
--dry-runbefore modifying the repository.
gitreforge is built for high-throughput history rewrites and performs
significantly better than most general-purpose alternatives on large
repositories.
That performance comes from working directly with Git objects and refs instead
of driving each change through Git commands. gitreforge reads pack files through
memory-mapped I/O, parallelizes rewrite work, uses a release allocator tuned for
throughput, and avoids external command execution in the core rewrite and
signing paths. Rewritten objects go into a single new pack file, with trees
stored as deltas, and the refs into packed-refs, instead of creating one file
per object and ref.
Wall-clock time to rewrite the full history of a mirror clone, compared with git filter-repo 2.47 and BFG Repo-Cleaner 1.15:
| Repository | Task | gitreforge | git filter-repo | BFG |
|---|---|---|---|---|
| tokio (24k commits) | Remove tokio-util/ |
0.26 s | 2.0 s (7.6×) | 5.1 s (19×) |
| tokio (24k commits) | Remove tokio-*/ (wildcard) |
0.27 s | 2.1 s (7.7×) | 8.6 s (32×) |
| git (205k commits) | Remove Documentation/ |
2.8 s | 69 s (25×) | fails after 94 s¹ |
| git (205k commits) | Remove every Makefile |
5.5 s | 78 s (14×) | fails after 142 s¹ |
| git (205k commits) | Remove every *.po file (regex) |
5.3 s | 72 s (14×) | 103 s (20×) |
| git (205k commits) | Rename an author (7 identities) | 0.86 s | 61 s (71×) | not supported |
¹ BFG aborts with a NullPointerException at 95% of the commits on two old
tags in git/git that have no tagger line, and leaves all refs unchanged.
All tools only rewrite history: no git gc or repack, and empty commits are
kept, so every tool that finishes produces the same trees and identities. Measured with
hyperfine on an AMD Ryzen Z2 Extreme
with NVMe storage. Reproduce with bench/rewrite.sh <scenario>; the script
downloads the other tools and lists the available scenarios.
The other commands on the same git/git mirror:
| Task | gitreforge | Reference |
|---|---|---|
contributor list |
0.43 s | git log --all piped into sort -u: 1.17 s (2.7×) |
--dry-run remove --directory /Documentation |
2.3 s | the same rewrite without --dry-run: 3.0 s |
prune-empty after removing Documentation/ |
1.43 s | git filter-repo --prune-empty always: 68 s (48×)² |
² gitreforge removes 11,485 commits, filter-repo 11,292: every commit
filter-repo removes, plus merges that merge nothing once pruning makes one
parent an ancestor of the other.
Reproduce with bench/features.sh after bench/rewrite.sh git-docs.
Build from source with Cargo:
cargo build --releaseThe binary is written to target/release/gitreforge.
Start from a mirror clone so tags, branches, and refs are available without using a working tree:
git clone --mirror git@example.com:org/project.git project.git
cd project.gitRun gitreforge against the mirror repository:
gitreforge . contributor list
gitreforge --dry-run . remove --file secrets.env
gitreforge . prune-emptyAfter a successful rewrite, validate the repository with normal Git tooling. When the result is correct, push the rewritten refs according to your repository hosting policy.
List all identities:
gitreforge /path/to/project.git contributor listRewrite identities by piping mappings into contributor rewrite. Each input
line maps the full old identity to the full new identity:
cat mappings.txt | gitreforge /path/to/project.git contributor rewritemappings.txt:
Old Name <old@example.com> = New Name <new@example.com>
Another Old Name <old2@example.com> = Another New Name <new2@example.com>
Only commits matching the old author or committer identity are changed.
Remove a specific file everywhere it appears:
gitreforge /path/to/project.git remove --file secrets.envPaths that start with / are absolute and only match at that location.
Other paths match at any depth: config/secrets.env removes
config/secrets.env and app/config/secrets.env, but not
myconfig/secrets.env.
Remove a directory:
gitreforge /path/to/project.git remove --directory vendor/privateUse a regular expression when the simpler file or directory matchers are not
enough. It is matched against the full path of each file, which starts with a
/ (/src/debug-1.log):
gitreforge /path/to/project.git remove --regex '(^|/)debug-[^/]+\.log$'
gitreforge /path/to/project.git remove --regex '^/logs/.*\.log$'You can pass each matcher more than once, and you can combine matcher types in one command. Directories that are left without any files are removed as well.
After removing paths, some commits may no longer change the tree. Remove those non-merge commits with:
gitreforge /path/to/project.git prune-emptyMerge commits are preserved, unless pruning leaves them with only a single parent: a merge of a branch that only had empty commits is pruned like any other empty commit.
History rewrites invalidate existing commit signatures. gitreforge can add new SSH
signatures to rewritten commits whose committer email matches
--sign-committer.
Configure Git for SSH signing before running the rewrite:
git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pubThen select the committer email addresses that should be signed:
gitreforge --sign-committer alice@example.com /path/to/project.git remove --file secrets.env
gitreforge --sign-committer alice@example.com --sign-committer bob@example.com /path/to/project.git prune-emptySigning is applied by committer email, not author email. Commits with other committer emails are rewritten without a new signature. Commits that the rewrite doesn't change keep their id and their existing signature.
Supported signing configuration:
- The configuration is read from the repository's
configand the global git configuration ($GIT_CONFIG_GLOBAL, or$XDG_CONFIG_HOME/git/configand~/.gitconfig). gpg.formatmust bessh.user.signingkeymay point to a public key file, a private OpenSSH key file, an inlinessh-...public key, or akey::ssh-...value.- If
user.signingkeyis a public key or omitted, signing uses the first suitable key fromSSH_AUTH_SOCK. - Private key files are read and used directly. Encrypted private keys must be added to the SSH agent instead.
Each commit's id depends on the signature of its parent, so the signatures are made one after another. An Ed25519 key file is the fastest option, about 10 µs per commit; the SSH agent needs about 100 µs per commit, RSA keys a few milliseconds.
Current limitations:
- OpenPGP signing is not supported.
gpg.ssh.programis not supported becausegitreforgedoes not execute external signing commands.- SSH agent signing currently requires a Unix socket.
Add --dry-run before the repository path to verify which operations can run
without writing rewritten objects or updating refs:
gitreforge --dry-run /path/to/project.git remove --directory generatedgitreforge [OPTIONS] [REPOSITORY] <COMMAND>
REPOSITORY is the path to a bare or mirrored repository. If omitted, gitreforge
uses the current directory.
Every rewrite writes the old and new id of each changed commit to
object-id-map.old-new.txt in the current directory.
Global options:
-d,--dry-run: do not change the repository.--sign-committer <EMAIL>: re-sign rewritten commits whose committer email matches this value. Can be specified multiple times.
Commands:
contributor list: list all author and committer identities.contributor rewrite: rewrite identities from stdin mappings in the formOld User <old@example.com> = New User <new@example.com>. Blank lines are ignored.remove --file <FILE>: remove matching files from history.*can be used as a wildcard at the beginning or end.remove --directory <DIRECTORY>: remove matching directories from history.*can be used as a wildcard at the beginning or end.remove --regex <REGEX>: remove files whose full repository path, starting with/, matches the regular expression.prune-empty: remove empty non-merge commits.
- Do not run
gitreforgeon a repository with a working copy. - Keep a backup or fresh mirror clone until you have verified the rewritten history.
- Coordinate force-pushes with other users of the repository.
- Treat old clones, forks, pull requests, and CI caches as possible places where removed data can still exist after a history rewrite.
gitreforge is free for personal use, hobby projects, research and noncommercial organizations under the PolyForm Noncommercial License 1.0.0. It is source-available rather than open source: commercial use needs a separate license.