// pulls recent commits from my repos on tangled and writes them into the // static html. // run with `mise run sync-commits`. no dependencies, node 18+. // // a page opts in with a pair of marker comments naming the repo and how many // commits to show; everything between them is replaced on every run: // // ... // // commits come from main, newest first, read from the knot that hosts my // repos (no login needed). if it can't be reached, the script exits non-zero // and touches nothing. import { readFile, writeFile } from "node:fs/promises"; const KNOT = "https://knot1.tangled.sh"; const OWNER = "did:plc:e3z5gj5iyuwgqao562bo4bdf"; // danieldaum.net const TZ = "America/Los_Angeles"; const PAGES = ["projects/index.html", "projects/holonet/index.html"]; const MARKER = /^([ \t]*)[\s\S]*?/gm; // --------------------------------------------------------------------------- // data async function fetchCommits(repo, limit) { const url = new URL("/xrpc/sh.tangled.repo.log", KNOT); url.searchParams.set("repo", `${OWNER}/${repo}`); url.searchParams.set("ref", "main"); url.searchParams.set("limit", String(limit)); const res = await fetch(url); if (!res.ok) throw new Error(`${repo} commits: ${res.status} ${res.statusText}`); const { commits } = await res.json(); return commits.map((c) => ({ sha: c.this, message: c.message, date: c.author.When })); } // --------------------------------------------------------------------------- // html function render(commits) { if (commits.length === 0) return `

no commits yet

`; const rows = commits.map((c) => [ `
  • `, ` ${c.sha.slice(0, 7)}`, ` ${esc(c.message.split("\n")[0])}`, ` `, `
  • `, ].join("\n"), ); return [`
      `, ...rows, `
    `].join("\n"); } // 09 Sep 26, like the writing list. en-US, because en-GB says "Sept" function formatDate(iso) { const parts = new Intl.DateTimeFormat("en-US", { timeZone: TZ, day: "2-digit", month: "short", year: "2-digit" }).formatToParts(new Date(iso)); const get = (type) => parts.find((p) => p.type === type).value; return `${get("day")} ${get("month")} ${get("year")}`; } function esc(s) { return String(s).replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); } // --------------------------------------------------------------------------- // writing const pages = await Promise.all(PAGES.map((page) => readFile(page, "utf8"))); // one request per repo, for the most commits any marker asks for const wanted = new Map(); for (const html of pages) { for (const [, , repo, n] of html.matchAll(MARKER)) { wanted.set(repo, Math.max(wanted.get(repo) ?? 0, Number(n))); } } const commits = new Map(); for (const [repo, n] of wanted) commits.set(repo, await fetchCommits(repo, n)); for (const [i, page] of PAGES.entries()) { const before = pages[i]; const after = before.replace(MARKER, (_, indent, repo, n) => { const body = render(commits.get(repo).slice(0, Number(n))).replace(/^(?=.)/gm, indent); return `${indent}\n${body}\n${indent}`; }); if (after !== before) { await writeFile(page, after); console.log(`updated ${page}`); } } for (const [repo, list] of commits) console.log(`${repo}: ${list.length} commit${list.length === 1 ? "" : "s"}`);