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.tsxBlame233 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 Text,
25} from "../views/ui";
79136bbClaude26
27const repoSettings = new Hono<AuthEnv>();
28
29repoSettings.use("*", softAuth);
30
31// Settings page
32repoSettings.get("/:owner/:repo/settings", requireAuth, async (c) => {
33 const { owner: ownerName, repo: repoName } = c.req.param();
34 const user = c.get("user")!;
35 const success = c.req.query("success");
36 const error = c.req.query("error");
37
38 const [owner] = await db
39 .select()
40 .from(users)
41 .where(eq(users.username, ownerName))
42 .limit(1);
43
44 if (!owner || owner.id !== user.id) {
45 return c.html(
46 <Layout title="Unauthorized" user={user}>
bb0f894Claude47 <EmptyState title="Unauthorized">
79136bbClaude48 <p>Only the repository owner can access settings.</p>
bb0f894Claude49 </EmptyState>
79136bbClaude50 </Layout>,
51 403
52 );
53 }
54
55 const [repo] = await db
56 .select()
57 .from(repositories)
58 .where(
59 and(eq(repositories.ownerId, owner.id), eq(repositories.name, repoName))
60 )
61 .limit(1);
62
63 if (!repo) return c.notFound();
64
65 const branches = await listBranches(ownerName, repoName);
66
67 return c.html(
68 <Layout title={`Settings — ${ownerName}/${repoName}`} user={user}>
69 <RepoHeader owner={ownerName} repo={repoName} />
bb0f894Claude70 <Container maxWidth={600}>
79136bbClaude71 <h2 style="margin-bottom: 20px">Repository settings</h2>
72 {success && (
bb0f894Claude73 <Alert variant="success">{decodeURIComponent(success)}</Alert>
79136bbClaude74 )}
75 {error && (
bb0f894Claude76 <Alert variant="error">{decodeURIComponent(error)}</Alert>
79136bbClaude77 )}
78
bb0f894Claude79 <Form
79136bbClaude80 action={`/${ownerName}/${repoName}/settings`}
bb0f894Claude81 method="POST"
79136bbClaude82 >
bb0f894Claude83 <FormGroup label="Description" htmlFor="description">
84 <Input
79136bbClaude85 name="description"
bb0f894Claude86 id="description"
79136bbClaude87 value={repo.description || ""}
88 placeholder="A short description"
89 />
bb0f894Claude90 </FormGroup>
91 <FormGroup label="Default branch" htmlFor="default_branch">
92 <Select name="default_branch" id="default_branch" value={repo.defaultBranch}>
79136bbClaude93 {branches.length === 0 ? (
94 <option value={repo.defaultBranch}>
95 {repo.defaultBranch}
96 </option>
97 ) : (
98 branches.map((b) => (
99 <option value={b} selected={b === repo.defaultBranch}>
100 {b}
101 </option>
102 ))
103 )}
bb0f894Claude104 </Select>
105 </FormGroup>
106 <FormGroup label="Visibility">
79136bbClaude107 <div class="visibility-options">
108 <label class="visibility-option">
109 <input
110 type="radio"
111 name="visibility"
112 value="public"
113 checked={!repo.isPrivate}
114 />
115 <div class="vis-label">Public</div>
116 </label>
117 <label class="visibility-option">
118 <input
119 type="radio"
120 name="visibility"
121 value="private"
122 checked={repo.isPrivate}
123 />
124 <div class="vis-label">Private</div>
125 </label>
126 </div>
bb0f894Claude127 </FormGroup>
128 <Button type="submit" variant="primary">
79136bbClaude129 Save changes
bb0f894Claude130 </Button>
131 </Form>
79136bbClaude132
133 <div
134 style="margin-top: 40px; padding: 20px; border: 1px solid var(--red); border-radius: var(--radius)"
135 >
136 <h3 style="color: var(--red); margin-bottom: 12px">Danger zone</h3>
bb0f894Claude137 <Text size={14} muted style="display:block;margin-bottom:12px">
79136bbClaude138 Permanently delete this repository and all its data.
bb0f894Claude139 </Text>
79136bbClaude140 <form
45e31d0Claude141 method="post"
79136bbClaude142 action={`/${ownerName}/${repoName}/settings/delete`}
143 onsubmit="return confirm('Are you sure? This cannot be undone.')"
144 >
bb0f894Claude145 <Button type="submit" variant="danger">
79136bbClaude146 Delete this repository
bb0f894Claude147 </Button>
79136bbClaude148 </form>
149 </div>
bb0f894Claude150 </Container>
79136bbClaude151 </Layout>
152 );
153});
154
155// Save settings
156repoSettings.post("/:owner/:repo/settings", requireAuth, async (c) => {
157 const { owner: ownerName, repo: repoName } = c.req.param();
158 const user = c.get("user")!;
159 const body = await c.req.parseBody();
160
161 const [owner] = await db
162 .select()
163 .from(users)
164 .where(eq(users.username, ownerName))
165 .limit(1);
166
167 if (!owner || owner.id !== user.id) {
168 return c.redirect(`/${ownerName}/${repoName}`);
169 }
170
171 await db
172 .update(repositories)
173 .set({
174 description: String(body.description || "").trim() || null,
175 defaultBranch: String(body.default_branch || "main"),
176 isPrivate: body.visibility === "private",
177 updatedAt: new Date(),
178 })
179 .where(
180 and(eq(repositories.ownerId, owner.id), eq(repositories.name, repoName))
181 );
182
183 return c.redirect(
184 `/${ownerName}/${repoName}/settings?success=Settings+saved`
185 );
186});
187
188// Delete repository
189repoSettings.post(
190 "/:owner/:repo/settings/delete",
191 requireAuth,
192 async (c) => {
193 const { owner: ownerName, repo: repoName } = c.req.param();
194 const user = c.get("user")!;
195
196 const [owner] = await db
197 .select()
198 .from(users)
199 .where(eq(users.username, ownerName))
200 .limit(1);
201
202 if (!owner || owner.id !== user.id) {
203 return c.redirect(`/${ownerName}/${repoName}`);
204 }
205
206 const [repo] = await db
207 .select()
208 .from(repositories)
209 .where(
210 and(
211 eq(repositories.ownerId, owner.id),
212 eq(repositories.name, repoName)
213 )
214 )
215 .limit(1);
216
217 if (!repo) return c.redirect(`/${ownerName}`);
218
219 // Delete from disk
220 try {
221 await rm(repo.diskPath, { recursive: true, force: true });
222 } catch {
223 // Disk cleanup best-effort
224 }
225
226 // Delete from DB (cascades to stars, issues, etc.)
227 await db.delete(repositories).where(eq(repositories.id, repo.id));
228
229 return c.redirect(`/${ownerName}`);
230 }
231);
232
233export default repoSettings;