flagon-io/g1t

public

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t/services/projects/src/transfer.ts

95 lines4,047 bytesCodeBlame
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
23import type { Statement } from "./rename";
24
25/** A name as an address: lowercase letters, digits, `-`, `_` and `.`. */
26export 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. */
36export 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 */
87export 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}