Bulk-rewrite any part of git commit history: messages, authors, emails, dates, and more
Project description
git-rewrite
Bulk-rewrite any part of git commit history: messages, author names, emails, and more.
Why
git-filter-repo is powerful but low-level — you write raw Python callbacks and run them blind. git filter-branch is worse. Neither tells you what they'll touch before they touch it, neither asks for confirmation, and neither reminds you to force-push afterwards.
git-rewrite wraps both with a human-friendly CLI:
- preview first — see exactly which commits match before changing anything
- dry-run — validate the full command without rewriting a single commit
- safe pattern embedding — patterns are repr-encoded so backslashes and quotes can't break the generated code
- callback validation — custom scripts are syntax-checked and inspected for
process_commitbefore history is touched - escape hatch — when regex isn't enough, drop down to a plain Python file and get full access to every commit field
Uses git-filter-repo when available, falling back to git filter-branch.
Installation
pip install -e /path/to/git-rewrite
This installs the git-rewrite console script.
Shell completions
Tab-completion for subcommands, --field choices, and --refs (populated from git branch -a) via argcomplete:
pip install 'git-rewrite[completions]'
# bash (add to ~/.bashrc)
eval "$(register-python-argcomplete git-rewrite)"
# zsh (add to ~/.zshrc)
autoload -U bashcompinit && bashcompinit
eval "$(register-python-argcomplete git-rewrite)"
# fish (add to ~/.config/fish/config.fish)
register-python-argcomplete --shell fish git-rewrite | source
Usage
git-rewrite <command> [options]
Commands
preview — find matching commits (read-only)
git-rewrite preview "Co-Authored-By: Claude"
git-rewrite preview "Co-Authored-By: Claude" --limit 50
git-rewrite preview "Co-Authored-By: Claude" --refs main
# Machine-readable NDJSON output (one object per match, pipe to jq)
git-rewrite preview "Co-Authored-By: Claude" --format json
git-rewrite preview "Co-Authored-By: Claude" --format json | jq '.sha'
strip — remove matching lines from commit messages
# Diff-style dry-run: see exactly what would change before rewriting
git-rewrite strip "Co-Authored-By: Claude.*<noreply@anthropic\.com>" --preview
# Dry run first
git-rewrite strip --dry-run "Co-Authored-By: Claude.*<noreply@anthropic\.com>"
# Remove the lines
git-rewrite strip "Co-Authored-By: Claude.*<noreply@anthropic\.com>"
# Target a different field (requires git-filter-repo)
git-rewrite strip --field author-email "old@example\.com"
# Keep only conventional-trailer lines, strip the rest of the body
git-rewrite strip --invert "^[A-Z][a-z-]+: " --field message
Note: Using
stripon a date field (--field author-dateor--field committer-date) zeroes the field to an empty byte string, producing an invalid date. Usereplaceinstead to rewrite specific parts of the date value while keeping it valid.
replace — substitute a pattern with a replacement
# Diff-style dry-run: see before/after before rewriting
git-rewrite replace "Co-Authored-By: Claude Sonnet \d+\.\d+" "Co-Authored-By: AI" --preview
git-rewrite replace "Co-Authored-By: Claude Sonnet \d+\.\d+" "Co-Authored-By: AI"
git-rewrite replace --field author-name "Old Name" "New Name"
# Normalize timezone offsets to UTC (requires git-filter-repo)
# Date values are in raw format: "<unix-timestamp> <tz-offset>", e.g. "1700000000 -0700"
git-rewrite replace --field author-date "[-+]\d{4}$" "+0000"
git-rewrite replace --field committer-date "[-+]\d{4}$" "+0000"
run — execute a custom Python callback
git-rewrite run my_callback.py --dry-run
git-rewrite run my_callback.py --refs main feature/branch
preset — run a named preset from the repo config
# Run the preset as-is
git-rewrite preset strip-ai
# Dry-run a preset
git-rewrite preset strip-ai --dry-run
# Override a flag from the CLI (CLI always wins over preset)
git-rewrite preset strip-ai --refs main --yes
Common flags
| Flag | Description |
|---|---|
--dry-run |
Show what would happen without modifying history |
--yes / -y |
Skip the confirmation prompt |
--refs REF … |
Limit to specific refs (default: all) |
--field FIELD |
Field to target: message, author-name, author-email, committer-name, committer-email, author-date, committer-date (date fields require git-filter-repo) |
--case-sensitive |
Disable case-insensitive matching |
--preview |
(strip/replace) Diff-style preview of changes — no history rewritten |
--invert |
(strip) Keep only matches; strip everything else |
--format FORMAT |
(preview) text (default) or json (NDJSON, one line per match) |
--no-color |
Disable colored output (also honored via NO_COLOR env var) |
Scoping flags (strip, replace, preview)
These flags narrow which commits are previewed and counted. DATE accepts any format git log understands (2024-01-01, 6 months ago, yesterday, etc.).
Note: Scoping flags filter which commits are shown/counted, not which commits the rewrite callback runs against. The actual rewrite still processes every commit in the given refs.
| Flag | Description |
|---|---|
--since DATE |
Only consider commits more recent than DATE |
--until DATE |
Only consider commits older than DATE |
--author PATTERN |
Only consider commits whose author name/email matches PATTERN (regex) |
# Preview only Claude co-authorship lines from the last 6 months
git-rewrite preview "Co-Authored-By: Claude" --since "6 months ago"
# Count matches by a specific contributor
git-rewrite strip --dry-run "Co-Authored-By: Claude" --author "alice@example.com"
# Scope by both date range and author
git-rewrite replace "OldOrg" "NewOrg" --since 2024-01-01 --until 2025-01-01 --author "dev@oldorg.com"
Custom callbacks (run)
Create a .py file defining process_commit:
import re
def process_commit(commit):
"""Receives a git-filter-repo commit object. Modify in place."""
commit.message = re.sub(rb"Co-Authored-By: Claude.*\n?", b"", commit.message)
# Also available:
# commit.author_name, commit.author_email
# commit.committer_name, commit.committer_email
# commit.author_date, commit.committer_date
The tool validates syntax and the presence of process_commit before touching history.
Config file
Store default options and named presets in a per-repository config file so teams can run the same cleanup repeatedly without repeating flags.
Config locations (checked in order)
.git-rewrite.tomlin the repo root[tool.git-rewrite]section inpyproject.toml
Schema
# .git-rewrite.toml
# Top-level defaults — applied to every command unless overridden on the CLI
default_refs = ["main", "develop"]
case_sensitive = false
# Named presets — run with: git-rewrite preset <name>
[presets.strip-ai]
command = "strip"
pattern = "Co-Authored-By:.*Claude.*"
field = "message"
[presets.fix-email]
command = "replace"
pattern = "old@example\\.com"
replacement = "new@example.com"
field = "author-email"
The same schema works inside pyproject.toml under [tool.git-rewrite]:
[tool.git-rewrite]
default_refs = ["main"]
[tool.git-rewrite.presets.strip-ai]
command = "strip"
pattern = "Co-Authored-By:.*Claude.*"
Precedence
CLI flag → preset value → top-level config default → built-in default
Note on
case_sensitive:--case-sensitiveis a boolean flag with no inverse (--case-insensitive). If you setcase_sensitive = truein the config and need to override it tofalsefor a single run, edit the config temporarily or omit the key.
Config parse errors
A malformed TOML file or an unknown preset name exits immediately with a clear error message listing what went wrong and (for unknown presets) the available preset names.
After rewriting
Force-push the affected refs:
git push --force-with-lease --all
Requirements
- Python 3.10+
- git
- git-filter-repo (recommended;
pip install git-filter-repoorbrew install git-filter-repo)- Required for
--dry-runand non-message fields - Falls back to
git filter-branchfor message-only rewrites
- Required for
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file git_rewrite_history-0.1.2.tar.gz.
File metadata
- Download URL: git_rewrite_history-0.1.2.tar.gz
- Upload date:
- Size: 29.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
62d61d15f24ac168eae9493c3f5b6917806ae79bba7755ee106137323af59f69
|
|
| MD5 |
1c4c7d80259391670693a13576fc1017
|
|
| BLAKE2b-256 |
2a8b677c407e2dfb1c575db613e4e7432cb6cf895b728fbfc38492de92d7a5e4
|
Provenance
The following attestation bundles were made for git_rewrite_history-0.1.2.tar.gz:
Publisher:
release-please.yml on gfargo/git-rewrite
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
git_rewrite_history-0.1.2.tar.gz -
Subject digest:
62d61d15f24ac168eae9493c3f5b6917806ae79bba7755ee106137323af59f69 - Sigstore transparency entry: 2071736844
- Sigstore integration time:
-
Permalink:
gfargo/git-rewrite@df03999320b9557ce84ab74529bf94d877b6bace -
Branch / Tag:
refs/heads/main - Owner: https://github.com/gfargo
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release-please.yml@df03999320b9557ce84ab74529bf94d877b6bace -
Trigger Event:
push
-
Statement type:
File details
Details for the file git_rewrite_history-0.1.2-py3-none-any.whl.
File metadata
- Download URL: git_rewrite_history-0.1.2-py3-none-any.whl
- Upload date:
- Size: 18.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6135280b9056ef904aea9c2dc0b751f4fb3391bdcb9586ede23344e1da2efd7d
|
|
| MD5 |
a45a026c08ba2ffcad2066bc67e12111
|
|
| BLAKE2b-256 |
b715b26f86381dff96dee9542a78ddc7047793912b49502e0765d70e2799e514
|
Provenance
The following attestation bundles were made for git_rewrite_history-0.1.2-py3-none-any.whl:
Publisher:
release-please.yml on gfargo/git-rewrite
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
git_rewrite_history-0.1.2-py3-none-any.whl -
Subject digest:
6135280b9056ef904aea9c2dc0b751f4fb3391bdcb9586ede23344e1da2efd7d - Sigstore transparency entry: 2071736861
- Sigstore integration time:
-
Permalink:
gfargo/git-rewrite@df03999320b9557ce84ab74529bf94d877b6bace -
Branch / Tag:
refs/heads/main - Owner: https://github.com/gfargo
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release-please.yml@df03999320b9557ce84ab74529bf94d877b6bace -
Trigger Event:
push
-
Statement type: