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 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.
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 pullrather 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" 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.
Related Jump to heading
- Repository Migration & Consolidation — the parent topic and the cost of rewriting.
- Migrating From Subversion to Git — where identity mapping is done at conversion time instead.
- Removing a Leaked Secret From Git History — the other rewrite with the same fallout profile.