Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Merge commit authors and hovercards: commits show their accounts, every name opens a card (repos 0016) | 1 | /** |
| 2 | * Who a commit is by, as g1t shows it: the account its author address | |
| 3 | * belongs to, else the name written on the commit. | |
| 4 | * | |
| 5 | * Git records a name and an address; g1t matches the address to an | |
| 6 | * account by a confirmed address or the account's noreply address | |
| 7 | * (`email_owners` in services/identity), never an unconfirmed one. The | |
| 8 | * address itself never reaches the page: a commit is shown with the | |
| 9 | * person, not the address, so a private address stays private. | |
| 10 | */ | |
| 11 | import type { Blame, Commit, Contributor, EmailOwner } from "@g1t/contracts"; | |
| Coding agents show as themselves on commits and contributors: Claude, Codex, Copilot, Gemini, Jules, Cursor, Devin, Aider, Dependabot, Renovate and GitHub Actions are known by the address they write on their commits (never by name), shown with their maker's mark, their own name and an agent or bot label, and a card saying what each is. Anyone else not on g1t gets a card too, saying so and how to claim the commits by adding the address. Contributor lists are worked out again (stats version 2) so an agent counts once. | 12 | import { codingAgentById, codingAgentFor } from "@g1t/contracts/coding-agents"; |
| Merge commit authors and hovercards: commits show their accounts, every name opens a card (repos 0016) | 13 | |
| 14 | /** | |
| 15 | * One person on a commit. `user` is an account; `g1t` is g1t itself; | |
| Coding agents show as themselves on commits and contributors: Claude, Codex, Copilot, Gemini, Jules, Cursor, Devin, Aider, Dependabot, Renovate and GitHub Actions are known by the address they write on their commits (never by name), shown with their maker's mark, their own name and an agent or bot label, and a card saying what each is. Anyone else not on g1t gets a card too, saying so and how to claim the commits by adding the address. Contributor lists are worked out again (stats version 2) so an agent counts once. | 16 | * `ghost` is an account that was deleted; `agent` is a known coding agent |
| 17 | * or bot, by the address on the commit (@g1t/contracts coding-agents); | |
| 18 | * `author` is nobody on g1t, shown by the name on the commit, with a card | |
| 19 | * that says so. | |
| Merge commit authors and hovercards: commits show their accounts, every name opens a card (repos 0016) | 20 | */ |
| 21 | export type CommitPerson = { | |
| Coding agents show as themselves on commits and contributors: Claude, Codex, Copilot, Gemini, Jules, Cursor, Devin, Aider, Dependabot, Renovate and GitHub Actions are known by the address they write on their commits (never by name), shown with their maker's mark, their own name and an agent or bot label, and a card saying what each is. Anyone else not on g1t gets a card too, saying so and how to claim the commits by adding the address. Contributor lists are worked out again (stats version 2) so an agent counts once. | 22 | kind: "user" | "g1t" | "ghost" | "agent" | "author"; |
| Merge commit authors and hovercards: commits show their accounts, every name opens a card (repos 0016) | 23 | /** The name written on the commit. Beside an account it is a hint. */ |
| 24 | name: string; | |
| 25 | /** The account's username; null for `author`. */ | |
| 26 | username: string | null; | |
| 27 | /** The account's uploaded avatar, by hash. */ | |
| 28 | avatar: string | null; | |
| Coding agents show as themselves on commits and contributors: Claude, Codex, Copilot, Gemini, Jules, Cursor, Devin, Aider, Dependabot, Renovate and GitHub Actions are known by the address they write on their commits (never by name), shown with their maker's mark, their own name and an agent or bot label, and a card saying what each is. Anyone else not on g1t gets a card too, saying so and how to claim the commits by adding the address. Contributor lists are worked out again (stats version 2) so an agent counts once. | 29 | /** The coding agent's id, for an `agent`. */ |
| 30 | agent?: string | null; | |
| Merge commit authors and hovercards: commits show their accounts, every name opens a card (repos 0016) | 31 | }; |
| 32 | ||
| 33 | /** A commit as the page gets it: its people, never their addresses. */ | |
| 34 | export type ShownCommit = Omit<Commit, "author"> & { | |
| 35 | author: CommitPerson; | |
| 36 | /** From `Co-authored-by` trailers, in order. */ | |
| 37 | coAuthors: CommitPerson[]; | |
| 38 | }; | |
| 39 | ||
| 40 | /** Who last changed each line, with the commits' people. */ | |
| 41 | export type ShownBlame = Omit<Blame, "commits"> & { commits: ShownCommit[] }; | |
| 42 | ||
| 43 | /** A commit as a file list's row shows it: no people at all. */ | |
| 44 | export type FileCommit = Pick<Commit, "hash" | "message" | "authoredAt">; | |
| 45 | ||
| 46 | /** Each entry's last commit; `complete` is false when some were not reached. */ | |
| 47 | export type FileCommits = { entries: { name: string; commit: FileCommit }[]; complete: boolean }; | |
| 48 | ||
| 49 | /** | |
| 50 | * The addresses on g1t's own commits: its noreply address, and those its | |
| 51 | * agents and merge queue used before (as "g1t agent"), which history | |
| 52 | * keeps. Every one is shown as g1t. | |
| 53 | */ | |
| 54 | export const G1T_COMMIT_EMAILS: ReadonlySet<string> = new Set([ | |
| 55 | "g1t@users.noreply.g1t.sh", | |
| 56 | "agent@g1t.sh", | |
| 57 | "queue@g1t.sh", | |
| 58 | "mergecheck@g1t.sh", | |
| 59 | ]); | |
| 60 | ||
| 61 | const CO_AUTHOR = /^co-authored-by:\s*(.*?)\s*<([^>]*)>\s*$/i; | |
| 62 | ||
| 63 | /** The name and address in each `Co-authored-by` trailer of a message. */ | |
| 64 | export function coAuthorsOf(message: string): { name: string; email: string }[] { | |
| 65 | const lines = message.trim().split("\n"); | |
| 66 | const last = lines.slice(lines.lastIndexOf("") + 1); | |
| 67 | const found: { name: string; email: string }[] = []; | |
| 68 | for (const line of last) { | |
| 69 | const match = CO_AUTHOR.exec(line.trim()); | |
| 70 | if (match) found.push({ name: match[1]!.trim(), email: match[2]!.trim() }); | |
| 71 | } | |
| 72 | return found; | |
| 73 | } | |
| 74 | ||
| 75 | /** Lowercased and trimmed, as `email_owners` keys them. */ | |
| 76 | export function normalizeEmail(email: string): string { | |
| 77 | return email.trim().toLowerCase(); | |
| 78 | } | |
| 79 | ||
| 80 | /** | |
| 81 | * Every distinct address the commits name, authors and co-authors, that | |
| 82 | * needs asking about: g1t's own are known already. | |
| 83 | */ | |
| 84 | export function addressesToMatch(commits: readonly Pick<Commit, "author" | "message">[]): string[] { | |
| 85 | const seen = new Set<string>(); | |
| 86 | const add = (email: string) => { | |
| 87 | const one = normalizeEmail(email); | |
| 88 | if (one && one.includes("@") && !G1T_COMMIT_EMAILS.has(one)) seen.add(one); | |
| 89 | }; | |
| 90 | for (const commit of commits) { | |
| 91 | add(commit.author.email); | |
| 92 | for (const co of coAuthorsOf(commit.message)) add(co.email); | |
| 93 | } | |
| 94 | return [...seen]; | |
| 95 | } | |
| 96 | ||
| 97 | /** The person a name and address on a commit belong to. */ | |
| 98 | export function personFor(name: string, email: string, owners: Readonly<Record<string, EmailOwner>>): CommitPerson { | |
| 99 | const written = name.trim() || "Unknown"; | |
| 100 | const address = normalizeEmail(email); | |
| 101 | if (G1T_COMMIT_EMAILS.has(address)) return { kind: "g1t", name: written, username: "g1t", avatar: null }; | |
| 102 | const owner = address ? owners[address] : undefined; | |
| 103 | if (owner?.username === "ghost") return { kind: "ghost", name: written, username: "ghost", avatar: null }; | |
| 104 | if (owner) return { kind: "user", name: written, username: owner.username, avatar: owner.avatar }; | |
| Coding agents show as themselves on commits and contributors: Claude, Codex, Copilot, Gemini, Jules, Cursor, Devin, Aider, Dependabot, Renovate and GitHub Actions are known by the address they write on their commits (never by name), shown with their maker's mark, their own name and an agent or bot label, and a card saying what each is. Anyone else not on g1t gets a card too, saying so and how to claim the commits by adding the address. Contributor lists are worked out again (stats version 2) so an agent counts once. | 105 | const agent = codingAgentFor(address); |
| 106 | if (agent) return { kind: "agent", name: written, username: null, avatar: null, agent: agent.id }; | |
| Merge commit authors and hovercards: commits show their accounts, every name opens a card (repos 0016) | 107 | return { kind: "author", name: written, username: null, avatar: null }; |
| 108 | } | |
| 109 | ||
| 110 | /** A commit with its people found and its author's address left out. */ | |
| 111 | export function showCommit(commit: Commit, owners: Readonly<Record<string, EmailOwner>>): ShownCommit { | |
| 112 | const { author, ...rest } = commit; | |
| 113 | return { | |
| 114 | ...rest, | |
| 115 | author: personFor(author.name, author.email, owners), | |
| 116 | coAuthors: coAuthorsOf(commit.message).map((co) => personFor(co.name, co.email, owners)), | |
| 117 | }; | |
| 118 | } | |
| 119 | ||
| 120 | /** | |
| 121 | * A repository's contributor as a person on its commits. The repos | |
| 122 | * service tallies them by the same rules (services/repos/src/contributors.rs: | |
| 123 | * `email_owners`, then g1t's addresses, then the name), so the About, the | |
| 124 | * Contributors page and every commit show one person the same way. | |
| 125 | */ | |
| Coding agents show as themselves on commits and contributors: Claude, Codex, Copilot, Gemini, Jules, Cursor, Devin, Aider, Dependabot, Renovate and GitHub Actions are known by the address they write on their commits (never by name), shown with their maker's mark, their own name and an agent or bot label, and a card saying what each is. Anyone else not on g1t gets a card too, saying so and how to claim the commits by adding the address. Contributor lists are worked out again (stats version 2) so an agent counts once. | 126 | export function contributorPerson(contributor: Pick<Contributor, "kind" | "name" | "username" | "avatar" | "agent">): CommitPerson { |
| Merge commit authors and hovercards: commits show their accounts, every name opens a card (repos 0016) | 127 | if (contributor.kind === "g1t") return { kind: "g1t", name: contributor.name, username: "g1t", avatar: null }; |
| 128 | if (contributor.kind === "user" && contributor.username === "ghost") return { kind: "ghost", name: contributor.name, username: "ghost", avatar: null }; | |
| 129 | if (contributor.kind === "user" && contributor.username) { | |
| 130 | return { kind: "user", name: contributor.name, username: contributor.username, avatar: contributor.avatar ?? null }; | |
| 131 | } | |
| Coding agents show as themselves on commits and contributors: Claude, Codex, Copilot, Gemini, Jules, Cursor, Devin, Aider, Dependabot, Renovate and GitHub Actions are known by the address they write on their commits (never by name), shown with their maker's mark, their own name and an agent or bot label, and a card saying what each is. Anyone else not on g1t gets a card too, saying so and how to claim the commits by adding the address. Contributor lists are worked out again (stats version 2) so an agent counts once. | 132 | if (contributor.kind === "agent" && codingAgentById(contributor.agent)) { |
| 133 | return { kind: "agent", name: contributor.name, username: null, avatar: null, agent: contributor.agent ?? null }; | |
| 134 | } | |
| Merge commit authors and hovercards: commits show their accounts, every name opens a card (repos 0016) | 135 | return { kind: "author", name: contributor.name, username: null, avatar: null }; |
| 136 | } | |
| 137 | ||
| Coding agents show as themselves on commits and contributors: Claude, Codex, Copilot, Gemini, Jules, Cursor, Devin, Aider, Dependabot, Renovate and GitHub Actions are known by the address they write on their commits (never by name), shown with their maker's mark, their own name and an agent or bot label, and a card saying what each is. Anyone else not on g1t gets a card too, saying so and how to claim the commits by adding the address. Contributor lists are worked out again (stats version 2) so an agent counts once. | 138 | /** What a person on a commit is called on the page: an agent by its own name, whatever the commit wrote. */ |
| Merge commit authors and hovercards: commits show their accounts, every name opens a card (repos 0016) | 139 | export function shownName(person: CommitPerson): string { |
| Coding agents show as themselves on commits and contributors: Claude, Codex, Copilot, Gemini, Jules, Cursor, Devin, Aider, Dependabot, Renovate and GitHub Actions are known by the address they write on their commits (never by name), shown with their maker's mark, their own name and an agent or bot label, and a card saying what each is. Anyone else not on g1t gets a card too, saying so and how to claim the commits by adding the address. Contributor lists are worked out again (stats version 2) so an agent counts once. | 140 | if (person.kind === "agent") return codingAgentById(person.agent)?.name ?? person.name; |
| Merge commit authors and hovercards: commits show their accounts, every name opens a card (repos 0016) | 141 | return person.username ?? person.name; |
| 142 | } | |
| 143 | ||
| 144 | /** Where a person on a commit links to, if anywhere. */ | |
| 145 | export function profileHref(person: CommitPerson): string | null { | |
| 146 | return person.kind === "user" && person.username ? `/u/${person.username}` : null; | |
| 147 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.