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

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.tsBlame117 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
3ef4c9dClaude70 const [newRepo] = await db
71 .insert(repositories)
72 .values({
73 name: repoName,
74 ownerId: user.id,
75 description: sourceRepo.description
76 ? `Fork of ${ownerName}/${repoName} — ${sourceRepo.description}`
77 : `Fork of ${ownerName}/${repoName}`,
78 isPrivate: false,
79 defaultBranch: sourceRepo.defaultBranch,
80 diskPath: destPath,
81 forkedFromId: sourceRepo.id,
82 })
83 .returning();
84
85 // Bootstrap the fork with green-by-default settings, protection, labels
86 if (newRepo) {
87 const { bootstrapRepository } = await import("../lib/repo-bootstrap");
88 await bootstrapRepository({
89 repositoryId: newRepo.id,
90 ownerUserId: user.id,
91 defaultBranch: sourceRepo.defaultBranch,
92 skipWelcomeIssue: true, // forks don't need a welcome issue
93 });
94 }
c81ab7aClaude95
96 // Update fork count
97 await db
98 .update(repositories)
99 .set({ forkCount: sourceRepo.forkCount + 1 })
100 .where(eq(repositories.id, sourceRepo.id));
101
102 // Log activity
103 try {
104 await db.insert(activityFeed).values({
105 repositoryId: sourceRepo.id,
106 userId: user.id,
107 action: "fork",
108 metadata: JSON.stringify({ forkOwner: user.username }),
109 });
110 } catch {
111 // best effort
112 }
113
114 return c.redirect(`/${user.username}/${repoName}`);
115});
116
117export default fork;