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

Each row of a repository's waves page shows the wave's headline and how many pulls it carries #5891

Merged opened this

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

Merged

scout/forge:scout/5878-each-row-of-a-repository-s-waves-page-sh → main 3 commits fork 4 threads · 4 unresolved

  • Merged. scout/5878-each-row-of-a-repository-s-waves-page-sh was merged into main as 2c22a5b.

Review

Sign in to review this change.
2 files changed +39 −4
src/web/pages/waves.ts +9 −3
@@ -12,7 +12,7 @@ import { holds, stamp, xml } from "../feeds.ts";
1212 import { notePhase, takeSessionTime, timePhase, timePhaseAsync } from "../../core/route_timing.ts";
1313 import { repoDir } from "../../git/repos.ts";
1414 import type { Ctx } from "../../http/context.ts";
15 import { VERSION } from "../../story/wave.ts";
15 import { VERSION, type Wave } from "../../story/wave.ts";
1616 import { carriedPulls, pullPath } from "../../story/wave_pulls.ts";
1717 import { pullRefs, readWave, readWaves } from "../../story/waves.ts";
1818 import { NO_WAVES } from "../../story/words.ts";
@@ -22,6 +22,11 @@ import { page } from "../layout.ts";
2222 import { empty, listCard, row } from "../ui.ts";
2323 import { localScope } from "../scope.ts";
2424
25 /** A wave's row title: `<version> · <headline> · N pulls`, N being the pulls this viewer can see. */
26 export function waveLine(w: Wave, visible: number): string {
27 return `${w.version} · ${w.headline} · ${visible} ${visible === 1 ? "pull" : "pulls"}`;
outdated

Counts raw refs, not what this viewer may see. This leaks private pulls and can disagree with the wave's page. Use the visible count from carriedPulls.

28 }
outdated

Dead branch: the wave format rejects an empty headline. Remove it.

29
2530 // GET /:owner/:name/waves
2631 export async function wavesPage(ctx: Ctx): Promise<Response> {
2732 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
@@ -42,8 +47,9 @@ export async function wavesPage(ctx: Ctx): Promise<Response> {
4247 e.ok
4348 ? row({
4449 href: fileHref(e.file),
45 title: e.wave.headline,
46 meta: html`${e.wave.version} · ${e.wave.date}`,
50 // The wave's own page lists these same pulls, so the two agree.
51 title: waveLine(e.wave, carriedPulls(ctx.app.db, pullRefs(e.wave), ctx.auth.user).length),
52 meta: html`${e.wave.date}`,

Per-row carriedPulls means a DB lookup per wave on every render. Acceptable at today's size; batch it if wave counts grow.

4753 })
4854 : row({
4955 href: fileHref(e.file),
test/waves-page.test.ts +30 −1
@@ -8,6 +8,7 @@ import type { Repo } from "../src/core/repos.ts";
88 import { createSession } from "../src/core/sessions.ts";
99 import { createUser, type User } from "../src/core/users.ts";
1010 import { serve, type Servers } from "../src/http/server.ts";
11 import { mergedPull, waveNaming } from "./helpers/wave-pulls.ts";
1112 import { writeWave, type Wave } from "../src/story/wave.ts";
1213
1314 const PASSWORD = "a-good-passphrase";
@@ -77,6 +78,17 @@ describe("waves page", () => {
7778 await put(priv, [
7879 { path: "waves/1.0.0.md", content: text(wave("1.0.0", "2026-03-01", "Secret launch")) },
7980 ]);
81 const shown = await mergedPull(app, alice, pub, "Shown work");
82 const secret = await mergedPull(app, alice, priv, "Secret work");
83 await put(pub, [
84 {
85 path: "waves/0.1.0.md",
86 content: waveNaming("0.1.0", [
87 { owner: "alice", name: "pub", number: shown },
88 { owner: "alice", name: "priv", number: secret },
89 ]),
90 },
91 ]);
8092 }, 60_000);
8193
8294 afterAll(async () => {
@@ -187,12 +199,29 @@ describe("waves page", () => {
187199 expect(res.status).toBe(200);
188200 expect(res.headers.get("content-type")).toContain("application/atom+xml");
189201 const body = await res.text();
190 expect(body.match(/<entry>/g)?.length).toBe(4);
202 expect(body.match(/<entry>/g)?.length).toBe(5);
191203 expect(body.indexOf("Tenth")).toBeLessThan(body.indexOf("Ninth"));
192204 expect(body).toContain('href="http://');
193205 expect(body).toContain("/alice/pub/waves/0.10.0");
194206 });
195207
208 test("a row counts the pulls this viewer can see, as the wave's page lists them", async () => {
209 const body = await (await get("/alice/pub/waves")).text();
210 expect(body).toContain("0.1.0 · Mixed · 2 pulls");
211 const guest = await (await get("/alice/pub/waves", false)).text();
212 expect(guest).toContain("0.1.0 · Mixed · 1 pull<");
213 expect(guest).not.toContain("2 pulls");
214 expect(guest).not.toContain("Secret work");
215 const page = await (await get("/alice/pub/waves/0.1.0", false)).text();
216 expect(page).toContain("Shown work");
217 expect(page).not.toContain("Secret work");
218 });
219
220 test("waves.atom titles an entry with the headline", async () => {
221 const body = await (await get("/alice/pub/waves.atom")).text();
222 expect(body).toContain("<title>Tenth</title>");
223 });
224
196225 test("waves.atom escapes markup in headlines and notes", async () => {
197226 const body = await (await get("/alice/pub/waves.atom")).text();
198227 expect(body).not.toContain("<script>");

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs