# The Commit Identity Teardown: Raw Author Strings to One Row per Human

*Take one repo from raw author strings to a contributor table with one row per human, a graded merge for every cluster, and a distinct-human count you can defend.*

- Canonical URL: https://www.refolk.ai/guides/commit-identity-collapse-teardown
- Pillar: Process, data, and compliance
- Format: Teardown
- Published: 2026-10-25
- Last reviewed: 2026-10-25
- Reading time: 21 min
- Keywords: how to deduplicate git contributors by email, count unique contributors in a git repository, merge duplicate commit authors mailmap, github contributors count different from git shortlog, resolve noreply github email to account, remove bots from contributor list

## Key takeaways

- GitHub's contributors endpoint links only the first 500 author emails in a repository to accounts, which is how apple/swift showed 1002 contributors through the API with anon=true against 834 on the web page.
- Only three merge keys count as proof that two commit identities are one person: a .mailmap line, a shared numeric ID in ID+login noreply addresses, and GitHub's own grouping of emails under one account.
- Across seven example repositories in repo-dive, same-name merges were one account about 97% of the time, and the misses included two different David Hus in react, so name matches go to a reviewer and are never automated.
- Only the ID+USERNAME noreply form, used for GitHub accounts created after July 18, 2017, resolves to an account deterministically; username-only noreply addresses stop being associated with the account after a rename.
- Co-author trailers are a second author channel that plain author counts miss, so run git shortlog --group=trailer:co-authored-by -sne after the mailmap is applied, before you call the bot pass finished.
- The best published configuration for mixed human and bot accounts found bot activity in only 10% of them, so automation run through personal accounts needs a hand check of message templating.

This is the procedure for turning one repository's raw commit authors into a contributor table where one row means one human, with the evidence for every merge recorded next to it. It is for recruiting operations, revenue operations, and anyone who will be asked how a contributor count was produced. I follow one repository, react, through every fold, with the commands, the published intermediate evidence, and the points where the obvious move turns out to be wrong.

## What counts as proof that two commit identities are one person?

Three things count as proof: a line in the repository's .mailmap, a shared numeric account ID inside GitHub noreply addresses, and GitHub's own grouping of several emails under one account. Everything else is a guess that a person has to sign off on, and that includes an identical full name.

Three terms come first, because the rest of the guide depends on them:

- **Identity**: one (name, email) pair as git recorded it on a commit. One human usually has several.
- **Edge**: one claim that two identities belong together. It records the key that matched and where the proof came from.
- **Cluster**: the set of identities joined by edges. In the finished table, one cluster is one row.

The grades below come from repo-dive, Alexander Kachkaev's tool for charting repository contributors. It tested its merge rules across seven example repositories. The grade column is my label for how much weight each rule can carry.

| Rule | Links | Confirmed | Grade |
|---|---|---|---|
| .mailmap | n/a | maintainer-asserted | proven |
| Noreply, same numeric ID | n/a | deterministic | proven |
| Handle rule | 41 | 38 (92.7%) | strong |
| Same full name | n/a | ~97% of checkable | guessed |

The mailmap is proven because the people who run the repository wrote it. The noreply ID is proven because GitHub puts the account's numeric ID inside the address. repo-dive's handle rule links identities through a shared handle. It made 41 links on the test data, and GitHub confirmed 38 of them. That is good enough to propose a merge but not good enough to accept one without a look. Name matching scores higher on paper, but the cases it gets wrong are the ones that do the most damage, as the react walkthrough shows.

#### Where a contributor's identity lives

1. **GitHub account numeric ID** - The one key that survives renames and email changes
2. **Emails GitHub links to the account** - Proven by the platform, capped at 500 emails per repo
3. **Mailmap canonical name and email** - Asserted by maintainers, applied only by some git commands
4. **Raw author name and email strings** - What git stores on each commit, often typed by hand
5. **Co-author trailer identities** - A second author channel that author-field counts miss

*The further down an identity sits, the more easily it changes and the less a match on it proves.*

> **Rule:** Guessed edges never chain
>
> Proven edges may link into one cluster. A guessed or reviewer-decided edge joins exactly the two identities it names and must never pull a third identity in through some other edge.

The risk in clustering is transitivity. Say identity A shares an email with B, and B shares a display name with C. If you take the union of every edge, A and C end up in one cluster even though nothing connects them directly. Nobody has published a measurement of how quickly this compounds errors, so I don't rely on an error rate. I block the mechanism instead. A separate question is whether .mailmap itself chains one mapping into another. The gitmailmap text I could verify doesn't say, so test it in a scratch repository before you depend on it.

## Why do the contributor counts disagree before any folding?

The counts disagree because each counter measures something different. git counts raw strings. The REST contributors endpoint counts GitHub accounts, plus anonymous email rows once a repo passes 500 emails. The web Contributors page shows its own cached figure. Some gap between them is normal, and its size tells you where to look.

The contributors endpoint groups contribution counts by GitHub user, including every email associated with that user. For performance reasons, only the first 500 author email addresses in a repository are linked to users. The rest come back as anonymous contributors with no account attached, and you only see them if you pass `anon=1`. The API caches contributor data, so the numbers can be a few hours old. A GitHub staff member has said that on larger repos you may not be able to reproduce the figure the website shows.

The one documented public case is apple/swift, from a 2020 community discussion:

| Counter | Value |
|---|---|
| Web Contributors page | 834 |
| API, anon=true | 1002 |
| Gap (derived) | 168, +20.1% |
| git shortlog -sne | not published |

Here's how the API ends up higher. Past 500 emails, every additional email becomes its own anonymous row. Someone who committed from two of those emails shows up as two rows. The API is counting addresses at that point, not people.

git shortlog answers a third question. It applies the repository's .mailmap automatically and counts distinct name and email pairs after mapping. It knows nothing about GitHub accounts. So when someone asks why the GitHub contributor count is different from git shortlog, the answer is that one counts accounts plus leftover emails and the other counts mapped strings.

Nobody has published a three-way comparison of API, web page, and `git shortlog -sne` on one large repo on the same date, so I can't give you a typical spread. Make your own baseline: log all three on the same day and write the date next to each number.

> **Watch out:** The anon count is not a headcount
>
> On any repo with more than 500 author emails, anonymous rows count addresses, not people. Compare the number of anonymous rows with the number of distinct author emails from git log. If the two track closely, the API is counting emails and the number cannot be quoted as contributors.

## The fold procedure, step by step

The procedure moves from the cheapest, most certain folds to the most expensive, least certain ones, so that hand review only ever sees what proof couldn't settle. Every step has a "done" state you can check.

#### Collapse one repository's identities into people

1. **Extract raw author strings** - Run git log --format='%an|%ae|%cn|%ce' and git shortlog -sne, and compare raw %an/%ae output with mailmapped %aN/%aE output. Done means a logged count of distinct (name, email) tuples, before and after the mailmap.
2. **Log the three platform counts on one date** - Record the REST contributors count without anon, the count with anon=1, and the web Contributors page figure on the same day, allowing for a cache lag of a few hours. Done means three dated numbers.
3. **Apply the repository's .mailmap** - Fold every identity the maintainers mapped to a canonical name and email, graded proven (maintainer-asserted). Done means the mailmapped tuple count is logged and each edge cites its mailmap line.
4. **Normalize emails and flag placeholders** - Lowercase every email and set aside addresses containing (none), localhost, or root@. Treat plus-tag stripping as guessed unless you know how the mail domain behaves.
5. **Fold noreply addresses by numeric ID** - Merge every ID+login and ID+oldlogin noreply address that shares a numeric ID, graded proven. Send username-only noreply addresses to the residue.
6. **Group by verified GitHub account** - Fold emails GitHub attributes to one account, graded proven by the platform. Emails past the 500-email cap stay in the residue.
7. **Remove bots and agents, then fold trailers** - Flag [bot] logins, app noreply addresses, BIMAN-style name patterns, templated messages, and agent trailers, then run git shortlog --group=trailer:co-authored-by -sne. Done means humans who appear only as co-authors are folded in, not lost or double-counted.
8. **Hand-review the residue** - Give every same-name or handle-only candidate a recorded reviewer decision, never an automatic merge. Done means every cluster carries its edges, keys, sources, and grades.

Published tools disagree on the order. repo-dive applies the mailmap and noreply folds first and saves hand-made alias groups for whatever is left. SortingHat, the identity store in the CHAOSS GrimoireLab project, imports everything first and then runs recommendation and unify jobs over the whole pile. For a single repository I use repo-dive's order. The deterministic folds shrink the residue, so every merge proposed later is one a reviewer actually reads. SortingHat's order makes more sense when many sources feed one long-lived store.

## Walking react through each fold

I use react as the worked case because its folding record is public. repo-dive documented which of its top contributors appeared under several identities, and which same-name pairs turned out to be different people. Where nobody has published a react count, I give the command and leave the number as a slot in your ledger. I won't invent one.

**Extraction and fold commands for one repository**

```
# Step 1: raw identities, author and committer (mailmap not applied by %an/%ae)
git log --format='%an|%ae|%cn|%ce' > raw_identities.txt
cut -d'|' -f1,2 raw_identities.txt | sort -u | wc -l

# Step 1: same tuples with the repo's .mailmap applied
git log --format='%aN|%aE' | sort -u | wc -l
git log --use-mailmap --format='%aN <%aE>' | sort -u > mailmapped_identities.txt

# Step 1: shortlog view (applies .mailmap automatically)
git shortlog -sne HEAD > shortlog.txt
wc -l < shortlog.txt

# Step 4: placeholders and machine-looking addresses
grep -Ei '\(none\)|localhost|root@' raw_identities.txt | sort -u
grep -Ei '\[bot\]|users\.noreply\.github\.com' raw_identities.txt | sort -u

# Step 7: co-author trailers, mailmap applied to parsed identities
git shortlog -sne --group=trailer:co-authored-by HEAD > trailers.txt

# Step 2: platform counts, same date (page through all results, count rows)
GET /repos/OWNER/REPO/contributors?per_page=100
GET /repos/OWNER/REPO/contributors?per_page=100&anon=1
```

*Run from a full clone; replace OWNER/REPO in the API line and keep every output file with its date.*

### Steps 1 and 2: raw strings and platform counts

The first number in the ledger is the distinct-tuple count from `raw_identities.txt`. Keep the committer fields in the raw file even though you're counting authors, because they can surface extra addresses worth checking against your clusters. Then log the three platform counts on the same day. react has no published three-way comparison, so this is your baseline, not a figure you can check against anyone else's.

### Step 3: the mailmap

In repo-dive's react data, Sebastian Markbåge, Andrew Clark, Brian Vaughn and Ben Alpert/Sophie Alpert each appeared two or three times in the top 20 before folding. So four people took up several slots each in a list meant to name the top 20 humans. The Alpert pair shows why proof keys matter. No name-similarity rule would join those two display names, but a maintainer-asserted mapping or a shared account joins them with proof.

The first wrong turn usually happens here. Both emails and names are matched case-insensitively in the mailmap, but only some commands use it. git shortlog applies it automatically, and so does git blame. git log does not unless you pass `--use-mailmap` or use the `%aN` and `%aE` placeholders. If you run git log with `%an` and conclude the repo's mailmap does nothing, you have just skipped the strongest proof the maintainers gave you.

### Step 4: normalization

Lowercase every email. This loses nothing, since mailmap matching is case-insensitive anyway. Plus-tag stripping, which treats `name+tag@domain` as `name@domain`, is not documented by git. Grade it guessed unless you know how that particular domain handles plus addressing.

Set placeholder identities aside at this step. The gitmailmap examples point to "the obviously broken <jane@laptop.(none)> and <jane@desktop.(none)> E-Mails" as addresses to map away. An address like that tells you which machine made the commit, not who made it, so never use it as a merge key. I haven't found a published base rate for these placeholders, so run the grep and count your own.

### Step 5: noreply by numeric ID

GitHub uses two noreply formats:

- **ID form**: `ID+USERNAME@users.noreply.github.com`, for accounts created after July 18, 2017. GitHub Support has said these continue to map to contributions through the user ID part.
- **Username form**: `USERNAME@users.noreply.github.com`, for older accounts that turned on email privacy before that date. After a username change, these addresses are no longer associated with the account.

Fold `ID+login` and `ID+oldlogin` into one cluster: the same ID means the same account, and the edge is proven. Username-form addresses go to the residue. After a rename they can point at nobody, or at whoever picked up the login next. A second rename effect to know about: verified commits signed with the previous noreply address lose their "Verified" status, so the badge isn't a stable signal across renames. I couldn't confirm from the documentation what happens to noreply-attributed commits after an account is deleted, so treat those as unresolved.

### Step 6: group by verified account

The contributors endpoint's per-user grouping is GitHub vouching that those emails belong to one account. Map each raw email to the account GitHub credits its commits to, then fold by account. This is where the 500-email cap matters. Emails past the cap come back anonymous, with no account behind them, so they stay in the residue. They don't become extra people.

If you use a handle rule to propose links among the leftovers, remember the repo-dive result: 41 links, 38 confirmed. Three wrong links in 41 means every handle edge gets a reviewer and a recorded decision.

### Step 7: bots and agents

Strip automation next, then fold trailers back in. The next section covers the markers and detectors. The point to carry into review is that the trailer pass has to run after the mailmap, or co-authors come back as unfolded duplicates.

### Step 8: the residue

repo-dive's same-name finding is the reason this step exists. Across its seven repositories, about 97% of the same-name merges GitHub could check were one account. The rest were different people: two David Hus and two Kevin Huangs in react, two Jason Millers, and two Michael Yangs in ollama. A name rule would have merged each of those pairs into one person, in a list built to rank people.

> A 97% agreement rate is a reason to review name merges, not to automate them.

Expect the hand-review load to be real. repo-dive's PR describing its hand alias groups says "The 90 groups cover the people who reach the charts - roughly the top 25 per repo by commits once merged." Stopping review at a commit threshold is a legitimate choice. If you do, report the long tail as unreviewed, not as resolved.

#### Deciding a residue pair

Horizontal axis runs from Names differ to Names agree. Vertical axis runs from No shared proof key to Shared proof key.

| Quadrant | What it means |
| --- | --- |
| Proof key, names differ | Merge; this is a rename or alias, like the Alpert pair |
| Proof key, names agree | Merge; the easy case, record the key |
| No key, names differ | Keep separate; nothing connects them |
| No key, names agree | Reviewer decides, default separate; this is the David Hu trap |

*A shared proof key decides a merge; matching names only decide who gets reviewed first.*

Ask me this: `Maintainers of the react repository who have committed in the last 12 months, excluding bots` - [run the search](https://www.refolk.ai/start?q=Maintainers%20of%20the%20react%20repository%20who%20have%20committed%20in%20the%20last%2012%20months%2C%20excluding%20bots).

*Returns the people behind recent react commits with bot accounts left out, which is a quick way to check your resolved top clusters against named humans.*

## How do you remove bots and agents without deleting humans?

Remove identities whose login, address, name pattern, or message template marks them as automation. Then fold co-author trailers back in, so the humans attached to bot and agent commits aren't lost. Assume mixed human and bot accounts will get past any automatic detector.

The markers, with what each proves and how each misleads:

- **`[bot]` login suffix.** Proves a GitHub App identity. It misleads when you drop the whole commit, because a human may be credited in a trailer on the same commit.
- **App noreply address**, such as `29139614+renovate[bot]@users.noreply.github.com`. Proves an app account. It looks like the human ID form, so keep the bot flag ahead of the ID fold.
- **Bot-like name and email patterns.** BIMAN, from Dey et al., matches the author's name and email against common bot patterns. This misleads when a human's handle happens to look like a bot's.
- **Templated commit messages and file association.** BIMAN's other signals. These are the best hand check for an account a human also uses for automation.
- **Agent trailers**, such as `Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>` or `Assisted-by:`. These prove an agent was involved. They don't prove there was no human, who may be the author or another trailer on the same commit.

Published detectors are strong on whole accounts and weak on mixed ones:

| Method | Result | Caveat |
|---|---|---|
| BoDeGHa | F1 0.98 on 5,000 accounts | Account-level verdicts |
| BotHunter, Random Forest | F1 92.4% | Account-level verdicts |
| Best config, mixed accounts | Bot activity found in 10% | Humans running automation slip through |

For scale, the BIMAN dataset covers 461 bots and 13,762,430 commits, and the BIN ground truth holds 13,150 bots and 13,150 humans drawn from World of Code. Nobody has published how many identities these markers remove from a typical mid-sized repository, so count your own removals and report them.

Trailers are a second author channel. More and more agent commits put a bot in the author field and the human in a trailer. If you strip bots by author field alone, you remove humans' work. If you parse trailers yourself with raw `%(trailers)` output, the mailmap isn't applied, so the same human comes back as an extra contributor. `git shortlog --group=trailer:co-authored-by -sne` parses each trailer value as an identity and applies the mailmap when parsing succeeds. Run it after step 3, drop the agent identities, and fold the human co-authors into their existing clusters.

## How identity collapse goes wrong

Most bad contributor tables fail in one of eight ways, and every one has a mechanical check. Name-only merges and the API's anonymous rows do the most damage, because they shift the headline count.

1. **Name-only merge.** Two different people get fused into one, like the two David Hus in react. Check: before merging, require a shared account ID, a verified email, or a mailmap line, or a reviewer decision written into the edge.
2. **Username-only noreply treated as an account key.** After a rename the address points at nobody, or at whoever holds the login now. Check: accept only the `ID+` form as proof.
3. **Trusting the API count on a repo with more than 500 emails.** Anonymous rows inflate the count, as at apple/swift (834 on the web page against 1002 from the API). Check: compare the anonymous row count with the number of distinct emails.
4. **Co-author trailers bypassing the fold.** A human co-author on a bot or AI commit turns into an extra contributor. Check: run `git shortlog --group=trailer:co-authored-by -sne` after the mailmap.
5. **Stale snapshots.** Snapshots already stored in a catalog keep the raw email, so later .mailmap edits quietly diverge from them. Check: re-scan after every mailmap change.
6. **Substring identity lookup.** SortingHat's old `term` filter "matches substrings across name, email and username and can return unrelated individuals." Check: use exact-match filters only.
7. **Bot detection on mixed accounts.** Humans running automation through a personal account get through, since only 10% are detected. Check: read each identity's commit messages for templating by hand.
8. **Placeholder identities**, such as `jane@laptop.(none)`. They chain unrelated people together if used as keys. Check: grep for `(none)`, `localhost`, and `root@` before any fold. The base rate is unverified.

Transitive chaining is behind several of these. A placeholder address or a common name becomes a bridge, and a union of all edges walks across it. The rule from the first section, that guessed edges never chain, closes that path whatever caused the bad edge.

## How do you report the count with merge evidence?

Report the distinct-human count together with four other numbers: how many clusters rest on proven edges, how many on strong or reviewer-decided edges, how many identities you removed as bots or agents, and how much residue was left unreviewed. Every cluster row carries its edges, so anyone can re-derive it.

SortingHat is a useful model for the cluster itself. It merges raw identities from different sources into a single Individual with a stable UUID. Each Individual has one Profile holding the canonical display name, email, country, gender, and a bot flag, plus `created_at` and `last_modified` fields, and its enrollments are time-bounded. It generates merge recommendations by comparing email, name, and username fields. What its documented schema does not include is per-merge evidence: the match key, the proof source, the confidence, and the reviewer. Add those fields yourself. They are what makes the count defensible.

**Cluster evidence record and count summary**

```
cluster_id: C-0001
canonical_name:
canonical_email:
github_account_id:            (numeric, from ID+ noreply or account grouping)
kind: human | bot | agent | mixed-unresolved
identities:
  - name | email | channel (author / committer / trailer)
edges:
  - identity_a <-> identity_b | match_key | proof_source | grade | reviewer | date
open_questions:

SUMMARY (repo, extraction date):
raw tuples: __ | after mailmap: __ | distinct humans: __
clusters proven-only: __ | with strong or reviewer-decided edge: __
removed as bot/agent: __ | unreviewed residue identities: __
API no-anon: __ | API anon=1: __ | web page: __ | counts dated: __
```

*One record per cluster; grade is proven, strong, or reviewer-decided, and the summary line goes at the top of the handover.*

Once the rows are humans, the next request is usually where those people are and who employs them. In Refolk's index of professional profiles, the headline "open source maintainer" appears on 124 profiles in the United States, 119 in India, and 47 in Germany.

**124 - US profiles headlined "open source maintainer" in Refolk's index**

India is close behind at 119, but 14 of 25 sampled Indian profiles are in Bengaluru, while the US sample is spread out.

| Country | Profiles | vs Germany (derived) |
|---|---|---|
| United States | 124 | 2.64x |
| India | 119 | 2.53x |
| Germany | 47 | 1.00x |

Two practical points follow from this. First, expect a folded contributor table to skew by geography, and expect parts of it to cluster in a single city. Second, employer is a poor disambiguator. In the sampled top-employer lists for each country, no company appears more than 3 times (Oracle, Siemens and IBM each have 3 in India). A profile match can support a reviewer's decision that two same-name clusters are different people, but it is not commit-level proof that two identities are the same person. Looking up where each resolved person sits is one search per row, and [Refolk](/) takes that as a plain-English request across GitHub, LinkedIn, and the open web, so you don't have to stitch the profiles together by hand.

#### Before you hand over the table

- [ ] Raw tuple count, mailmapped tuple count, and the three platform counts are logged with one extraction date.
- [ ] Every merge edge names its match key, proof source, grade, and, for anything not proven, a reviewer.
- [ ] No guessed or reviewer-decided edge links a third identity into a cluster.
- [ ] Only ID+ noreply addresses were used as account keys; username-form addresses sit in the residue.
- [ ] Placeholder addresses containing (none), localhost, or root@ were excluded from every fold.
- [ ] The co-author trailer pass ran after the mailmap, and agent identities were dropped from it.
- [ ] Every same-name pair without a proof key has a recorded decision, defaulting to separate.
- [ ] The summary reports unreviewed residue as unreviewed, not as resolved people.

## Keeping the resolved table current

A resolved table goes stale as soon as the mailmap changes, an account is renamed, or new emails push past the 500 cap. Re-run the folds from raw strings on a schedule instead of patching individual rows.

- **Re-scan after every mailmap change.** Stored snapshots keep the raw email, so a fix to .mailmap never reaches them unless you rebuild.
- **Re-pull all three platform counts on one date.** Allow for the cache lag of a few hours, and never compare an API figure from one day with a web figure from another.
- **Re-check renames.** ID-form noreply clusters survive a rename. Username-form addresses and "Verified" badges do not.
- **Use exact-match lookups only.** SortingHat's exact identity filters arrived well after substring matching had shipped in a widely used identity store, so check which mode your own tooling uses.
- **Treat generated recommendations as candidates.** SortingHat's merge recommendations compare email, name, and username, so each one still gets graded under the rules above.
- **Offer proven folds upstream.** A noreply-ID or account-grouping merge you found is a ready-made .mailmap line, and once maintainers accept it, it becomes maintainer-asserted for everyone.

> **Note:** Tools in this space fix one fold each
>
> mailmap-checker, BoDeGHa, and the github/contributors Action each address part of this job. Test what each one actually folds against your own repository before you put any of them in the pipeline.

When a cluster changes between runs, write the change into its edge history instead of overwriting the row. And when you need to know who a resolved person is today, run a fresh search rather than relying on an old profile match; Refolk returns the current record from the same plain-English request.

## Frequently asked questions

### Why does the GitHub contributors count differ from git shortlog?

They count different things. git shortlog -sne counts distinct name and email pairs after applying the repository's .mailmap, and it knows nothing about GitHub accounts. The REST contributors endpoint groups emails by GitHub user but links only the first 500 author emails, so the rest come back as anonymous rows. The web page shows its own cached figure. At apple/swift the web page showed 834 and the API with anon showed 1002.

### How do I resolve a noreply GitHub email to an account?

Use the numeric ID. Accounts created after July 18, 2017 get addresses of the form ID+USERNAME@users.noreply.github.com, and GitHub Support has said these continue to map to contributions through the ID part. So two addresses with the same ID are one account even if the login changed. Older username-only noreply addresses stop being associated with the account after a rename, so treat them as unresolved rather than as proof.

### Is it safe to merge commit authors who have the same full name?

No, not automatically. In repo-dive's test across seven repositories, about 97% of the same-name merges GitHub could check were one account. The rest were different people, including two David Hus and two Kevin Huangs in react. Collisions cluster in large repos with common names, which is exactly where the count matters, so every same-name candidate needs a shared account ID, a verified email, or a mailmap line, or a recorded reviewer decision.

### Does .mailmap apply to git log and to co-author trailers?

Only in some places. git shortlog and git blame apply the mailmap automatically. git log needs --use-mailmap or the %aN and %aE placeholders. When shortlog groups by trailer, it parses each trailer value as an identity and applies the mailmap if parsing works, but raw %(trailers) output is not mailmapped. So use git shortlog --group=trailer:co-authored-by -sne rather than parsing trailers yourself.

### How do I remove bots from a contributor list without losing humans?

Remove identities with [bot] logins, app noreply addresses, bot-like name and email patterns, or templated messages, and drop agent trailer identities such as noreply addresses from AI tools. Then fold co-author trailers back in, because a bot or agent commit often carries the human in a trailer. Expect mixed accounts to slip through: the best published configuration found bot activity in only 10% of mixed accounts.

### Does .mailmap chain one mapping into another?

I can't tell you for sure, because the gitmailmap documentation I checked does not state a chaining rule either way. Test it in a scratch repository. Map address A to B in one line, map B to C in another, then run git shortlog -sne and see whether commits from A land under B or C. Write the mapping you depend on directly, from every raw address to the final canonical one.

---

*From the Refolk guide library. I revise these guides rather than replacing them, so the current version is always at https://www.refolk.ai/guides/commit-identity-collapse-teardown*
