Public: anyone can read this; the crew can change it.

A repository's home shows its latest wave's headline in one line, linking to the wave #5923

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/5905-a-repository-s-home-shows-its-latest-wav → main 1 commit fork 2 threads · 2 unresolved

  • Merged. scout/5905-a-repository-s-home-shows-its-latest-wav was merged into main as 8249cbd.

Review

Sign in to review this change.
4 files changed +51 −2
src/story/waves.ts +25
@@ -124,3 +124,28 @@ export async function readWaves(
124124 return [...good, ...bad];
125125 }
126126
127
128 /** Unreadable files tried before giving up on finding the newest good wave. */

Doubled blank line before the doc comment; trim to one.

129 const LATEST_TRIES = 5;
130
131 /**
132 * The newest wave that parses, or null. Reads the tree once and only as many
133 * files as it takes, so a home page can ask without paying for the archive.
134 */
135 export async function latestWave(dir: string, branch: string): Promise<Wave | null> {
136 const tree = await listTree(dir, branch, "waves");
137 if (!tree.ok) return null;
138 const files = tree.value
139 .filter((e) => e.type === "blob" && e.name.endsWith(".md") && WAVE_VERSION.test(e.name.slice(0, -3)))
140 .sort((x, y) => byVersionDesc(x.name.slice(0, -3), y.name.slice(0, -3)))
141 .slice(0, LATEST_TRIES);
142 const reader = readerFor(dir);
143 for (const f of files) {
144 if (f.size > MAX_BYTES) continue;
145 const obj = await reader.object(f.oid);
146 if (!obj.ok) continue;
147 const parsed = parseWave(`waves/${f.name}`, new TextDecoder().decode(obj.value.body));
148 if (parsed.ok) return parsed.value;
149 }
150 return null;
151 }
src/web/pages/repo.ts +9
@@ -47,6 +47,7 @@ import {
4747 } from "../chrome.ts";
4848 import { codeJs } from "../client.ts";
4949 import { highlightBlob } from "../code.ts";
50 import { latestWave } from "../../story/waves.ts";
5051 import { notePhase, takeSessionTime, timePhase, timePhaseAsync } from "../../core/route_timing.ts";
5152 import { liveClientJs, liveMount } from "../live.ts";
5253 import { appStrip } from "./apps.ts";
@@ -1033,10 +1034,18 @@ async function codeHome(ctx: Ctx, repo: Repo): Promise<Response> {
10331034 readmeFor(scope.source, ref, tree, ""),
10341035 );
10351036 const following = await timePhaseAsync(ctx.request, "following", () => followingOf(ctx, repo));
1037 const latest = await timePhaseAsync(ctx.request, "wave", () =>
1038 latestWave(repoDir(ctx.app.reposRoot, repo.ownerName, repo.name), repo.defaultBranch),
1039 );
10361040 const rendering = performance.now();
10371041 const body = html`${scopeHeader(ctx, scope, "code", {
10381042 branchCount: refs.branches.length,
10391043 })}${archivedLine(ctx, repo)}${noApp}
1044 ${latest === null
1045 ? ""
1046 : html`<p class="muted latest-wave">Latest:

Nice-to-have: put the middle dot and date in a small named component, and wrap the version/headline so it truncates on a 360px phone instead of wrapping to several lines.

1047 <a href="${repoBase(repo)}/waves/${encodeURIComponent(latest.version)}">${latest.version}</a>
1048 · ${latest.headline} · ${latest.date}</p>`}
10401049 ${frozenBlock(ctx, repo)} ${appStrip(ctx, repo)}
10411050 ${following === undefined
10421051 ? forkBar(ctx, repo)
test/repo-timing.test.ts +2 −2
@@ -51,10 +51,10 @@ afterAll(async () => {
5151 await rm(dataDir, { recursive: true, force: true });
5252 });
5353
54 test("a repository home names session, appof, refs, tree, readme, following and render in order", async () => {
54 test("a repository home names session, appof, refs, tree, readme, following, wave and render in order", async () => {
5555 const res = await fetch(`${servers.url}/alice/r`, { headers: { cookie } });
5656 expect(res.status).toBe(200);
5757 await res.arrayBuffer();
5858 const names = (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!);
59 expect(names).toEqual(["app", "session", "appof", "refs", "tree", "readme", "following", "render"]);
59 expect(names).toEqual(["app", "session", "appof", "refs", "tree", "readme", "following", "wave", "render"]);
6060 });
test/waves-page.test.ts +15
@@ -117,6 +117,21 @@ describe("waves page", () => {
117117 expect(body).toContain('href="/alice/pub/waves"');
118118 });
119119
120 test("the repository home draws the newest wave in one line, linked", async () => {
121 const res = await get("/alice/pub");
122 const body = await res.text();
123 expect(body).toContain("latest-wave");
124 expect(body).toContain('href="/alice/pub/waves/0.10.0"');
125 expect(body).toContain("Tenth · 2026-02-01");
126 expect(body).not.toContain("Ninth");
127 expect(res.headers.get("server-timing")).toContain("wave");
128 });
129
130 test("the repository home without waves draws no wave line", async () => {
131 const body = await (await get("/alice/bare")).text();
132 expect(body).not.toContain("latest-wave");
133 });
134
120135 test("no waves/ gets the empty state", async () => {
121136 const res = await get("/alice/bare/waves");
122137 expect(res.status).toBe(200);

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs