lib/commit-authors.ts
Rama main · réplica de lectura del repositorio original.
/**
* The AUTHORS of a commit — plural, because a commit often has them
* plusieurs.
*
* Git only knows one author; the `Co-authored-by:` convention in trailer of the
* message adds the others, and it is this that the forges read to display
* several avatars. In minddy this is the COMMON case: any commit written with
* Numo, or with a code agent, carries his co-signer.
*
* Pure and shared client/server, like `pr-review-threads`: the same list is used
* Commits tab and PR activity, and letting them diverge would mean
* two different things from the same commit in the same page.
*/
export interface CommitAuthor {
/**
* The forge account, when she knew how to link the email to a user.
* `null` = known person behind this name: the display falls to `name`
* and on an avatar derived from him, like everywhere else in minddy.
*/
login: string | null;
/** The name written in the commit or its trailer — always present. */
name: string;
/** The ACCOUNT avatar. Never the identicon that the forge makes for an email
unknown: it would read like someone's photo. */
avatar_url: string | null;
}
/**
* `Co-authored-by: Nom <email>` — the convention, such as GitHub and GitLab
* read. Insensitive to the case of the keyword (`Co-Authored-By` is also common
* than the lowercase form), space tolerant, and without email anywhere:
* a trailer without `<…>` is not one.
*
* Fallback from the rich path: on the GitHub side, the forge resolves these trailers itself by
* ACCOUNTS (with avatars) and his answer wins. Here we only have what is
* written in the message — names. This is what's left when GraphQL doesn't have
* answered, and all GitLab will ever give: its API serves no account
* on his commits.
*/
const CO_AUTHOR = /^\s*co-authored-by:\s*(.+?)\s*<([^>]+)>\s*$/i;
export function parseCoAuthorTrailers(message: string): Array<CommitAuthor & { email: string }> {
const out: Array<CommitAuthor & { email: string }> = [];
for (const line of (message ?? "").split("\n")) {
const m = line.match(CO_AUTHOR);
if (!m) continue;
const name = m[1].trim();
if (!name) continue;
out.push({ login: null, name, avatar_url: null, email: m[2].trim().toLowerCase() });
}
return out;
}
/** Two entries designate the same person if their email — otherwise their name —
coincides. Email is the key to the forge; the name is what remains without it. */
function identityKey(author: { login?: string | null; name: string; email?: string | null }): string {
if (author.email) return `email:${author.email.toLowerCase()}`;
if (author.login) return `login:${author.login.toLowerCase()}`;
return `name:${author.name.toLowerCase()}`;
}
/**
* The list of authors to DISPLAY, main author at the top.
*
* `fromForge` is the response from the forge when it knows how to resolve trailers
* itself (GitHub does it in GraphQL, deduplication included): it wins
* entire, because it alone carries the accounts and their avatars.
*
* Otherwise we reconstruct: the author of the commit, then his co-signatories read in the
* message. Deduplication matters — the primary author is very often
* re-declared in trailer, and displaying it twice would give two avatars for one
* seule personne.
*
* Empty only if the commit has neither author nor name: the caller then falls back to
* what he displayed before.
*/
export function commitAuthors(
commit: {
message: string;
author: { login: string; avatar_url: string | null } | null;
authorName: string | null;
authorEmail: string | null;
},
fromForge?: CommitAuthor[],
): CommitAuthor[] {
if (fromForge?.length) return fromForge;
const primary: CommitAuthor | null =
commit.author || commit.authorName
? {
login: commit.author?.login ?? null,
name: commit.authorName ?? commit.author?.login ?? "",
avatar_url: commit.author?.avatar_url ?? null,
}
: null;
const seen = new Set<string>();
const out: CommitAuthor[] = [];
if (primary) {
seen.add(identityKey({ ...primary, email: commit.authorEmail }));
out.push(primary);
}
for (const co of parseCoAuthorTrailers(commit.message)) {
const key = identityKey(co);
if (seen.has(key)) continue;
// Without a common email, the name remains the only recourse against the duplicate: a
// trailer which repeats word for word the name of the author is the same human.
if (out.some((a) => a.name.toLowerCase() === co.name.toLowerCase())) continue;
seen.add(key);
out.push({ login: co.login, name: co.name, avatar_url: co.avatar_url });
}
return out;
}
/**
* Authors of MULTIPLE commits, deduplicated — what a push says
* folded (“so-and-so pushed 5 commits”). The order of first
* appearance is preserved: it is that of history.
*/
export function mergeCommitAuthors(lists: CommitAuthor[][]): CommitAuthor[] {
const seen = new Set<string>();
const out: CommitAuthor[] = [];
for (const author of lists.flat()) {
const key = identityKey(author);
if (seen.has(key)) continue;
seen.add(key);
out.push(author);
}
return out;
}