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
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/5713-a-branch-s-sentences-live-in-one-module → main 2 commits fork 4 threads · 4 unresolved
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."; | |
| 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"; | ||
| 173 | 173 | import { isSafeRef, isSafeTreePath } from "../git/guard.ts"; |
| 174 | 174 | import { blobForRender, listTree } from "../git/read.ts"; |
| 175 | 175 | import { CREATE_ERRORS, DELETE_ERRORS } from "../web/pages/branches.ts"; |
| 176 | import { BRANCH_NOT_CREATED, BRANCH_NOT_DELETED } from "../actions/branch_words.ts"; | |
| 176 | 177 | import { askToLiveFor } from "../web/ask_to_live.ts"; |
| 177 | 178 | import { getAppByRepo } from "../core/apps.ts"; |
| 178 | 179 | import { previewEnv } from "../apps/data.ts"; |
| @@ -2362,12 +2363,12 @@ const TOOLS: readonly Tool[] = [ | ||
| 2362 | 2363 | ...(from === undefined || from === "" ? {} : { from }), |
| 2363 | 2364 | }); |
| 2364 | 2365 | 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); | |
| 2366 | 2367 | return good({ says: `Created ${made.value.name} at ${made.value.oid.slice(0, 7)}.`, url: base }); |
| 2367 | 2368 | } |
| 2368 | 2369 | const gone = await deleteBranch(ctx.app, user, repo, name); |
| 2369 | 2370 | 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); | |
| 2371 | 2372 | return good({ says: `Deleted ${gone.value.name}.`, url: base }); |
| 2372 | 2373 | }, |
| 2373 | 2374 | }, |
src/tui/screens/refs.ts +2 −1
| @@ -7,6 +7,7 @@ | ||
| 7 | 7 | */ |
| 8 | 8 | |
| 9 | 9 | import { createBranch, deleteBranch } from "../../actions/branches.ts"; |
| 10 | import { NAME_THE_BRANCH } from "../../actions/branch_words.ts"; | |
| 10 | 11 | import { listBranches } from "../../git/branches.ts"; |
| 11 | 12 | import type { RepoSource } from "../../view/source.ts"; |
| 12 | 13 | import type { Ctx, Screen } from "../app/app.ts"; |
| @@ -209,7 +210,7 @@ export function branchesScreen( | ||
| 209 | 210 | ? "created here" |
| 210 | 211 | : `created on ${mirrored.fullName} first, then here`, |
| 211 | 212 | onSubmit: (name) => { |
| 212 | if (name === "") return "give the branch a name"; | |
| 213 | if (name === "") return NAME_THE_BRANCH; | |
| 213 | 214 | return (async () => { |
| 214 | 215 | const made = await createBranch(f.app, f.user, repo, { |
| 215 | 216 | name, |
src/tui/screens/workspace.ts +2 −1
| @@ -37,6 +37,7 @@ | ||
| 37 | 37 | import { editFiles } from "../../actions/contents.ts"; |
| 38 | 38 | import { err, ok } from "../../kernel/result.ts"; |
| 39 | 39 | import { createBranch } from "../../actions/branches.ts"; |
| 40 | import { NAME_THE_BRANCH } from "../../actions/branch_words.ts"; | |
| 40 | 41 | import { pinRepo, syncRepoNow, unpinRepo } from "../../actions/origins.ts"; |
| 41 | 42 | import { isPinned } from "../../core/pins.ts"; |
| 42 | 43 | import { setDecisionsBy } from "../../actions/repos.ts"; |
| @@ -997,7 +998,7 @@ export function workspaceScreen( | ||
| 997 | 998 | ? "created here, and the workspace switches to it" |
| 998 | 999 | : `created on ${repo.origin.fullName} first, then here`, |
| 999 | 1000 | onSubmit: (name) => { |
| 1000 | if (name === "") return "give the branch a name"; | |
| 1001 | if (name === "") return NAME_THE_BRANCH; | |
| 1001 | 1002 | return (async () => { |
| 1002 | 1003 | const made = await createBranch(f.app, f.user, repo, { |
| 1003 | 1004 | name, |
src/web/pages/branches.ts +3 −2
| @@ -41,6 +41,7 @@ import { | ||
| 41 | 41 | sha, |
| 42 | 42 | time, |
| 43 | 43 | } from "../ui.ts"; |
| 44 | import { BRANCH_NOT_CREATED, BRANCH_NOT_DELETED } from "../../actions/branch_words.ts"; | |
| 44 | 45 | import { frozenBlock } from "./frozen.ts"; |
| 45 | 46 | import { refSetFrom } from "./refs.ts"; |
| 46 | 47 | |
| @@ -337,7 +338,7 @@ export async function branchCreate(ctx: Ctx): Promise<Response> { | ||
| 337 | 338 | if (!created.ok) { |
| 338 | 339 | return flashTo(ctx, base, { |
| 339 | 340 | tone: "danger", |
| 340 | message: CREATE_ERRORS[created.error] ?? "That branch was not created.", | |
| 341 | message: CREATE_ERRORS[created.error] ?? BRANCH_NOT_CREATED, | |
| 341 | 342 | }); |
| 342 | 343 | } |
| 343 | 344 | return flashTo(ctx, base, { |
| @@ -368,7 +369,7 @@ export async function branchDelete(ctx: Ctx): Promise<Response> { | ||
| 368 | 369 | if (!deleted.ok) { |
| 369 | 370 | return flashTo(ctx, base, { |
| 370 | 371 | tone: "danger", |
| 371 | message: DELETE_ERRORS[deleted.error] ?? "That branch was not deleted.", | |
| 372 | message: DELETE_ERRORS[deleted.error] ?? BRANCH_NOT_DELETED, | |
| 372 | 373 | }); |
| 373 | 374 | } |
| 374 | 375 | 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 | }); |
new above
offline
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.