g1t/services/projects/src/transfer.ts
Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 1 | /** |
| 2 | * A repository moved: transferred to another workspace, or renamed within | |
| 3 | * its own. Its projects go with it. | |
| 4 | * | |
| 5 | * A project lives in the workspace its repository is in, so each of the | |
| 6 | * repository's projects under a stale path's workspace moves to the | |
| 7 | * workspace the repository is in now, keeping its id (deployments, | |
| 8 | * secrets and the rest are keyed by it) and its slug. Projects elsewhere | |
| 9 | * that build from the repository only learn its new path. Dependencies are | |
| 10 | * within a workspace, so those between a moved project and one that stayed | |
| 11 | * behind are dropped. | |
| 12 | * | |
| 13 | * Renamed, the repository's own project (its primary one, named after it) | |
| 14 | * takes the new name as its slug, so its pages at `/<workspace>/<name>` | |
| 15 | * follow, unless the workspace already has a project of that slug; it | |
| 16 | * keeps its old slug then. Its display name follows when it was still the | |
| 17 | * repository's old name. | |
| 18 | * | |
| 19 | * Each statement changes nothing once the rows have moved, so a repeated | |
| 20 | * or late delivery is harmless. | |
| 21 | */ | |
| 22 | ||
| 23 | import type { Statement } from "./rename"; | |
| 24 | ||
| 25 | /** A name as an address: lowercase letters, digits, `-`, `_` and `.`. */ | |
| 26 | export function slugOf(name: string): string { | |
| 27 | return name | |
| 28 | .trim() | |
| 29 | .toLowerCase() | |
| 30 | .replace(/[^a-z0-9._-]+/g, "-") | |
| 31 | .replace(/^[-.]+|[-.]+$/g, "") | |
| 32 | .slice(0, 100); | |
| 33 | } | |
| 34 | ||
| 35 | /** `stale` and `current` are `namespace/name` paths. */ | |
| 36 | export function moveStatements(stale: string[], current: string, repoId: string): Statement[] { | |
| 37 | const [workspace, name] = current.split("/") as [string, string]; | |
| 38 | const moves = [...new Set(stale.map((path) => path.split("/")[0]!))] | |
| 39 | .filter((from) => from !== workspace) | |
| 40 | .map((from) => ({ | |
| 41 | // A project of the same slug already in the destination keeps it; the | |
| 42 | // moving one is given a free slug afterwards (see `strandedQuery`). | |
| 43 | sql: "UPDATE OR IGNORE projects SET workspace = ?, repo_namespace = ? WHERE repo_id = ? AND workspace = ?", | |
| 44 | params: [workspace, workspace, repoId, from], | |
| 45 | })); | |
| 46 | const renames = [...new Set(stale.map((path) => path.split("/")[1]!))] | |
| 47 | .filter((old) => old !== name) | |
| 48 | .flatMap((old) => { | |
| 49 | const statements: Statement[] = []; | |
| 50 | const [from, to] = [slugOf(old), slugOf(name)]; | |
| 51 | if (from && to && from !== to) { | |
| 52 | // Ignored when the workspace already has a project called `to`. | |
| 53 | statements.push({ | |
| 54 | sql: "UPDATE OR IGNORE projects SET slug = ? WHERE repo_id = ? AND is_primary = 1 AND workspace = ? AND slug = ?", | |
| 55 | params: [to, repoId, workspace, from], | |
| 56 | }); | |
| 57 | } | |
| 58 | statements.push({ | |
| 59 | sql: "UPDATE projects SET name = ? WHERE repo_id = ? AND is_primary = 1 AND name = ?", | |
| 60 | params: [name, repoId, old], | |
| 61 | }); | |
| 62 | return statements; | |
| 63 | }); | |
| 64 | return [ | |
| 65 | ...moves, | |
| 66 | ...renames, | |
| 67 | { | |
| 68 | sql: "UPDATE projects SET repo_namespace = ?, repo_name = ? WHERE repo_id = ?", | |
| 69 | params: [workspace, name, repoId], | |
| 70 | }, | |
| 71 | { | |
| 72 | sql: `DELETE FROM dependencies | |
| 73 | WHERE (project_id IN (SELECT id FROM projects WHERE repo_id = ?) | |
| 74 | AND depends_on_id IN (SELECT id FROM projects WHERE workspace <> ?)) | |
| 75 | OR (depends_on_id IN (SELECT id FROM projects WHERE repo_id = ?) | |
| 76 | AND project_id IN (SELECT id FROM projects WHERE workspace <> ?))`, | |
| 77 | params: [repoId, workspace, repoId, workspace], | |
| 78 | }, | |
| 79 | ]; | |
| 80 | } | |
| 81 | ||
| 82 | /** | |
| 83 | * The repository's projects still under a stale workspace after the moves: | |
| 84 | * those whose slug the destination already had. Null for a rename within | |
| 85 | * the workspace, which moves no project between workspaces. | |
| 86 | */ | |
| 87 | export function strandedQuery(stale: string[], current: string, repoId: string): Statement | null { | |
| 88 | const workspace = current.split("/")[0]!; | |
| 89 | const from = [...new Set(stale.map((path) => path.split("/")[0]!))].filter((ns) => ns !== workspace); | |
| 90 | if (from.length === 0) return null; | |
| 91 | return { | |
| 92 | sql: `SELECT * FROM projects WHERE repo_id = ? AND workspace IN (${from.map(() => "?").join(", ")})`, | |
| 93 | params: [repoId, ...from], | |
| 94 | }; | |
| 95 | } |