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

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

templates.tsBlame122 lines · 1 contributor
71cd5ecClaude1/**
2 * Block I2 — Template repositories.
3 *
4 * POST /:owner/:repo/use-template — clone a template into a new repo owned
5 * by the current user. Similar to fork, but:
6 * - source must have `is_template = true`
7 * - destination is given a new name via the `name` form field
8 * - `forked_from_id` is NOT set (templates break lineage)
9 * - default branch is reset to a clean history (for now, we use the
10 * template's history, matching GitHub's optional `--include-all-branches`
11 * behavior from the template's default branch only)
12 */
13
14import { Hono } from "hono";
15import { and, eq } from "drizzle-orm";
16import { db } from "../db";
17import { activityFeed, repositories, users } from "../db/schema";
18import { softAuth, requireAuth } from "../middleware/auth";
19import type { AuthEnv } from "../middleware/auth";
20import { getRepoPath, repoExists } from "../git/repository";
21import { config } from "../lib/config";
22import { join } from "path";
23
24const templates = new Hono<AuthEnv>();
25templates.use("*", softAuth);
26
27templates.post("/:owner/:repo/use-template", requireAuth, async (c) => {
28 const { owner: ownerName, repo: repoName } = c.req.param();
29 const user = c.get("user")!;
30 const body = await c.req.parseBody();
31 const newName = String(body.name || "").trim();
32 if (!newName) {
33 return c.redirect(`/${ownerName}/${repoName}?error=Name+required`);
34 }
35 if (!/^[a-zA-Z0-9._-]+$/.test(newName)) {
36 return c.redirect(`/${ownerName}/${repoName}?error=Invalid+name`);
37 }
38
39 if (!(await repoExists(ownerName, repoName))) {
40 return c.redirect(`/${ownerName}/${repoName}`);
41 }
42
43 const [sourceOwner] = await db
44 .select()
45 .from(users)
46 .where(eq(users.username, ownerName))
47 .limit(1);
48 if (!sourceOwner) return c.redirect(`/${ownerName}/${repoName}`);
49
50 const [sourceRepo] = await db
51 .select()
52 .from(repositories)
53 .where(
54 and(
55 eq(repositories.ownerId, sourceOwner.id),
56 eq(repositories.name, repoName)
57 )
58 )
59 .limit(1);
60 if (!sourceRepo) return c.redirect(`/${ownerName}/${repoName}`);
61 if (!sourceRepo.isTemplate) {
62 return c.redirect(`/${ownerName}/${repoName}?error=Not+a+template`);
63 }
64
65 // Refuse if the user already has a repo by that name
66 if (await repoExists(user.username, newName)) {
67 return c.redirect(`/${user.username}/${newName}`);
68 }
69
70 const sourcePath = getRepoPath(ownerName, repoName);
71 const destPath = join(config.gitReposPath, user.username, `${newName}.git`);
72 const proc = Bun.spawn(["git", "clone", "--bare", sourcePath, destPath], {
73 stdout: "pipe",
74 stderr: "pipe",
75 });
76 await proc.exited;
77
78 const [newRepo] = await db
79 .insert(repositories)
80 .values({
81 name: newName,
82 ownerId: user.id,
83 description: sourceRepo.description
84 ? `Seeded from ${ownerName}/${repoName} template`
85 : `Seeded from ${ownerName}/${repoName} template`,
86 isPrivate: false,
87 defaultBranch: sourceRepo.defaultBranch,
88 diskPath: destPath,
89 // Intentionally no forkedFromId — templates break lineage
90 })
91 .returning();
92
93 if (newRepo) {
94 try {
95 const { bootstrapRepository } = await import("../lib/repo-bootstrap");
96 await bootstrapRepository({
97 repositoryId: newRepo.id,
98 ownerUserId: user.id,
99 defaultBranch: sourceRepo.defaultBranch,
100 skipWelcomeIssue: true,
101 });
102 } catch {
103 // bootstrap failures shouldn't break the primary create
104 }
105 try {
106 await db.insert(activityFeed).values({
107 repositoryId: newRepo.id,
108 userId: user.id,
109 action: "created",
110 metadata: JSON.stringify({
111 fromTemplate: `${ownerName}/${repoName}`,
112 }),
113 });
114 } catch {
115 // best effort
116 }
117 }
118
119 return c.redirect(`/${user.username}/${newName}`);
120});
121
122export default templates;