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.tsxBlame2122 lines · 3 contributors
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";
ec9e3e3Claude8import { repositories, users, repoTransfers, branchProtection } from "../db/schema";
79136bbClaude9import { Layout } from "../views/layout";
ae2a071ccanty labs10import { RepoHeader, RepoNav } from "../views/components";
79136bbClaude11import { softAuth, requireAuth } from "../middleware/auth";
12import type { AuthEnv } from "../middleware/auth";
febd4f0Claude13import { requireRepoAccess } from "../middleware/repo-access";
79136bbClaude14import { listBranches } from "../git/repository";
534f04aClaude15import { audit } from "../lib/notify";
79136bbClaude16import { rm } from "fs/promises";
58307aeClaude17import { EmptyState } from "../views/ui";
79136bbClaude18
19const repoSettings = new Hono<AuthEnv>();
20
21repoSettings.use("*", softAuth);
22
58307aeClaude23// Inline, scoped CSS — every class prefixed `.repo-settings-` so styles cannot
24// bleed into other surfaces. Pattern mirrors the user-settings polish
25// (commit 98eb360) and the admin-panel polish (commit 07f4b70).
26const repoSettingsStyles = `
eed4684Claude27 .repo-settings-container { max-width: 1200px; margin: 0 auto; padding: 0 var(--space-3); }
58307aeClaude28
29 /* ─── Hero ─── */
30 .repo-settings-hero {
31 position: relative;
32 margin: var(--space-5) 0 var(--space-5);
33 padding: var(--space-5) var(--space-6);
34 background: var(--bg-elevated);
35 border: 1px solid var(--border);
36 border-radius: 16px;
37 overflow: hidden;
38 }
39 .repo-settings-hero::before {
40 content: '';
41 position: absolute;
42 top: 0; left: 0; right: 0;
43 height: 2px;
6fd5915Claude44 background: linear-gradient(90deg, transparent 0%, #5b6ee8 30%, #5f8fa0 70%, transparent 100%);
58307aeClaude45 opacity: 0.7;
46 pointer-events: none;
47 }
48 .repo-settings-hero-bg {
49 position: absolute;
50 inset: -20% -10% auto auto;
51 width: 360px; height: 360px;
52 pointer-events: none;
53 z-index: 0;
54 }
55 .repo-settings-hero-orb {
56 position: absolute;
57 inset: 0;
6fd5915Claude58 background: radial-gradient(circle, rgba(91,110,232,0.18), rgba(95,143,160,0.09) 45%, transparent 70%);
58307aeClaude59 filter: blur(80px);
60 opacity: 0.65;
61 animation: repoSettingsHeroOrb 14s ease-in-out infinite;
62 }
63 @keyframes repoSettingsHeroOrb {
64 0%, 100% { transform: scale(1) translate(0, 0); opacity: 0.55; }
65 50% { transform: scale(1.08) translate(-8px, 6px); opacity: 0.78; }
66 }
67 @media (prefers-reduced-motion: reduce) {
68 .repo-settings-hero-orb { animation: none; }
69 }
70 .repo-settings-hero-inner {
71 position: relative;
72 z-index: 1;
73 max-width: 640px;
74 }
75 .repo-settings-hero-eyebrow {
76 font-size: 13px;
77 color: var(--text-muted);
78 margin-bottom: var(--space-2);
79 letter-spacing: -0.005em;
80 }
81 .repo-settings-hero-eyebrow .repo-settings-hero-repo {
82 color: var(--accent);
83 font-weight: 600;
84 }
85 .repo-settings-hero-title {
86 font-size: clamp(28px, 4vw, 40px);
87 font-family: var(--font-display);
88 font-weight: 800;
89 letter-spacing: -0.028em;
90 line-height: 1.05;
91 margin: 0 0 var(--space-2);
92 color: var(--text-strong);
93 }
94 .repo-settings-hero-title .gradient-text {
6fd5915Claude95 background-image: linear-gradient(135deg, #5b6ee8 0%, #5b6ee8 50%, #5f8fa0 100%);
58307aeClaude96 -webkit-background-clip: text;
97 background-clip: text;
98 -webkit-text-fill-color: transparent;
99 color: transparent;
100 }
101 .repo-settings-hero-sub {
102 font-size: 15px;
103 color: var(--text-muted);
104 margin: 0;
105 line-height: 1.5;
106 }
107 .repo-settings-hero-link {
108 display: inline-flex;
109 align-items: center;
110 gap: 6px;
111 margin-top: var(--space-3);
112 font-size: 13.5px;
113 color: var(--text-link, var(--accent));
114 text-decoration: none;
115 font-weight: 500;
116 }
117 .repo-settings-hero-link:hover { text-decoration: underline; }
118 .repo-settings-hero-link .arrow { transition: transform 120ms ease; }
119 .repo-settings-hero-link:hover .arrow { transform: translateX(2px); }
120
121 /* ─── Banners (success / error) ─── */
122 .repo-settings-banner {
123 display: flex;
124 align-items: center;
125 gap: 10px;
126 padding: 12px 16px;
127 border-radius: 12px;
128 font-size: 13.5px;
129 margin-bottom: var(--space-4);
130 line-height: 1.5;
131 }
132 .repo-settings-banner-success {
133 background: rgba(52,211,153,0.08);
e589f77ccantynz-alt134 color: var(--green);
58307aeClaude135 box-shadow: inset 0 0 0 1px rgba(52,211,153,0.30);
136 }
137 .repo-settings-banner-error {
138 background: rgba(248,113,113,0.08);
e589f77ccantynz-alt139 color: var(--red);
58307aeClaude140 box-shadow: inset 0 0 0 1px rgba(248,113,113,0.30);
141 }
142 .repo-settings-banner-icon {
143 width: 18px; height: 18px;
144 border-radius: 9999px;
145 flex-shrink: 0;
146 display: inline-flex;
147 align-items: center;
148 justify-content: center;
149 font-size: 12px;
150 font-weight: 700;
151 }
152 .repo-settings-banner-success .repo-settings-banner-icon {
153 background: rgba(52,211,153,0.18);
e589f77ccantynz-alt154 color: var(--green);
58307aeClaude155 }
156 .repo-settings-banner-error .repo-settings-banner-icon {
157 background: rgba(248,113,113,0.18);
e589f77ccantynz-alt158 color: var(--red);
58307aeClaude159 }
160
161 /* ─── Section cards ─── */
162 .repo-settings-section {
163 background: var(--bg-elevated);
164 border: 1px solid var(--border);
165 border-radius: 14px;
166 margin-bottom: var(--space-5);
167 overflow: hidden;
168 }
169 .repo-settings-section-head {
170 padding: var(--space-4) var(--space-5) var(--space-3);
171 border-bottom: 1px solid var(--border);
172 }
173 .repo-settings-section-eyebrow {
174 font-size: 11px;
175 font-weight: 600;
176 letter-spacing: 0.08em;
177 text-transform: uppercase;
178 color: var(--accent);
179 margin-bottom: 6px;
180 }
181 .repo-settings-section-title {
182 font-family: var(--font-display);
183 font-size: 18px;
184 font-weight: 700;
185 letter-spacing: -0.018em;
186 margin: 0 0 4px;
187 color: var(--text-strong);
188 }
189 .repo-settings-section-desc {
190 font-size: 13.5px;
191 color: var(--text-muted);
192 margin: 0;
193 line-height: 1.5;
194 }
195 .repo-settings-section-body { padding: var(--space-4) var(--space-5); }
196 .repo-settings-section-foot {
197 padding: var(--space-3) var(--space-5);
198 border-top: 1px solid var(--border);
199 background: rgba(255,255,255,0.012);
200 display: flex;
201 justify-content: flex-end;
202 gap: var(--space-2);
203 align-items: center;
204 flex-wrap: wrap;
205 }
206 .repo-settings-section-foot .repo-settings-foot-hint {
207 margin-right: auto;
208 font-size: 12.5px;
209 color: var(--text-muted);
210 }
211
212 /* ─── Form rows ─── */
213 .repo-settings-field { margin-bottom: var(--space-4); }
214 .repo-settings-field:last-child { margin-bottom: 0; }
215 .repo-settings-field-label {
216 display: block;
217 font-size: 13px;
218 font-weight: 600;
219 color: var(--text-strong);
220 margin-bottom: 6px;
221 letter-spacing: -0.005em;
222 }
223 .repo-settings-field-hint {
224 font-size: 12.5px;
225 color: var(--text-muted);
226 margin-top: 6px;
227 line-height: 1.45;
228 }
229 .repo-settings-input,
230 .repo-settings-select {
231 width: 100%;
232 padding: 9px 12px;
233 font-size: 14px;
234 color: var(--text);
235 background: var(--bg);
236 border: 1px solid var(--border-strong, var(--border));
237 border-radius: 8px;
238 outline: none;
239 transition: border-color 120ms ease, box-shadow 120ms ease;
240 font-family: var(--font-sans);
241 }
242 .repo-settings-input:focus,
243 .repo-settings-select:focus {
244 border-color: var(--border-focus, var(--accent));
6fd5915Claude245 box-shadow: 0 0 0 3px rgba(91,110,232,0.18);
58307aeClaude246 }
247
248 /* ─── Visibility radio cards ─── */
249 .repo-settings-visibility {
250 display: grid;
251 grid-template-columns: 1fr 1fr;
252 gap: var(--space-2);
253 }
254 @media (max-width: 600px) {
255 .repo-settings-visibility { grid-template-columns: 1fr; }
256 }
257 .repo-settings-vis-card {
258 display: flex;
259 gap: 10px;
260 padding: 14px;
261 background: var(--bg);
262 border: 1px solid var(--border);
263 border-radius: 12px;
264 cursor: pointer;
265 transition: border-color 120ms ease, background 120ms ease;
266 }
267 .repo-settings-vis-card:hover { border-color: var(--border-strong, var(--border)); }
268 .repo-settings-vis-card:has(input:checked) {
6fd5915Claude269 border-color: rgba(91,110,232,0.55);
270 background: rgba(91,110,232,0.06);
271 box-shadow: 0 0 0 1px rgba(91,110,232,0.25);
58307aeClaude272 }
273 .repo-settings-vis-radio {
274 margin-top: 3px;
275 accent-color: var(--accent);
276 }
277 .repo-settings-vis-body { display: flex; flex-direction: column; gap: 4px; }
278 .repo-settings-vis-label {
279 font-size: 14px;
280 font-weight: 600;
281 color: var(--text-strong);
282 }
283 .repo-settings-vis-desc {
284 font-size: 12.5px;
285 color: var(--text-muted);
286 line-height: 1.4;
287 }
288
289 /* ─── Toggle rows (stale-sweep) ─── */
290 .repo-settings-toggle-row {
291 display: flex;
292 align-items: flex-start;
293 gap: var(--space-3);
294 padding: 12px 14px;
295 border-radius: 10px;
296 border: 1px solid var(--border-subtle, var(--border));
297 background: var(--bg-secondary, var(--bg));
298 margin-bottom: 8px;
299 transition: border-color 120ms ease, background 120ms ease;
300 cursor: pointer;
301 }
302 .repo-settings-toggle-row:last-of-type { margin-bottom: 0; }
303 .repo-settings-toggle-row:hover {
304 border-color: var(--border);
305 background: rgba(255,255,255,0.02);
306 }
307 .repo-settings-toggle-row:has(input:checked) {
6fd5915Claude308 border-color: rgba(91,110,232,0.45);
309 background: rgba(91,110,232,0.05);
58307aeClaude310 }
311 .repo-settings-toggle-row input[type="checkbox"] {
312 margin-top: 2px;
313 flex-shrink: 0;
314 width: 16px; height: 16px;
315 accent-color: var(--accent);
316 cursor: pointer;
317 }
318 .repo-settings-toggle-text {
319 flex: 1;
320 font-size: 14px;
321 color: var(--text);
322 line-height: 1.45;
323 }
324 .repo-settings-toggle-text-title {
325 display: block;
326 font-weight: 600;
327 color: var(--text-strong);
328 margin-bottom: 2px;
329 }
330 .repo-settings-toggle-text-hint {
331 display: block;
332 margin-top: 3px;
333 font-size: 12.5px;
334 color: var(--text-muted);
335 }
336
337 /* ─── Primary CTA ─── */
338 .repo-settings-cta {
339 display: inline-flex;
340 align-items: center;
341 gap: 6px;
342 padding: 9px 16px;
343 font-size: 14px;
344 font-weight: 600;
345 color: white;
6fd5915Claude346 background: linear-gradient(135deg, #5b6ee8 0%, #5b7bff 100%);
58307aeClaude347 border: none;
348 border-radius: 8px;
349 cursor: pointer;
350 transition: transform 120ms ease, box-shadow 120ms ease, filter 120ms ease;
351 text-decoration: none;
352 font-family: inherit;
353 }
354 .repo-settings-cta:hover {
355 filter: brightness(1.08);
6fd5915Claude356 box-shadow: 0 6px 18px rgba(91,110,232,0.25);
58307aeClaude357 transform: translateY(-1px);
358 }
359 .repo-settings-cta .arrow { transition: transform 120ms ease; }
360 .repo-settings-cta:hover .arrow { transform: translateX(2px); }
361 .repo-settings-cta-secondary {
362 display: inline-flex;
363 align-items: center;
364 gap: 6px;
365 padding: 8px 14px;
366 font-size: 13.5px;
367 font-weight: 500;
368 color: var(--text);
369 background: var(--bg);
370 border: 1px solid var(--border-strong, var(--border));
371 border-radius: 8px;
372 cursor: pointer;
373 text-decoration: none;
374 transition: border-color 120ms ease, background 120ms ease;
375 font-family: inherit;
376 }
377 .repo-settings-cta-secondary:hover {
378 border-color: var(--accent);
6fd5915Claude379 background: rgba(91,110,232,0.06);
58307aeClaude380 }
381
382 /* ─── Inline transfer row ─── */
383 .repo-settings-inline-row {
384 display: flex;
385 gap: var(--space-2);
386 align-items: stretch;
387 flex-wrap: wrap;
388 }
389 .repo-settings-inline-row .repo-settings-input { flex: 1; min-width: 220px; }
390
391 /* ─── Status pill (template / archived) ─── */
392 .repo-settings-pill {
393 display: inline-flex;
394 align-items: center;
395 gap: 6px;
396 padding: 4px 10px;
397 border-radius: 9999px;
398 font-size: 11.5px;
399 font-weight: 600;
400 letter-spacing: 0.01em;
401 margin-bottom: var(--space-2);
402 }
403 .repo-settings-pill.is-on {
6fd5915Claude404 background: rgba(91,110,232,0.14);
e589f77ccantynz-alt405 color: var(--accent);
6fd5915Claude406 box-shadow: inset 0 0 0 1px rgba(91,110,232,0.35);
58307aeClaude407 }
408 .repo-settings-pill.is-off {
409 background: rgba(255,255,255,0.04);
410 color: var(--text-muted);
411 box-shadow: inset 0 0 0 1px var(--border);
412 }
413 .repo-settings-pill .dot {
414 width: 6px; height: 6px;
415 border-radius: 9999px;
416 background: currentColor;
417 box-shadow: 0 0 8px currentColor;
418 }
419 .repo-settings-pill.is-off .dot { box-shadow: none; opacity: 0.6; }
420
421 /* ─── Danger zone ─── */
422 .repo-settings-danger {
423 position: relative;
424 margin-top: var(--space-6);
425 padding: 0;
426 border: 1px solid rgba(248,113,113,0.30);
427 border-radius: 14px;
428 background:
429 linear-gradient(180deg, rgba(248,113,113,0.05) 0%, rgba(248,113,113,0.02) 100%),
430 var(--bg-elevated);
431 overflow: hidden;
432 }
433 .repo-settings-danger::before {
434 content: '';
435 position: absolute;
436 top: 0; left: 0; right: 0;
437 height: 2px;
438 background: linear-gradient(90deg, transparent 0%, #f87171 30%, #ffb45e 70%, transparent 100%);
439 opacity: 0.7;
440 pointer-events: none;
441 }
442 .repo-settings-danger-head {
443 padding: var(--space-4) var(--space-5) var(--space-3);
444 border-bottom: 1px solid rgba(248,113,113,0.15);
445 }
446 .repo-settings-danger-eyebrow {
447 font-size: 11px;
448 font-weight: 600;
449 letter-spacing: 0.08em;
450 text-transform: uppercase;
e589f77ccantynz-alt451 color: var(--red);
58307aeClaude452 margin-bottom: 6px;
453 }
454 .repo-settings-danger-title {
455 font-family: var(--font-display);
456 font-size: 18px;
457 font-weight: 700;
458 letter-spacing: -0.018em;
459 margin: 0 0 4px;
460 color: var(--text-strong);
461 }
462 .repo-settings-danger-desc {
463 font-size: 13.5px;
464 color: var(--text-muted);
465 margin: 0;
466 line-height: 1.5;
467 }
468 .repo-settings-danger-body { padding: var(--space-4) var(--space-5); }
469 .repo-settings-danger-body p { margin: 0 0 var(--space-3); font-size: 14px; line-height: 1.55; }
470 .repo-settings-danger-body p:last-child { margin-bottom: 0; }
471 .repo-settings-danger-body p.muted { color: var(--text-muted); font-size: 13px; }
472 .repo-settings-danger-btn {
473 display: inline-flex;
474 align-items: center;
475 gap: 6px;
476 padding: 9px 16px;
477 font-size: 14px;
478 font-weight: 600;
479 color: #fff;
480 background: linear-gradient(135deg, #f87171 0%, #ef4444 100%);
481 border: none;
482 border-radius: 8px;
483 cursor: pointer;
484 font-family: inherit;
485 transition: filter 120ms ease, box-shadow 120ms ease, transform 120ms ease;
486 }
487 .repo-settings-danger-btn:hover {
488 filter: brightness(1.08);
489 box-shadow: 0 6px 18px rgba(248,113,113,0.25);
490 transform: translateY(-1px);
491 }
492
493 /* ─── Responsive ─── */
494 @media (max-width: 720px) {
495 .repo-settings-hero { padding: var(--space-4) var(--space-4); }
496 .repo-settings-section-head,
497 .repo-settings-section-body,
498 .repo-settings-section-foot,
499 .repo-settings-danger-head,
500 .repo-settings-danger-body { padding-left: var(--space-4); padding-right: var(--space-4); }
501 }
2289dc6Claude502
503 /* ─── Settings sidebar layout ─── */
504 .rsettings-layout {
505 display: flex;
506 gap: var(--space-5);
507 align-items: flex-start;
508 }
509 .rsettings-sidebar {
510 width: 220px;
511 flex-shrink: 0;
512 position: sticky;
513 top: var(--space-4);
514 background: var(--bg-elevated);
515 border: 1px solid var(--border);
516 border-radius: 12px;
517 overflow: hidden;
518 }
519 .rsettings-sidebar-label {
520 padding: 10px 14px 8px;
521 font-size: 11px;
522 font-weight: 700;
523 letter-spacing: 0.07em;
524 text-transform: uppercase;
525 color: var(--text-subtle, var(--text-muted));
526 border-bottom: 1px solid var(--border);
527 }
528 .rsettings-nav {
529 display: flex;
530 flex-direction: column;
531 padding: 6px;
532 gap: 2px;
533 }
534 .rsettings-nav-item {
535 display: flex;
536 align-items: center;
537 gap: 8px;
538 padding: 7px 10px;
539 border-radius: 8px;
540 font-size: 13.5px;
541 font-weight: 500;
542 color: var(--text-muted);
543 text-decoration: none;
544 transition: background 100ms ease, color 100ms ease;
545 line-height: 1.3;
546 }
547 .rsettings-nav-item:hover {
548 background: var(--bg-surface, rgba(255,255,255,0.04));
549 color: var(--text);
550 }
551 .rsettings-nav-item.is-active {
552 background: var(--bg-surface, rgba(255,255,255,0.06));
553 color: var(--text);
554 font-weight: 600;
555 }
556 .rsettings-nav-divider {
557 height: 1px;
558 background: var(--border);
559 margin: 4px 6px;
560 }
561 .rsettings-content {
562 flex: 1;
563 min-width: 0;
564 }
565 @media (max-width: 860px) {
566 .rsettings-layout { flex-direction: column; }
567 .rsettings-sidebar {
568 width: 100%;
569 position: static;
570 }
571 .rsettings-nav { flex-direction: row; flex-wrap: wrap; }
572 }
58307aeClaude573`;
574
575/** Hero header for the repo settings page. */
576function RepoSettingsHero(props: { owner: string; repo: string }) {
577 const { owner, repo } = props;
578 return (
579 <div class="repo-settings-hero">
580 <div class="repo-settings-hero-bg" aria-hidden="true">
581 <div class="repo-settings-hero-orb" />
582 </div>
583 <div class="repo-settings-hero-inner">
584 <div class="repo-settings-hero-eyebrow">
585 Repository settings ·{" "}
586 <span class="repo-settings-hero-repo">
587 {owner}/{repo}
588 </span>
589 </div>
590 <h1 class="repo-settings-hero-title">
591 <span class="gradient-text">Configure</span>.
592 </h1>
593 <p class="repo-settings-hero-sub">
594 Description, visibility, branches, automation. Owners only.
595 </p>
596 <a
597 href={`/${owner}/${repo}/settings/collaborators`}
598 class="repo-settings-hero-link"
599 >
600 Manage collaborators <span class="arrow">→</span>
601 </a>
602 </div>
603 </div>
604 );
605}
606
607function Banner(props: { kind: "success" | "error"; text: string }) {
608 return (
609 <div
610 class={`repo-settings-banner repo-settings-banner-${props.kind}`}
611 role="status"
612 >
613 <span class="repo-settings-banner-icon" aria-hidden="true">
614 {props.kind === "success" ? "✓" : "!"}
615 </span>
616 <span>{props.text}</span>
617 </div>
618 );
619}
620
2289dc6Claude621/** Left-hand settings navigation sidebar. */
ae2a071ccanty labs622export function RepoSettingsSidebar(props: { owner: string; repo: string; currentPath: string }) {
2289dc6Claude623 const { owner, repo, currentPath } = props;
624 const base = `/${owner}/${repo}`;
625
626 type NavItem =
627 | { kind: "link"; label: string; href: string }
628 | { kind: "divider" };
629
630 const items: NavItem[] = [
631 { kind: "link", label: "General", href: `${base}/settings` },
632 { kind: "link", label: "Collaborators", href: `${base}/settings/collaborators` },
633 { kind: "link", label: "Automation", href: `${base}/settings/automation` },
634 { kind: "divider" },
635 { kind: "link", label: "Branch Protection", href: `${base}/gates` },
636 { kind: "link", label: "Protected Tags", href: `${base}/settings/protected-tags` },
637 { kind: "link", label: "Rulesets", href: `${base}/settings/rulesets` },
638 { kind: "divider" },
639 { kind: "link", label: "Webhooks", href: `${base}/settings/webhooks` },
640 { kind: "link", label: "Environments", href: `${base}/settings/environments` },
641 { kind: "link", label: "Pages", href: `${base}/settings/pages` },
642 { kind: "link", label: "Workflow Secrets", href: `${base}/settings/secrets` },
643 { kind: "divider" },
644 { kind: "link", label: "Mirrors", href: `${base}/settings/mirror` },
645 { kind: "link", label: "Dep Updater", href: `${base}/settings/dep-updater` },
646 { kind: "link", label: "Audit Log", href: `${base}/settings/audit` },
647 ];
648
649 return (
650 <nav class="rsettings-sidebar" aria-label="Settings navigation">
651 <div class="rsettings-sidebar-label">Settings</div>
652 <ul class="rsettings-nav" role="list" style="list-style:none;margin:0;padding:6px;">
653 {items.map((item) => {
654 if (item.kind === "divider") {
655 return <li class="rsettings-nav-divider" role="separator" />;
656 }
657 // Exact match for "General" (base settings URL), prefix match for sub-pages
658 const isActive =
659 item.href === `${base}/settings`
660 ? currentPath === `${base}/settings` || currentPath === `${base}/settings/`
661 : currentPath === item.href || currentPath.startsWith(item.href + "/");
662 return (
663 <li>
664 <a
665 href={item.href}
666 class={`rsettings-nav-item${isActive ? " is-active" : ""}`}
667 aria-current={isActive ? "page" : undefined}
668 >
669 {item.label}
670 </a>
671 </li>
672 );
673 })}
674 </ul>
675 </nav>
676 );
677}
678
79136bbClaude679// Settings page
febd4f0Claude680repoSettings.get("/:owner/:repo/settings", requireAuth, requireRepoAccess("admin"), async (c) => {
79136bbClaude681 const { owner: ownerName, repo: repoName } = c.req.param();
682 const user = c.get("user")!;
683 const success = c.req.query("success");
684 const error = c.req.query("error");
685
686 const [owner] = await db
687 .select()
688 .from(users)
689 .where(eq(users.username, ownerName))
690 .limit(1);
691
692 if (!owner || owner.id !== user.id) {
693 return c.html(
694 <Layout title="Unauthorized" user={user}>
bb0f894Claude695 <EmptyState title="Unauthorized">
79136bbClaude696 <p>Only the repository owner can access settings.</p>
bb0f894Claude697 </EmptyState>
79136bbClaude698 </Layout>,
699 403
700 );
701 }
702
703 const [repo] = await db
704 .select()
705 .from(repositories)
706 .where(
707 and(eq(repositories.ownerId, owner.id), eq(repositories.name, repoName))
708 )
709 .limit(1);
710
711 if (!repo) return c.notFound();
712
713 const branches = await listBranches(ownerName, repoName);
714
ec9e3e3Claude715 // Branch protection rules for the "Branch protection" settings section.
716 const existingBranchRules = await db
717 .select()
718 .from(branchProtection)
719 .where(eq(branchProtection.repositoryId, repo.id))
720 .catch(() => [] as typeof branchProtection.$inferSelect[]);
721
79136bbClaude722 return c.html(
723 <Layout title={`Settings — ${ownerName}/${repoName}`} user={user}>
724 <RepoHeader owner={ownerName} repo={repoName} />
ae2a071ccanty labs725 <RepoNav owner={ownerName} repo={repoName} active="settings" />
58307aeClaude726 <style dangerouslySetInnerHTML={{ __html: repoSettingsStyles }} />
727 <div class="repo-settings-container">
728 <RepoSettingsHero owner={ownerName} repo={repoName} />
2289dc6Claude729 <div class="rsettings-layout">
730 <RepoSettingsSidebar
731 owner={ownerName}
732 repo={repoName}
733 currentPath={`/${ownerName}/${repoName}/settings`}
734 />
735 <div class="rsettings-content">
58307aeClaude736
79136bbClaude737 {success && (
58307aeClaude738 <Banner kind="success" text={decodeURIComponent(success)} />
79136bbClaude739 )}
58307aeClaude740 {error && <Banner kind="error" text={decodeURIComponent(error)} />}
79136bbClaude741
58307aeClaude742 {/* ─── General: description + default branch + visibility ─── */}
743 <section class="repo-settings-section">
744 <div class="repo-settings-section-head">
745 <div class="repo-settings-section-eyebrow">General</div>
746 <h2 class="repo-settings-section-title">Repository basics</h2>
747 <p class="repo-settings-section-desc">
748 The headline details visitors see — description, the default
749 branch they land on, and who can browse the code.
750 </p>
751 </div>
752 <form
753 method="post"
754 action={`/${ownerName}/${repoName}/settings`}
755 >
756 <div class="repo-settings-section-body">
757 <div class="repo-settings-field">
758 <label class="repo-settings-field-label" for="description">
759 Description
760 </label>
79136bbClaude761 <input
58307aeClaude762 class="repo-settings-input"
763 name="description"
764 id="description"
765 value={repo.description || ""}
766 placeholder="A short description"
79136bbClaude767 />
58307aeClaude768 <div class="repo-settings-field-hint">
769 Shown on the repo home, search results, and explore pages.
770 </div>
771 </div>
772 <div class="repo-settings-field">
773 <label
774 class="repo-settings-field-label"
775 for="default_branch"
776 >
777 Default branch
778 </label>
779 <select
780 class="repo-settings-select"
781 name="default_branch"
782 id="default_branch"
783 >
784 {branches.length === 0 ? (
785 <option value={repo.defaultBranch}>
786 {repo.defaultBranch}
787 </option>
788 ) : (
789 branches.map((b) => (
790 <option value={b} selected={b === repo.defaultBranch}>
791 {b}
792 </option>
793 ))
794 )}
795 </select>
796 <div class="repo-settings-field-hint">
797 Where pulls land, where compare views start, and what
798 clones check out by default.
799 </div>
800 </div>
801 <div class="repo-settings-field">
802 <label class="repo-settings-field-label">Visibility</label>
803 <div class="repo-settings-visibility">
804 <label class="repo-settings-vis-card">
805 <input
806 type="radio"
807 name="visibility"
808 value="public"
809 checked={!repo.isPrivate}
810 class="repo-settings-vis-radio"
811 aria-label="Public"
812 />
813 <span class="repo-settings-vis-body">
814 <span class="repo-settings-vis-label">Public</span>
815 <span class="repo-settings-vis-desc">
816 Anyone can see this repository. You choose who can
817 commit.
818 </span>
819 </span>
820 </label>
821 <label class="repo-settings-vis-card">
822 <input
823 type="radio"
824 name="visibility"
825 value="private"
826 checked={repo.isPrivate}
827 class="repo-settings-vis-radio"
828 aria-label="Private"
829 />
830 <span class="repo-settings-vis-body">
831 <span class="repo-settings-vis-label">Private</span>
832 <span class="repo-settings-vis-desc">
833 Only you (and collaborators you invite) can see this
834 repository.
835 </span>
836 </span>
837 </label>
838 </div>
839 </div>
44f1a02Claude840 <div class="repo-settings-field">
841 <label class="repo-settings-field-label">Data region</label>
842 <div style="display:flex; align-items:center; gap:10px; flex-wrap:wrap;">
843 <span
844 style={`display:inline-flex; align-items:center; gap:6px; padding:4px 12px; border-radius:9999px; font-size:12px; font-weight:600; font-family:var(--font-mono); ${
845 repo.dataRegion === "eu"
6fd5915Claude846 ? "background:rgba(95,143,160,0.10); color:#67e8f9; border:1px solid rgba(95,143,160,0.28);"
847 : "background:rgba(91,110,232,0.10); color:#c4b5fd; border:1px solid rgba(91,110,232,0.28);"
44f1a02Claude848 }`}
849 >
850 {repo.dataRegion === "eu" ? "EU · Frankfurt" : "US · Default"}
851 </span>
852 <span class="repo-settings-field-hint" style="margin:0;">
853 Data region is set at creation and cannot be changed. EU data
854 residency requires a{" "}
855 <a href="/pricing" style="color:var(--accent);text-decoration:none;">Pro plan or higher</a>.
856 </span>
857 </div>
858 </div>
79136bbClaude859 </div>
58307aeClaude860 <div class="repo-settings-section-foot">
861 <button type="submit" class="repo-settings-cta">
862 Save changes <span class="arrow">→</span>
863 </button>
864 </div>
865 </form>
866 </section>
14c3cc8Claude867
58307aeClaude868 {/* ─── Spec to PR ─── */}
869 <section class="repo-settings-section">
870 <div class="repo-settings-section-head">
871 <div class="repo-settings-section-eyebrow">
872 Automation · experimental
873 </div>
874 <h2 class="repo-settings-section-title">Spec to PR</h2>
875 <p class="repo-settings-section-desc">
876 Paste a plain-English feature spec and let Claude draft a pull
877 request for you. PRs open as drafts — review every line before
878 merging.
879 </p>
880 </div>
881 <div class="repo-settings-section-body">
882 <a
883 href={`/${ownerName}/${repoName}/spec`}
884 class="repo-settings-cta"
885 >
886 Open Spec to PR <span class="arrow">→</span>
887 </a>
888 </div>
889 </section>
890
479dcd9Claude891 {/* ─── Automation hub ─── */}
892 <section class="repo-settings-section">
893 <div class="repo-settings-section-head">
894 <div class="repo-settings-section-eyebrow">Automation</div>
895 <h2 class="repo-settings-section-title">Automation settings</h2>
896 <p class="repo-settings-section-desc">
897 Every automation on this repository in one place — AI code
898 review, PR/issue triage, auto-merge, CI auto-fix, test
899 generation, and dependency updates. Flip each between off,
900 suggest (manual), and automatic.
901 </p>
902 </div>
903 <div class="repo-settings-section-body">
904 <a
905 href={`/${ownerName}/${repoName}/settings/automation`}
906 class="repo-settings-cta"
907 >
908 Open Automation settings <span class="arrow">→</span>
909 </a>
910 </div>
911 </section>
912
58307aeClaude913 {/* ─── Template repository ─── */}
914 <section class="repo-settings-section">
915 <div class="repo-settings-section-head">
916 <div class="repo-settings-section-eyebrow">Template</div>
917 <h2 class="repo-settings-section-title">Template repository</h2>
918 <p class="repo-settings-section-desc">
71cd5ecClaude919 {repo.isTemplate
58307aeClaude920 ? "This repository is a template. Users can click “Use this template” to create a new repository with the same files."
921 : "Mark this repository as a template so others can seed new repositories from its files."}
922 </p>
923 </div>
924 <div class="repo-settings-section-body">
925 <span
926 class={`repo-settings-pill ${repo.isTemplate ? "is-on" : "is-off"}`}
927 >
928 <span class="dot" aria-hidden="true" />
929 {repo.isTemplate ? "Template enabled" : "Not a template"}
930 </span>
931 <form
932 method="post"
933 action={`/${ownerName}/${repoName}/settings/template`}
934 style="margin-top: var(--space-3)"
935 >
936 <input
937 type="hidden"
938 name="template"
939 value={repo.isTemplate ? "0" : "1"}
940 />
941 <button type="submit" class="repo-settings-cta-secondary">
942 {repo.isTemplate ? "Unmark as template" : "Mark as template"}
943 </button>
944 </form>
945 </div>
946 </section>
71cd5ecClaude947
58307aeClaude948 {/* ─── Transfer ownership ─── */}
949 <section class="repo-settings-section">
950 <div class="repo-settings-section-head">
951 <div class="repo-settings-section-eyebrow">Ownership</div>
952 <h2 class="repo-settings-section-title">Transfer ownership</h2>
953 <p class="repo-settings-section-desc">
954 Hand this repository to another user. The new owner can accept
955 or decline the transfer by viewing it.
956 </p>
957 </div>
71cd5ecClaude958 <form
e7e240eClaude959 method="post"
71cd5ecClaude960 action={`/${ownerName}/${repoName}/settings/transfer`}
961 onsubmit="return confirm('Transfer this repository? The new owner will have full control.')"
962 >
58307aeClaude963 <div class="repo-settings-section-body">
964 <div class="repo-settings-field">
965 <label class="repo-settings-field-label" for="new_owner">
966 New owner username
967 </label>
968 <div class="repo-settings-inline-row">
969 <input
970 type="text"
971 name="new_owner"
972 id="new_owner"
973 class="repo-settings-input"
974 placeholder="new-owner-username"
975 required
976 aria-label="New owner username"
977 />
978 <button type="submit" class="repo-settings-cta-secondary">
979 Transfer
980 </button>
981 </div>
982 <div class="repo-settings-field-hint">
983 Transfers are immediate. The new owner can rename or delete
984 the repository at any time.
985 </div>
986 </div>
987 </div>
71cd5ecClaude988 </form>
58307aeClaude989 </section>
71cd5ecClaude990
58307aeClaude991 {/* ─── Archive ─── */}
992 <section class="repo-settings-section">
993 <div class="repo-settings-section-head">
994 <div class="repo-settings-section-eyebrow">Lifecycle</div>
995 <h2 class="repo-settings-section-title">
996 {repo.isArchived ? "Unarchive repository" : "Archive repository"}
997 </h2>
998 <p class="repo-settings-section-desc">
999 {repo.isArchived
1000 ? "This repository is archived and read-only. Unarchive to allow pushes and issue/PR activity again."
1001 : "Mark this repository as archived. It becomes read-only — no pushes, no new issues or PRs. You can unarchive at any time."}
1002 </p>
1003 </div>
1004 <div class="repo-settings-section-body">
1005 <span
1006 class={`repo-settings-pill ${repo.isArchived ? "is-on" : "is-off"}`}
534f04aClaude1007 >
58307aeClaude1008 <span class="dot" aria-hidden="true" />
1009 {repo.isArchived ? "Archived" : "Active"}
1010 </span>
1011 <form
1012 method="post"
1013 action={`/${ownerName}/${repoName}/settings/archive`}
1014 style="margin-top: var(--space-3)"
534f04aClaude1015 >
1016 <input
58307aeClaude1017 type="hidden"
1018 name="archive"
1019 value={repo.isArchived ? "0" : "1"}
534f04aClaude1020 />
58307aeClaude1021 <button type="submit" class="repo-settings-cta-secondary">
1022 {repo.isArchived ? "Unarchive" : "Archive"} this repository
1023 </button>
1024 </form>
1025 </div>
1026 </section>
534f04aClaude1027
1d4ff60Claude1028 {/* ─── AI test generator ─── */}
1029 <section class="repo-settings-section">
1030 <div class="repo-settings-section-head">
1031 <div class="repo-settings-section-eyebrow">AI tests</div>
1032 <h2 class="repo-settings-section-title">Auto-generate tests on PR open</h2>
1033 <p class="repo-settings-section-desc">
1034 When a pull request opens, Gluecron AI reads the diff and writes
1035 tests for the new code, matching whatever framework your repo
1036 already uses. Tests land on the same branch. Default off —
1037 opt in here.
1038 </p>
1039 </div>
1040 <form
1041 method="post"
1042 action={`/${ownerName}/${repoName}/settings/ai-tests`}
1043 >
1044 <div class="repo-settings-section-body">
1045 <label
1046 class="repo-settings-toggle-row"
1047 aria-label="Auto-generate tests when a PR opens"
1048 >
1049 <input
1050 type="checkbox"
1051 name="auto_generate_tests"
1052 value="1"
1053 checked={repo.autoGenerateTests}
1054 />
1055 <span class="repo-settings-toggle-text">
1056 <span class="repo-settings-toggle-text-title">
1057 Auto-generate tests on PR open
1058 </span>
1059 <span class="repo-settings-toggle-text-hint">
1060 Requires <code>ANTHROPIC_API_KEY</code>. Skips PRs without
1061 source-file changes and PRs already opened by AI.
1062 </span>
1063 </span>
1064 </label>
1065 </div>
1066 <div class="repo-settings-section-foot">
1067 <button type="submit" class="repo-settings-cta">
1068 Save AI test settings <span class="arrow">→</span>
1069 </button>
1070 </div>
1071 </form>
1072 </section>
1073
f5ad215Claude1074 {/* ─── Dependency auto-updater ─── */}
1075 <section
1076 id="dep-updater"
1077 class="repo-settings-section"
1078 >
1079 <div class="repo-settings-section-head">
1080 <div class="repo-settings-section-eyebrow">AI dependency updates</div>
1081 <h2 class="repo-settings-section-title">Automatic dependency updates</h2>
1082 <p class="repo-settings-section-desc">
1083 Once per day, Gluecron reads your <code>package.json</code>,
1084 checks npm for patch and minor updates, and applies them
1085 automatically. If the gate check passes, the PR is auto-merged.
1086 If it fails, Gluecron opens a PR with an AI-written guide
1087 explaining what broke and how to fix it. Major updates always
1088 require human review and are handled separately by the migration
1089 watcher. Default off.
1090 </p>
1091 </div>
1092 <form
1093 method="post"
1094 action={`/${ownerName}/${repoName}/settings/dep-updater`}
1095 >
1096 <div class="repo-settings-section-body">
1097 <label
1098 class="repo-settings-toggle-row"
1099 aria-label="Enable automatic dependency updates for this repo"
1100 >
1101 <input
1102 type="checkbox"
1103 name="dep_updater_enabled"
1104 value="1"
1105 checked={
1106 (repo as { depUpdaterEnabled?: boolean }).depUpdaterEnabled ??
1107 false
1108 }
1109 />
1110 <span class="repo-settings-toggle-text">
1111 <span class="repo-settings-toggle-text-title">
1112 Enable automatic dependency updates
1113 </span>
1114 <span class="repo-settings-toggle-text-hint">
1115 Patch and minor updates only. Runs once per day; max 2
1116 packages per sweep. Requires <code>DEP_UPDATER_ENABLED=1</code>{" "}
1117 on the server. Auto-merges when gate passes; opens a PR
1118 with an AI migration guide when it fails.
1119 </span>
1120 </span>
1121 </label>
1122 </div>
1123 <div class="repo-settings-section-foot">
1124 <button type="submit" class="repo-settings-cta">
1125 Save dependency update settings <span class="arrow">→</span>
1126 </button>
1127 </div>
1128 </form>
1129 </section>
1130
9b3a183Claude1131 {/* ─── Cloud dev environments ─── */}
1132 <section
1133 id="dev-envs"
1134 class="repo-settings-section"
1135 >
1136 <div class="repo-settings-section-head">
1137 <div class="repo-settings-section-eyebrow">Dev environments</div>
1138 <h2 class="repo-settings-section-title">Enable cloud dev environments</h2>
1139 <p class="repo-settings-section-desc">
1140 When enabled, anyone with read access can hit{" "}
1141 <code>/{ownerName}/{repoName}/dev</code> to spin up a
1142 hosted VS Code IDE in the browser, backed by a cold-start
1143 container. We read <code>.gluecron/dev.yml</code> for the
1144 image + install commands; idle envs stop themselves after
1145 30 minutes. Default off — each env burns a container.
1146 </p>
1147 </div>
1148 <form
1149 method="post"
1150 action={`/${ownerName}/${repoName}/settings/dev-envs`}
1151 >
1152 <div class="repo-settings-section-body">
1153 <label
1154 class="repo-settings-toggle-row"
1155 aria-label="Enable cloud dev environments for this repo"
1156 >
1157 <input
1158 type="checkbox"
1159 name="dev_envs_enabled"
1160 value="1"
1161 checked={
1162 (repo as { devEnvsEnabled?: boolean }).devEnvsEnabled ??
1163 false
1164 }
1165 />
1166 <span class="repo-settings-toggle-text">
1167 <span class="repo-settings-toggle-text-title">
1168 Enable dev environments
1169 </span>
1170 <span class="repo-settings-toggle-text-hint">
1171 Surfaces the <code>/{ownerName}/{repoName}/dev</code>{" "}
1172 route. Commit <code>.gluecron/dev.yml</code> to your
1173 repo to customise the image, ports, and extensions.
1174 </span>
1175 </span>
1176 </label>
1177 </div>
1178 <div class="repo-settings-section-foot">
1179 <button type="submit" class="repo-settings-cta">
1180 Save dev env settings <span class="arrow">→</span>
1181 </button>
1182 </div>
1183 </form>
1184 </section>
1185
58307aeClaude1186 {/* ─── Stale activity ─── */}
1187 <section class="repo-settings-section">
1188 <div class="repo-settings-section-head">
1189 <div class="repo-settings-section-eyebrow">Stale sweep</div>
1190 <h2 class="repo-settings-section-title">Stale activity</h2>
1191 <p class="repo-settings-section-desc">
1192 Autopilot pokes PRs and issues that have gone quiet, then offers
1193 a one-click close path. Each toggle controls the final close
1194 step — pokes always happen, but they&apos;re harmless reminders.
1195 </p>
1196 </div>
79136bbClaude1197 <form
e7e240eClaude1198 method="post"
58307aeClaude1199 action={`/${ownerName}/${repoName}/settings/stale`}
79136bbClaude1200 >
58307aeClaude1201 <div class="repo-settings-section-body">
1202 <label
1203 class="repo-settings-toggle-row"
1204 aria-label="Auto-close stale PRs after 14 days of no activity post-poke"
1205 >
1206 <input
1207 type="checkbox"
1208 name="auto_close_stale_prs"
1209 value="1"
1210 checked={repo.autoCloseStalePrs}
1211 />
1212 <span class="repo-settings-toggle-text">
1213 <span class="repo-settings-toggle-text-title">
1214 Auto-close stale PRs
1215 </span>
1216 <span class="repo-settings-toggle-text-hint">
1217 Close PRs that go quiet for 14 days after autopilot pokes
1218 them.
1219 </span>
1220 </span>
1221 </label>
1222 <label
1223 class="repo-settings-toggle-row"
1224 aria-label="Auto-close stale issues after 60 days of no activity post-poke"
1225 >
1226 <input
1227 type="checkbox"
1228 name="auto_close_stale_issues"
1229 value="1"
1230 checked={repo.autoCloseStaleIssues}
1231 />
1232 <span class="repo-settings-toggle-text">
1233 <span class="repo-settings-toggle-text-title">
1234 Auto-close stale issues
1235 </span>
1236 <span class="repo-settings-toggle-text-hint">
1237 Close issues that go quiet for 60 days after autopilot
1238 pokes them.
1239 </span>
1240 </span>
1241 </label>
1242 </div>
1243 <div class="repo-settings-section-foot">
1244 <button type="submit" class="repo-settings-cta">
1245 Save stale settings <span class="arrow">→</span>
1246 </button>
1247 </div>
79136bbClaude1248 </form>
58307aeClaude1249 </section>
1250
1df50d5Claude1251 {/* ─── PR preview builds (migration 0077) ─── */}
1252 <section class="repo-settings-section">
1253 <div class="repo-settings-section-head">
1254 <div class="repo-settings-section-eyebrow">Preview builds</div>
1255 <h2 class="repo-settings-section-title">PR preview environments</h2>
1256 <p class="repo-settings-section-desc">
1257 When a build command is set, every PR push automatically clones
1258 the branch, runs the command, and serves the built output from a
1259 unique preview URL — like Vercel, but native to Gluecron. Leave
1260 blank to use URL-only previews (no build runs).
1261 </p>
1262 </div>
1263 <form
1264 method="post"
1265 action={`/${ownerName}/${repoName}/settings/previews`}
1266 >
1267 <div class="repo-settings-section-body">
1268 <div class="repo-settings-field">
1269 <label class="repo-settings-field-label" for="preview_build_command">
1270 Build command
1271 </label>
1272 <input
1273 id="preview_build_command"
1274 class="repo-settings-input"
1275 type="text"
1276 name="preview_build_command"
1277 placeholder="npm run build"
1278 value={
1279 (repo as { previewBuildCommand?: string | null }).previewBuildCommand ?? ""
1280 }
1281 />
1282 <p class="repo-settings-field-hint">
1283 e.g. <code>npm run build</code>, <code>bun run build</code>, or{" "}
1284 <code>hugo</code>. Runs inside the cloned branch directory.
1285 </p>
1286 </div>
1287 <div class="repo-settings-field">
1288 <label class="repo-settings-field-label" for="preview_output_dir">
1289 Output directory
1290 </label>
1291 <input
1292 id="preview_output_dir"
1293 class="repo-settings-input"
1294 type="text"
1295 name="preview_output_dir"
1296 placeholder="dist"
1297 value={
1298 (repo as { previewOutputDir?: string | null }).previewOutputDir ?? "dist"
1299 }
1300 />
1301 <p class="repo-settings-field-hint">
1302 The directory your build writes to. Common values:{" "}
1303 <code>dist</code>, <code>public</code>, <code>out</code>,{" "}
1304 <code>build</code>. Served from{" "}
1305 <code>/previews/{ownerName}/{repoName}/{"<branch>"}/</code>
1306 </p>
1307 </div>
1308 </div>
1309 <div class="repo-settings-section-foot">
1310 <button type="submit" class="repo-settings-cta">
1311 Save preview settings <span class="arrow">→</span>
1312 </button>
1313 </div>
1314 </form>
1315 </section>
1316
ec9e3e3Claude1317 {/* ─── Branch protection ─── */}
1318 <section class="repo-settings-section" id="branch-protection">
1319 <div class="repo-settings-section-head">
1320 <div class="repo-settings-section-eyebrow">Branch protection</div>
1321 <h2 class="repo-settings-section-title">Required reviews before merge</h2>
1322 <p class="repo-settings-section-desc">
1323 Protect branches by requiring a minimum number of human approvals before
1324 a pull request can be merged. Patterns support wildcards (e.g.{" "}
1325 <code>release/*</code>). CODEOWNERS auto-assignment applies independently.
1326 </p>
1327 </div>
1328
1329 {/* Existing rules list */}
1330 {existingBranchRules.length > 0 && (
1331 <div style="padding: var(--space-3) var(--space-5) 0">
1332 {existingBranchRules.map((rule) => (
1333 <div style="display:flex;align-items:center;gap:12px;padding:10px 0;border-bottom:1px solid var(--border)">
1334 <span style="flex:1;font-family:var(--font-mono);font-size:13px;color:var(--text-strong)">
1335 {rule.pattern}
1336 </span>
1337 <span style="font-size:12.5px;color:var(--text-muted)">
1338 {rule.requiredApprovals} required approval{rule.requiredApprovals !== 1 ? "s" : ""}
1339 </span>
1340 {rule.requireHumanReview && (
e589f77ccantynz-alt1341 <span style="font-size:11px;font-weight:600;padding:2px 8px;border-radius:9999px;background:rgba(91,110,232,0.12);color:var(--accent)">
ec9e3e3Claude1342 codeowner review required
1343 </span>
1344 )}
1345 {rule.dismissStaleReviews && (
1346 <span style="font-size:11px;color:var(--text-muted)">
1347 dismiss stale
1348 </span>
1349 )}
1350 <form method="post" action={`/${ownerName}/${repoName}/settings/branch-protection/${rule.id}/delete`}>
1351 <button type="submit"
1352 class="repo-settings-cta-secondary"
1353 style="padding:4px 10px;font-size:12px;color:var(--red,#f87171)"
1354 onclick="return confirm('Delete this branch protection rule?')"
1355 >
1356 Delete
1357 </button>
1358 </form>
1359 </div>
1360 ))}
1361 </div>
1362 )}
1363
1364 {/* Add new rule form */}
1365 <form
1366 method="post"
1367 action={`/${ownerName}/${repoName}/settings/branch-protection`}
1368 >
1369 <div class="repo-settings-section-body">
1370 <div style="display:grid;grid-template-columns:1fr 120px;gap:var(--space-3);align-items:end">
1371 <div class="repo-settings-field" style="margin-bottom:0">
1372 <label class="repo-settings-field-label" for="bp_pattern">
1373 Branch name pattern
1374 </label>
1375 <input
1376 class="repo-settings-input"
1377 name="pattern"
1378 id="bp_pattern"
1379 placeholder="main"
1380 required
1381 aria-label="Branch name pattern"
1382 />
1383 <div class="repo-settings-field-hint">
1384 Exact name or glob like <code>release/*</code>
1385 </div>
1386 </div>
1387 <div class="repo-settings-field" style="margin-bottom:0">
1388 <label class="repo-settings-field-label" for="bp_required">
1389 Required approvals
1390 </label>
1391 <input
1392 class="repo-settings-input"
1393 name="required_approvals"
1394 id="bp_required"
1395 type="number"
1396 min="0"
1397 max="10"
1398 value="1"
1399 aria-label="Number of required approvals"
1400 />
1401 </div>
1402 </div>
1403 <div style="margin-top:var(--space-3);display:flex;flex-direction:column;gap:8px">
1404 <label class="repo-settings-toggle-row" aria-label="Require codeowner review">
1405 <input
1406 type="checkbox"
1407 name="require_codeowner_review"
1408 value="1"
1409 />
1410 <span class="repo-settings-toggle-text">
1411 <span class="repo-settings-toggle-text-title">
1412 Require codeowner review
1413 </span>
1414 <span class="repo-settings-toggle-text-hint">
1415 At least one CODEOWNERS-matched reviewer must approve before merging.
1416 </span>
1417 </span>
1418 </label>
1419 <label class="repo-settings-toggle-row" aria-label="Dismiss stale reviews">
1420 <input
1421 type="checkbox"
1422 name="dismiss_stale_reviews"
1423 value="1"
1424 />
1425 <span class="repo-settings-toggle-text">
1426 <span class="repo-settings-toggle-text-title">
1427 Dismiss stale reviews on new push
1428 </span>
1429 <span class="repo-settings-toggle-text-hint">
1430 Prior approvals are dismissed when the head branch receives a new commit.
1431 </span>
1432 </span>
1433 </label>
1434 </div>
1435 </div>
1436 <div class="repo-settings-section-foot">
1437 <button type="submit" class="repo-settings-cta">
1438 Add rule <span class="arrow">→</span>
1439 </button>
1440 </div>
1441 </form>
1442 </section>
1443
58307aeClaude1444 {/* ─── Danger zone ─── */}
1445 <section class="repo-settings-danger">
1446 <div class="repo-settings-danger-head">
1447 <div class="repo-settings-danger-eyebrow">Danger zone</div>
1448 <h2 class="repo-settings-danger-title">Delete this repository</h2>
1449 <p class="repo-settings-danger-desc">
1450 Permanently remove this repository and every byte of its history,
1451 issues, PRs, stars, and webhooks. There is no undo.
1452 </p>
1453 </div>
1454 <div class="repo-settings-danger-body">
1455 <p class="muted">
1456 Once deleted, the URL <code>{ownerName}/{repoName}</code> frees
1457 up immediately. Open clones will fail to fetch or push.
1458 </p>
1459 <form
1460 method="post"
1461 action={`/${ownerName}/${repoName}/settings/delete`}
1462 onsubmit="return confirm('Are you sure? This cannot be undone.')"
1463 >
1464 <button type="submit" class="repo-settings-danger-btn">
1465 Delete this repository
1466 </button>
1467 </form>
1468 </div>
1469 </section>
2289dc6Claude1470 </div>{/* .rsettings-content */}
1471 </div>{/* .rsettings-layout */}
58307aeClaude1472 </div>
79136bbClaude1473 </Layout>
1474 );
1475});
1476
1477// Save settings
febd4f0Claude1478repoSettings.post("/:owner/:repo/settings", requireAuth, requireRepoAccess("admin"), async (c) => {
79136bbClaude1479 const { owner: ownerName, repo: repoName } = c.req.param();
1480 const user = c.get("user")!;
1481 const body = await c.req.parseBody();
1482
1483 const [owner] = await db
1484 .select()
1485 .from(users)
1486 .where(eq(users.username, ownerName))
1487 .limit(1);
1488
1489 if (!owner || owner.id !== user.id) {
1490 return c.redirect(`/${ownerName}/${repoName}`);
1491 }
1492
1493 await db
1494 .update(repositories)
1495 .set({
1496 description: String(body.description || "").trim() || null,
1497 defaultBranch: String(body.default_branch || "main"),
1498 isPrivate: body.visibility === "private",
1499 updatedAt: new Date(),
1500 })
1501 .where(
1502 and(eq(repositories.ownerId, owner.id), eq(repositories.name, repoName))
1503 );
1504
1505 return c.redirect(
1506 `/${ownerName}/${repoName}/settings?success=Settings+saved`
1507 );
1508});
1509
71cd5ecClaude1510// Toggle template flag
1511repoSettings.post(
1512 "/:owner/:repo/settings/template",
1513 requireAuth,
febd4f0Claude1514 requireRepoAccess("admin"),
71cd5ecClaude1515 async (c) => {
1516 const { owner: ownerName, repo: repoName } = c.req.param();
1517 const user = c.get("user")!;
1518 const body = await c.req.parseBody();
1519 const [owner] = await db
1520 .select()
1521 .from(users)
1522 .where(eq(users.username, ownerName))
1523 .limit(1);
1524 if (!owner || owner.id !== user.id) {
1525 return c.redirect(`/${ownerName}/${repoName}`);
1526 }
1527 const target = String(body.template || "1") === "1";
1528 await db
1529 .update(repositories)
1530 .set({ isTemplate: target, updatedAt: new Date() })
1531 .where(
1532 and(
1533 eq(repositories.ownerId, owner.id),
1534 eq(repositories.name, repoName)
1535 )
1536 );
1537 return c.redirect(
1538 `/${ownerName}/${repoName}/settings?success=${
1539 target ? "Marked+as+template" : "Unmarked+as+template"
1540 }`
1541 );
1542 }
1543);
1544
1545// Transfer repository to a new owner (by username)
1546repoSettings.post(
1547 "/:owner/:repo/settings/transfer",
1548 requireAuth,
febd4f0Claude1549 requireRepoAccess("admin"),
71cd5ecClaude1550 async (c) => {
1551 const { owner: ownerName, repo: repoName } = c.req.param();
1552 const user = c.get("user")!;
1553 const body = await c.req.parseBody();
1554 const newOwnerName = String(body.new_owner || "").trim();
1555 if (!newOwnerName) {
1556 return c.redirect(
1557 `/${ownerName}/${repoName}/settings?error=New+owner+required`
1558 );
1559 }
1560 const [owner] = await db
1561 .select()
1562 .from(users)
1563 .where(eq(users.username, ownerName))
1564 .limit(1);
1565 if (!owner || owner.id !== user.id) {
1566 return c.redirect(`/${ownerName}/${repoName}`);
1567 }
1568 const [newOwner] = await db
1569 .select()
1570 .from(users)
1571 .where(eq(users.username, newOwnerName))
1572 .limit(1);
1573 if (!newOwner) {
1574 return c.redirect(
1575 `/${ownerName}/${repoName}/settings?error=User+not+found`
1576 );
1577 }
1578 if (newOwner.id === owner.id) {
1579 return c.redirect(
1580 `/${ownerName}/${repoName}/settings?error=Same+owner`
1581 );
1582 }
1583 // Reject if new owner already has a repo by this name
1584 const [conflict] = await db
1585 .select()
1586 .from(repositories)
1587 .where(
1588 and(
1589 eq(repositories.ownerId, newOwner.id),
1590 eq(repositories.name, repoName)
1591 )
1592 )
1593 .limit(1);
1594 if (conflict) {
1595 return c.redirect(
1596 `/${ownerName}/${repoName}/settings?error=Target+owner+already+has+a+repo+by+that+name`
1597 );
1598 }
1599 const [repo] = await db
1600 .select()
1601 .from(repositories)
1602 .where(
1603 and(
1604 eq(repositories.ownerId, owner.id),
1605 eq(repositories.name, repoName)
1606 )
1607 )
1608 .limit(1);
1609 if (!repo) return c.notFound();
1610 await db
1611 .update(repositories)
1612 .set({ ownerId: newOwner.id, orgId: null, updatedAt: new Date() })
1613 .where(eq(repositories.id, repo.id));
1614 await db.insert(repoTransfers).values({
1615 repositoryId: repo.id,
1616 fromOwnerId: owner.id,
1617 fromOrgId: repo.orgId,
1618 toOwnerId: newOwner.id,
1619 toOrgId: null,
1620 initiatedBy: user.id,
1621 });
1622 return c.redirect(`/${newOwnerName}/${repoName}`);
1623 }
1624);
1625
1626// Archive / unarchive repository
1627repoSettings.post(
1628 "/:owner/:repo/settings/archive",
1629 requireAuth,
febd4f0Claude1630 requireRepoAccess("admin"),
71cd5ecClaude1631 async (c) => {
1632 const { owner: ownerName, repo: repoName } = c.req.param();
1633 const user = c.get("user")!;
1634 const body = await c.req.parseBody();
1635 const [owner] = await db
1636 .select()
1637 .from(users)
1638 .where(eq(users.username, ownerName))
1639 .limit(1);
1640 if (!owner || owner.id !== user.id) {
1641 return c.redirect(`/${ownerName}/${repoName}`);
1642 }
1643 const target = String(body.archive || "1") === "1";
1644 await db
1645 .update(repositories)
1646 .set({ isArchived: target, updatedAt: new Date() })
1647 .where(
1648 and(
1649 eq(repositories.ownerId, owner.id),
1650 eq(repositories.name, repoName)
1651 )
1652 );
1653 return c.redirect(
1654 `/${ownerName}/${repoName}/settings?success=${
1655 target ? "Repository+archived" : "Repository+unarchived"
1656 }`
1657 );
1658 }
1659);
1660
534f04aClaude1661// Block M5: stale activity opt-out flags. Owner-only; audits each toggle.
1662repoSettings.post(
1663 "/:owner/:repo/settings/stale",
1664 requireAuth,
1665 requireRepoAccess("admin"),
1666 async (c) => {
1667 const { owner: ownerName, repo: repoName } = c.req.param();
1668 const user = c.get("user")!;
1669 const body = await c.req.parseBody();
1670 const [owner] = await db
1671 .select()
1672 .from(users)
1673 .where(eq(users.username, ownerName))
1674 .limit(1);
1675 if (!owner || owner.id !== user.id) {
1676 return c.redirect(`/${ownerName}/${repoName}`);
1677 }
1678 const [repo] = await db
1679 .select()
1680 .from(repositories)
1681 .where(
1682 and(
1683 eq(repositories.ownerId, owner.id),
1684 eq(repositories.name, repoName)
1685 )
1686 )
1687 .limit(1);
1688 if (!repo) return c.notFound();
1689
1690 // Unchecked checkboxes are absent from the form payload, so coerce to bool.
1691 const newPrs = body.auto_close_stale_prs === "1";
1692 const newIssues = body.auto_close_stale_issues === "1";
1693
1694 await db
1695 .update(repositories)
1696 .set({
1697 autoCloseStalePrs: newPrs,
1698 autoCloseStaleIssues: newIssues,
1699 updatedAt: new Date(),
1700 })
1701 .where(eq(repositories.id, repo.id));
1702
1703 // Audit toggle deltas so the repo's audit log shows the change. Two
1704 // separate rows (one per flag) so the action names stay stable + grep-able.
1705 if (newPrs !== repo.autoCloseStalePrs) {
1706 await audit({
1707 userId: user.id,
1708 repositoryId: repo.id,
1709 action: "repo.auto_close_stale_prs.toggled",
1710 targetType: "repository",
1711 targetId: repo.id,
1712 metadata: { from: repo.autoCloseStalePrs, to: newPrs },
1713 });
1714 }
1715 if (newIssues !== repo.autoCloseStaleIssues) {
1716 await audit({
1717 userId: user.id,
1718 repositoryId: repo.id,
1719 action: "repo.auto_close_stale_issues.toggled",
1720 targetType: "repository",
1721 targetId: repo.id,
1722 metadata: { from: repo.autoCloseStaleIssues, to: newIssues },
1723 });
1724 }
1725
1726 return c.redirect(
1727 `/${ownerName}/${repoName}/settings?success=Stale+settings+saved`
1728 );
1729 }
1730);
1731
1d4ff60Claude1732// AI test generator opt-in. Owner-only; audits the toggle delta so the
1733// repo's audit log shows the change.
1734repoSettings.post(
1735 "/:owner/:repo/settings/ai-tests",
1736 requireAuth,
1737 requireRepoAccess("admin"),
1738 async (c) => {
1739 const { owner: ownerName, repo: repoName } = c.req.param();
1740 const user = c.get("user")!;
1741 const body = await c.req.parseBody();
1742 const [owner] = await db
1743 .select()
1744 .from(users)
1745 .where(eq(users.username, ownerName))
1746 .limit(1);
1747 if (!owner || owner.id !== user.id) {
1748 return c.redirect(`/${ownerName}/${repoName}`);
1749 }
1750 const [repo] = await db
1751 .select()
1752 .from(repositories)
1753 .where(
1754 and(
1755 eq(repositories.ownerId, owner.id),
1756 eq(repositories.name, repoName)
1757 )
1758 )
1759 .limit(1);
1760 if (!repo) return c.notFound();
1761
1762 // Unchecked checkboxes are absent from the form payload, so coerce.
1763 const next = body.auto_generate_tests === "1";
1764
1765 await db
1766 .update(repositories)
1767 .set({
1768 autoGenerateTests: next,
1769 updatedAt: new Date(),
1770 })
1771 .where(eq(repositories.id, repo.id));
1772
1773 if (next !== repo.autoGenerateTests) {
1774 await audit({
1775 userId: user.id,
1776 repositoryId: repo.id,
1777 action: "repo.auto_generate_tests.toggled",
1778 targetType: "repository",
1779 targetId: repo.id,
1780 metadata: { from: repo.autoGenerateTests, to: next },
1781 });
1782 }
1783
1784 return c.redirect(
1785 `/${ownerName}/${repoName}/settings?success=AI+test+settings+saved`
1786 );
1787 }
1788);
1789
9b3a183Claude1790// Migration 0072 — toggle cloud dev environments. Owner-only; audits
1791// the toggle delta so the repo's audit log shows the change.
1792repoSettings.post(
1793 "/:owner/:repo/settings/dev-envs",
1794 requireAuth,
1795 requireRepoAccess("admin"),
1796 async (c) => {
1797 const { owner: ownerName, repo: repoName } = c.req.param();
1798 const user = c.get("user")!;
1799 const body = await c.req.parseBody();
1800 const [owner] = await db
1801 .select()
1802 .from(users)
1803 .where(eq(users.username, ownerName))
1804 .limit(1);
1805 if (!owner || owner.id !== user.id) {
1806 return c.redirect(`/${ownerName}/${repoName}`);
1807 }
1808 const [repo] = await db
1809 .select()
1810 .from(repositories)
1811 .where(
1812 and(
1813 eq(repositories.ownerId, owner.id),
1814 eq(repositories.name, repoName)
1815 )
1816 )
1817 .limit(1);
1818 if (!repo) return c.notFound();
1819
1820 const next = body.dev_envs_enabled === "1";
1821 const prev = (repo as { devEnvsEnabled?: boolean }).devEnvsEnabled ?? false;
1822
1823 await db
1824 .update(repositories)
1825 .set({
1826 devEnvsEnabled: next,
1827 updatedAt: new Date(),
1828 })
1829 .where(eq(repositories.id, repo.id));
1830
1831 if (next !== prev) {
1832 await audit({
1833 userId: user.id,
1834 repositoryId: repo.id,
1835 action: "repo.dev_envs_enabled.toggled",
1836 targetType: "repository",
1837 targetId: repo.id,
1838 metadata: { from: prev, to: next },
1839 });
1840 }
1841
1842 return c.redirect(
1843 `/${ownerName}/${repoName}/settings?success=Dev+env+settings+saved#dev-envs`
1844 );
1845 }
1846);
1847
f5ad215Claude1848// Migration 0077 — toggle AI dependency auto-updater. Owner-only; audits
1849// the toggle delta so the repo's audit log shows the change.
1850repoSettings.post(
1851 "/:owner/:repo/settings/dep-updater",
1852 requireAuth,
1853 requireRepoAccess("admin"),
1854 async (c) => {
1855 const { owner: ownerName, repo: repoName } = c.req.param();
1856 const user = c.get("user")!;
1857 const body = await c.req.parseBody();
1858 const [owner] = await db
1859 .select()
1860 .from(users)
1861 .where(eq(users.username, ownerName))
1862 .limit(1);
1863 if (!owner || owner.id !== user.id) {
1864 return c.redirect(`/${ownerName}/${repoName}`);
1865 }
1866 const [repo] = await db
1867 .select()
1868 .from(repositories)
1869 .where(
1870 and(eq(repositories.ownerId, owner.id), eq(repositories.name, repoName))
1871 )
1872 .limit(1);
1873 if (!repo) return c.notFound();
1874
1875 const next = body.dep_updater_enabled === "1";
1876 const prev =
1877 (repo as { depUpdaterEnabled?: boolean }).depUpdaterEnabled ?? false;
1878
1879 await db
1880 .update(repositories)
1881 .set({
1882 depUpdaterEnabled: next,
1883 updatedAt: new Date(),
1884 })
1885 .where(eq(repositories.id, repo.id));
1886
1887 if (next !== prev) {
1888 await audit({
1889 userId: user.id,
1890 repositoryId: repo.id,
1891 action: "repo.dep_updater_enabled.toggled",
1892 targetType: "repository",
1893 targetId: repo.id,
1894 metadata: { from: prev, to: next },
1895 });
1896 }
1897
1898 return c.redirect(
1899 `/${ownerName}/${repoName}/settings?success=Dependency+update+settings+saved#dep-updater`
1900 );
1901 }
1902);
1903
79136bbClaude1904// Delete repository
1905repoSettings.post(
1906 "/:owner/:repo/settings/delete",
1907 requireAuth,
febd4f0Claude1908 requireRepoAccess("admin"),
79136bbClaude1909 async (c) => {
1910 const { owner: ownerName, repo: repoName } = c.req.param();
1911 const user = c.get("user")!;
1912
1913 const [owner] = await db
1914 .select()
1915 .from(users)
1916 .where(eq(users.username, ownerName))
1917 .limit(1);
1918
1919 if (!owner || owner.id !== user.id) {
1920 return c.redirect(`/${ownerName}/${repoName}`);
1921 }
1922
1923 const [repo] = await db
1924 .select()
1925 .from(repositories)
1926 .where(
1927 and(
1928 eq(repositories.ownerId, owner.id),
1929 eq(repositories.name, repoName)
1930 )
1931 )
1932 .limit(1);
1933
1934 if (!repo) return c.redirect(`/${ownerName}`);
1935
1936 // Delete from disk
1937 try {
1938 await rm(repo.diskPath, { recursive: true, force: true });
1939 } catch {
1940 // Disk cleanup best-effort
1941 }
1942
1943 // Delete from DB (cascades to stars, issues, etc.)
1944 await db.delete(repositories).where(eq(repositories.id, repo.id));
1945
1946 return c.redirect(`/${ownerName}`);
1947 }
1948);
1949
1df50d5Claude1950// Migration 0077 — PR preview build configuration. Owner-only.
1951repoSettings.post(
1952 "/:owner/:repo/settings/previews",
1953 requireAuth,
1954 requireRepoAccess("admin"),
1955 async (c) => {
1956 const { owner: ownerName, repo: repoName } = c.req.param();
1957 const user = c.get("user")!;
1958 const body = await c.req.parseBody();
1959
1960 const [owner] = await db
1961 .select()
1962 .from(users)
1963 .where(eq(users.username, ownerName))
1964 .limit(1);
1965 if (!owner || owner.id !== user.id) {
1966 return c.redirect(`/${ownerName}/${repoName}`);
1967 }
1968
1969 const [repo] = await db
1970 .select()
1971 .from(repositories)
1972 .where(
1973 and(eq(repositories.ownerId, owner.id), eq(repositories.name, repoName))
1974 )
1975 .limit(1);
1976 if (!repo) return c.notFound();
1977
1978 const buildCommand = String(body.preview_build_command || "").trim() || null;
1979 const outputDir = String(body.preview_output_dir || "").trim() || "dist";
1980
1981 await db
1982 .update(repositories)
1983 .set({
1984 previewBuildCommand: buildCommand,
1985 previewOutputDir: outputDir,
1986 updatedAt: new Date(),
1987 })
1988 .where(eq(repositories.id, repo.id));
1989
1990 return c.redirect(
1991 `/${ownerName}/${repoName}/settings?success=Preview+build+settings+saved`
1992 );
1993 }
1994);
1995
ec9e3e3Claude1996// ─── Branch protection: add rule ───────────────────────────────────────────
1997repoSettings.post(
1998 "/:owner/:repo/settings/branch-protection",
1999 requireAuth,
2000 requireRepoAccess("admin"),
2001 async (c) => {
2002 const { owner: ownerName, repo: repoName } = c.req.param();
2003 const user = c.get("user")!;
2004 const body = await c.req.parseBody();
2005
2006 const [owner] = await db
2007 .select()
2008 .from(users)
2009 .where(eq(users.username, ownerName))
2010 .limit(1);
2011 if (!owner || owner.id !== user.id) {
2012 return c.redirect(`/${ownerName}/${repoName}`);
2013 }
2014
2015 const [repo] = await db
2016 .select()
2017 .from(repositories)
2018 .where(and(eq(repositories.ownerId, owner.id), eq(repositories.name, repoName)))
2019 .limit(1);
2020 if (!repo) return c.notFound();
2021
2022 const pattern = String(body.pattern || "").trim();
2023 if (!pattern) {
2024 return c.redirect(
2025 `/${ownerName}/${repoName}/settings?error=Pattern+is+required#branch-protection`
2026 );
2027 }
2028
2029 const requiredApprovals = Math.max(0, parseInt(String(body.required_approvals || "1"), 10) || 0);
2030 const requireHumanReview = body.require_codeowner_review === "1";
2031 const dismissStaleReviews = body.dismiss_stale_reviews === "1";
2032
2033 try {
2034 await db
2035 .insert(branchProtection)
2036 .values({
2037 repositoryId: repo.id,
2038 pattern,
2039 requiredApprovals,
2040 requireHumanReview,
2041 dismissStaleReviews,
2042 })
2043 .onConflictDoUpdate({
2044 target: [branchProtection.repositoryId, branchProtection.pattern],
2045 set: {
2046 requiredApprovals,
2047 requireHumanReview,
2048 dismissStaleReviews,
2049 updatedAt: new Date(),
2050 },
2051 });
2052
2053 await audit({
2054 userId: user.id,
2055 repositoryId: repo.id,
2056 action: "branch_protection.updated",
2057 targetType: "repository",
2058 targetId: repo.id,
2059 metadata: { pattern, requiredApprovals, requireHumanReview, dismissStaleReviews },
2060 });
2061 } catch (err) {
2062 return c.redirect(
2063 `/${ownerName}/${repoName}/settings?error=${encodeURIComponent("Failed to save rule: " + String(err instanceof Error ? err.message : err))}#branch-protection`
2064 );
2065 }
2066
2067 return c.redirect(
2068 `/${ownerName}/${repoName}/settings?success=Branch+protection+rule+saved#branch-protection`
2069 );
2070 }
2071);
2072
2073// ─── Branch protection: delete rule ────────────────────────────────────────
2074repoSettings.post(
2075 "/:owner/:repo/settings/branch-protection/:ruleId/delete",
2076 requireAuth,
2077 requireRepoAccess("admin"),
2078 async (c) => {
2079 const { owner: ownerName, repo: repoName, ruleId } = c.req.param();
2080 const user = c.get("user")!;
2081
2082 const [owner] = await db
2083 .select()
2084 .from(users)
2085 .where(eq(users.username, ownerName))
2086 .limit(1);
2087 if (!owner || owner.id !== user.id) {
2088 return c.redirect(`/${ownerName}/${repoName}`);
2089 }
2090
2091 const [repo] = await db
2092 .select()
2093 .from(repositories)
2094 .where(and(eq(repositories.ownerId, owner.id), eq(repositories.name, repoName)))
2095 .limit(1);
2096 if (!repo) return c.notFound();
2097
2098 await db
2099 .delete(branchProtection)
2100 .where(
2101 and(
2102 eq(branchProtection.id, ruleId),
2103 eq(branchProtection.repositoryId, repo.id)
2104 )
2105 );
2106
2107 await audit({
2108 userId: user.id,
2109 repositoryId: repo.id,
2110 action: "branch_protection.deleted",
2111 targetType: "repository",
2112 targetId: repo.id,
2113 metadata: { ruleId },
2114 });
2115
2116 return c.redirect(
2117 `/${ownerName}/${repoName}/settings?success=Branch+protection+rule+deleted#branch-protection`
2118 );
2119 }
2120);
2121
79136bbClaude2122export default repoSettings;