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

mirrors.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.

mirrors.tsxBlame389 lines · 2 contributors
4a0dea1Claude1/**
2 * Block I9 — Repository mirroring.
3 *
4 * GET /:owner/:repo/settings/mirror — config form + recent runs
5 * POST /:owner/:repo/settings/mirror — save upstream URL + interval
6 * POST /:owner/:repo/settings/mirror/delete — remove mirror config
7 * POST /:owner/:repo/settings/mirror/sync — run one sync now (owner-only)
8 * POST /admin/mirrors/sync-all — site admin, run all due mirrors
9 */
10
11import { Hono } from "hono";
12import { and, eq } from "drizzle-orm";
13import { db } from "../db";
14import { repositories, users } from "../db/schema";
15import { Layout } from "../views/layout";
16import { RepoHeader } from "../views/components";
17import { softAuth, requireAuth } from "../middleware/auth";
18import type { AuthEnv } from "../middleware/auth";
19import { isSiteAdmin } from "../lib/admin";
20import { audit } from "../lib/notify";
21import {
22 deleteMirror,
23 getMirrorForRepo,
24 listRecentRuns,
25 runMirrorSync,
26 safeUrlForLog,
27 syncAllDue,
28 upsertMirror,
29 validateUpstreamUrl,
30} from "../lib/mirrors";
31
32const mirrors = new Hono<AuthEnv>();
33mirrors.use("*", softAuth);
34
35async function ownerGate(c: any): Promise<
36 | Response
37 | {
38 user: any;
39 ownerName: string;
40 repoName: string;
41 repo: typeof repositories.$inferSelect;
42 }
43> {
44 const user = c.get("user");
45 if (!user) return c.redirect("/login");
46 const { owner: ownerName, repo: repoName } = c.req.param();
47 const [owner] = await db
48 .select()
49 .from(users)
50 .where(eq(users.username, ownerName))
51 .limit(1);
52 if (!owner || owner.id !== user.id) {
53 return c.html(
54 <Layout title="Forbidden" user={user}>
55 <div class="empty-state">
56 <h2>403</h2>
57 <p>Only the repository owner can configure mirroring.</p>
58 </div>
59 </Layout>,
60 403
61 );
62 }
63 const [repo] = await db
64 .select()
65 .from(repositories)
66 .where(
67 and(eq(repositories.ownerId, owner.id), eq(repositories.name, repoName))
68 )
69 .limit(1);
70 if (!repo) return c.notFound();
71 return { user, ownerName, repoName, repo };
72}
73
74// ---------- Config page ----------
75
76mirrors.get("/:owner/:repo/settings/mirror", requireAuth, async (c) => {
77 const g = await ownerGate(c);
78 if (g instanceof Response) return g;
79 const { user, ownerName, repoName, repo } = g;
80
81 const mirror = await getMirrorForRepo(repo.id);
82 const runs = mirror ? await listRecentRuns(mirror.id, 20) : [];
83
84 const success = c.req.query("success");
85 const error = c.req.query("error");
86
87 return c.html(
88 <Layout title={`Mirror — ${ownerName}/${repoName}`} user={user}>
89 <RepoHeader owner={ownerName} repo={repoName} />
90 <div class="settings-container" style="max-width:720px">
91 <h2>Mirror settings</h2>
92 <p style="color:var(--text-muted)">
93 Keep this repository in sync with an upstream URL by periodically
94 running <code>git fetch --prune</code>. Only <code>https://</code>,{" "}
95 <code>http://</code>, and <code>git://</code> URLs are accepted —
96 SSH and local paths are not supported.
97 </p>
98
99 {success && (
100 <div class="auth-success">{decodeURIComponent(success)}</div>
101 )}
102 {error && (
103 <div class="auth-error">{decodeURIComponent(error)}</div>
104 )}
105
106 <form
cce7944Claude107 method="post"
4a0dea1Claude108 action={`/${ownerName}/${repoName}/settings/mirror`}
109 class="panel"
110 style="padding:16px;margin:16px 0"
111 >
112 <div class="form-group">
113 <label for="upstream_url">Upstream URL</label>
114 <input
115 type="text"
116 id="upstream_url"
117 name="upstream_url"
118 value={mirror?.upstreamUrl || ""}
119 placeholder="https://github.com/torvalds/linux.git"
120 required
121 style="font-family:var(--font-mono)"
122 />
123 </div>
124 <div class="form-group">
125 <label for="interval_minutes">Sync interval (minutes)</label>
126 <input
127 type="number"
128 id="interval_minutes"
129 name="interval_minutes"
130 value={mirror?.intervalMinutes ?? 1440}
131 min="5"
132 max="43200"
133 style="width:160px"
134 />
135 </div>
136 <label
137 style="display:flex;gap:8px;align-items:center;margin-bottom:12px"
138 >
139 <input
140 type="checkbox"
141 name="is_enabled"
142 value="1"
143 checked={mirror ? mirror.isEnabled : true}
5db1b25copilot-swe-agent[bot]144 aria-label="Enabled"
4a0dea1Claude145 />
146 <span>Enabled</span>
147 </label>
148 <button type="submit" class="btn btn-primary">
149 {mirror ? "Update mirror" : "Enable mirror"}
150 </button>
151 </form>
152
153 {mirror && (
154 <>
155 <div style="display:flex;gap:8px;margin:12px 0">
156 <form
cce7944Claude157 method="post"
4a0dea1Claude158 action={`/${ownerName}/${repoName}/settings/mirror/sync`}
159 >
160 <button type="submit" class="btn">
161 Sync now
162 </button>
163 </form>
164 <form
cce7944Claude165 method="post"
4a0dea1Claude166 action={`/${ownerName}/${repoName}/settings/mirror/delete`}
167 onsubmit="return confirm('Remove mirror configuration?')"
168 >
169 <button type="submit" class="btn btn-danger">
170 Remove mirror
171 </button>
172 </form>
173 </div>
174
175 <h3 style="margin-top:20px">Last run</h3>
176 <div class="panel" style="padding:12px">
177 {mirror.lastSyncedAt ? (
178 <div>
179 <div
180 style="font-size:12px;color:var(--text-muted);text-transform:uppercase"
181 >
182 {mirror.lastStatus === "ok" ? "Success" : "Error"} —{" "}
183 {new Date(
184 mirror.lastSyncedAt as unknown as string
185 ).toLocaleString()}
186 </div>
187 {mirror.lastError && (
188 <pre
189 style="margin-top:8px;padding:8px;background:var(--bg-subtle);border-radius:4px;font-size:12px;overflow-x:auto;color:var(--red)"
190 >
191 {mirror.lastError}
192 </pre>
193 )}
194 </div>
195 ) : (
196 <div style="color:var(--text-muted)">Never synced.</div>
197 )}
198 </div>
199
200 <h3 style="margin-top:20px">Recent runs</h3>
201 <div class="panel">
202 {runs.length === 0 ? (
203 <div class="panel-empty">No runs yet.</div>
204 ) : (
205 runs.map((r) => (
206 <div
207 class="panel-item"
208 style="justify-content:space-between;flex-wrap:wrap;gap:6px"
209 >
210 <div>
211 <span
212 style={`font-size:11px;text-transform:uppercase;margin-right:8px;color:${
213 r.status === "ok"
214 ? "var(--green)"
215 : r.status === "error"
216 ? "var(--red)"
217 : "var(--text-muted)"
218 }`}
219 >
220 {r.status}
221 </span>
222 <span
223 style="font-size:12px;color:var(--text-muted);font-family:var(--font-mono)"
224 >
225 {new Date(
226 r.startedAt as unknown as string
227 ).toLocaleString()}
228 </span>
229 </div>
230 {r.message && (
231 <span
232 style="font-size:12px;color:var(--text-muted);max-width:360px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap"
233 title={r.message}
234 >
235 {r.message.split("\n")[0]}
236 </span>
237 )}
238 </div>
239 ))
240 )}
241 </div>
242 <p
243 style="font-size:12px;color:var(--text-muted);margin-top:12px"
244 >
245 Upstream (logged, credentials redacted):{" "}
246 <code>{safeUrlForLog(mirror.upstreamUrl)}</code>
247 </p>
248 </>
249 )}
250 </div>
251 </Layout>
252 );
253});
254
255// ---------- Save config ----------
256
257mirrors.post("/:owner/:repo/settings/mirror", requireAuth, async (c) => {
258 const g = await ownerGate(c);
259 if (g instanceof Response) return g;
260 const { user, ownerName, repoName, repo } = g;
261 const body = await c.req.parseBody();
262 const upstreamUrl = String(body.upstream_url || "").trim();
263 const intervalRaw = Number(body.interval_minutes || 1440);
264 const interval = Math.max(5, Math.min(43200, Math.floor(intervalRaw || 1440)));
265 const isEnabled = String(body.is_enabled || "") === "1";
266
267 const v = validateUpstreamUrl(upstreamUrl);
268 if (!v.ok) {
269 return c.redirect(
270 `/${ownerName}/${repoName}/settings/mirror?error=${encodeURIComponent(
271 v.error || "Invalid URL"
272 )}`
273 );
274 }
275
276 const result = await upsertMirror({
277 repositoryId: repo.id,
278 upstreamUrl,
279 intervalMinutes: interval,
280 isEnabled,
281 });
282 if (!result.ok) {
283 return c.redirect(
284 `/${ownerName}/${repoName}/settings/mirror?error=${encodeURIComponent(
285 result.error
286 )}`
287 );
288 }
289
290 await audit({
291 userId: user.id,
292 repositoryId: repo.id,
293 action: "mirror.configure",
294 metadata: {
295 upstream: safeUrlForLog(upstreamUrl),
296 intervalMinutes: interval,
297 isEnabled,
298 },
299 });
300
301 return c.redirect(
302 `/${ownerName}/${repoName}/settings/mirror?success=${encodeURIComponent(
303 "Mirror configuration saved."
304 )}`
305 );
306});
307
308// ---------- Delete ----------
309
310mirrors.post("/:owner/:repo/settings/mirror/delete", requireAuth, async (c) => {
311 const g = await ownerGate(c);
312 if (g instanceof Response) return g;
313 const { user, ownerName, repoName, repo } = g;
314
315 await deleteMirror(repo.id);
316 await audit({
317 userId: user.id,
318 repositoryId: repo.id,
319 action: "mirror.delete",
320 });
321
322 return c.redirect(
323 `/${ownerName}/${repoName}/settings/mirror?success=${encodeURIComponent(
324 "Mirror removed."
325 )}`
326 );
327});
328
329// ---------- Sync now ----------
330
331mirrors.post("/:owner/:repo/settings/mirror/sync", requireAuth, async (c) => {
332 const g = await ownerGate(c);
333 if (g instanceof Response) return g;
334 const { user, ownerName, repoName, repo } = g;
335 const mirror = await getMirrorForRepo(repo.id);
336 if (!mirror) {
337 return c.redirect(
338 `/${ownerName}/${repoName}/settings/mirror?error=${encodeURIComponent(
339 "No mirror configured"
340 )}`
341 );
342 }
343
344 const result = await runMirrorSync(mirror.id);
345 await audit({
346 userId: user.id,
347 repositoryId: repo.id,
348 action: "mirror.sync",
349 metadata: { ok: result.ok, exitCode: result.exitCode },
350 });
351 const msg = result.ok
352 ? "Mirror sync completed."
353 : `Sync failed: ${result.message.split("\n")[0]}`;
354 return c.redirect(
355 `/${ownerName}/${repoName}/settings/mirror?${
356 result.ok ? "success" : "error"
357 }=${encodeURIComponent(msg)}`
358 );
359});
360
361// ---------- Admin: sync all due ----------
362
363mirrors.post("/admin/mirrors/sync-all", requireAuth, async (c) => {
364 const user = c.get("user")!;
365 if (!(await isSiteAdmin(user.id))) {
366 return c.html(
367 <Layout title="Forbidden" user={user}>
368 <div class="empty-state">
369 <h2>403</h2>
370 <p>Site admin only.</p>
371 </div>
372 </Layout>,
373 403
374 );
375 }
376 const summary = await syncAllDue();
377 await audit({
378 userId: user.id,
379 action: "admin.mirrors.sync-all",
380 metadata: summary,
381 });
382 return c.redirect(
383 `/admin?message=${encodeURIComponent(
384 `Mirror sync: ${summary.total} due, ${summary.ok} ok, ${summary.failed} failed.`
385 )}`
386 );
387});
388
389export default mirrors;