Changelog eba2e7f
Let the changelog reload uncommitted work without restarting the dev server.
5 files of code, design or docs +77 −7 lines
Code, design and documents 5 files
src/components/UncommittedReload.astro +21 −0
@@ -0,0 +1,21 @@ +--- +// The uncommitted changelog entry is a snapshot. Reload asks the dev server to read the working +// tree again (src/lib/gitlog.js). Shown only in dev; a production build has no server to ask. +const { at } = Astro.props; +const when = new Date(at).toLocaleString("en-GB", { day: "numeric", month: "short", hour: "2-digit", minute: "2-digit" }); +--- +<p class="gl-asof"> + <time datetime={at}>As of {when}</time> + <button type="button" class="btn" data-gl-reload>Reload</button> +</p> +<script> + for (const b of document.querySelectorAll("[data-gl-reload]")) { + if (b.dataset.bound) continue; + b.dataset.bound = "1"; + b.addEventListener("click", () => { + b.disabled = true; + b.textContent = "Reading…"; + location.reload(); + }); + } +</script>
src/lib/gitlog.js +19 −0
@@ -188,4 +188,7 @@ function readLog() { // Built on this computer (not by the deploy workflow, which sets CI), the changes not yet committed // come first, as one more change: { hash: "WORKTREE", short: "uncommitted", ... } +// The dev server keeps this module loaded, so that snapshot would stay as it was when the server +// started. The changelog pages call reloadUncommitted() on each request, and the page has a Reload +// button for when it is left open while files change. function uncommitted() { if (process.env.CI) return []; @@ -208,4 +211,20 @@ function uncommitted() { export const commits = [...uncommitted(), ...readLog()].map((c) => ({ ...c, groups: workOut(c) })); +const EMPTY = { history: null, places: [], texts: [], data: [], code: [] }; + +// Read the working tree again and put that in front of `commits`. Null when everything is committed. +export function reloadUncommitted() { + if (!import.meta.env.DEV) return commits.find((c) => c.local) ?? null; + const [c] = uncommitted(); + const fresh = c ? { ...c, groups: workOut(c) } : null; + const i = commits.findIndex((x) => x.local); + if (fresh && i >= 0) commits[i] = fresh; + else if (fresh) commits.unshift(fresh); + else if (i >= 0) commits.splice(i, 1); + return fresh; +} + +export { EMPTY as emptyGroups }; + // one line of what a commit did: [[count, label, the id of its section on the commit's page]] export function summaryOf(c) {
src/pages/changelog/[hash].astro +24 −6
@@ -2,13 +2,31 @@ // One change to the site: its message, and what it did, section by section (src/lib/gitlog.js). import Base from "../../layouts/Base.astro"; -import { commits, summaryOf } from "../../lib/gitlog.js"; +import UncommittedReload from "../../components/UncommittedReload.astro"; +import { commits, emptyGroups, reloadUncommitted, summaryOf } from "../../lib/gitlog.js"; import { formatDate, places, SEGMENTS, segmentPoints } from "../../lib/countries.js"; import { tierColor } from "../../lib/tiers.js"; export function getStaticPaths() { - return commits.map((c, i) => ({ params: { hash: c.short }, props: { c, newer: commits[i - 1], older: commits[i + 1] } })); + // Keep the address available in dev even if the tree was clean when the server started, so Reload + // still has a page to land on after a later edit. + const list = [...commits]; + if (import.meta.env.DEV && !list.some((c) => c.local)) { + list.unshift({ + hash: "WORKTREE", short: "uncommitted", date: new Date().toISOString(), + subject: "Uncommitted changes", body: "Work on this computer that is not committed yet. Shown only in local builds.", + parent: list[0]?.hash ?? null, files: [], add: 0, del: 0, local: true, groups: emptyGroups, + }); + } + return list.map((c, i) => ({ params: { hash: c.short }, props: { c, newer: list[i - 1] ?? null, older: list[i + 1] ?? null } })); } -const { c, newer, older } = Astro.props; +let { c, newer, older } = Astro.props; const base = import.meta.env.BASE_URL; +if (import.meta.env.DEV) Astro.response.headers.set("Cache-Control", "no-store"); +if (import.meta.env.DEV && c.local) { + c = reloadUncommitted() ?? { + ...c, date: new Date().toISOString(), files: [], add: 0, del: 0, groups: emptyGroups, + body: "Nothing on this computer is uncommitted.", + }; +} const g = c.groups; const CAP = 200; // longest list shown in full @@ -28,10 +46,10 @@ const moves = g.places.filter((p) => p.scores?.length || (p.status === "added" & <Base title={`${c.subject} · Changelog`} description={`${formatDate(c.date.slice(0, 10))}: ${summary.map(([n, w]) => `${n} ${w}`).join(", ") || "changes to the site"}.`}> <main class="changes-page gl gl-commit"> - <div class="page-bar"><a class="back" href={`${base}changelog/`}>← Changelog</a></div> <section class="hero hero-sm"> - <p class="eyebrow">Changelog <span><time datetime={c.date}>{formatDate(c.date.slice(0, 10))}</time></span> <span>{c.short}</span></p> + <p class="eyebrow"><a href={`${base}changelog/`}>Changelog</a> <span><time datetime={c.date}>{formatDate(c.date.slice(0, 10))}</time></span> <span>{c.short}</span></p> <h1>{c.subject}</h1> {c.body && <p class="lede gl-body">{c.body}</p>} - <p class="gl-sum gl-sum-big">{summary.map(([n, what, at]) => <a href={`#${at}`}><em>{n}</em> {what}</a>)} <span class="gl-lines"><ins>+{c.add.toLocaleString("en-GB")}</ins> <del>−{c.del.toLocaleString("en-GB")}</del> lines</span></p> + {import.meta.env.DEV && c.local && <UncommittedReload at={c.date} />} + {(summary.length > 0 || c.add + c.del > 0) && <p class="gl-sum gl-sum-big">{summary.map(([n, what, at]) => <a href={`#${at}`}><em>{n}</em> {what}</a>)} <span class="gl-lines"><ins>+{c.add.toLocaleString("en-GB")}</ins> <del>−{c.del.toLocaleString("en-GB")}</del> lines</span></p>} </section>
src/pages/changelog/index.astro +8 −1
@@ -4,7 +4,13 @@ // scores alone are on /score-changes/. import Base from "../../layouts/Base.astro"; -import { commits, summaryOf } from "../../lib/gitlog.js"; +import UncommittedReload from "../../components/UncommittedReload.astro"; +import { commits, reloadUncommitted, summaryOf } from "../../lib/gitlog.js"; import { formatDate } from "../../lib/countries.js"; +if (import.meta.env.DEV) { + Astro.response.headers.set("Cache-Control", "no-store"); + reloadUncommitted(); +} +const local = commits.find((c) => c.local); const base = import.meta.env.BASE_URL; // grouped by month, newest first @@ -22,4 +28,5 @@ for (const c of commits) { <h1>Every change, and what exactly changed</h1> <p class="lede">Everything done to the index, newest first: new history, rescored countries, rewritten articles, new datasets and changes to the site itself. Open one to see it word by word. Only the changes that moved a country's score are on <a href={`${base}score-changes/`}>Score changes</a>.</p> + {import.meta.env.DEV && local && <UncommittedReload at={local.date} />} </section> {months.map((g) => (
src/styles/global.css +5 −0
@@ -3001,4 +3001,6 @@ a.ch-db-track:hover { background: color-mix(in srgb, var(--text) 6%, transparent /* the changelog (src/pages/changelog/): commits and what exactly they changed */ +.gl-commit .eyebrow a { color: var(--text-2); border-bottom: 1px solid var(--muted); } +.gl-commit .eyebrow a:hover { color: var(--text); } .gl-list { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--border); } .gl-list a { display: grid; grid-template-columns: 120px minmax(0, 1fr) auto; gap: 4px 18px; padding: 14px 0; border-bottom: 1px solid var(--border); color: inherit; text-decoration: none; } @@ -3008,4 +3010,7 @@ a.ch-db-track:hover { background: color-mix(in srgb, var(--text) 6%, transparent .gl-local a { border-left: 3px dashed #f0b54a; padding-left: 12px; } .gl-local b::after { content: "local only"; margin-left: 10px; padding: 1px 6px; border: 1px solid #f0b54a; border-radius: 2px; font: 400 10px var(--mono); text-transform: uppercase; letter-spacing: .08em; color: #f0b54a; vertical-align: middle; } +.gl-asof { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; margin: 16px 0 0; font: 400 13px var(--mono); color: var(--muted); } +.gl-asof .btn { padding: 5px 12px; font-size: 13px; } +.gl-asof .btn:disabled { opacity: .6; cursor: progress; } .gl-list .gl-sum { grid-column: 2; } .gl-list .gl-lines { grid-row: 1; grid-column: 3; }