Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesPull RequestsActionsSecurityInsightsSettings
✨ AI
More
Blame · Line-by-line history

import.tsx

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.tsxBlame337 lines · 1 contributor
bdbd0deClaude1/**
2 * GitHub Import — automatic migration from GitHub to gluecron.
3 *
4 * Developer connects GitHub, gluecron pulls ALL their repos
5 * automatically. Issues, descriptions, branches — everything.
6 * One click. Walk away. Come back to everything migrated.
7 */
8
9import { Hono } from "hono";
10import { eq } from "drizzle-orm";
11import { db } from "../db";
12import { repositories, users } from "../db/schema";
13import { Layout } from "../views/layout";
14import { softAuth, requireAuth } from "../middleware/auth";
a4f3e24Claude15import { requireAdmin } from "../middleware/admin";
bdbd0deClaude16import type { AuthEnv } from "../middleware/auth";
17import { config } from "../lib/config";
18import { mkdir } from "fs/promises";
19import { join } from "path";
20
21const importRoutes = new Hono<AuthEnv>();
22
23importRoutes.use("*", softAuth);
24
25interface GitHubRepo {
26 name: string;
27 full_name: string;
28 description: string | null;
29 private: boolean;
30 clone_url: string;
31 default_branch: string;
32 stargazers_count: number;
33 fork: boolean;
34 language: string | null;
35}
36
37// ─── IMPORT PAGE ─────────────────────────────────────────────
38
a4f3e24Claude39importRoutes.get("/import", requireAuth, requireAdmin, async (c) => {
bdbd0deClaude40 const user = c.get("user")!;
41 const success = c.req.query("success");
42 const error = c.req.query("error");
43 const imported = c.req.query("imported");
44
45 return c.html(
46 <Layout title="Import from GitHub" user={user}>
47 <div style="max-width: 700px">
48 <h2 style="margin-bottom: 4px">Import from GitHub</h2>
49 <p style="color: var(--text-muted); font-size: 14px; margin-bottom: 20px">
50 Migrate your repositories from GitHub to gluecron automatically.
51 All branches, all history, all code — one click.
52 </p>
53 {success && (
54 <div class="auth-success">
55 {decodeURIComponent(success)}
56 {imported && (
57 <div style="margin-top: 8px">
58 Successfully imported {decodeURIComponent(imported)} repositories.
59 </div>
60 )}
61 </div>
62 )}
63 {error && (
64 <div class="auth-error">{decodeURIComponent(error)}</div>
65 )}
66
67 <div style="background: var(--bg-secondary); border: 1px solid var(--border); border-radius: var(--radius); padding: 20px; margin-bottom: 24px">
68 <h3 style="margin-bottom: 12px">Option 1: Import by username</h3>
69 <p style="font-size: 13px; color: var(--text-muted); margin-bottom: 12px">
70 Import all public repositories from a GitHub user or organization.
71 </p>
72 <form method="POST" action="/import/github/user">
73 <div style="display: flex; gap: 8px">
74 <input
75 type="text"
76 name="github_username"
77 required
78 placeholder="GitHub username or org"
79 style="flex: 1; padding: 8px 12px; background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius); color: var(--text); font-size: 14px"
80 />
81 <button type="submit" class="btn btn-primary">
82 Import all repos
83 </button>
84 </div>
85 </form>
86 </div>
87
88 <div style="background: var(--bg-secondary); border: 1px solid var(--border); border-radius: var(--radius); padding: 20px; margin-bottom: 24px">
89 <h3 style="margin-bottom: 12px">Option 2: Import single repo</h3>
90 <p style="font-size: 13px; color: var(--text-muted); margin-bottom: 12px">
91 Import a specific repository by URL.
92 </p>
93 <form method="POST" action="/import/github/repo">
94 <div style="display: flex; gap: 8px">
95 <input
96 type="text"
97 name="repo_url"
98 required
99 placeholder="https://github.com/owner/repo"
100 style="flex: 1; padding: 8px 12px; background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius); color: var(--text); font-size: 14px"
101 />
102 <button type="submit" class="btn btn-primary">
103 Import
104 </button>
105 </div>
106 </form>
107 </div>
108
109 <div style="background: var(--bg-secondary); border: 1px solid var(--border); border-radius: var(--radius); padding: 20px">
110 <h3 style="margin-bottom: 12px">Option 3: Import with token (private repos)</h3>
111 <p style="font-size: 13px; color: var(--text-muted); margin-bottom: 12px">
112 Use a GitHub personal access token to import private repositories too.
113 Generate one at github.com → Settings → Developer settings → Personal access tokens.
114 </p>
115 <form method="POST" action="/import/github/user">
116 <div class="form-group">
117 <input
118 type="text"
119 name="github_username"
120 required
121 placeholder="GitHub username"
122 style="padding: 8px 12px; background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius); color: var(--text); font-size: 14px; width: 100%"
123 />
124 </div>
125 <div class="form-group">
126 <input
127 type="password"
128 name="github_token"
129 placeholder="ghp_xxxxxxxxxxxx (GitHub personal access token)"
130 style="padding: 8px 12px; background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius); color: var(--text); font-size: 14px; font-family: var(--font-mono); width: 100%"
131 />
132 </div>
133 <button type="submit" class="btn btn-primary">
134 Import all repos (public + private)
135 </button>
136 </form>
137 </div>
138 </div>
139 </Layout>
140 );
141});
142
143// ─── IMPORT ALL REPOS FROM GITHUB USER ───────────────────────
144
a4f3e24Claude145importRoutes.post("/import/github/user", requireAuth, requireAdmin, async (c) => {
bdbd0deClaude146 const user = c.get("user")!;
147 const body = await c.req.parseBody();
148 const githubUsername = String(body.github_username || "").trim();
149 const githubToken = String(body.github_token || "").trim() || null;
150
151 if (!githubUsername) {
152 return c.redirect("/import?error=GitHub+username+is+required");
153 }
154
155 try {
156 // Fetch repos from GitHub API
157 const headers: Record<string, string> = {
158 Accept: "application/vnd.github.v3+json",
159 "User-Agent": "gluecron/1.0",
160 };
161 if (githubToken) {
162 headers.Authorization = `Bearer ${githubToken}`;
163 }
164
165 const repos: GitHubRepo[] = [];
166 let page = 1;
167 while (true) {
168 const url = githubToken
169 ? `https://api.github.com/user/repos?per_page=100&page=${page}&affiliation=owner`
170 : `https://api.github.com/users/${githubUsername}/repos?per_page=100&page=${page}`;
171
172 const res = await fetch(url, { headers });
173 if (!res.ok) {
174 const errText = await res.text();
175 return c.redirect(
176 `/import?error=${encodeURIComponent(`GitHub API error (${res.status}): ${errText.slice(0, 100)}`)}`
177 );
178 }
179 const batch: GitHubRepo[] = await res.json();
180 if (batch.length === 0) break;
181 repos.push(...batch);
182 page++;
183 if (page > 10) break; // safety limit: 1000 repos
184 }
185
186 if (repos.length === 0) {
187 return c.redirect("/import?error=No+repositories+found+for+this+user");
188 }
189
190 // Import each repo
191 let imported = 0;
192 let skipped = 0;
193
194 for (const ghRepo of repos) {
195 // Check if already exists
196 const [existing] = await db
197 .select()
198 .from(repositories)
199 .where(
200 eq(repositories.name, ghRepo.name)
201 )
202 .limit(1);
203
204 if (existing && existing.ownerId === user.id) {
205 skipped++;
206 continue;
207 }
208
209 try {
210 await importSingleRepo(user, ghRepo, githubToken);
211 imported++;
212 } catch (err) {
213 console.error(`[import] failed to import ${ghRepo.full_name}:`, err);
214 }
215 }
216
217 return c.redirect(
218 `/import?success=Import+complete&imported=${imported}+imported%2C+${skipped}+skipped+(already+exist)`
219 );
220 } catch (err) {
221 console.error("[import] error:", err);
222 return c.redirect(
223 `/import?error=${encodeURIComponent(`Import failed: ${String(err)}`)}`
224 );
225 }
226});
227
228// ─── IMPORT SINGLE REPO BY URL ───────────────────────────────
229
a4f3e24Claude230importRoutes.post("/import/github/repo", requireAuth, requireAdmin, async (c) => {
bdbd0deClaude231 const user = c.get("user")!;
232 const body = await c.req.parseBody();
233 const repoUrl = String(body.repo_url || "").trim();
234
235 if (!repoUrl) {
236 return c.redirect("/import?error=Repository+URL+is+required");
237 }
238
239 // Parse GitHub URL
240 const match = repoUrl.match(
241 /github\.com\/([^/]+)\/([^/.]+)/
242 );
243 if (!match) {
244 return c.redirect("/import?error=Invalid+GitHub+URL");
245 }
246
247 const [, ghOwner, ghRepo] = match;
248
249 try {
250 // Fetch repo info
251 const res = await fetch(
252 `https://api.github.com/repos/${ghOwner}/${ghRepo}`,
253 {
254 headers: {
255 Accept: "application/vnd.github.v3+json",
256 "User-Agent": "gluecron/1.0",
257 },
258 }
259 );
260
261 if (!res.ok) {
262 return c.redirect("/import?error=Repository+not+found+on+GitHub");
263 }
264
265 const ghRepoData: GitHubRepo = await res.json();
266
267 await importSingleRepo(user, ghRepoData, null);
268
269 return c.redirect(
270 `/${user.username}/${ghRepoData.name}`
271 );
272 } catch (err) {
273 console.error("[import] error:", err);
274 return c.redirect(
275 `/import?error=${encodeURIComponent(`Import failed: ${String(err)}`)}`
276 );
277 }
278});
279
280// ─── CORE IMPORT FUNCTION ────────────────────────────────────
281
282async function importSingleRepo(
283 user: { id: string; username: string },
284 ghRepo: GitHubRepo,
285 token: string | null
286): Promise<void> {
287 const destPath = join(
288 config.gitReposPath,
289 user.username,
290 `${ghRepo.name}.git`
291 );
292
293 // Ensure parent directory exists
294 await mkdir(join(config.gitReposPath, user.username), { recursive: true });
295
296 // Clone bare from GitHub (with token if provided for private repos)
297 let cloneUrl = ghRepo.clone_url;
298 if (token) {
299 // Inject token into URL for private repo access
300 cloneUrl = cloneUrl.replace(
301 "https://github.com/",
302 `https://${token}@github.com/`
303 );
304 }
305
306 console.log(`[import] cloning ${ghRepo.full_name} -> ${destPath}`);
307
308 const proc = Bun.spawn(
309 ["git", "clone", "--bare", "--mirror", cloneUrl, destPath],
310 {
311 stdout: "pipe",
312 stderr: "pipe",
313 env: { ...process.env, GIT_TERMINAL_PROMPT: "0" },
314 }
315 );
316 const stderr = await new Response(proc.stderr).text();
317 const exitCode = await proc.exited;
318
319 if (exitCode !== 0) {
320 throw new Error(`git clone failed: ${stderr}`);
321 }
322
323 // Insert into database
324 await db.insert(repositories).values({
325 name: ghRepo.name,
326 ownerId: user.id,
327 description: ghRepo.description,
328 isPrivate: ghRepo.private,
329 defaultBranch: ghRepo.default_branch || "main",
330 diskPath: destPath,
331 starCount: 0,
332 });
333
334 console.log(`[import] ${ghRepo.full_name} imported successfully`);
335}
336
337export default importRoutes;