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
claude/adoring-hopper-5x74bqclaude/affectionate-feynman-ykrf1hclaude/architecture-audit-design-wxprenclaude/build-status-update-3MXsfclaude/charming-meitner-mllb5rclaude/compare-gate-gluecron-s4mFQclaude/confident-faraday-tikcwbclaude/continue-work-XMTlIclaude/crontech-gluecron-deploy-7MIECclaude/crontech-platform-setup-SeKfwclaude/design-2026claude/ecstatic-ptolemy-jMdigclaude/enhance-github-integration-QNHdGclaude/fix-aa-loop-issue-PonMQclaude/fix-actions-and-processclaude/fix-desktop-errors-XqoW8claude/fix-red-workflowsclaude/fix-website-access-6FKJNclaude/gatetest-integration-hardeningclaude/github-audit-improvements-bDFr9claude/gluecron-launch-status-FoMRlclaude/hopeful-lamport-olfCTclaude/issue-to-pr-and-protectionsclaude/jolly-heisenberg-2sg1Qclaude/launch-preparation-QmTb6claude/new-session-xk1l7claude/plan-platform-architecture-kkN4yclaude/platform-analysis-roadmap-1nUGLclaude/platform-launch-assessment-8dWV8claude/polish-platform-release-AeDrUclaude/resume-previous-work-KzyLwclaude/review-crontech-handoff-qYEVqclaude/review-project-completeness-lHhS2claude/review-readme-docs-ulqPKclaude/serene-edison-rj87weclaude/setup-multi-repo-dev-BCwNQclaude/ship-fixes-and-tests-Jvz1cclaude/site-audit-competitive-pctlwgclaude/site-migration-vercel-XstpKclaude/standalone-product-repos-XHFTDcopilot/feat-smart-empty-states-keyboard-first-enhancementcopilot/feat-smart-morning-digest-review-context-restorecopilot/fix-and-process-workflowscopilot/update-ai-powered-code-reviewfeat/debt-mapfeat/push-policy-codeowners-hardeningfeat/smart-digest-contextfeat/stage-impactfeat/t1-secret-migrationfeat/u-polishfeat/w-self-hostfeat/w2-claude-configfix/agent-journey-orphan-sweepgatetest/auto-fix-1776586424172gatetest/auto-fix-1776586534814gatetest/auto-fix-1776590685143gatetest/auto-fix-1776590808199mainops/redeploy-retriggerstyle/dxt-cta-themeworktree-agent-a3377aad30d55da26worktree-agent-a7ef607b7ee1d6c74
import-verify.ts4.5 KB · 153 lines
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
/**
 * Post-migration smoke verifier.
 *
 * After an imported repo lands on disk + in the DB, we run a trio of cheap
 * checks to make sure the import actually produced a usable repository:
 *
 *   1. `clonable`          — required bare-repo files exist on disk
 *   2. `hasDefaultBranch`  — `git symbolic-ref HEAD` resolves to a real ref
 *   3. `commitCount`       — `git rev-list --count HEAD` returns > 0
 *
 * Every failure mode is collected into `issues` as a plain string. This
 * function never throws — callers (the /migrations UI in a parallel PR)
 * render `issues` directly.
 */
import { join } from "path";
import { eq } from "drizzle-orm";
import { db } from "../db";
import { repositories, users } from "../db/schema";

export interface VerifyMigrationResult {
  repoId: number;
  clonable: boolean;
  hasDefaultBranch: boolean;
  commitCount: number;
  issues: string[];
}

/**
 * Run a shell command (always as argv, never as a shell string — prevents
 * injection via owner/repo names). Returns stdout/stderr/exitCode and never
 * throws; caller decides what to do with a failed exit.
 */
async function runGit(
  args: string[]
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
  try {
    const proc = Bun.spawn(args, {
      stdout: "pipe",
      stderr: "pipe",
      env: { ...process.env, GIT_TERMINAL_PROMPT: "0" },
    });
    const [stdout, stderr] = await Promise.all([
      new Response(proc.stdout).text(),
      new Response(proc.stderr).text(),
    ]);
    const exitCode = await proc.exited;
    return { stdout, stderr, exitCode };
  } catch (err) {
    return { stdout: "", stderr: String(err), exitCode: -1 };
  }
}

export async function verifyMigration(
  repoId: number
): Promise<VerifyMigrationResult> {
  const issues: string[] = [];
  const result: VerifyMigrationResult = {
    repoId,
    clonable: false,
    hasDefaultBranch: false,
    commitCount: 0,
    issues,
  };

  // 1. Look up the repo joined with its owner so we can resolve the
  //    on-disk path without any extra round-trips.
  let row:
    | { repoName: string; ownerName: string | null }
    | undefined;
  try {
    const rows = await db
      .select({
        repoName: repositories.name,
        ownerName: users.username,
      })
      .from(repositories)
      .leftJoin(users, eq(users.id, repositories.ownerId))
      .where(eq(repositories.id, repoId as unknown as string))
      .limit(1);
    row = rows[0];
  } catch (err) {
    issues.push(`db lookup failed: ${String(err).slice(0, 200)}`);
    return result;
  }

  if (!row || !row.ownerName || !row.repoName) {
    issues.push("repo not found");
    return result;
  }

  const base = process.env.GIT_REPOS_PATH || "./repos";
  const path = join(base, row.ownerName, `${row.repoName}.git`);

  // 2. Clonable = the three sentinel files a bare repo always has.
  try {
    const [head, cfg, objects] = await Promise.all([
      Bun.file(join(path, "HEAD")).exists(),
      Bun.file(join(path, "config")).exists(),
      Bun.file(join(path, "objects")).exists(),
    ]);
    if (!head) issues.push("missing HEAD file");
    if (!cfg) issues.push("missing config file");
    if (!objects) issues.push("missing objects directory");
    result.clonable = head && cfg && objects;
  } catch (err) {
    issues.push(`filesystem check failed: ${String(err).slice(0, 200)}`);
  }

  // 3. Default branch = `symbolic-ref HEAD` succeeds AND points somewhere
  //    non-empty. An "unborn" ref still resolves (exit 0) but we require
  //    the commit count below to confirm the ref has history.
  {
    const { stdout, stderr, exitCode } = await runGit([
      "git",
      "-C",
      path,
      "symbolic-ref",
      "HEAD",
    ]);
    if (exitCode === 0 && stdout.trim().length > 0) {
      result.hasDefaultBranch = true;
    } else {
      const detail = (stderr || stdout).trim().slice(0, 200);
      issues.push(
        `symbolic-ref HEAD failed${detail ? `: ${detail}` : ""}`
      );
    }
  }

  // 4. Commit count — if HEAD is unborn or the objects are corrupt,
  //    `rev-list` exits non-zero and we return 0.
  {
    const { stdout, stderr, exitCode } = await runGit([
      "git",
      "-C",
      path,
      "rev-list",
      "--count",
      "HEAD",
    ]);
    if (exitCode === 0) {
      const n = parseInt(stdout.trim(), 10);
      result.commitCount = Number.isFinite(n) && n >= 0 ? n : 0;
    } else {
      const detail = (stderr || stdout).trim().slice(0, 200);
      issues.push(
        `rev-list failed${detail ? `: ${detail}` : ""}`
      );
    }
  }

  return result;
}