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