Public: anyone can read this; the crew can change it.
A repository name's rules and its disk-init failure live in one module, read by migrate, apps and new repository #5600
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/5584-a-repository-name-s-rules-and-its-disk-i → main 1 commit fork 1 thread · 1 unresolved
5 files
changed +49
−6
src/actions/migrate.ts +3 −2
| @@ -64,6 +64,7 @@ import { | ||
| 64 | 64 | } from "../origins/jobs.ts"; |
| 65 | 65 | import type { Provider } from "../origins/types.ts"; |
| 66 | 66 | import { requireUser } from "./access.ts"; |
| 67 | import { REPO_INIT_FAILED, REPO_NAME_RULES } from "./repo_words.ts"; | |
| 67 | 68 | import { enableApp } from "./apps.ts"; |
| 68 | 69 | import { addToCrew } from "./crew.ts"; |
| 69 | 70 | import { |
| @@ -271,9 +272,9 @@ export function migrateMessage(error: MigrateError, limit = "at the repositories | ||
| 271 | 272 | case "name_taken": |
| 272 | 273 | return "That owner already has a repository by that name. Pick another name."; |
| 273 | 274 | case "invalid_name": |
| 274 | return "Names start with a letter or digit; no spaces, no .git."; | |
| 275 | return REPO_NAME_RULES; | |
| 275 | 276 | case "git_init_failed": |
| 276 | return "The repository could not be initialised on disk."; | |
| 277 | return REPO_INIT_FAILED; | |
| 277 | 278 | case "forbidden_owner": |
| 278 | 279 | return "You cannot create repositories for that owner."; |
| 279 | 280 | case "unauthorized": |
src/actions/repo_words.ts added +11
| @@ -0,0 +1,11 @@ | ||
| 1 | /** | |
| 2 | * The words a person reads when a new repository is refused: its name | |
| 3 | * breaks the rules, or git could not make it on disk. Migrate, a new app | |
| 4 | * and a new repository all say them, so they are said from here. | |
| 5 | */ | |
| 6 | ||
| 7 | /** A name `createRepo` refused as `invalid_name`. */ | |
| 8 | export const REPO_NAME_RULES = "Names start with a letter or digit; no spaces, no .git."; | |
| 9 | ||
| 10 | /** `createRepo` failed as `git_init_failed`. */ | |
| 11 | export const REPO_INIT_FAILED = "The repository could not be initialised on disk."; |
src/web/pages/apps.ts +3 −2
| @@ -14,6 +14,7 @@ | ||
| 14 | 14 | |
| 15 | 15 | import { storageLimitMessage } from "../../core/storage.ts"; |
| 16 | 16 | import { repositoryLimitMessage } from "../../actions/repos.ts"; |
| 17 | import { REPO_INIT_FAILED, REPO_NAME_RULES } from "../../actions/repo_words.ts"; | |
| 17 | 18 | import { appLimitWords } from "../../actions/app_limit.ts"; |
| 18 | 19 | import { getUserByName } from "../../core/users.ts"; |
| 19 | 20 | import { |
| @@ -433,8 +434,8 @@ const MAX_NAME_ATTEMPTS = 30; | ||
| 433 | 434 | |
| 434 | 435 | const NEW_APP_ERRORS: Record<string, string> = { |
| 435 | 436 | name_taken: "That owner already has a repository by that name.", |
| 436 | invalid_name: "Names start with a letter or digit; no spaces, no .git.", | |
| 437 | git_init_failed: "The repository could not be initialised on disk.", | |
| 437 | invalid_name: REPO_NAME_RULES, | |
| 438 | git_init_failed: REPO_INIT_FAILED, | |
| 438 | 439 | invalid_slug: |
| 439 | 440 | "That name makes no usable hostname — start it with a letter or digit.", |
| 440 | 441 | slug_taken: |
src/web/pages/site.ts +3 −2
| @@ -17,6 +17,7 @@ import { clientAddress } from "../../http/peer.ts"; | ||
| 17 | 17 | import { verifyEmail } from "../../actions/verify.ts"; |
| 18 | 18 | import { STRANGER_MAX } from "../../http/limits.ts"; |
| 19 | 19 | import { createRepo, repositoryLimitMessage, resolveOwner } from "../../actions/repos.ts"; |
| 20 | import { REPO_INIT_FAILED, REPO_NAME_RULES } from "../../actions/repo_words.ts"; | |
| 20 | 21 | import { HUMAN_CHECK_MESSAGE, INVITE_SPENT_MESSAGE, PASSKEY_REQUIRED_MESSAGE, SIGNUP_CLOSED_MESSAGE, signUp, signupClosed } from "../../actions/users.ts"; |
| 21 | 22 | import { accessAllows, listOrgsForUser, orgAccess } from "../../core/orgs.ts"; |
| 22 | 23 | import { listPins } from "../../core/pins.ts"; |
| @@ -842,8 +843,8 @@ export async function newRepoSubmit(ctx: Ctx): Promise<Response> { | ||
| 842 | 843 | repository_limit: `Not made: ${repositoryLimitMessage(ctx.app, ctx.auth.user)}.`, |
| 843 | 844 | storage_limit: `Not made: ${storageLimitMessage(ctx.app, resolveOwner(ctx.app, ctx.auth.user, values.owner) ?? ctx.auth.user.id)}.`, |
| 844 | 845 | name_taken: "That owner already has a repository by that name.", |
| 845 | invalid_name: "Names start with a letter or digit; no spaces, no .git.", | |
| 846 | git_init_failed: "The repository could not be initialised on disk.", | |
| 846 | invalid_name: REPO_NAME_RULES, | |
| 847 | git_init_failed: REPO_INIT_FAILED, | |
| 847 | 848 | }; |
| 848 | 849 | return page( |
| 849 | 850 | { |
test/repo-words.test.ts added +29
| @@ -0,0 +1,29 @@ | ||
| 1 | /** | |
| 2 | * A refused repository name and a failed disk init read the same from | |
| 3 | * migrate, a new app and a new repository, because each says them from | |
| 4 | * src/actions/repo_words.ts. A copy typed back into one of them fails here. | |
| 5 | */ | |
| 6 | ||
| 7 | import { describe, expect, test } from "bun:test"; | |
| 8 | import { readFileSync } from "node:fs"; | |
| 9 | import { join } from "node:path"; | |
| 10 | import { REPO_INIT_FAILED, REPO_NAME_RULES } from "../src/actions/repo_words.ts"; | |
| 11 | ||
| 12 | const WORDS = { REPO_NAME_RULES, REPO_INIT_FAILED }; | |
| 13 | const READERS = ["src/actions/migrate.ts", "src/web/pages/apps.ts", "src/web/pages/site.ts"]; | |
| 14 | ||
| 15 | describe("a repository's refusal words", () => { | |
| 16 | for (const [name, words] of Object.entries(WORDS)) { | |
| 17 | test(`${name} is a sentence`, () => { | |
| 18 | expect(words.trim().length).toBeGreaterThan(0); | |
| 19 | }); | |
| 20 | } | |
| 21 | ||
| 22 | for (const file of READERS) { | |
| 23 | test(`${file} imports the words and types no copy`, () => { | |
| 24 | const source = readFileSync(join(import.meta.dir, "..", file), "utf8"); | |
| 25 | expect(source).toContain("repo_words.ts"); | |
| 26 | for (const words of Object.values(WORDS)) expect(source).not.toContain(words); | |
| 27 | }); | |
| 28 | } | |
| 29 | }); |
new above
offline
This asserts on source text, so it fails on a harmless import reshuffle and passes if the constant is imported but unused. Call migrateMessage({invalid_name}) and compare it to REPO_NAME_RULES instead.