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.tsxBlame234 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";
bb0f894Claude15import {
16 Container,
17 Form,
18 FormGroup,
19 Input,
20 Select,
21 Button,
22 Alert,
23 EmptyState,
24 Section,
25 Text,
26} from "../views/ui";
79136bbClaude27
28const repoSettings = new Hono<AuthEnv>();
29
30repoSettings.use("*", softAuth);
31
32// Settings page
33repoSettings.get("/:owner/:repo/settings", requireAuth, async (c) => {
34 const { owner: ownerName, repo: repoName } = c.req.param();
35 const user = c.get("user")!;
36 const success = c.req.query("success");
37 const error = c.req.query("error");
38
39 const [owner] = await db
40 .select()
41 .from(users)
42 .where(eq(users.username, ownerName))
43 .limit(1);
44
45 if (!owner || owner.id !== user.id) {
46 return c.html(
47 <Layout title="Unauthorized" user={user}>
bb0f894Claude48 <EmptyState title="Unauthorized">
79136bbClaude49 <p>Only the repository owner can access settings.</p>
bb0f894Claude50 </EmptyState>
79136bbClaude51 </Layout>,
52 403
53 );
54 }
55
56 const [repo] = await db
57 .select()
58 .from(repositories)
59 .where(
60 and(eq(repositories.ownerId, owner.id), eq(repositories.name, repoName))
61 )
62 .limit(1);
63
64 if (!repo) return c.notFound();
65
66 const branches = await listBranches(ownerName, repoName);
67
68 return c.html(
69 <Layout title={`Settings — ${ownerName}/${repoName}`} user={user}>
70 <RepoHeader owner={ownerName} repo={repoName} />
bb0f894Claude71 <Container maxWidth={600}>
79136bbClaude72 <h2 style="margin-bottom: 20px">Repository settings</h2>
73 {success && (
bb0f894Claude74 <Alert variant="success">{decodeURIComponent(success)}</Alert>
79136bbClaude75 )}
76 {error && (
bb0f894Claude77 <Alert variant="error">{decodeURIComponent(error)}</Alert>
79136bbClaude78 )}
79
bb0f894Claude80 <Form
79136bbClaude81 action={`/${ownerName}/${repoName}/settings`}
bb0f894Claude82 method="POST"
79136bbClaude83 >
bb0f894Claude84 <FormGroup label="Description" htmlFor="description">
85 <Input
79136bbClaude86 name="description"
bb0f894Claude87 id="description"
79136bbClaude88 value={repo.description || ""}
89 placeholder="A short description"
90 />
bb0f894Claude91 </FormGroup>
92 <FormGroup label="Default branch" htmlFor="default_branch">
93 <Select name="default_branch" id="default_branch" value={repo.defaultBranch}>
79136bbClaude94 {branches.length === 0 ? (
95 <option value={repo.defaultBranch}>
96 {repo.defaultBranch}
97 </option>
98 ) : (
99 branches.map((b) => (
100 <option value={b} selected={b === repo.defaultBranch}>
101 {b}
102 </option>
103 ))
104 )}
bb0f894Claude105 </Select>
106 </FormGroup>
107 <FormGroup label="Visibility">
79136bbClaude108 <div class="visibility-options">
109 <label class="visibility-option">
110 <input
111 type="radio"
112 name="visibility"
113 value="public"
114 checked={!repo.isPrivate}
115 />
116 <div class="vis-label">Public</div>
117 </label>
118 <label class="visibility-option">
119 <input
120 type="radio"
121 name="visibility"
122 value="private"
123 checked={repo.isPrivate}
124 />
125 <div class="vis-label">Private</div>
126 </label>
127 </div>
bb0f894Claude128 </FormGroup>
129 <Button type="submit" variant="primary">
79136bbClaude130 Save changes
bb0f894Claude131 </Button>
132 </Form>
79136bbClaude133
134 <div
135 style="margin-top: 40px; padding: 20px; border: 1px solid var(--red); border-radius: var(--radius)"
136 >
137 <h3 style="color: var(--red); margin-bottom: 12px">Danger zone</h3>
bb0f894Claude138 <Text size={14} muted style="display:block;margin-bottom:12px">
79136bbClaude139 Permanently delete this repository and all its data.
bb0f894Claude140 </Text>
79136bbClaude141 <form
45e31d0Claude142 method="post"
79136bbClaude143 action={`/${ownerName}/${repoName}/settings/delete`}
144 onsubmit="return confirm('Are you sure? This cannot be undone.')"
145 >
bb0f894Claude146 <Button type="submit" variant="danger">
79136bbClaude147 Delete this repository
bb0f894Claude148 </Button>
79136bbClaude149 </form>
150 </div>
bb0f894Claude151 </Container>
79136bbClaude152 </Layout>
153 );
154});
155
156// Save settings
157repoSettings.post("/:owner/:repo/settings", requireAuth, async (c) => {
158 const { owner: ownerName, repo: repoName } = c.req.param();
159 const user = c.get("user")!;
160 const body = await c.req.parseBody();
161
162 const [owner] = await db
163 .select()
164 .from(users)
165 .where(eq(users.username, ownerName))
166 .limit(1);
167
168 if (!owner || owner.id !== user.id) {
169 return c.redirect(`/${ownerName}/${repoName}`);
170 }
171
172 await db
173 .update(repositories)
174 .set({
175 description: String(body.description || "").trim() || null,
176 defaultBranch: String(body.default_branch || "main"),
177 isPrivate: body.visibility === "private",
178 updatedAt: new Date(),
179 })
180 .where(
181 and(eq(repositories.ownerId, owner.id), eq(repositories.name, repoName))
182 );
183
184 return c.redirect(
185 `/${ownerName}/${repoName}/settings?success=Settings+saved`
186 );
187});
188
189// Delete repository
190repoSettings.post(
191 "/:owner/:repo/settings/delete",
192 requireAuth,
193 async (c) => {
194 const { owner: ownerName, repo: repoName } = c.req.param();
195 const user = c.get("user")!;
196
197 const [owner] = await db
198 .select()
199 .from(users)
200 .where(eq(users.username, ownerName))
201 .limit(1);
202
203 if (!owner || owner.id !== user.id) {
204 return c.redirect(`/${ownerName}/${repoName}`);
205 }
206
207 const [repo] = await db
208 .select()
209 .from(repositories)
210 .where(
211 and(
212 eq(repositories.ownerId, owner.id),
213 eq(repositories.name, repoName)
214 )
215 )
216 .limit(1);
217
218 if (!repo) return c.redirect(`/${ownerName}`);
219
220 // Delete from disk
221 try {
222 await rm(repo.diskPath, { recursive: true, force: true });
223 } catch {
224 // Disk cleanup best-effort
225 }
226
227 // Delete from DB (cascades to stars, issues, etc.)
228 await db.delete(repositories).where(eq(repositories.id, repo.id));
229
230 return c.redirect(`/${ownerName}`);
231 }
232);
233
234export default repoSettings;