Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

142 linesCodeBlameRaw
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 */
11import type { Blame, Commit, Contributor, EmailOwner } from "@g1t/contracts";
12import { codingAgentById, codingAgentFor } from "@g1t/contracts/coding-agents";
13
14/**
15 * One person on a commit. `user` is an account; `g1t` is g1t itself;
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.
20 */
21export type CommitPerson = {
22 kind: "user" | "g1t" | "ghost" | "agent" | "author";
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;
29 /** The coding agent's id, for an `agent`. */
30 agent?: string | null;
31};
32
33/** A commit as the page gets it: its people, never their addresses. */
34export 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. */
41export type ShownBlame = Omit<Blame, "commits"> & { commits: ShownCommit[] };
42
43/** A commit as a file list's row shows it: no people at all. */
44export type FileCommit = Pick<Commit, "hash" | "message" | "authoredAt">;
45
46/** Each entry's last commit; `complete` is false when some were not reached. */
47export 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 */
54export const G1T_COMMIT_EMAILS: ReadonlySet<string> = new Set(["g1t@users.noreply.g1t.sh", "agent@g1t.sh", "queue@g1t.sh", "mergecheck@g1t.sh"]);
55
56const CO_AUTHOR = /^co-authored-by:\s*(.*?)\s*<([^>]*)>\s*$/i;
57
58/** The name and address in each `Co-authored-by` trailer of a message. */
59export function coAuthorsOf(message: string): { name: string; email: string }[] {
60 const lines = message.trim().split("\n");
61 const last = lines.slice(lines.lastIndexOf("") + 1);
62 const found: { name: string; email: string }[] = [];
63 for (const line of last) {
64 const match = CO_AUTHOR.exec(line.trim());
65 if (match) found.push({ name: match[1]!.trim(), email: match[2]!.trim() });
66 }
67 return found;
68}
69
70/** Lowercased and trimmed, as `email_owners` keys them. */
71export function normalizeEmail(email: string): string {
72 return email.trim().toLowerCase();
73}
74
75/**
76 * Every distinct address the commits name, authors and co-authors, that
77 * needs asking about: g1t's own are known already.
78 */
79export function addressesToMatch(commits: readonly Pick<Commit, "author" | "message">[]): string[] {
80 const seen = new Set<string>();
81 const add = (email: string) => {
82 const one = normalizeEmail(email);
83 if (one && one.includes("@") && !G1T_COMMIT_EMAILS.has(one)) seen.add(one);
84 };
85 for (const commit of commits) {
86 add(commit.author.email);
87 for (const co of coAuthorsOf(commit.message)) add(co.email);
88 }
89 return [...seen];
90}
91
92/** The person a name and address on a commit belong to. */
93export function personFor(name: string, email: string, owners: Readonly<Record<string, EmailOwner>>): CommitPerson {
94 const written = name.trim() || "Unknown";
95 const address = normalizeEmail(email);
96 if (G1T_COMMIT_EMAILS.has(address)) return { kind: "g1t", name: written, username: "g1t", avatar: null };
97 const owner = address ? owners[address] : undefined;
98 if (owner?.username === "ghost") return { kind: "ghost", name: written, username: "ghost", avatar: null };
99 if (owner) return { kind: "user", name: written, username: owner.username, avatar: owner.avatar };
100 const agent = codingAgentFor(address);
101 if (agent) return { kind: "agent", name: written, username: null, avatar: null, agent: agent.id };
102 return { kind: "author", name: written, username: null, avatar: null };
103}
104
105/** A commit with its people found and its author's address left out. */
106export function showCommit(commit: Commit, owners: Readonly<Record<string, EmailOwner>>): ShownCommit {
107 const { author, ...rest } = commit;
108 return {
109 ...rest,
110 author: personFor(author.name, author.email, owners),
111 coAuthors: coAuthorsOf(commit.message).map((co) => personFor(co.name, co.email, owners)),
112 };
113}
114
115/**
116 * A repository's contributor as a person on its commits. The repos
117 * service tallies them by the same rules (services/repos/src/contributors.rs:
118 * `email_owners`, then g1t's addresses, then the name), so the About, the
119 * Contributors page and every commit show one person the same way.
120 */
121export function contributorPerson(contributor: Pick<Contributor, "kind" | "name" | "username" | "avatar" | "agent">): CommitPerson {
122 if (contributor.kind === "g1t") return { kind: "g1t", name: contributor.name, username: "g1t", avatar: null };
123 if (contributor.kind === "user" && contributor.username === "ghost") return { kind: "ghost", name: contributor.name, username: "ghost", avatar: null };
124 if (contributor.kind === "user" && contributor.username) {
125 return { kind: "user", name: contributor.name, username: contributor.username, avatar: contributor.avatar ?? null };
126 }
127 if (contributor.kind === "agent" && codingAgentById(contributor.agent)) {
128 return { kind: "agent", name: contributor.name, username: null, avatar: null, agent: contributor.agent ?? null };
129 }
130 return { kind: "author", name: contributor.name, username: null, avatar: null };
131}
132
133/** What a person on a commit is called on the page: an agent by its own name, whatever the commit wrote. */
134export function shownName(person: CommitPerson): string {
135 if (person.kind === "agent") return codingAgentById(person.agent)?.name ?? person.name;
136 return person.username ?? person.name;
137}
138
139/** Where a person on a commit links to, if anywhere. */
140export function profileHref(person: CommitPerson): string | null {
141 return person.kind === "user" && person.username ? `/u/${person.username}` : null;
142}