.mailmap Tester
Paste a .mailmap and a list of author identities to see what each one resolves to, which entry did it, and which people finally merge into one.
Identities
8
After mailmap
5
Rewritten
5
Entries
5
Findings
2
| Identity in the commit | What git shows | Entry that decided it |
|---|---|---|
| jane <jane@corp.example> | Jane Doe <jane@corp.example> | :2 name only, keyed on the address Jane Doe <jane@corp.example> |
| Jane D <jane@laptop.local> | Jane D <jane@corp.example> | :3 address only, keyed on the address <jane@corp.example> <jane@laptop.local> |
| JANE DOE <jdoe@oldcorp.example> | Jane Doe <jane@corp.example> | :4 name and address, keyed on the address Jane Doe <jane@corp.example> <jdoe@oldcorp.example> |
| jane <ci@build.example> | Jane Doe <jane@corp.example> | :5 keyed on the name and the address Jane Doe <jane@corp.example> jane <ci@build.example> |
| release-bot <ci@build.example> | release-bot <ci@build.example> | the address is listed, but only under these names: jane |
| Ravi <ravi@users.noreply.github.com> | Ravi Patel <ravi@corp.example> | :6 name and address, keyed on the address Ravi Patel <ravi@corp.example> <ravi@users.noreply.github.com> |
| Ravi Patel <ravi@corp.example> | Ravi Patel <ravi@corp.example> | no entry for this address |
| dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> | dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> | no entry for this address |
Each block below becomes one line in git shortlog -sn. Anything not listed here still stands on its own.
- Jane Doe <jane@corp.example> · 3 identities
- jane <jane@corp.example>
- JANE DOE <jdoe@oldcorp.example>
- jane <ci@build.example>
- Ravi Patel <ravi@corp.example> · 2 identities
- Ravi <ravi@users.noreply.github.com>
- Ravi Patel <ravi@corp.example>
Entries that do not do what they look like, and identities git reads differently than you would.
- hit the address but not the name, so git left it alone: release-bot <ci@build.example> · jane
- :3 this address is itself the key of another entry, and mailmap never chains: jane@corp.example
Everything runs in your browser. Nothing is uploaded and no repository is read.
How to use
- Paste your
.mailmapinto the first box, exactly as it is on disk. Comments, blank lines, Windows line endings and trailing spaces are all read the way git reads them, so you can copy the file rather than clean it up. - List the identities you want to check in the second box, one per line.
git log --format='%an <%ae>' | sort -uprints exactly this list for authors; use%cn <%ce>for committers, since a rebase or a merge often leaves them different. - Read each row: the identity you pasted, what git resolves it to, and the entry that decided it — with its line number and which of the four forms it is. A row citing two lines means the name came from one entry and the address from another.
- Check the merge panel. Each block there is one line in
git shortlog -sn; an identity you expected to be merged and that is not listed is the duplicate you are chasing. - Work through the findings. An entry whose commit address appears in none of your identities never fires, one that a later line overwrites contributes nothing, and a name-keyed entry that never matched a name is usually a fourth-form line that should have been a third-form one.
About this tool
ShowHide
A .mailmap is what stops git shortlog -sn from listing the same colleague three times, and it fails in a way that gives you nothing to go on: the entry is there, it looks like the examples, and the duplicate stays. Paste the file and the identities your history actually contains — Name <email>, one per line, exactly as git log prints them — and this tester shows what each identity becomes, which line decided it, and which of the four forms that line is. It is the answer git check-mailmap gives in a real checkout, worked out in the browser.
The four forms look almost the same and are not. Proper Name <commit@x> rewrites the name, <proper@x> <commit@x> rewrites the address, and Proper Name <proper@x> <commit@x> rewrites both — all three keyed on the commit address alone, so every commit from that address is caught. The fourth, Proper Name <proper@x> Commit Name <commit@x>, is keyed on the name and the address: with Both Fixed <both@x> Old Name <both-old@x> in the file, Old Name <both-old@x> is rewritten and Wrong Name <both-old@x> is left completely alone. That single difference is behind most of the entries that "do nothing", and the table names it on every row.
The rest of the format is just as literal. Addresses and names are compared without regard to case, but the name is otherwise compared character for character: a double space inside it counts, and so does anything in front of it on the identity line. A # opens a comment only in the very first column, so an indented one becomes part of a name. The address is whatever sits between < and >, spaces included, and the first < on the line opens it — a < inside a name swallows the rest of the line into the key. Two entries for the same commit address do not replace one another: the map holds one name and one address per key, each entry writes the fields it carries, and you can end up with the name from one line beside the address from another. And the lookup happens exactly once, so a → b next to b → c leaves a sitting at b; mailmap does not chain.
Above the table are the questions you actually came with: how many distinct people the list collapses to, which canonical identities absorbed more than one input, and which identities came through untouched. The findings panel names entries that never fired because their commit address appears in no identity you pasted, entries a later line overwrote, a name-keyed entry whose name never matched, and an identity git refuses to parse at all.
What it cannot tell you is which identities your repository really has — that list has to come from git log, and an entry for an address that never appears in it is the most common reason nothing changes. Nor does it model mailmap.file or mailmap.blob, which move the file elsewhere. And remember what .mailmap is: a display-time lookup read from the working tree of the checked-out repository. It never rewrites a commit, never changes an author stored in the object database, and is not read by GitHub for its contributor graph.
Frequently asked questions
I added an entry and git shortlog -sn still lists the person twice. Why?
Nine times out of ten the entry is the fourth form and the name does not match. Jane Doe <jane@corp.example> jane <ci@build.example> only catches commits whose author name is exactly jane (case aside) — a commit from the same address under release-bot is untouched, because a name-keyed entry that misses falls back to the plain entry for that address, and there may not be one. Drop the Commit Name part and the line catches every commit from that address instead. The other common causes are an address that never appears in the history at all (check with git log --format='%an <%ae>' | sort -u, and watch for +tag forms and GitHub users.noreply addresses), and a .mailmap that is not at the top level of the working tree, where git looks for it.
What is the difference between the four forms?
How they are looked up, and how much they replace. The first three are filed under the commit address alone: Proper Name <commit@x> replaces the name, <proper@x> <commit@x> replaces the address and keeps whatever name the commit had, and Proper Name <proper@x> <commit@x> replaces both. The fourth adds a name in front of the commit address, and is filed under that name as well, so it applies only to commits that match on both halves. When both kinds exist for one address, git tries the name-keyed table first and uses the plain entry as the fallback — in whichever order you wrote them, since the file is a map, not a script.
Does a mailmap chain? I mapped A to B and B to C.
It does not. The lookup runs once per identity and the result is never fed back in, so an identity at A comes out at B and stops there, even though B is the key of another entry sitting in the same file. This bites when a file grows over years: someone folds an old laptop address into a work address, then later folds that work address into a personal one, and the first group never follows. The fix is to point every entry at the final identity directly. This tester flags any entry whose canonical address is itself a key elsewhere.
Is the matching case-sensitive?
Neither half of the key is: addresses and names are both compared without case, so <OLD@EXAMPLE.COM> in a commit matches an entry written <old@example.com>, and a fourth-form entry keyed on Old Name also catches OLD NAME. Everything else about the name is literal, though. Two Spaces with two spaces does not match Two Spaces with one, and git does not trim what comes before a name on the identity line — an indented identity keeps that indentation as part of its name and stops matching a name-keyed entry. Inside the angle brackets nothing is trimmed either, so < old@x > is a key with spaces in it that no commit will ever have.
I have two entries for the same commit address. Which one wins?
Both, partly. git keeps one canonical name and one canonical address per commit address, and each entry writes only the fields it carries, so Alpha <alpha@x> <dup@x> followed by <beta@x> <dup@x> resolves to Alpha <beta@x> — a name from the first line and an address from the second, an identity that appears nowhere in the file. Only a later entry carrying the same field overwrites it. That is why a duplicated address can leave a line in the file that quietly contributes nothing, and why this tester marks a row whose name and address came from different lines.
Does .mailmap change my history?
No. It is read at display time, from the working tree of the repository you are standing in, and applied by git log, git shortlog, git blame, git check-mailmap and anything run with --use-mailmap or with log.mailmap set. The author recorded in each commit object is untouched, which is the point: correcting a name this way costs nothing and breaks no signatures, while rewriting history changes every commit id below the change. The flip side is that the file only works where it is checked out — a forge that builds its own contributor list, GitHub included, does not consult it.
Related tools
.gitignore Tester
Paste a .gitignore and a list of paths to see which are ignored and exactly which line decided — the answer git check-ignore -v gives.
Developer
Git Config Precedence Tester
Paste your git config files and see which value wins for every key, the scope, file and line it came from, what it overrode, and which includeIf fired.
Developer
.gitattributes Tester
Paste a .gitattributes and a list of paths to see every attribute git ends up setting on each path, and which line — or which macro — set it.
Developer
Static Library (.a) Inspector
Read a .a archive in the browser: members with sizes and dates, the symbols each object defines and needs, and whether the symbol index has gone stale.
Developer
JSON Canonicalize (RFC 8785 JCS)
Canonicalize JSON per RFC 8785 and hash it: UTF-16 key order, ECMAScript number form, no whitespace, with a SHA-256 of the exact bytes.
Developer
Conventional Commit Message Builder
Build a Conventional Commits message with type, scope, breaking change, and footers, in your browser.
Developer