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

Take the parent's `following` feature, its three conflicts resolved for the studio (second half of #250) #559

Merged opened this

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

Merged

scout/forge:scout/258-take-the-parent-s-following-feature-its → main 4 commits fork

  • Merged. scout/258-take-the-parent-s-following-feature-its was merged into main as 3731fa1.

Review

Sign in to review this change.
5 files changed +37 −2
docs/STUDIO.md +9
@@ -167,6 +167,15 @@ reader sees none of this. Both surfaces call the one function that knows
167167 the fact, `terminalFor` (`src/actions/apps.ts`), so the pull's card and
168168 the studio page never carry two ideas of what the command is.
169169
170 **Following** comes first when the app is a fork of another: one sentence on
171 where it stands against what it follows — in step, behind, ahead, or diverged
172 — and one thing to do about it. Behind, or diverged where git merges cleanly,
173 is **Take updates**, a click. Diverged where both sides changed the same file
174 is **Converge with the crew**: a builder merges the parent's branch, keeps
175 this app's direction where the two cannot both be kept, and the result comes
176 back as a change to look at, or lands on its own when the app runs on its
177 own. The words are the studio's; the mechanics are `docs/FOLLOWING.md`.
178
170179 **The box** is the front door. A sentence becomes an issue — the first line
171180 its title, the rest its body — and the routing decision hands it to a
172181 builder: the builder with the least in flight, then by name. That is one
scripts/gate.ts +1 −1
@@ -41,6 +41,7 @@ export const GATE: readonly string[] = [
4141 "test/autonomy.test.ts",
4242 "test/studio.test.ts",
4343 "test/studio-words.test.ts",
44 "test/following.test.ts",
4445 "test/forks.test.ts",
4546 "test/fork-flow.test.ts",
4647 "test/cross-repo-pull.test.ts",
@@ -140,7 +141,6 @@ export const EXCLUDED: readonly Exclusion[] = [
140141 },
141142 { file: "test/feeds.test.ts", reason: "not yet admitted", issue: 261 },
142143 { file: "test/fleet-graph.test.ts", reason: "not yet admitted", issue: 261 },
143 { file: "test/following.test.ts", reason: "not yet admitted", issue: 261 },
144144 { file: "test/git-local.test.ts", reason: "not yet admitted", issue: 261 },
145145 { file: "test/git-ssh.test.ts", reason: "not yet admitted", issue: 261 },
146146 { file: "test/git.test.ts", reason: "not yet admitted", issue: 261 },
src/web/pages/following.ts +5
@@ -324,6 +324,11 @@ export async function followingSubmit(ctx: Ctx): Promise<Response> {
324324 const parentName = `${parent.ownerName}/${parent.name}`;
325325
326326 if (field(form, "do") === "converge") {
327 if (!canWrite(ctx.app.db, repo, ctx.auth.user))
328 return flashOrJson(ctx, base, {
329 tone: "danger",
330 message: "Only a writer can ask the crew to converge.",
331 });
327332 const outcome = await convergeFork(ctx.app, repo, parent, ctx.auth.user);
328333 return flashOrJson(ctx, base, convergeWords(outcome, parentName));
329334 }
src/web/studio/app.ts +5
@@ -30,6 +30,7 @@ import { appTailJs, liveRegionsJs } from "../client.ts";
3030 import { html, when, type Html } from "../html.ts";
3131 import { page } from "../layout.ts";
3232 import { liveClientJs, liveMount } from "../live.ts";
33 import { followingCard, followingOf } from "../pages/following.ts";
3334 import {
3435 avatar,
3536 btn,
@@ -421,10 +422,14 @@ export async function studioApp(
421422 )}
422423 </p>`;
423424
425 // An app that follows another says so before anything else: whether it
426 // is in step is the first thing its owner wants to know.
427 const following = await followingOf(ctx, repo);
424428 const body = html`${repoHeader(ctx, repo, "code", {
425429 compact: true,
426430 ownTitle: true,
427431 })}
432 ${following === undefined ? "" : followingCard(ctx, repo, following)}
428433 <div id="app-head">${head}</div>
429434 ${await policyLine(ctx, repo, record)}
430435 <div class="dash">
test/following.test.ts +17 −1
@@ -32,7 +32,7 @@ import { getIssueById, listIssues } from "../src/core/issues.ts";
3232 import { getRepoById, type Repo } from "../src/core/repos.ts";
3333 import { listRuns } from "../src/core/runs.ts";
3434 import { createToken } from "../src/core/tokens.ts";
35 import { createUser, type User } from "../src/core/users.ts";
35 import { createUser, getUserByName, type User } from "../src/core/users.ts";
3636 import { resolveOid } from "../src/git/merge.ts";
3737 import { repoDir } from "../src/git/repos.ts";
3838 import { serve, type Servers } from "../src/http/server.ts";
@@ -243,6 +243,22 @@ describe("a fork that follows its parent", () => {
243243 expect(page).toContain("Nobody is on the crew");
244244 });
245245
246 test("the page form refuses a reader who cannot write, and opens nothing", async () => {
247 const eve = getUserByName(app.db, "eve")!;
248 const before = listIssues(app.db, fork.id, { state: "open" }).length;
249 const res = await fetch(`${base}/bob/game/following`, {
250 method: "POST",
251 headers: {
252 authorization: `Bearer ${createToken(app.db, eve.id, "form", "rw").token}`,
253 "content-type": "application/x-www-form-urlencoded",
254 accept: "application/json",
255 },
256 body: "do=converge",
257 });
258 expect(res.status).toBe(422);
259 expect(listIssues(app.db, fork.id, { state: "open" }).length).toBe(before);
260 });
261
246262 test("without a crew, converging is refused honestly", async () => {
247263 const res = await fetch(
248264 `${base}/api/v1/repos/bob/game/following/converge`,

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs