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

A branch's sentences live in one module, read by the branches page, MCP, the mirror and the terminal #5759

Merged opened this

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

Merged

mason/forge:mason/5713-a-branch-s-sentences-live-in-one-module → main 2 commits fork 4 threads · 4 unresolved

  • Merged. mason/5713-a-branch-s-sentences-live-in-one-module was merged into main as 1a360b2.

Review

Sign in to review this change.
6 files changed +71 −6
src/actions/branch_words.ts added +14
@@ -0,0 +1,14 @@
1 /**
2 * A branch's sentences (#5713): making or deleting a branch reads the same
3 * from the branches page, the MCP and the terminal, so each is
4 * typed once and read from here.
5 */
6
7 /** A branch create refused for a reason with no sentence of its own. */
8 export const BRANCH_NOT_CREATED = "That branch was not created.";

This is the only place the sentence is typed now; the page and the MCP both fall back to it after CREATE_ERRORS/DELETE_ERRORS. That fits the pattern.

9
10 /** A branch delete refused for a reason with no sentence of its own. */
11 export const BRANCH_NOT_DELETED = "That branch was not deleted.";
12
13 /** The terminal's answer to a branch name left empty. */
14 export const NAME_THE_BRANCH = "give the branch a name";
src/http/mcp.ts +3 −2
@@ -173,6 +173,7 @@ import { runGit } from "../git/env.ts";
173173 import { isSafeRef, isSafeTreePath } from "../git/guard.ts";
174174 import { blobForRender, listTree } from "../git/read.ts";
175175 import { CREATE_ERRORS, DELETE_ERRORS } from "../web/pages/branches.ts";
176 import { BRANCH_NOT_CREATED, BRANCH_NOT_DELETED } from "../actions/branch_words.ts";
176177 import { askToLiveFor } from "../web/ask_to_live.ts";
177178 import { getAppByRepo } from "../core/apps.ts";
178179 import { previewEnv } from "../apps/data.ts";
@@ -2362,12 +2363,12 @@ const TOOLS: readonly Tool[] = [
23622363 ...(from === undefined || from === "" ? {} : { from }),
23632364 });
23642365 if (!made.ok)
2365 return bad(made.error, CREATE_ERRORS[made.error] ?? "That branch was not created.");
2366 return bad(made.error, CREATE_ERRORS[made.error] ?? BRANCH_NOT_CREATED);
23662367 return good({ says: `Created ${made.value.name} at ${made.value.oid.slice(0, 7)}.`, url: base });
23672368 }
23682369 const gone = await deleteBranch(ctx.app, user, repo, name);
23692370 if (!gone.ok)
2370 return bad(gone.error, DELETE_ERRORS[gone.error] ?? "That branch was not deleted.");
2371 return bad(gone.error, DELETE_ERRORS[gone.error] ?? BRANCH_NOT_DELETED);
23712372 return good({ says: `Deleted ${gone.value.name}.`, url: base });
23722373 },
23732374 },
src/tui/screens/refs.ts +2 −1
@@ -7,6 +7,7 @@
77 */
88
99 import { createBranch, deleteBranch } from "../../actions/branches.ts";
10 import { NAME_THE_BRANCH } from "../../actions/branch_words.ts";
1011 import { listBranches } from "../../git/branches.ts";
1112 import type { RepoSource } from "../../view/source.ts";
1213 import type { Ctx, Screen } from "../app/app.ts";
@@ -209,7 +210,7 @@ export function branchesScreen(
209210 ? "created here"
210211 : `created on ${mirrored.fullName} first, then here`,
211212 onSubmit: (name) => {
212 if (name === "") return "give the branch a name";
213 if (name === "") return NAME_THE_BRANCH;
213214 return (async () => {
214215 const made = await createBranch(f.app, f.user, repo, {
215216 name,
src/tui/screens/workspace.ts +2 −1
@@ -37,6 +37,7 @@
3737 import { editFiles } from "../../actions/contents.ts";
3838 import { err, ok } from "../../kernel/result.ts";
3939 import { createBranch } from "../../actions/branches.ts";
40 import { NAME_THE_BRANCH } from "../../actions/branch_words.ts";
4041 import { pinRepo, syncRepoNow, unpinRepo } from "../../actions/origins.ts";
4142 import { isPinned } from "../../core/pins.ts";
4243 import { setDecisionsBy } from "../../actions/repos.ts";
@@ -997,7 +998,7 @@ export function workspaceScreen(
997998 ? "created here, and the workspace switches to it"
998999 : `created on ${repo.origin.fullName} first, then here`,
9991000 onSubmit: (name) => {
1000 if (name === "") return "give the branch a name";
1001 if (name === "") return NAME_THE_BRANCH;
10011002 return (async () => {
10021003 const made = await createBranch(f.app, f.user, repo, {
10031004 name,
src/web/pages/branches.ts +3 −2
@@ -41,6 +41,7 @@ import {
4141 sha,
4242 time,
4343 } from "../ui.ts";
44 import { BRANCH_NOT_CREATED, BRANCH_NOT_DELETED } from "../../actions/branch_words.ts";
4445 import { frozenBlock } from "./frozen.ts";
4546 import { refSetFrom } from "./refs.ts";
4647
@@ -337,7 +338,7 @@ export async function branchCreate(ctx: Ctx): Promise<Response> {
337338 if (!created.ok) {
338339 return flashTo(ctx, base, {
339340 tone: "danger",
340 message: CREATE_ERRORS[created.error] ?? "That branch was not created.",
341 message: CREATE_ERRORS[created.error] ?? BRANCH_NOT_CREATED,
341342 });
342343 }
343344 return flashTo(ctx, base, {
@@ -368,7 +369,7 @@ export async function branchDelete(ctx: Ctx): Promise<Response> {
368369 if (!deleted.ok) {
369370 return flashTo(ctx, base, {
370371 tone: "danger",
371 message: DELETE_ERRORS[deleted.error] ?? "That branch was not deleted.",
372 message: DELETE_ERRORS[deleted.error] ?? BRANCH_NOT_DELETED,
372373 });
373374 }
374375 return flashTo(ctx, base, { message: `Deleted ${deleted.value.name}.` });
test/branch-words.test.ts added +47
@@ -0,0 +1,47 @@
1 /**
2 * A branch's sentences are typed once, in `src/actions/branch_words.ts`
3 * (#5713), and the branches page, the MCP and the terminal read
4 * them from there. This fails if a copy of a sentence is typed back.
5 */
6
7 import { describe, expect, test } from "bun:test";
8 import { readFileSync } from "node:fs";
9 import { join } from "node:path";
10 import {
11 BRANCH_NOT_CREATED,
12 BRANCH_NOT_DELETED,
13 NAME_THE_BRANCH,
14 } from "../src/actions/branch_words.ts";
15
16 const ROOT = join(import.meta.dir, "..");
17 const read = (path: string): string => readFileSync(join(ROOT, path), "utf8");
18
19 const SENTENCES = {
20 BRANCH_NOT_CREATED: "That branch was not created.",
21 BRANCH_NOT_DELETED: "That branch was not deleted.",
22 NAME_THE_BRANCH: "give the branch a name",
23 } as const;
24
25 /** Each file, and the sentences it reads by name. */
26 const READERS: Record<string, readonly (keyof typeof SENTENCES)[]> = {
27 "src/web/pages/branches.ts": ["BRANCH_NOT_CREATED", "BRANCH_NOT_DELETED"],
28 "src/http/mcp.ts": ["BRANCH_NOT_CREATED", "BRANCH_NOT_DELETED"],
29 "src/tui/screens/refs.ts": ["NAME_THE_BRANCH"],
30 "src/tui/screens/workspace.ts": ["NAME_THE_BRANCH"],
31 };
32
33 describe("a branch's words", () => {
34 test("the module says each sentence, word for word", () => {
35 expect({ BRANCH_NOT_CREATED, BRANCH_NOT_DELETED, NAME_THE_BRANCH }).toEqual({
36 ...SENTENCES,
37 });
38 });
39
40 for (const [file, names] of Object.entries(READERS)) {
41 test(`${file} types none of the sentences and names the ones it says`, () => {
42 const text = read(file);
43 for (const sentence of Object.values(SENTENCES)) expect(text).not.toContain(sentence);
44 for (const name of names) expect(text).toContain(name);
45 });
46 }
47 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs