Overview
Commit message rewriting allows you to fix typos, update references, add metadata, or standardize formatting across your entire repository history.
Replace Text in Messages
Basic Replacement
Replace text in commit and tag messages:
Create expressions.txt
Run filter-repo
# Fix typo
teh==>the
recieve==>receive
# Update project name
old-project-name==>new-project-name
# Fix issue tracker references
bug#==>issue#
git filter-repo --replace-message expressions.txt
--replace-message uses the same syntax as --replace-text, but only applies to commit and tag messages, not file contents.
Replacement Syntax
Supports literal text, globs, and regex:
# Simple text replacement
foo==>bar
OldCo==>NewCo
Using Message Callback
Add Signed-off-by or other metadata:
git filter-repo --message-callback '
# Add Signed-off-by if missing
if b"Signed-off-by:" not in message:
message += b"\n\nSigned-off-by: John Doe <[email protected]>"
return message
'
Standardize commit message format:
git filter-repo --message-callback '
lines = message.split(b"\n")
# Ensure first line is capitalized
if lines and len(lines[0]) > 0:
lines[0] = lines[0][0:1].upper() + lines[0][1:]
# Remove trailing whitespace from all lines
lines = [line.rstrip() for line in lines]
# Ensure blank line between subject and body
if len(lines) > 1 and lines[1] != b"":
lines.insert(1, b"")
return b"\n".join(lines)
'
Update Issue References
Convert between issue tracking systems:
git filter-repo --message-callback '
# Convert #123 to JIRA-123
message = re.sub(b"#(\\d+)", b"JIRA-\\1", message)
# Convert "Fixes #123" to "Closes JIRA-123"
message = re.sub(b"Fixes #(\\d+)", b"Closes JIRA-\\1", message)
return message
'
Rewriting Commit Hashes
Automatic Hash Updates
By default, git-filter-repo updates commit hash references in messages:
# Before filtering:
"Reverts commit abc123def"
# After filtering (if abc123def was rewritten to xyz789abc):
"Reverts commit xyz789abc"
Automatic hash rewriting:
- Preserves abbreviated hash length
- Only updates hashes of commits being rewritten
- Skips hashes that don’t exist or were pruned
Disable Hash Rewriting
Prevent automatic hash updates:
git filter-repo --preserve-commit-hashes \
--replace-message expressions.txt
Disabling hash rewriting means commit references in messages will be incorrect after filtering.
Encoding Handling
Default Behavior
By default, git-filter-repo re-encodes commit messages to UTF-8:
# Commit with encoding=iso-8859-1 will be converted to UTF-8
git filter-repo ...
Preserve Original Encoding
Keep commit message encodings unchanged:
git filter-repo --preserve-commit-encoding \
--replace-message expressions.txt
If a commit message isn’t valid in its specified encoding, git won’t re-encode it (it will leave both the message and encoding header unchanged).
Complex Examples
Enforce Conventional Commits format:
git filter-repo --message-callback '
lines = message.split(b"\n")
if not lines:
return message
subject = lines[0]
# Add type if missing
if not re.match(b"^(feat|fix|docs|style|refactor|test|chore):", subject):
# Guess type based on content
if b"test" in subject.lower():
subject = b"test: " + subject
elif b"doc" in subject.lower():
subject = b"docs: " + subject
elif b"fix" in subject.lower():
subject = b"fix: " + subject
else:
subject = b"chore: " + subject
lines[0] = subject
return b"\n".join(lines)
'
Add Issue Links
Automatically link to issue tracker:
git filter-repo --message-callback '
# Convert "Fixes #123" to "Fixes #123\nSee: https://github.com/user/repo/issues/123"
def add_link(match):
issue = match.group(1)
url = b"https://github.com/user/repo/issues/" + issue
return match.group(0) + b"\nSee: " + url
message = re.sub(
b"(Fixes|Closes|Resolves) #(\\d+)",
add_link,
message
)
return message
'
Redact sensitive data from commit messages:
cat > redact.txt <<EOF
# Remove passwords
regex:password\s*[:=]\s*\S+==>[REDACTED]
# Remove API keys
regex:api[_-]?key\s*[:=]\s*\S+==>[REDACTED]
# Remove emails
regex:[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}==>[EMAIL]
EOF
git filter-repo --replace-message redact.txt
Prefix All Commits
Add a prefix to all commit messages:
git filter-repo --message-callback '
prefix = b"[LEGACY] "
if not message.startswith(prefix):
message = prefix + message
return message
'
Clean Up Merge Messages
Standardize merge commit messages:
git filter-repo --commit-callback '
# Only process merge commits
if len(commit.parents) <= 1:
return
# Standardize merge message format
lines = commit.message.split(b"\n")
if lines[0].startswith(b"Merge "):
# Extract branch name if possible
match = re.search(b"Merge branch \'([^\']+)\'", lines[0])
if match:
branch = match.group(1)
commit.message = b"Merge branch %s\n\nMerged %s into current branch." % (branch, branch)
'
Tag Messages
Modify Tag Messages
Tag messages are modified with --replace-message too:
git filter-repo --replace-message expressions.txt
Or use --tag-callback for more control:
git filter-repo --tag-callback '
# Add release notes link
tag.message += b"\n\nRelease notes: https://example.com/releases/" + tag.ref
'
git filter-repo --tag-callback '
if tag.tagger_name:
tag.message += b"\n\nTagged by %s <%s> on %s" % (
tag.tagger_name,
tag.tagger_email,
tag.tagger_date
)
'
Multi-Line Messages
Handle messages with multiple paragraphs:
git filter-repo --message-callback '
lines = message.split(b"\n")
# Capitalize first line
if lines:
lines[0] = lines[0].capitalize()
# Wrap long lines in body (after blank line)
result = []
in_body = False
for line in lines:
if not line.strip():
in_body = True
result.append(line)
elif in_body and len(line) > 72:
# Simple word wrap
words = line.split()
current = b""
for word in words:
if len(current) + len(word) + 1 > 72:
result.append(current)
current = word
else:
current += b" " + word if current else word
if current:
result.append(current)
else:
result.append(line)
return b"\n".join(result)
'
Common Patterns
Fix Common Typos
regex:\bteh\b==>the
regex:\brecieve\b==>receive
regex:\boccured\b==>occurred
regex:\bseperate\b==>separate
regex:\bdefinately\b==>definitely
git filter-repo --replace-message typos.txt
Update Project URLs
http://old-site.com==>https://new-site.com
github.com/olduser/repo==>github.com/newuser/repo
git filter-repo --replace-message urls.txt
Remove Debug Messages
git filter-repo --message-callback '
# Remove lines starting with DEBUG:
lines = message.split(b"\n")
lines = [l for l in lines if not l.startswith(b"DEBUG:")]
return b"\n".join(lines)
'
Add Co-authored-by
Add co-authors based on commit content:
git filter-repo --commit-callback '
# Add co-author if commit touches certain directories
touched_dirs = set()
for change in commit.file_changes:
if change.filename.startswith(b"frontend/"):
touched_dirs.add(b"frontend")
elif change.filename.startswith(b"backend/"):
touched_dirs.add(b"backend")
# Add co-authors
if b"frontend" in touched_dirs:
if b"Co-authored-by: Frontend Dev" not in commit.message:
commit.message += b"\nCo-authored-by: Frontend Dev <[email protected]>"
if b"backend" in touched_dirs:
if b"Co-authored-by: Backend Dev" not in commit.message:
commit.message += b"\nCo-authored-by: Backend Dev <[email protected]>"
'
Limitations
Hash Rewriting LimitationsCommit hashes in messages won’t be updated if:
- The hash doesn’t exist in the repository
- The commit was pruned during filtering
- The abbreviated hash isn’t unique
- You used
--preserve-commit-hashes
Suboptimal cases are logged to .git/filter-repo/suboptimal-issues
Binary DataCommit messages are bytes, not strings. Always use:
b"text" for literals
.split(b"\n") for line splitting
b"".join() for joining
Testing Changes
Preview with —dry-run
git filter-repo --dry-run \
--replace-message expressions.txt
This saves filtered output to files for inspection without modifying the repository.
Test on Single Branch
git filter-repo --refs test-branch \
--message-callback '...'
Verify Results
Check commit messages after filtering:
# View all commit messages
git log --all --oneline
# Search for specific text
git log --all --grep="search term"
# Check a specific commit
git show COMMIT_HASH
Best Practices
Testing Strategy
- Test on a small branch first
- Use
--dry-run for complex operations
- Run
--analyze before and after to compare
- Search for patterns you’re trying to remove
- Check edge cases (empty messages, very long messages)
Preserve HistoryBefore running:# Make a backup branch
git branch backup-before-filter
# Or make a backup clone
git clone --no-local . ../backup-repo
Next Steps