Rewriting author emails during a migration Jump to heading

Wrong author identities are the most common reason teams reach for a history rewrite, and usually the weakest. A rewrite changes every commit id from the earliest affected commit forward, invalidating signatures, breaking external references and forcing everyone to reclone — to fix a display problem that .mailmap solves with no side effects at all. There are cases where a real rewrite is justified, and this recipe covers both, starting with the cheap answer, within repository migration and consolidation.

When to use this approach Jump to heading

  • History contains addresses that are wrong, obsolete, or machine-generated.
  • Contribution statistics attribute one person’s work to three identities.
  • A conversion from another system left bare usernames with no domain.
  • Someone’s personal address appears where a work address belongs.
  • If nothing but display is affected, stop after Step 1 — the rewrite in Steps 3 onwards is rarely worth its cost.

Step 1 — Try mailmap first, because it is free Jump to heading

.mailmap maps identities at read time. Commits are untouched, nothing breaks, and git log, git shortlog and most forges honour it.

# See the identities as they currently stand
git shortlog -sne --all | head -20
# .mailmap — canonical identity on the left, historical one on the right
cat > .mailmap <<'MAP'
Ada Lovelace <[email protected]> <ada@localhost>
Ada Lovelace <[email protected]> <[email protected]>
Ada Lovelace <[email protected]> ada <ada>
Release Bot <[email protected]> <jenkins@build01>
MAP
git add .mailmap && git commit -m 'chore: map historical author identities'
# Verification: the same command now shows merged identities
git shortlog -sne --all | head -20
git log --format='%aN <%aE>' -5
mailmap against a history rewriteA mailmap file fixes attribution everywhere identities are displayed, costs one commit, and breaks nothing. A rewrite changes the stored bytes, which means new commit ids, invalid signatures, broken external links and a mandatory reclone for everyone.History rewrite.mailmapcommit idsall changeunchangedsignaturesinvalidatedstill validexisting clonesmust recloneunaffectedefforthours, plus falloutone commitstored bytes correctedyesno — display onlythe last row is the only thing a rewrite buys, and it rarely matters

Step 2 — Decide whether a rewrite is genuinely required Jump to heading

Write down which of these applies. If none does, the mailmap is the answer.

  • A legal or contractual requirement that the stored data itself change.
  • A conversion where the identities are meaningless and the history is about to be published for the first time.
  • Addresses that leak personal information and must not remain in the objects.
  • A repository being extracted and republished, where the rewrite is happening anyway.
# How much history would a rewrite touch?
git log --format='%aE' --all | sort | uniq -c | sort -rn | head
git rev-list --count --all

Step 3 — Build the map and rehearse Jump to heading

git clone --mirror https://github.com/acme/app.git backup-app.git
git clone --no-local backup-app.git rewrite && cd rewrite
# filter-repo reads the same format as .mailmap
cp ../.mailmap ../authors.map

# Record what must NOT change: the tree of every commit
git log --format='%T' --all | sort > /tmp/before-trees.txt
git filter-repo --mailmap ../authors.map
# Verification: every tree hash survives; only identity fields changed
git log --format='%T' --all | sort > /tmp/after-trees.txt
diff /tmp/before-trees.txt /tmp/after-trees.txt && echo "content untouched"
git shortlog -sne --all | head

The tree comparison is the whole verification. Identical trees prove that no file content moved, so anything that did change is in the commit headers — which is exactly the intent.

Why a rewrite invalidates everything downstreamA commit id is a hash over its content, including the author and committer fields. Changing an identity changes the hash, which changes the id of every descendant commit, which invalidates every signature and every external reference to them.Identity editedauthor fieldNew commit idhash covers headersAll descendantsnew ids tooSignatures, linksinvalid, brokenthis cascade is the cost mailmap avoids entirely

Step 4 — Publish and communicate Jump to heading

git filter-repo --mailmap ../authors.map    # already run above
git remote add origin [email protected]:acme/app.git
git push --force --mirror origin
# Publish the id mapping so old references can be resolved
cp .git/filter-repo/commit-map ../commit-map.txt
wc -l ../commit-map.txt
# Verification: the mapping resolves an old id to a new one
grep '^a1b2c3' ../commit-map.txt

SAFETY WARNING — after a rewrite, anyone who runs git pull rather than recloning will merge the old history back in, and the result looks superficially fine while containing every commit twice. Announce the reclone instruction before pushing, not after, and check the default branch for duplicated history in the days that follow.

Step 5 — Prevent the problem recurring Jump to heading

Most identity mess comes from unconfigured machines. A small check stops the next one.

# A pre-commit hook that refuses an address outside the expected domains
#!/usr/bin/env sh
email=$(git config user.email || echo '')
case "$email" in
  *@acme.com|*@users.noreply.github.com) exit 0 ;;
  *) echo "commit-msg: configure user.email to your work address (currently: $email)" >&2
     exit 1 ;;
esac
# Verification: the hook accepts a correct address and rejects a stray one
git config user.email [email protected] && sh .husky/pre-commit && echo ok
git config user.email ada@localhost && sh .husky/pre-commit || echo "rejected"
Choosing between mailmap and a rewriteIf only the displayed attribution is wrong, mailmap fixes it with no side effects. If the stored data itself must change for legal or privacy reasons, a rewrite is justified. If the repository is being rewritten anyway for another reason, fold the identity map into that pass.Does the stored data itself have to change?no — display only.mailmapone commit, no falloutyes — legal or privacyRewriteplan the reclonealready rewritingFold it inone pass, one disruptionthe middle branch is rarer than the number of teams that take it

Validation checklist Jump to heading

Frequently Asked Questions Jump to heading

Does mailmap work on the forge as well as locally? Jump to heading

Most forges honour it for contribution graphs and blame views, though support varies and some ignore it in certain views. Locally it is universal. Test it against the views your team actually uses before deciding it is insufficient — the answer is often that it covers everything that matters.

Can we rewrite only recent history? Jump to heading

Yes, by limiting the rewrite to a commit range, and it substantially reduces the blast radius: anything before the range keeps its ids. If the wrong identities are recent, this is the sensible middle path.

What about the committer field, not just the author? Jump to heading

filter-repo --mailmap rewrites both, which is usually correct. Be aware that this changes commits where the committer was a rebasing colleague or an automation account — worth checking the result, because a release bot appearing as a person is its own kind of wrong.