Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .claude/skills/records/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,8 +202,9 @@ node scripts/commitRecords.js --out page.html [--pr <number>] [--title <text>] \
## What the page shows

The page is the heading, then a card per commit. A card shows the commit's
short sha, author and co-authors (an AI assistant by its short name, such
as Claude), date, the pull request a `(#1234)` at the end of the subject
short sha, author (in a pull request's page and comment, the GitHub login
linking the profile) and co-authors (an AI assistant by its short name,
such as Claude), date, the pull request a `(#1234)` at the end of the subject
names, and the message drawn: the subject, the prose, each object block
with its example and forms, the `Breaking:` sections as banners, and the
`Review:` sections as a list.
10 changes: 9 additions & 1 deletion .github/workflows/semantic_diff.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ jobs:
persist-credentials: false

- name: Render semantic diff
env:
GH_TOKEN: ${{ github.token }}
run: |
git fetch --no-tags --filter=blob:none origin "refs/pull/${PR_NUMBER}/head:refs/heads/pr-${PR_NUMBER}"
base=$(git merge-base "$BASE_SHA" "pr-${PR_NUMBER}")
Expand All @@ -72,7 +74,13 @@ jobs:
previous=(--previous "$BEFORE_SHA")
fi
mkdir -p semantic-diff
node scripts/commitRecords.js --out semantic-diff/index.html --markdown semantic-diff/comment.md "${previous[@]}" --url "$url" --pr "$PR_NUMBER" --title "$PR_TITLE" "${base}..pr-${PR_NUMBER}"
# The authors' GitHub logins, which git doesn't know; the API links
# a commit to the account its email belongs to. Without them, the
# authors are drawn by name.
gh api --paginate "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/commits?per_page=100" \
--jq '.[] | select(.author != null) | "\(.sha) \(.author.login)"' > semantic-diff/logins.txt ||
: > semantic-diff/logins.txt
node scripts/commitRecords.js --out semantic-diff/index.html --markdown semantic-diff/comment.md "${previous[@]}" --logins semantic-diff/logins.txt --url "$url" --pr "$PR_NUMBER" --title "$PR_TITLE" "${base}..pr-${PR_NUMBER}"
- name: Setup Rclone
uses: cometkim/rclone-actions/setup-rclone@8d9c6b95341f7698609deee65c3d62af42893288 # main, 2025-06-18
Expand Down
29 changes: 23 additions & 6 deletions scripts/commitRecords.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@
* node scripts/commitRecords.js --out review.html <base>..<tip>...
* --pr <number> and --title <text> head the page with the pull request.
* --markdown <file> also draws the records as the pull request's comment
* (scripts/recordMarkdown.js), linking the page at --url <url>. With
* (scripts/recordMarkdown.js), linking the page at --url <url>.
* --logins <file>, lines `<sha> <login>`, names the GitHub accounts of
* the commits' authors, drawn in place of their names. With
* --previous <rev>, the pull request's head before the last push, the
* comment marks the commits that push added or whose message it edited.
*/

import { execFileSync } from "node:child_process";
import { writeFileSync } from "node:fs";
import { readFileSync, writeFileSync } from "node:fs";
import { commentMarkdown } from "./recordMarkdown.js";
import { CSS, esc, pageBody, pageTitle, parseRecord } from "./recordRender.js";

Expand Down Expand Up @@ -57,6 +59,7 @@ function readRecord(rev) {
const record = parseRecord({
sha: show("--format=%h", "--abbrev=9").trim(),
author: show("--format=%an").trim(),
login: logins.get(show("--format=%H").trim()),
date: utc(new Date(show("--format=%aI").trim())),
message: show("--format=%B"),
});
Expand Down Expand Up @@ -131,9 +134,15 @@ const options = {};
const targets = [];
for (let i = 0; i < args.length; i++) {
if (
["--out", "--pr", "--title", "--markdown", "--url", "--previous"].includes(
args[i],
) &&
[
"--out",
"--pr",
"--title",
"--markdown",
"--url",
"--previous",
"--logins",
].includes(args[i]) &&
i + 1 < args.length
)
options[args[i].slice(2)] = args[++i];
Expand All @@ -142,10 +151,18 @@ for (let i = 0; i < args.length; i++) {
const outFile = options.out;
if (!outFile) {
console.error(
"usage: commitRecords.js --out <page.html> [--pr <number>] [--title <text>] [--markdown <comment.md> [--url <page url>] [--previous <rev>]] <rev | base..tip>...",
"usage: commitRecords.js --out <page.html> [--pr <number>] [--title <text>] [--markdown <comment.md> [--url <page url>] [--previous <rev>]] [--logins <file>] <rev | base..tip>...",
);
process.exit(1);
}
/** The GitHub logins of the commits' authors, by full sha. */
const logins = new Map(
(options.logins ? readFileSync(options.logins, "utf8") : "")
.split("\n")
.map(l => l.trim().split(/\s+/))
.filter(f => f.length === 2)
.map(([sha, login]) => [sha, login]),
);
const heading = {
pr: (options.pr ?? "").replace(/\D/g, ""),
title: options.title ?? "",
Expand Down
46 changes: 46 additions & 0 deletions scripts/commitRecords.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -866,6 +866,52 @@ test("--markdown draws every commit into the comment, with the page's link", asy
has(comment, "Second change");
});

test("--logins draws an author by the GitHub login, linked, others by name", async () => {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "commit-records-"));
const git = (/** @type {string[]} */ ...a) =>
execFileSync("git", a, { cwd: dir, encoding: "utf8" }).trim();
git("init", "-q", "-b", "main");
git("config", "user.name", "A Person");
git("config", "user.email", "a@example.com");
await commit(git, dir, { "a.txt": "a\n" }, "base");
await commit(
git,
dir,
{},
`Known\n\nCo-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>\n${SIGNED}`,
);
const known = git("rev-parse", "HEAD");
await commit(git, dir, {}, `Unknown\n\n${SIGNED}`);
await commit(git, dir, {}, `Odd login\n\n${SIGNED}`);
const odd = git("rev-parse", "HEAD");
const logins = path.join(dir, "logins.txt");
await fs.writeFile(logins, `${known} a-person\n${odd} bot[bot]\n`);
const out = path.join(dir, "page.html");
const md = path.join(dir, "comment.md");
const r = spawnSync(
"node",
[
script,
...["--out", out, "--markdown", md, "--logins", logins],
"HEAD~3..HEAD",
],
{ cwd: dir, encoding: "utf8" },
);
assert.equal(r.status, 0, r.stderr);
const html = await fs.readFile(out, "utf8");
has(
html,
'</code> · <a href="https://github.com/a-person">a-person</a> + Claude · ',
);
const comment = await fs.readFile(md, "utf8");
has(comment, " · [a-person](https://github.com/a-person) + Claude · ");
const card = (/** @type {string} */ subject) =>
comment.split("<details").find(d => d.includes(`<b>${subject}</b>`)) ?? "";
has(card("Unknown"), " · A Person · ");
has(card("Odd login"), " · A Person · ");
lacks(comment, "@a-person");
});

test("--previous marks the commits the last push added or edited", async () => {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "commit-records-"));
const git = (/** @type {string[]} */ ...a) =>
Expand Down
2 changes: 1 addition & 1 deletion scripts/recordMarkdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,7 @@ export function commitMarkdown(r, full, change = "") {
[...s.text, ...s.block.map(l => l.trim())].join(" ").trim();
const out = [
`<details open><summary>${change ? `${SIGNS[/** @type {keyof typeof SIGNS} */ (change)] ?? ""} ` : ""}<b>${inlineHtml(subject)}</b></summary>`,
`<sub>${esc(r.sha)} · ${inline(byline(r))}${r.date ? ` · ${esc(r.date)}` : ""}${pr ? ` · #${pr}` : ""}${change ? ` · ${esc(change)}` : ""}</sub>`,
`<sub>${esc(r.sha)} · ${byline(r, inline, (login, url) => `[${login}](${url})`)}${r.date ? ` · ${esc(r.date)}` : ""}${pr ? ` · #${pr}` : ""}${change ? ` · ${esc(change)}` : ""}</sub>`,
...sectionGroups(r.sections.filter(s => !BELOW.has(s.name))).map(g =>
section(g.section, g.parts, full),
),
Expand Down
21 changes: 17 additions & 4 deletions scripts/recordRender.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@ export const PART = /^(?:Example|(Before|After) \(([a-z]+)\))$/;

/**
* @typedef {{ name: string, text: string[], block: string[] }} Section
* @typedef {{ sha: string, author: string, date: string, message: string }} CommitInfo
* @typedef {{
* sha: string, author: string, login?: string, date: string, message: string
* }} CommitInfo
* @typedef {CommitInfo & {
* subject: string, sections: Section[], coauthors: string[]
* }} CommitRecord
Expand Down Expand Up @@ -192,10 +194,21 @@ export function parseRecord(commit) {
}

/**
* Who made a commit: its author and co-authors, as `A Person + Claude`.
* Who made a commit, as `A Person + Claude`: its author, by the GitHub login
* when known, and its co-authors, each name drawn by `name` and the login
* as a link to the profile by `profile` (a link, not an `@` mention, which
* would notify the user).
* @param {CommitRecord} r
* @param {(name: string) => string} name
* @param {(login: string, url: string) => string} profile
*/
export const byline = r => [r.author, ...r.coauthors].join(" + ");
export const byline = (r, name, profile) =>
[
r.login && /^[A-Za-z0-9-]+$/.test(r.login)
? profile(r.login, `https://github.com/${r.login}`)
: name(r.author),
...r.coauthors.map(name),
].join(" + ");

// ---------------------------------------------------------------------------
// Code, highlighted by token
Expand Down Expand Up @@ -1530,7 +1543,7 @@ function card(r) {
[...s.text, ...s.block.map(l => l.trim())].join(" ").trim();
const out = [
`<article class="card${breaking.length ? " breaking" : ""}" id="c-${esc(r.sha)}">`,
`<header><div class="meta"><code>${esc(r.sha)}</code> · ${esc(byline(r))}${r.date ? ` · ${esc(r.date)}` : ""}${
`<header><div class="meta"><code>${esc(r.sha)}</code> · ${byline(r, esc, (login, url) => `<a href="${url}">${login}</a>`)}${r.date ? ` · ${esc(r.date)}` : ""}${
pr ? ` · <a href="${REPO_URL}/pull/${pr}">#${pr}</a>` : ""
}</div><h3>${inline(subject)}</h3></header>`,
];
Expand Down
Loading