Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

import-verify.ts

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

import-verify.tsBlame153 lines · 1 contributor
14c3cc8Claude1/**
2 * Post-migration smoke verifier.
3 *
4 * After an imported repo lands on disk + in the DB, we run a trio of cheap
5 * checks to make sure the import actually produced a usable repository:
6 *
7 * 1. `clonable` — required bare-repo files exist on disk
8 * 2. `hasDefaultBranch` — `git symbolic-ref HEAD` resolves to a real ref
9 * 3. `commitCount` — `git rev-list --count HEAD` returns > 0
10 *
11 * Every failure mode is collected into `issues` as a plain string. This
12 * function never throws — callers (the /migrations UI in a parallel PR)
13 * render `issues` directly.
14 */
15import { join } from "path";
16import { eq } from "drizzle-orm";
17import { db } from "../db";
18import { repositories, users } from "../db/schema";
19
20export interface VerifyMigrationResult {
21 repoId: number;
22 clonable: boolean;
23 hasDefaultBranch: boolean;
24 commitCount: number;
25 issues: string[];
26}
27
28/**
29 * Run a shell command (always as argv, never as a shell string — prevents
30 * injection via owner/repo names). Returns stdout/stderr/exitCode and never
31 * throws; caller decides what to do with a failed exit.
32 */
33async function runGit(
34 args: string[]
35): Promise<{ stdout: string; stderr: string; exitCode: number }> {
36 try {
37 const proc = Bun.spawn(args, {
38 stdout: "pipe",
39 stderr: "pipe",
40 env: { ...process.env, GIT_TERMINAL_PROMPT: "0" },
41 });
42 const [stdout, stderr] = await Promise.all([
43 new Response(proc.stdout).text(),
44 new Response(proc.stderr).text(),
45 ]);
46 const exitCode = await proc.exited;
47 return { stdout, stderr, exitCode };
48 } catch (err) {
49 return { stdout: "", stderr: String(err), exitCode: -1 };
50 }
51}
52
53export async function verifyMigration(
54 repoId: number
55): Promise<VerifyMigrationResult> {
56 const issues: string[] = [];
57 const result: VerifyMigrationResult = {
58 repoId,
59 clonable: false,
60 hasDefaultBranch: false,
61 commitCount: 0,
62 issues,
63 };
64
65 // 1. Look up the repo joined with its owner so we can resolve the
66 // on-disk path without any extra round-trips.
67 let row:
68 | { repoName: string; ownerName: string | null }
69 | undefined;
70 try {
71 const rows = await db
72 .select({
73 repoName: repositories.name,
74 ownerName: users.username,
75 })
76 .from(repositories)
77 .leftJoin(users, eq(users.id, repositories.ownerId))
78 .where(eq(repositories.id, repoId as unknown as string))
79 .limit(1);
80 row = rows[0];
81 } catch (err) {
82 issues.push(`db lookup failed: ${String(err).slice(0, 200)}`);
83 return result;
84 }
85
86 if (!row || !row.ownerName || !row.repoName) {
87 issues.push("repo not found");
88 return result;
89 }
90
91 const base = process.env.GIT_REPOS_PATH || "./repos";
92 const path = join(base, row.ownerName, `${row.repoName}.git`);
93
94 // 2. Clonable = the three sentinel files a bare repo always has.
95 try {
96 const [head, cfg, objects] = await Promise.all([
97 Bun.file(join(path, "HEAD")).exists(),
98 Bun.file(join(path, "config")).exists(),
99 Bun.file(join(path, "objects")).exists(),
100 ]);
101 if (!head) issues.push("missing HEAD file");
102 if (!cfg) issues.push("missing config file");
103 if (!objects) issues.push("missing objects directory");
104 result.clonable = head && cfg && objects;
105 } catch (err) {
106 issues.push(`filesystem check failed: ${String(err).slice(0, 200)}`);
107 }
108
109 // 3. Default branch = `symbolic-ref HEAD` succeeds AND points somewhere
110 // non-empty. An "unborn" ref still resolves (exit 0) but we require
111 // the commit count below to confirm the ref has history.
112 {
113 const { stdout, stderr, exitCode } = await runGit([
114 "git",
115 "-C",
116 path,
117 "symbolic-ref",
118 "HEAD",
119 ]);
120 if (exitCode === 0 && stdout.trim().length > 0) {
121 result.hasDefaultBranch = true;
122 } else {
123 const detail = (stderr || stdout).trim().slice(0, 200);
124 issues.push(
125 `symbolic-ref HEAD failed${detail ? `: ${detail}` : ""}`
126 );
127 }
128 }
129
130 // 4. Commit count — if HEAD is unborn or the objects are corrupt,
131 // `rev-list` exits non-zero and we return 0.
132 {
133 const { stdout, stderr, exitCode } = await runGit([
134 "git",
135 "-C",
136 path,
137 "rev-list",
138 "--count",
139 "HEAD",
140 ]);
141 if (exitCode === 0) {
142 const n = parseInt(stdout.trim(), 10);
143 result.commitCount = Number.isFinite(n) && n >= 0 ? n : 0;
144 } else {
145 const detail = (stderr || stdout).trim().slice(0, 200);
146 issues.push(
147 `rev-list failed${detail ? `: ${detail}` : ""}`
148 );
149 }
150 }
151
152 return result;
153}