diff --git a/src/components/CommitAuthors.svelte b/src/components/CommitAuthors.svelte index 9147d3ba7..ce36ea404 100644 --- a/src/components/CommitAuthors.svelte +++ b/src/components/CommitAuthors.svelte @@ -19,31 +19,7 @@ // Which identity was last copied, so the rows keep independent feedback. let copied: string | undefined = $state(); - // The identity as it appears in a commit trailer, so that a copied result can - // be pasted straight into `Co-authored-by:` and the like. - function identity(who: GitIdentity): string { - return who.name ? `${who.name} <${who.email}>` : who.email; - } - - // Co-authors are not part of the commit's git identity fields; they are a - // convention carried in the message trailers. The same person can be - // trailered more than once, and squashing gathers several messages' trailers - // into one, so only the first mention of each is kept. - const coAuthors = $derived.by(() => { - const credited = new Set([ - utils.identityKey(commit.author), - utils.identityKey(commit.committer), - ]); - - return utils.coAuthors(commit.message).filter((who, index, all) => { - const key = utils.identityKey(who); - - return ( - !credited.has(key) && - all.findIndex(other => utils.identityKey(other) === key) === index - ); - }); - }); + const coAuthors = $derived(utils.creditedCoAuthors(commit)); const authorIsCommitter = $derived( utils.identityKey(commit.author) === utils.identityKey(commit.committer), @@ -66,7 +42,7 @@ }, 1000); async function copyIdentity(who: GitIdentity) { - await writeToClipboard(identity(who)); + await writeToClipboard(utils.formatGitIdentity(who)); copied = utils.identityKey(who); restoreIcon(); } @@ -223,7 +199,7 @@ type="button" class="email-row" class:nameless={!who.name} - title={`Copy "${identity(who)}"`} + title={`Copy "${utils.formatGitIdentity(who)}"`} onclick={event => { event.stopPropagation(); void copyIdentity(who); diff --git a/src/lib/utils.ts b/src/lib/utils.ts index fc01caebb..9eb10e4eb 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -445,3 +445,30 @@ export function coAuthors(message: string): GitIdentity[] { return authors; } + +// The identity as it appears in a commit trailer, so that a copied result can +// be pasted straight into `Co-authored-by:` and the like. +export function formatGitIdentity(who: GitIdentity): string { + return who.name ? `${who.name} <${who.email}>` : who.email; +} + +// Co-authors are not part of the commit's git identity fields; they are a +// convention carried in the message trailers. The same person can be +// trailered more than once, and squashing gathers several messages' trailers +// into one, so only the first mention of each is kept. +export function creditedCoAuthors(commit: { + author: GitIdentity; + committer: GitIdentity; + message: string; +}): GitIdentity[] { + const credited = new Set([ + identityKey(commit.author), + identityKey(commit.committer), + ]); + return coAuthors(commit.message).filter(who => { + const key = identityKey(who); + if (credited.has(key)) return false; + credited.add(key); + return true; + }); +} diff --git a/tests/unit/utils.test.ts b/tests/unit/utils.test.ts index 017bb595f..4331eaad6 100644 --- a/tests/unit/utils.test.ts +++ b/tests/unit/utils.test.ts @@ -4,8 +4,10 @@ import { describe, expect, test, vi } from "vitest"; import { coAuthors, + creditedCoAuthors, explorerHost, explorerUrl, + formatGitIdentity, formatRepositoryId, formatTimestamp, identityKey, @@ -301,3 +303,29 @@ describe("verdicts", () => { }, ); }); + +describe("commit credits", () => { + const alice = { name: "Alice", email: "alice@example.com" }; + const bob = { name: "Bob", email: "bob@example.com" }; + const carol = { name: "Carol", email: "carol@example.com" }; + + test("formatGitIdentity writes a trailer identity", () => { + expect(formatGitIdentity(alice)).toBe("Alice "); + expect(formatGitIdentity({ name: "", email: "a@b.c" })).toBe("a@b.c"); + }); + + test("creditedCoAuthors skips the author, the committer and repeats", () => { + const message = [ + "Squashed", + "", + "Co-authored-by: Alice ", + "Co-authored-by: Bob ", + "Co-authored-by: Carol ", + "Co-authored-by: Carol C. ", + ].join("\n"); + + expect( + creditedCoAuthors({ author: alice, committer: bob, message }), + ).toEqual([carol]); + }); +});