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

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

fork.tsBlame103 lines · 1 contributor
c81ab7aClaude1/**
2 * Fork route — copy a repository into your account.
3 */
4
5import { Hono } from "hono";
6import { eq, and } from "drizzle-orm";
7import { db } from "../db";
8import { repositories, users, activityFeed } from "../db/schema";
9import { softAuth, requireAuth } from "../middleware/auth";
10import type { AuthEnv } from "../middleware/auth";
11import { getRepoPath, repoExists, initBareRepo } from "../git/repository";
12import { config } from "../lib/config";
13import { join } from "path";
14
15const fork = new Hono<AuthEnv>();
16
17fork.use("*", softAuth);
18
19// Fork a repository
20fork.post("/:owner/:repo/fork", requireAuth, async (c) => {
21 const { owner: ownerName, repo: repoName } = c.req.param();
22 const user = c.get("user")!;
23
24 // Can't fork your own repo
25 if (ownerName === user.username) {
26 return c.redirect(`/${ownerName}/${repoName}`);
27 }
28
29 // Check source exists
30 if (!(await repoExists(ownerName, repoName))) {
31 return c.redirect(`/${ownerName}/${repoName}`);
32 }
33
34 // Check if already forked
35 if (await repoExists(user.username, repoName)) {
36 return c.redirect(`/${user.username}/${repoName}`);
37 }
38
39 // Get source repo from DB
40 const [sourceOwner] = await db
41 .select()
42 .from(users)
43 .where(eq(users.username, ownerName))
44 .limit(1);
45 if (!sourceOwner) return c.redirect(`/${ownerName}/${repoName}`);
46
47 const [sourceRepo] = await db
48 .select()
49 .from(repositories)
50 .where(
51 and(
52 eq(repositories.ownerId, sourceOwner.id),
53 eq(repositories.name, repoName)
54 )
55 )
56 .limit(1);
57 if (!sourceRepo) return c.redirect(`/${ownerName}/${repoName}`);
58
59 // Clone the bare repo
60 const sourcePath = getRepoPath(ownerName, repoName);
61 const destPath = join(config.gitReposPath, user.username, `${repoName}.git`);
62
63 const proc = Bun.spawn(["git", "clone", "--bare", sourcePath, destPath], {
64 stdout: "pipe",
65 stderr: "pipe",
66 });
67 await proc.exited;
68
69 // Insert into DB
70 await db.insert(repositories).values({
71 name: repoName,
72 ownerId: user.id,
73 description: sourceRepo.description
74 ? `Fork of ${ownerName}/${repoName} — ${sourceRepo.description}`
75 : `Fork of ${ownerName}/${repoName}`,
76 isPrivate: false,
77 defaultBranch: sourceRepo.defaultBranch,
78 diskPath: destPath,
79 forkedFromId: sourceRepo.id,
80 });
81
82 // Update fork count
83 await db
84 .update(repositories)
85 .set({ forkCount: sourceRepo.forkCount + 1 })
86 .where(eq(repositories.id, sourceRepo.id));
87
88 // Log activity
89 try {
90 await db.insert(activityFeed).values({
91 repositoryId: sourceRepo.id,
92 userId: user.id,
93 action: "fork",
94 metadata: JSON.stringify({ forkOwner: user.username }),
95 });
96 } catch {
97 // best effort
98 }
99
100 return c.redirect(`/${user.username}/${repoName}`);
101});
102
103export default fork;