flagon-io/g1t

public

Git for AI scale: a forge for thousands of agents working on the same code at once.

Commit

Commit pages credit co-authors instead of printing their trailers

A commit's trailers (Co-Authored-By and the like) are the last paragraph of its message. Co-authors now read beside the author, as in 'g1t agent and Claude Sonnet 5.5 committed', and other trailers show as quiet metadata under the body. The site gets a test script for its pure helpers.

syntaqxcommitted Parent53f99fdBrowse files
5 files+80−20/5 viewed
+27−0
1+import assert from "node:assert/strict";
2+import { test } from "node:test";
3+
4+import { parseCommitMessage } from "./commit-message.ts";
5+
6+test("co-authors leave the body and keep only their names", () => {
7+ const parsed = parseCommitMessage(
8+ "Document every option\n\nAdds the missing rows.\n\nCo-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>",
9+ );
10+ assert.equal(parsed.subject, "Document every option");
11+ assert.equal(parsed.body, "Adds the missing rows.");
12+ assert.deepEqual(parsed.coAuthors, ["Claude Sonnet 5.5"]);
13+ assert.deepEqual(parsed.trailers, []);
14+});
15+
16+test("a message that is only trailers has no body", () => {
17+ const parsed = parseCommitMessage("Fix it\n\nCo-Authored-By: Ada <ada@example.com>\nG1t-Pull: syntaqx/hello#72");
18+ assert.equal(parsed.body, "");
19+ assert.deepEqual(parsed.coAuthors, ["Ada"]);
20+ assert.deepEqual(parsed.trailers, [{ key: "G1t-Pull", value: "syntaqx/hello#72" }]);
21+});
22+
23+test("prose that happens to contain a colon stays prose", () => {
24+ const parsed = parseCommitMessage("Fix it\n\nNote: this also renames a function.\nAnd tidies the tests.");
25+ assert.equal(parsed.body, "Note: this also renames a function.\nAnd tidies the tests.");
26+ assert.deepEqual(parsed.coAuthors, []);
27+});
+35−0
1+/**
2+ * A commit message's parts: its subject, the prose of its body, and the
3+ * trailers git and tools add at the end (`Co-Authored-By: …`), which read
4+ * better as metadata than as prose.
5+ */
6+export type CommitMessage = {
7+ subject: string;
8+ body: string;
9+ /** The names in `Co-Authored-By` trailers, without their addresses. */
10+ coAuthors: string[];
11+ /** Every other trailer, as written. */
12+ trailers: { key: string; value: string }[];
13+};
14+
15+const TRAILER = /^([A-Za-z][A-Za-z0-9-]*):\s+(.+)$/;
16+
17+export function parseCommitMessage(message: string): CommitMessage {
18+ const [subject = "", ...rest] = message.split("\n");
19+ const lines = rest.join("\n").trim().split("\n");
20+ // Trailers are the last paragraph, when every line of it is one.
21+ const lastBreak = lines.lastIndexOf("");
22+ const last = lines.slice(lastBreak + 1);
23+ const isTrailers = last.length > 0 && last.every((line) => TRAILER.test(line.trim()));
24+ const prose = isTrailers ? lines.slice(0, Math.max(lastBreak, 0)) : lines;
25+ const coAuthors: string[] = [];
26+ const trailers: { key: string; value: string }[] = [];
27+ if (isTrailers) {
28+ for (const line of last) {
29+ const [, key, value] = TRAILER.exec(line.trim())!;
30+ if (key.toLowerCase() === "co-authored-by") coAuthors.push(value.replace(/\s*<[^>]*>\s*$/, "").trim());
31+ else trailers.push({ key, value });
32+ }
33+ }
34+ return { subject, body: prose.join("\n").trim(), coAuthors, trailers };
35+}
+16−2
44
55 import type { Comparison, Pull } from "@g1t/contracts";
66
7+import { parseCommitMessage } from "../../lib/commit-message";
78 import type { Route } from "./+types/commit";
89 import { DiffView } from "../../components/diff-view";
910 import { Avatar, TimeAgo } from "../../components/ui";
121122 export default function CommitPage({ loaderData, params }: Route.ComponentProps) {
122123 const { commit, pull, comparison } = loaderData;
123124 const base = `/${params.owner}/${params.repo}`;
124− const [subject, ...rest] = commit.message.split("\n");
125− const body = rest.join("\n").trim();
125+ const { subject, body, coAuthors, trailers } = parseCommitMessage(commit.message);
126126 return (
127127 <div>
128128 <section className="overflow-hidden rounded-xl border border-line bg-surface">
135135 </p>
136136 <h2 className="mt-2 text-xl font-semibold tracking-tight text-balance">{subject}</h2>
137137 {body && <p className="mt-3 max-w-3xl text-sm whitespace-pre-wrap text-muted">{body}</p>}
138+ {trailers.length > 0 && (
139+ <dl className="mt-3 space-y-0.5 font-mono text-xs text-faint">
140+ {trailers.map((trailer) => (
141+ <div key={`${trailer.key}:${trailer.value}`}>
142+ <dt className="inline">{trailer.key}:</dt> <dd className="inline text-muted">{trailer.value}</dd>
143+ </div>
144+ ))}
145+ </dl>
146+ )}
138147 </div>
139148 <div className="flex flex-wrap items-center gap-x-5 gap-y-3 border-t border-line bg-bg/40 px-5 py-3 text-sm">
140149 <span className="flex items-center gap-2">
141150 <Avatar name={commit.author.name} size={20} />
142151 <span className="font-medium">{commit.author.name}</span>
152+ {coAuthors.length > 0 && (
153+ <span className="text-muted">
154+ and <span className="font-medium text-fg">{coAuthors.join(", ")}</span>
155+ </span>
156+ )}
143157 <span className="text-muted">
144158 committed <TimeAgo at={commit.authoredAt} />
145159 </span>
+1−0
44 "type": "module",
55 "scripts": {
66 "build": "react-router build",
7+ "test": "node --test app/**/*.test.ts",
78 "dev": "react-router dev",
89 "typecheck": "wrangler types --include-env=false && react-router typegen && tsc -b --force",
910 "deploy": "npm run build && wrangler deploy",
+1−0
88 "workers/**/*",
99 "worker-configuration.d.ts"
1010 ],
11+ "exclude": ["app/**/*.test.ts"],
1112 "compilerOptions": {
1213 "composite": true,
1314 "strict": true,