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

repo-settings.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.

repo-settings.tsxBlame227 lines · 1 contributor
79136bbClaude1/**
2 * Repository settings — description, visibility, default branch, danger zone.
3 */
4
5import { Hono } from "hono";
6import { eq, and } from "drizzle-orm";
7import { db } from "../db";
8import { repositories, users } from "../db/schema";
9import { Layout } from "../views/layout";
10import { RepoHeader } from "../views/components";
11import { softAuth, requireAuth } from "../middleware/auth";
12import type { AuthEnv } from "../middleware/auth";
13import { listBranches } from "../git/repository";
14import { rm } from "fs/promises";
15
16const repoSettings = new Hono<AuthEnv>();
17
18repoSettings.use("*", softAuth);
19
20// Settings page
21repoSettings.get("/:owner/:repo/settings", requireAuth, async (c) => {
22 const { owner: ownerName, repo: repoName } = c.req.param();
23 const user = c.get("user")!;
24 const success = c.req.query("success");
25 const error = c.req.query("error");
26
27 const [owner] = await db
28 .select()
29 .from(users)
30 .where(eq(users.username, ownerName))
31 .limit(1);
32
33 if (!owner || owner.id !== user.id) {
34 return c.html(
35 <Layout title="Unauthorized" user={user}>
36 <div class="empty-state">
37 <h2>Unauthorized</h2>
38 <p>Only the repository owner can access settings.</p>
39 </div>
40 </Layout>,
41 403
42 );
43 }
44
45 const [repo] = await db
46 .select()
47 .from(repositories)
48 .where(
49 and(eq(repositories.ownerId, owner.id), eq(repositories.name, repoName))
50 )
51 .limit(1);
52
53 if (!repo) return c.notFound();
54
55 const branches = await listBranches(ownerName, repoName);
56
57 return c.html(
58 <Layout title={`Settings — ${ownerName}/${repoName}`} user={user}>
59 <RepoHeader owner={ownerName} repo={repoName} />
60 <div style="max-width: 600px">
61 <h2 style="margin-bottom: 20px">Repository settings</h2>
62 {success && (
63 <div class="auth-success">{decodeURIComponent(success)}</div>
64 )}
65 {error && (
66 <div class="auth-error">{decodeURIComponent(error)}</div>
67 )}
68
69 <form
70 method="POST"
71 action={`/${ownerName}/${repoName}/settings`}
72 >
73 <div class="form-group">
74 <label for="description">Description</label>
75 <input
76 type="text"
77 id="description"
78 name="description"
79 value={repo.description || ""}
80 placeholder="A short description"
81 />
82 </div>
83 <div class="form-group">
84 <label for="default_branch">Default branch</label>
85 <select id="default_branch" name="default_branch">
86 {branches.length === 0 ? (
87 <option value={repo.defaultBranch}>
88 {repo.defaultBranch}
89 </option>
90 ) : (
91 branches.map((b) => (
92 <option value={b} selected={b === repo.defaultBranch}>
93 {b}
94 </option>
95 ))
96 )}
97 </select>
98 </div>
99 <div class="form-group">
100 <label>Visibility</label>
101 <div class="visibility-options">
102 <label class="visibility-option">
103 <input
104 type="radio"
105 name="visibility"
106 value="public"
107 checked={!repo.isPrivate}
108 />
109 <div class="vis-label">Public</div>
110 </label>
111 <label class="visibility-option">
112 <input
113 type="radio"
114 name="visibility"
115 value="private"
116 checked={repo.isPrivate}
117 />
118 <div class="vis-label">Private</div>
119 </label>
120 </div>
121 </div>
122 <button type="submit" class="btn btn-primary">
123 Save changes
124 </button>
125 </form>
126
127 <div
128 style="margin-top: 40px; padding: 20px; border: 1px solid var(--red); border-radius: var(--radius)"
129 >
130 <h3 style="color: var(--red); margin-bottom: 12px">Danger zone</h3>
131 <p style="font-size: 14px; color: var(--text-muted); margin-bottom: 12px">
132 Permanently delete this repository and all its data.
133 </p>
134 <form
135 method="POST"
136 action={`/${ownerName}/${repoName}/settings/delete`}
137 onsubmit="return confirm('Are you sure? This cannot be undone.')"
138 >
139 <button type="submit" class="btn btn-danger">
140 Delete this repository
141 </button>
142 </form>
143 </div>
144 </div>
145 </Layout>
146 );
147});
148
149// Save settings
150repoSettings.post("/:owner/:repo/settings", requireAuth, async (c) => {
151 const { owner: ownerName, repo: repoName } = c.req.param();
152 const user = c.get("user")!;
153 const body = await c.req.parseBody();
154
155 const [owner] = await db
156 .select()
157 .from(users)
158 .where(eq(users.username, ownerName))
159 .limit(1);
160
161 if (!owner || owner.id !== user.id) {
162 return c.redirect(`/${ownerName}/${repoName}`);
163 }
164
165 await db
166 .update(repositories)
167 .set({
168 description: String(body.description || "").trim() || null,
169 defaultBranch: String(body.default_branch || "main"),
170 isPrivate: body.visibility === "private",
171 updatedAt: new Date(),
172 })
173 .where(
174 and(eq(repositories.ownerId, owner.id), eq(repositories.name, repoName))
175 );
176
177 return c.redirect(
178 `/${ownerName}/${repoName}/settings?success=Settings+saved`
179 );
180});
181
182// Delete repository
183repoSettings.post(
184 "/:owner/:repo/settings/delete",
185 requireAuth,
186 async (c) => {
187 const { owner: ownerName, repo: repoName } = c.req.param();
188 const user = c.get("user")!;
189
190 const [owner] = await db
191 .select()
192 .from(users)
193 .where(eq(users.username, ownerName))
194 .limit(1);
195
196 if (!owner || owner.id !== user.id) {
197 return c.redirect(`/${ownerName}/${repoName}`);
198 }
199
200 const [repo] = await db
201 .select()
202 .from(repositories)
203 .where(
204 and(
205 eq(repositories.ownerId, owner.id),
206 eq(repositories.name, repoName)
207 )
208 )
209 .limit(1);
210
211 if (!repo) return c.redirect(`/${ownerName}`);
212
213 // Delete from disk
214 try {
215 await rm(repo.diskPath, { recursive: true, force: true });
216 } catch {
217 // Disk cleanup best-effort
218 }
219
220 // Delete from DB (cascades to stars, issues, etc.)
221 await db.delete(repositories).where(eq(repositories.id, repo.id));
222
223 return c.redirect(`/${ownerName}`);
224 }
225);
226
227export default repoSettings;