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.tsxBlame2121 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";
ec9e3e3Claude8import { repositories, users, repoTransfers, branchProtection } from "../db/schema";
79136bbClaude9import { Layout } from "../views/layout";
10import { RepoHeader } from "../views/components";
11import { 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);
134 color: #6ee7b7;
135 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);
139 color: #fca5a5;
140 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);
154 color: #34d399;
155 }
156 .repo-settings-banner-error .repo-settings-banner-icon {
157 background: rgba(248,113,113,0.18);
158 color: #f87171;
159 }
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);
405 color: #5b6ee8;
406 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;
451 color: #f87171;
452 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. */
622function RepoSettingsSidebar(props: { owner: string; repo: string; currentPath: string }) {
623 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} />
58307aeClaude725 <style dangerouslySetInnerHTML={{ __html: repoSettingsStyles }} />
726 <div class="repo-settings-container">
727 <RepoSettingsHero owner={ownerName} repo={repoName} />
2289dc6Claude728 <div class="rsettings-layout">
729 <RepoSettingsSidebar
730 owner={ownerName}
731 repo={repoName}
732 currentPath={`/${ownerName}/${repoName}/settings`}
733 />
734 <div class="rsettings-content">
58307aeClaude735
79136bbClaude736 {success && (
58307aeClaude737 <Banner kind="success" text={decodeURIComponent(success)} />
79136bbClaude738 )}
58307aeClaude739 {error && <Banner kind="error" text={decodeURIComponent(error)} />}
79136bbClaude740
58307aeClaude741 {/* ─── General: description + default branch + visibility ─── */}
742 <section class="repo-settings-section">
743 <div class="repo-settings-section-head">
744 <div class="repo-settings-section-eyebrow">General</div>
745 <h2 class="repo-settings-section-title">Repository basics</h2>
746 <p class="repo-settings-section-desc">
747 The headline details visitors see — description, the default
748 branch they land on, and who can browse the code.
749 </p>
750 </div>
751 <form
752 method="post"
753 action={`/${ownerName}/${repoName}/settings`}
754 >
755 <div class="repo-settings-section-body">
756 <div class="repo-settings-field">
757 <label class="repo-settings-field-label" for="description">
758 Description
759 </label>
79136bbClaude760 <input
58307aeClaude761 class="repo-settings-input"
762 name="description"
763 id="description"
764 value={repo.description || ""}
765 placeholder="A short description"
79136bbClaude766 />
58307aeClaude767 <div class="repo-settings-field-hint">
768 Shown on the repo home, search results, and explore pages.
769 </div>
770 </div>
771 <div class="repo-settings-field">
772 <label
773 class="repo-settings-field-label"
774 for="default_branch"
775 >
776 Default branch
777 </label>
778 <select
779 class="repo-settings-select"
780 name="default_branch"
781 id="default_branch"
782 >
783 {branches.length === 0 ? (
784 <option value={repo.defaultBranch}>
785 {repo.defaultBranch}
786 </option>
787 ) : (
788 branches.map((b) => (
789 <option value={b} selected={b === repo.defaultBranch}>
790 {b}
791 </option>
792 ))
793 )}
794 </select>
795 <div class="repo-settings-field-hint">
796 Where pulls land, where compare views start, and what
797 clones check out by default.
798 </div>
799 </div>
800 <div class="repo-settings-field">
801 <label class="repo-settings-field-label">Visibility</label>
802 <div class="repo-settings-visibility">
803 <label class="repo-settings-vis-card">
804 <input
805 type="radio"
806 name="visibility"
807 value="public"
808 checked={!repo.isPrivate}
809 class="repo-settings-vis-radio"
810 aria-label="Public"
811 />
812 <span class="repo-settings-vis-body">
813 <span class="repo-settings-vis-label">Public</span>
814 <span class="repo-settings-vis-desc">
815 Anyone can see this repository. You choose who can
816 commit.
817 </span>
818 </span>
819 </label>
820 <label class="repo-settings-vis-card">
821 <input
822 type="radio"
823 name="visibility"
824 value="private"
825 checked={repo.isPrivate}
826 class="repo-settings-vis-radio"
827 aria-label="Private"
828 />
829 <span class="repo-settings-vis-body">
830 <span class="repo-settings-vis-label">Private</span>
831 <span class="repo-settings-vis-desc">
832 Only you (and collaborators you invite) can see this
833 repository.
834 </span>
835 </span>
836 </label>
837 </div>
838 </div>
44f1a02Claude839 <div class="repo-settings-field">
840 <label class="repo-settings-field-label">Data region</label>
841 <div style="display:flex; align-items:center; gap:10px; flex-wrap:wrap;">
842 <span
843 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); ${
844 repo.dataRegion === "eu"
6fd5915Claude845 ? "background:rgba(95,143,160,0.10); color:#67e8f9; border:1px solid rgba(95,143,160,0.28);"
846 : "background:rgba(91,110,232,0.10); color:#c4b5fd; border:1px solid rgba(91,110,232,0.28);"
44f1a02Claude847 }`}
848 >
849 {repo.dataRegion === "eu" ? "EU · Frankfurt" : "US · Default"}
850 </span>
851 <span class="repo-settings-field-hint" style="margin:0;">
852 Data region is set at creation and cannot be changed. EU data
853 residency requires a{" "}
854 <a href="/pricing" style="color:var(--accent);text-decoration:none;">Pro plan or higher</a>.
855 </span>
856 </div>
857 </div>
79136bbClaude858 </div>
58307aeClaude859 <div class="repo-settings-section-foot">
860 <button type="submit" class="repo-settings-cta">
861 Save changes <span class="arrow">→</span>
862 </button>
863 </div>
864 </form>
865 </section>
14c3cc8Claude866
58307aeClaude867 {/* ─── Spec to PR ─── */}
868 <section class="repo-settings-section">
869 <div class="repo-settings-section-head">
870 <div class="repo-settings-section-eyebrow">
871 Automation · experimental
872 </div>
873 <h2 class="repo-settings-section-title">Spec to PR</h2>
874 <p class="repo-settings-section-desc">
875 Paste a plain-English feature spec and let Claude draft a pull
876 request for you. PRs open as drafts — review every line before
877 merging.
878 </p>
879 </div>
880 <div class="repo-settings-section-body">
881 <a
882 href={`/${ownerName}/${repoName}/spec`}
883 class="repo-settings-cta"
884 >
885 Open Spec to PR <span class="arrow">→</span>
886 </a>
887 </div>
888 </section>
889
479dcd9Claude890 {/* ─── Automation hub ─── */}
891 <section class="repo-settings-section">
892 <div class="repo-settings-section-head">
893 <div class="repo-settings-section-eyebrow">Automation</div>
894 <h2 class="repo-settings-section-title">Automation settings</h2>
895 <p class="repo-settings-section-desc">
896 Every automation on this repository in one place — AI code
897 review, PR/issue triage, auto-merge, CI auto-fix, test
898 generation, and dependency updates. Flip each between off,
899 suggest (manual), and automatic.
900 </p>
901 </div>
902 <div class="repo-settings-section-body">
903 <a
904 href={`/${ownerName}/${repoName}/settings/automation`}
905 class="repo-settings-cta"
906 >
907 Open Automation settings <span class="arrow">→</span>
908 </a>
909 </div>
910 </section>
911
58307aeClaude912 {/* ─── Template repository ─── */}
913 <section class="repo-settings-section">
914 <div class="repo-settings-section-head">
915 <div class="repo-settings-section-eyebrow">Template</div>
916 <h2 class="repo-settings-section-title">Template repository</h2>
917 <p class="repo-settings-section-desc">
71cd5ecClaude918 {repo.isTemplate
58307aeClaude919 ? "This repository is a template. Users can click “Use this template” to create a new repository with the same files."
920 : "Mark this repository as a template so others can seed new repositories from its files."}
921 </p>
922 </div>
923 <div class="repo-settings-section-body">
924 <span
925 class={`repo-settings-pill ${repo.isTemplate ? "is-on" : "is-off"}`}
926 >
927 <span class="dot" aria-hidden="true" />
928 {repo.isTemplate ? "Template enabled" : "Not a template"}
929 </span>
930 <form
931 method="post"
932 action={`/${ownerName}/${repoName}/settings/template`}
933 style="margin-top: var(--space-3)"
934 >
935 <input
936 type="hidden"
937 name="template"
938 value={repo.isTemplate ? "0" : "1"}
939 />
940 <button type="submit" class="repo-settings-cta-secondary">
941 {repo.isTemplate ? "Unmark as template" : "Mark as template"}
942 </button>
943 </form>
944 </div>
945 </section>
71cd5ecClaude946
58307aeClaude947 {/* ─── Transfer ownership ─── */}
948 <section class="repo-settings-section">
949 <div class="repo-settings-section-head">
950 <div class="repo-settings-section-eyebrow">Ownership</div>
951 <h2 class="repo-settings-section-title">Transfer ownership</h2>
952 <p class="repo-settings-section-desc">
953 Hand this repository to another user. The new owner can accept
954 or decline the transfer by viewing it.
955 </p>
956 </div>
71cd5ecClaude957 <form
e7e240eClaude958 method="post"
71cd5ecClaude959 action={`/${ownerName}/${repoName}/settings/transfer`}
960 onsubmit="return confirm('Transfer this repository? The new owner will have full control.')"
961 >
58307aeClaude962 <div class="repo-settings-section-body">
963 <div class="repo-settings-field">
964 <label class="repo-settings-field-label" for="new_owner">
965 New owner username
966 </label>
967 <div class="repo-settings-inline-row">
968 <input
969 type="text"
970 name="new_owner"
971 id="new_owner"
972 class="repo-settings-input"
973 placeholder="new-owner-username"
974 required
975 aria-label="New owner username"
976 />
977 <button type="submit" class="repo-settings-cta-secondary">
978 Transfer
979 </button>
980 </div>
981 <div class="repo-settings-field-hint">
982 Transfers are immediate. The new owner can rename or delete
983 the repository at any time.
984 </div>
985 </div>
986 </div>
71cd5ecClaude987 </form>
58307aeClaude988 </section>
71cd5ecClaude989
58307aeClaude990 {/* ─── Archive ─── */}
991 <section class="repo-settings-section">
992 <div class="repo-settings-section-head">
993 <div class="repo-settings-section-eyebrow">Lifecycle</div>
994 <h2 class="repo-settings-section-title">
995 {repo.isArchived ? "Unarchive repository" : "Archive repository"}
996 </h2>
997 <p class="repo-settings-section-desc">
998 {repo.isArchived
999 ? "This repository is archived and read-only. Unarchive to allow pushes and issue/PR activity again."
1000 : "Mark this repository as archived. It becomes read-only — no pushes, no new issues or PRs. You can unarchive at any time."}
1001 </p>
1002 </div>
1003 <div class="repo-settings-section-body">
1004 <span
1005 class={`repo-settings-pill ${repo.isArchived ? "is-on" : "is-off"}`}
534f04aClaude1006 >
58307aeClaude1007 <span class="dot" aria-hidden="true" />
1008 {repo.isArchived ? "Archived" : "Active"}
1009 </span>
1010 <form
1011 method="post"
1012 action={`/${ownerName}/${repoName}/settings/archive`}
1013 style="margin-top: var(--space-3)"
534f04aClaude1014 >
1015 <input
58307aeClaude1016 type="hidden"
1017 name="archive"
1018 value={repo.isArchived ? "0" : "1"}
534f04aClaude1019 />
58307aeClaude1020 <button type="submit" class="repo-settings-cta-secondary">
1021 {repo.isArchived ? "Unarchive" : "Archive"} this repository
1022 </button>
1023 </form>
1024 </div>
1025 </section>
534f04aClaude1026
1d4ff60Claude1027 {/* ─── AI test generator ─── */}
1028 <section class="repo-settings-section">
1029 <div class="repo-settings-section-head">
1030 <div class="repo-settings-section-eyebrow">AI tests</div>
1031 <h2 class="repo-settings-section-title">Auto-generate tests on PR open</h2>
1032 <p class="repo-settings-section-desc">
1033 When a pull request opens, Gluecron AI reads the diff and writes
1034 tests for the new code, matching whatever framework your repo
1035 already uses. Tests land on the same branch. Default off —
1036 opt in here.
1037 </p>
1038 </div>
1039 <form
1040 method="post"
1041 action={`/${ownerName}/${repoName}/settings/ai-tests`}
1042 >
1043 <div class="repo-settings-section-body">
1044 <label
1045 class="repo-settings-toggle-row"
1046 aria-label="Auto-generate tests when a PR opens"
1047 >
1048 <input
1049 type="checkbox"
1050 name="auto_generate_tests"
1051 value="1"
1052 checked={repo.autoGenerateTests}
1053 />
1054 <span class="repo-settings-toggle-text">
1055 <span class="repo-settings-toggle-text-title">
1056 Auto-generate tests on PR open
1057 </span>
1058 <span class="repo-settings-toggle-text-hint">
1059 Requires <code>ANTHROPIC_API_KEY</code>. Skips PRs without
1060 source-file changes and PRs already opened by AI.
1061 </span>
1062 </span>
1063 </label>
1064 </div>
1065 <div class="repo-settings-section-foot">
1066 <button type="submit" class="repo-settings-cta">
1067 Save AI test settings <span class="arrow">→</span>
1068 </button>
1069 </div>
1070 </form>
1071 </section>
1072
f5ad215Claude1073 {/* ─── Dependency auto-updater ─── */}
1074 <section
1075 id="dep-updater"
1076 class="repo-settings-section"
1077 >
1078 <div class="repo-settings-section-head">
1079 <div class="repo-settings-section-eyebrow">AI dependency updates</div>
1080 <h2 class="repo-settings-section-title">Automatic dependency updates</h2>
1081 <p class="repo-settings-section-desc">
1082 Once per day, Gluecron reads your <code>package.json</code>,
1083 checks npm for patch and minor updates, and applies them
1084 automatically. If the gate check passes, the PR is auto-merged.
1085 If it fails, Gluecron opens a PR with an AI-written guide
1086 explaining what broke and how to fix it. Major updates always
1087 require human review and are handled separately by the migration
1088 watcher. Default off.
1089 </p>
1090 </div>
1091 <form
1092 method="post"
1093 action={`/${ownerName}/${repoName}/settings/dep-updater`}
1094 >
1095 <div class="repo-settings-section-body">
1096 <label
1097 class="repo-settings-toggle-row"
1098 aria-label="Enable automatic dependency updates for this repo"
1099 >
1100 <input
1101 type="checkbox"
1102 name="dep_updater_enabled"
1103 value="1"
1104 checked={
1105 (repo as { depUpdaterEnabled?: boolean }).depUpdaterEnabled ??
1106 false
1107 }
1108 />
1109 <span class="repo-settings-toggle-text">
1110 <span class="repo-settings-toggle-text-title">
1111 Enable automatic dependency updates
1112 </span>
1113 <span class="repo-settings-toggle-text-hint">
1114 Patch and minor updates only. Runs once per day; max 2
1115 packages per sweep. Requires <code>DEP_UPDATER_ENABLED=1</code>{" "}
1116 on the server. Auto-merges when gate passes; opens a PR
1117 with an AI migration guide when it fails.
1118 </span>
1119 </span>
1120 </label>
1121 </div>
1122 <div class="repo-settings-section-foot">
1123 <button type="submit" class="repo-settings-cta">
1124 Save dependency update settings <span class="arrow">→</span>
1125 </button>
1126 </div>
1127 </form>
1128 </section>
1129
9b3a183Claude1130 {/* ─── Cloud dev environments ─── */}
1131 <section
1132 id="dev-envs"
1133 class="repo-settings-section"
1134 >
1135 <div class="repo-settings-section-head">
1136 <div class="repo-settings-section-eyebrow">Dev environments</div>
1137 <h2 class="repo-settings-section-title">Enable cloud dev environments</h2>
1138 <p class="repo-settings-section-desc">
1139 When enabled, anyone with read access can hit{" "}
1140 <code>/{ownerName}/{repoName}/dev</code> to spin up a
1141 hosted VS Code IDE in the browser, backed by a cold-start
1142 container. We read <code>.gluecron/dev.yml</code> for the
1143 image + install commands; idle envs stop themselves after
1144 30 minutes. Default off — each env burns a container.
1145 </p>
1146 </div>
1147 <form
1148 method="post"
1149 action={`/${ownerName}/${repoName}/settings/dev-envs`}
1150 >
1151 <div class="repo-settings-section-body">
1152 <label
1153 class="repo-settings-toggle-row"
1154 aria-label="Enable cloud dev environments for this repo"
1155 >
1156 <input
1157 type="checkbox"
1158 name="dev_envs_enabled"
1159 value="1"
1160 checked={
1161 (repo as { devEnvsEnabled?: boolean }).devEnvsEnabled ??
1162 false
1163 }
1164 />
1165 <span class="repo-settings-toggle-text">
1166 <span class="repo-settings-toggle-text-title">
1167 Enable dev environments
1168 </span>
1169 <span class="repo-settings-toggle-text-hint">
1170 Surfaces the <code>/{ownerName}/{repoName}/dev</code>{" "}
1171 route. Commit <code>.gluecron/dev.yml</code> to your
1172 repo to customise the image, ports, and extensions.
1173 </span>
1174 </span>
1175 </label>
1176 </div>
1177 <div class="repo-settings-section-foot">
1178 <button type="submit" class="repo-settings-cta">
1179 Save dev env settings <span class="arrow">→</span>
1180 </button>
1181 </div>
1182 </form>
1183 </section>
1184
58307aeClaude1185 {/* ─── Stale activity ─── */}
1186 <section class="repo-settings-section">
1187 <div class="repo-settings-section-head">
1188 <div class="repo-settings-section-eyebrow">Stale sweep</div>
1189 <h2 class="repo-settings-section-title">Stale activity</h2>
1190 <p class="repo-settings-section-desc">
1191 Autopilot pokes PRs and issues that have gone quiet, then offers
1192 a one-click close path. Each toggle controls the final close
1193 step — pokes always happen, but they&apos;re harmless reminders.
1194 </p>
1195 </div>
79136bbClaude1196 <form
e7e240eClaude1197 method="post"
58307aeClaude1198 action={`/${ownerName}/${repoName}/settings/stale`}
79136bbClaude1199 >
58307aeClaude1200 <div class="repo-settings-section-body">
1201 <label
1202 class="repo-settings-toggle-row"
1203 aria-label="Auto-close stale PRs after 14 days of no activity post-poke"
1204 >
1205 <input
1206 type="checkbox"
1207 name="auto_close_stale_prs"
1208 value="1"
1209 checked={repo.autoCloseStalePrs}
1210 />
1211 <span class="repo-settings-toggle-text">
1212 <span class="repo-settings-toggle-text-title">
1213 Auto-close stale PRs
1214 </span>
1215 <span class="repo-settings-toggle-text-hint">
1216 Close PRs that go quiet for 14 days after autopilot pokes
1217 them.
1218 </span>
1219 </span>
1220 </label>
1221 <label
1222 class="repo-settings-toggle-row"
1223 aria-label="Auto-close stale issues after 60 days of no activity post-poke"
1224 >
1225 <input
1226 type="checkbox"
1227 name="auto_close_stale_issues"
1228 value="1"
1229 checked={repo.autoCloseStaleIssues}
1230 />
1231 <span class="repo-settings-toggle-text">
1232 <span class="repo-settings-toggle-text-title">
1233 Auto-close stale issues
1234 </span>
1235 <span class="repo-settings-toggle-text-hint">
1236 Close issues that go quiet for 60 days after autopilot
1237 pokes them.
1238 </span>
1239 </span>
1240 </label>
1241 </div>
1242 <div class="repo-settings-section-foot">
1243 <button type="submit" class="repo-settings-cta">
1244 Save stale settings <span class="arrow">→</span>
1245 </button>
1246 </div>
79136bbClaude1247 </form>
58307aeClaude1248 </section>
1249
1df50d5Claude1250 {/* ─── PR preview builds (migration 0077) ─── */}
1251 <section class="repo-settings-section">
1252 <div class="repo-settings-section-head">
1253 <div class="repo-settings-section-eyebrow">Preview builds</div>
1254 <h2 class="repo-settings-section-title">PR preview environments</h2>
1255 <p class="repo-settings-section-desc">
1256 When a build command is set, every PR push automatically clones
1257 the branch, runs the command, and serves the built output from a
1258 unique preview URL — like Vercel, but native to Gluecron. Leave
1259 blank to use URL-only previews (no build runs).
1260 </p>
1261 </div>
1262 <form
1263 method="post"
1264 action={`/${ownerName}/${repoName}/settings/previews`}
1265 >
1266 <div class="repo-settings-section-body">
1267 <div class="repo-settings-field">
1268 <label class="repo-settings-field-label" for="preview_build_command">
1269 Build command
1270 </label>
1271 <input
1272 id="preview_build_command"
1273 class="repo-settings-input"
1274 type="text"
1275 name="preview_build_command"
1276 placeholder="npm run build"
1277 value={
1278 (repo as { previewBuildCommand?: string | null }).previewBuildCommand ?? ""
1279 }
1280 />
1281 <p class="repo-settings-field-hint">
1282 e.g. <code>npm run build</code>, <code>bun run build</code>, or{" "}
1283 <code>hugo</code>. Runs inside the cloned branch directory.
1284 </p>
1285 </div>
1286 <div class="repo-settings-field">
1287 <label class="repo-settings-field-label" for="preview_output_dir">
1288 Output directory
1289 </label>
1290 <input
1291 id="preview_output_dir"
1292 class="repo-settings-input"
1293 type="text"
1294 name="preview_output_dir"
1295 placeholder="dist"
1296 value={
1297 (repo as { previewOutputDir?: string | null }).previewOutputDir ?? "dist"
1298 }
1299 />
1300 <p class="repo-settings-field-hint">
1301 The directory your build writes to. Common values:{" "}
1302 <code>dist</code>, <code>public</code>, <code>out</code>,{" "}
1303 <code>build</code>. Served from{" "}
1304 <code>/previews/{ownerName}/{repoName}/{"<branch>"}/</code>
1305 </p>
1306 </div>
1307 </div>
1308 <div class="repo-settings-section-foot">
1309 <button type="submit" class="repo-settings-cta">
1310 Save preview settings <span class="arrow">→</span>
1311 </button>
1312 </div>
1313 </form>
1314 </section>
1315
ec9e3e3Claude1316 {/* ─── Branch protection ─── */}
1317 <section class="repo-settings-section" id="branch-protection">
1318 <div class="repo-settings-section-head">
1319 <div class="repo-settings-section-eyebrow">Branch protection</div>
1320 <h2 class="repo-settings-section-title">Required reviews before merge</h2>
1321 <p class="repo-settings-section-desc">
1322 Protect branches by requiring a minimum number of human approvals before
1323 a pull request can be merged. Patterns support wildcards (e.g.{" "}
1324 <code>release/*</code>). CODEOWNERS auto-assignment applies independently.
1325 </p>
1326 </div>
1327
1328 {/* Existing rules list */}
1329 {existingBranchRules.length > 0 && (
1330 <div style="padding: var(--space-3) var(--space-5) 0">
1331 {existingBranchRules.map((rule) => (
1332 <div style="display:flex;align-items:center;gap:12px;padding:10px 0;border-bottom:1px solid var(--border)">
1333 <span style="flex:1;font-family:var(--font-mono);font-size:13px;color:var(--text-strong)">
1334 {rule.pattern}
1335 </span>
1336 <span style="font-size:12.5px;color:var(--text-muted)">
1337 {rule.requiredApprovals} required approval{rule.requiredApprovals !== 1 ? "s" : ""}
1338 </span>
1339 {rule.requireHumanReview && (
6fd5915Claude1340 <span style="font-size:11px;font-weight:600;padding:2px 8px;border-radius:9999px;background:rgba(91,110,232,0.12);color:#5b6ee8">
ec9e3e3Claude1341 codeowner review required
1342 </span>
1343 )}
1344 {rule.dismissStaleReviews && (
1345 <span style="font-size:11px;color:var(--text-muted)">
1346 dismiss stale
1347 </span>
1348 )}
1349 <form method="post" action={`/${ownerName}/${repoName}/settings/branch-protection/${rule.id}/delete`}>
1350 <button type="submit"
1351 class="repo-settings-cta-secondary"
1352 style="padding:4px 10px;font-size:12px;color:var(--red,#f87171)"
1353 onclick="return confirm('Delete this branch protection rule?')"
1354 >
1355 Delete
1356 </button>
1357 </form>
1358 </div>
1359 ))}
1360 </div>
1361 )}
1362
1363 {/* Add new rule form */}
1364 <form
1365 method="post"
1366 action={`/${ownerName}/${repoName}/settings/branch-protection`}
1367 >
1368 <div class="repo-settings-section-body">
1369 <div style="display:grid;grid-template-columns:1fr 120px;gap:var(--space-3);align-items:end">
1370 <div class="repo-settings-field" style="margin-bottom:0">
1371 <label class="repo-settings-field-label" for="bp_pattern">
1372 Branch name pattern
1373 </label>
1374 <input
1375 class="repo-settings-input"
1376 name="pattern"
1377 id="bp_pattern"
1378 placeholder="main"
1379 required
1380 aria-label="Branch name pattern"
1381 />
1382 <div class="repo-settings-field-hint">
1383 Exact name or glob like <code>release/*</code>
1384 </div>
1385 </div>
1386 <div class="repo-settings-field" style="margin-bottom:0">
1387 <label class="repo-settings-field-label" for="bp_required">
1388 Required approvals
1389 </label>
1390 <input
1391 class="repo-settings-input"
1392 name="required_approvals"
1393 id="bp_required"
1394 type="number"
1395 min="0"
1396 max="10"
1397 value="1"
1398 aria-label="Number of required approvals"
1399 />
1400 </div>
1401 </div>
1402 <div style="margin-top:var(--space-3);display:flex;flex-direction:column;gap:8px">
1403 <label class="repo-settings-toggle-row" aria-label="Require codeowner review">
1404 <input
1405 type="checkbox"
1406 name="require_codeowner_review"
1407 value="1"
1408 />
1409 <span class="repo-settings-toggle-text">
1410 <span class="repo-settings-toggle-text-title">
1411 Require codeowner review
1412 </span>
1413 <span class="repo-settings-toggle-text-hint">
1414 At least one CODEOWNERS-matched reviewer must approve before merging.
1415 </span>
1416 </span>
1417 </label>
1418 <label class="repo-settings-toggle-row" aria-label="Dismiss stale reviews">
1419 <input
1420 type="checkbox"
1421 name="dismiss_stale_reviews"
1422 value="1"
1423 />
1424 <span class="repo-settings-toggle-text">
1425 <span class="repo-settings-toggle-text-title">
1426 Dismiss stale reviews on new push
1427 </span>
1428 <span class="repo-settings-toggle-text-hint">
1429 Prior approvals are dismissed when the head branch receives a new commit.
1430 </span>
1431 </span>
1432 </label>
1433 </div>
1434 </div>
1435 <div class="repo-settings-section-foot">
1436 <button type="submit" class="repo-settings-cta">
1437 Add rule <span class="arrow">→</span>
1438 </button>
1439 </div>
1440 </form>
1441 </section>
1442
58307aeClaude1443 {/* ─── Danger zone ─── */}
1444 <section class="repo-settings-danger">
1445 <div class="repo-settings-danger-head">
1446 <div class="repo-settings-danger-eyebrow">Danger zone</div>
1447 <h2 class="repo-settings-danger-title">Delete this repository</h2>
1448 <p class="repo-settings-danger-desc">
1449 Permanently remove this repository and every byte of its history,
1450 issues, PRs, stars, and webhooks. There is no undo.
1451 </p>
1452 </div>
1453 <div class="repo-settings-danger-body">
1454 <p class="muted">
1455 Once deleted, the URL <code>{ownerName}/{repoName}</code> frees
1456 up immediately. Open clones will fail to fetch or push.
1457 </p>
1458 <form
1459 method="post"
1460 action={`/${ownerName}/${repoName}/settings/delete`}
1461 onsubmit="return confirm('Are you sure? This cannot be undone.')"
1462 >
1463 <button type="submit" class="repo-settings-danger-btn">
1464 Delete this repository
1465 </button>
1466 </form>
1467 </div>
1468 </section>
2289dc6Claude1469 </div>{/* .rsettings-content */}
1470 </div>{/* .rsettings-layout */}
58307aeClaude1471 </div>
79136bbClaude1472 </Layout>
1473 );
1474});
1475
1476// Save settings
febd4f0Claude1477repoSettings.post("/:owner/:repo/settings", requireAuth, requireRepoAccess("admin"), async (c) => {
79136bbClaude1478 const { owner: ownerName, repo: repoName } = c.req.param();
1479 const user = c.get("user")!;
1480 const body = await c.req.parseBody();
1481
1482 const [owner] = await db
1483 .select()
1484 .from(users)
1485 .where(eq(users.username, ownerName))
1486 .limit(1);
1487
1488 if (!owner || owner.id !== user.id) {
1489 return c.redirect(`/${ownerName}/${repoName}`);
1490 }
1491
1492 await db
1493 .update(repositories)
1494 .set({
1495 description: String(body.description || "").trim() || null,
1496 defaultBranch: String(body.default_branch || "main"),
1497 isPrivate: body.visibility === "private",
1498 updatedAt: new Date(),
1499 })
1500 .where(
1501 and(eq(repositories.ownerId, owner.id), eq(repositories.name, repoName))
1502 );
1503
1504 return c.redirect(
1505 `/${ownerName}/${repoName}/settings?success=Settings+saved`
1506 );
1507});
1508
71cd5ecClaude1509// Toggle template flag
1510repoSettings.post(
1511 "/:owner/:repo/settings/template",
1512 requireAuth,
febd4f0Claude1513 requireRepoAccess("admin"),
71cd5ecClaude1514 async (c) => {
1515 const { owner: ownerName, repo: repoName } = c.req.param();
1516 const user = c.get("user")!;
1517 const body = await c.req.parseBody();
1518 const [owner] = await db
1519 .select()
1520 .from(users)
1521 .where(eq(users.username, ownerName))
1522 .limit(1);
1523 if (!owner || owner.id !== user.id) {
1524 return c.redirect(`/${ownerName}/${repoName}`);
1525 }
1526 const target = String(body.template || "1") === "1";
1527 await db
1528 .update(repositories)
1529 .set({ isTemplate: target, updatedAt: new Date() })
1530 .where(
1531 and(
1532 eq(repositories.ownerId, owner.id),
1533 eq(repositories.name, repoName)
1534 )
1535 );
1536 return c.redirect(
1537 `/${ownerName}/${repoName}/settings?success=${
1538 target ? "Marked+as+template" : "Unmarked+as+template"
1539 }`
1540 );
1541 }
1542);
1543
1544// Transfer repository to a new owner (by username)
1545repoSettings.post(
1546 "/:owner/:repo/settings/transfer",
1547 requireAuth,
febd4f0Claude1548 requireRepoAccess("admin"),
71cd5ecClaude1549 async (c) => {
1550 const { owner: ownerName, repo: repoName } = c.req.param();
1551 const user = c.get("user")!;
1552 const body = await c.req.parseBody();
1553 const newOwnerName = String(body.new_owner || "").trim();
1554 if (!newOwnerName) {
1555 return c.redirect(
1556 `/${ownerName}/${repoName}/settings?error=New+owner+required`
1557 );
1558 }
1559 const [owner] = await db
1560 .select()
1561 .from(users)
1562 .where(eq(users.username, ownerName))
1563 .limit(1);
1564 if (!owner || owner.id !== user.id) {
1565 return c.redirect(`/${ownerName}/${repoName}`);
1566 }
1567 const [newOwner] = await db
1568 .select()
1569 .from(users)
1570 .where(eq(users.username, newOwnerName))
1571 .limit(1);
1572 if (!newOwner) {
1573 return c.redirect(
1574 `/${ownerName}/${repoName}/settings?error=User+not+found`
1575 );
1576 }
1577 if (newOwner.id === owner.id) {
1578 return c.redirect(
1579 `/${ownerName}/${repoName}/settings?error=Same+owner`
1580 );
1581 }
1582 // Reject if new owner already has a repo by this name
1583 const [conflict] = await db
1584 .select()
1585 .from(repositories)
1586 .where(
1587 and(
1588 eq(repositories.ownerId, newOwner.id),
1589 eq(repositories.name, repoName)
1590 )
1591 )
1592 .limit(1);
1593 if (conflict) {
1594 return c.redirect(
1595 `/${ownerName}/${repoName}/settings?error=Target+owner+already+has+a+repo+by+that+name`
1596 );
1597 }
1598 const [repo] = await db
1599 .select()
1600 .from(repositories)
1601 .where(
1602 and(
1603 eq(repositories.ownerId, owner.id),
1604 eq(repositories.name, repoName)
1605 )
1606 )
1607 .limit(1);
1608 if (!repo) return c.notFound();
1609 await db
1610 .update(repositories)
1611 .set({ ownerId: newOwner.id, orgId: null, updatedAt: new Date() })
1612 .where(eq(repositories.id, repo.id));
1613 await db.insert(repoTransfers).values({
1614 repositoryId: repo.id,
1615 fromOwnerId: owner.id,
1616 fromOrgId: repo.orgId,
1617 toOwnerId: newOwner.id,
1618 toOrgId: null,
1619 initiatedBy: user.id,
1620 });
1621 return c.redirect(`/${newOwnerName}/${repoName}`);
1622 }
1623);
1624
1625// Archive / unarchive repository
1626repoSettings.post(
1627 "/:owner/:repo/settings/archive",
1628 requireAuth,
febd4f0Claude1629 requireRepoAccess("admin"),
71cd5ecClaude1630 async (c) => {
1631 const { owner: ownerName, repo: repoName } = c.req.param();
1632 const user = c.get("user")!;
1633 const body = await c.req.parseBody();
1634 const [owner] = await db
1635 .select()
1636 .from(users)
1637 .where(eq(users.username, ownerName))
1638 .limit(1);
1639 if (!owner || owner.id !== user.id) {
1640 return c.redirect(`/${ownerName}/${repoName}`);
1641 }
1642 const target = String(body.archive || "1") === "1";
1643 await db
1644 .update(repositories)
1645 .set({ isArchived: target, updatedAt: new Date() })
1646 .where(
1647 and(
1648 eq(repositories.ownerId, owner.id),
1649 eq(repositories.name, repoName)
1650 )
1651 );
1652 return c.redirect(
1653 `/${ownerName}/${repoName}/settings?success=${
1654 target ? "Repository+archived" : "Repository+unarchived"
1655 }`
1656 );
1657 }
1658);
1659
534f04aClaude1660// Block M5: stale activity opt-out flags. Owner-only; audits each toggle.
1661repoSettings.post(
1662 "/:owner/:repo/settings/stale",
1663 requireAuth,
1664 requireRepoAccess("admin"),
1665 async (c) => {
1666 const { owner: ownerName, repo: repoName } = c.req.param();
1667 const user = c.get("user")!;
1668 const body = await c.req.parseBody();
1669 const [owner] = await db
1670 .select()
1671 .from(users)
1672 .where(eq(users.username, ownerName))
1673 .limit(1);
1674 if (!owner || owner.id !== user.id) {
1675 return c.redirect(`/${ownerName}/${repoName}`);
1676 }
1677 const [repo] = await db
1678 .select()
1679 .from(repositories)
1680 .where(
1681 and(
1682 eq(repositories.ownerId, owner.id),
1683 eq(repositories.name, repoName)
1684 )
1685 )
1686 .limit(1);
1687 if (!repo) return c.notFound();
1688
1689 // Unchecked checkboxes are absent from the form payload, so coerce to bool.
1690 const newPrs = body.auto_close_stale_prs === "1";
1691 const newIssues = body.auto_close_stale_issues === "1";
1692
1693 await db
1694 .update(repositories)
1695 .set({
1696 autoCloseStalePrs: newPrs,
1697 autoCloseStaleIssues: newIssues,
1698 updatedAt: new Date(),
1699 })
1700 .where(eq(repositories.id, repo.id));
1701
1702 // Audit toggle deltas so the repo's audit log shows the change. Two
1703 // separate rows (one per flag) so the action names stay stable + grep-able.
1704 if (newPrs !== repo.autoCloseStalePrs) {
1705 await audit({
1706 userId: user.id,
1707 repositoryId: repo.id,
1708 action: "repo.auto_close_stale_prs.toggled",
1709 targetType: "repository",
1710 targetId: repo.id,
1711 metadata: { from: repo.autoCloseStalePrs, to: newPrs },
1712 });
1713 }
1714 if (newIssues !== repo.autoCloseStaleIssues) {
1715 await audit({
1716 userId: user.id,
1717 repositoryId: repo.id,
1718 action: "repo.auto_close_stale_issues.toggled",
1719 targetType: "repository",
1720 targetId: repo.id,
1721 metadata: { from: repo.autoCloseStaleIssues, to: newIssues },
1722 });
1723 }
1724
1725 return c.redirect(
1726 `/${ownerName}/${repoName}/settings?success=Stale+settings+saved`
1727 );
1728 }
1729);
1730
1d4ff60Claude1731// AI test generator opt-in. Owner-only; audits the toggle delta so the
1732// repo's audit log shows the change.
1733repoSettings.post(
1734 "/:owner/:repo/settings/ai-tests",
1735 requireAuth,
1736 requireRepoAccess("admin"),
1737 async (c) => {
1738 const { owner: ownerName, repo: repoName } = c.req.param();
1739 const user = c.get("user")!;
1740 const body = await c.req.parseBody();
1741 const [owner] = await db
1742 .select()
1743 .from(users)
1744 .where(eq(users.username, ownerName))
1745 .limit(1);
1746 if (!owner || owner.id !== user.id) {
1747 return c.redirect(`/${ownerName}/${repoName}`);
1748 }
1749 const [repo] = await db
1750 .select()
1751 .from(repositories)
1752 .where(
1753 and(
1754 eq(repositories.ownerId, owner.id),
1755 eq(repositories.name, repoName)
1756 )
1757 )
1758 .limit(1);
1759 if (!repo) return c.notFound();
1760
1761 // Unchecked checkboxes are absent from the form payload, so coerce.
1762 const next = body.auto_generate_tests === "1";
1763
1764 await db
1765 .update(repositories)
1766 .set({
1767 autoGenerateTests: next,
1768 updatedAt: new Date(),
1769 })
1770 .where(eq(repositories.id, repo.id));
1771
1772 if (next !== repo.autoGenerateTests) {
1773 await audit({
1774 userId: user.id,
1775 repositoryId: repo.id,
1776 action: "repo.auto_generate_tests.toggled",
1777 targetType: "repository",
1778 targetId: repo.id,
1779 metadata: { from: repo.autoGenerateTests, to: next },
1780 });
1781 }
1782
1783 return c.redirect(
1784 `/${ownerName}/${repoName}/settings?success=AI+test+settings+saved`
1785 );
1786 }
1787);
1788
9b3a183Claude1789// Migration 0072 — toggle cloud dev environments. Owner-only; audits
1790// the toggle delta so the repo's audit log shows the change.
1791repoSettings.post(
1792 "/:owner/:repo/settings/dev-envs",
1793 requireAuth,
1794 requireRepoAccess("admin"),
1795 async (c) => {
1796 const { owner: ownerName, repo: repoName } = c.req.param();
1797 const user = c.get("user")!;
1798 const body = await c.req.parseBody();
1799 const [owner] = await db
1800 .select()
1801 .from(users)
1802 .where(eq(users.username, ownerName))
1803 .limit(1);
1804 if (!owner || owner.id !== user.id) {
1805 return c.redirect(`/${ownerName}/${repoName}`);
1806 }
1807 const [repo] = await db
1808 .select()
1809 .from(repositories)
1810 .where(
1811 and(
1812 eq(repositories.ownerId, owner.id),
1813 eq(repositories.name, repoName)
1814 )
1815 )
1816 .limit(1);
1817 if (!repo) return c.notFound();
1818
1819 const next = body.dev_envs_enabled === "1";
1820 const prev = (repo as { devEnvsEnabled?: boolean }).devEnvsEnabled ?? false;
1821
1822 await db
1823 .update(repositories)
1824 .set({
1825 devEnvsEnabled: next,
1826 updatedAt: new Date(),
1827 })
1828 .where(eq(repositories.id, repo.id));
1829
1830 if (next !== prev) {
1831 await audit({
1832 userId: user.id,
1833 repositoryId: repo.id,
1834 action: "repo.dev_envs_enabled.toggled",
1835 targetType: "repository",
1836 targetId: repo.id,
1837 metadata: { from: prev, to: next },
1838 });
1839 }
1840
1841 return c.redirect(
1842 `/${ownerName}/${repoName}/settings?success=Dev+env+settings+saved#dev-envs`
1843 );
1844 }
1845);
1846
f5ad215Claude1847// Migration 0077 — toggle AI dependency auto-updater. Owner-only; audits
1848// the toggle delta so the repo's audit log shows the change.
1849repoSettings.post(
1850 "/:owner/:repo/settings/dep-updater",
1851 requireAuth,
1852 requireRepoAccess("admin"),
1853 async (c) => {
1854 const { owner: ownerName, repo: repoName } = c.req.param();
1855 const user = c.get("user")!;
1856 const body = await c.req.parseBody();
1857 const [owner] = await db
1858 .select()
1859 .from(users)
1860 .where(eq(users.username, ownerName))
1861 .limit(1);
1862 if (!owner || owner.id !== user.id) {
1863 return c.redirect(`/${ownerName}/${repoName}`);
1864 }
1865 const [repo] = await db
1866 .select()
1867 .from(repositories)
1868 .where(
1869 and(eq(repositories.ownerId, owner.id), eq(repositories.name, repoName))
1870 )
1871 .limit(1);
1872 if (!repo) return c.notFound();
1873
1874 const next = body.dep_updater_enabled === "1";
1875 const prev =
1876 (repo as { depUpdaterEnabled?: boolean }).depUpdaterEnabled ?? false;
1877
1878 await db
1879 .update(repositories)
1880 .set({
1881 depUpdaterEnabled: next,
1882 updatedAt: new Date(),
1883 })
1884 .where(eq(repositories.id, repo.id));
1885
1886 if (next !== prev) {
1887 await audit({
1888 userId: user.id,
1889 repositoryId: repo.id,
1890 action: "repo.dep_updater_enabled.toggled",
1891 targetType: "repository",
1892 targetId: repo.id,
1893 metadata: { from: prev, to: next },
1894 });
1895 }
1896
1897 return c.redirect(
1898 `/${ownerName}/${repoName}/settings?success=Dependency+update+settings+saved#dep-updater`
1899 );
1900 }
1901);
1902
79136bbClaude1903// Delete repository
1904repoSettings.post(
1905 "/:owner/:repo/settings/delete",
1906 requireAuth,
febd4f0Claude1907 requireRepoAccess("admin"),
79136bbClaude1908 async (c) => {
1909 const { owner: ownerName, repo: repoName } = c.req.param();
1910 const user = c.get("user")!;
1911
1912 const [owner] = await db
1913 .select()
1914 .from(users)
1915 .where(eq(users.username, ownerName))
1916 .limit(1);
1917
1918 if (!owner || owner.id !== user.id) {
1919 return c.redirect(`/${ownerName}/${repoName}`);
1920 }
1921
1922 const [repo] = await db
1923 .select()
1924 .from(repositories)
1925 .where(
1926 and(
1927 eq(repositories.ownerId, owner.id),
1928 eq(repositories.name, repoName)
1929 )
1930 )
1931 .limit(1);
1932
1933 if (!repo) return c.redirect(`/${ownerName}`);
1934
1935 // Delete from disk
1936 try {
1937 await rm(repo.diskPath, { recursive: true, force: true });
1938 } catch {
1939 // Disk cleanup best-effort
1940 }
1941
1942 // Delete from DB (cascades to stars, issues, etc.)
1943 await db.delete(repositories).where(eq(repositories.id, repo.id));
1944
1945 return c.redirect(`/${ownerName}`);
1946 }
1947);
1948
1df50d5Claude1949// Migration 0077 — PR preview build configuration. Owner-only.
1950repoSettings.post(
1951 "/:owner/:repo/settings/previews",
1952 requireAuth,
1953 requireRepoAccess("admin"),
1954 async (c) => {
1955 const { owner: ownerName, repo: repoName } = c.req.param();
1956 const user = c.get("user")!;
1957 const body = await c.req.parseBody();
1958
1959 const [owner] = await db
1960 .select()
1961 .from(users)
1962 .where(eq(users.username, ownerName))
1963 .limit(1);
1964 if (!owner || owner.id !== user.id) {
1965 return c.redirect(`/${ownerName}/${repoName}`);
1966 }
1967
1968 const [repo] = await db
1969 .select()
1970 .from(repositories)
1971 .where(
1972 and(eq(repositories.ownerId, owner.id), eq(repositories.name, repoName))
1973 )
1974 .limit(1);
1975 if (!repo) return c.notFound();
1976
1977 const buildCommand = String(body.preview_build_command || "").trim() || null;
1978 const outputDir = String(body.preview_output_dir || "").trim() || "dist";
1979
1980 await db
1981 .update(repositories)
1982 .set({
1983 previewBuildCommand: buildCommand,
1984 previewOutputDir: outputDir,
1985 updatedAt: new Date(),
1986 })
1987 .where(eq(repositories.id, repo.id));
1988
1989 return c.redirect(
1990 `/${ownerName}/${repoName}/settings?success=Preview+build+settings+saved`
1991 );
1992 }
1993);
1994
ec9e3e3Claude1995// ─── Branch protection: add rule ───────────────────────────────────────────
1996repoSettings.post(
1997 "/:owner/:repo/settings/branch-protection",
1998 requireAuth,
1999 requireRepoAccess("admin"),
2000 async (c) => {
2001 const { owner: ownerName, repo: repoName } = c.req.param();
2002 const user = c.get("user")!;
2003 const body = await c.req.parseBody();
2004
2005 const [owner] = await db
2006 .select()
2007 .from(users)
2008 .where(eq(users.username, ownerName))
2009 .limit(1);
2010 if (!owner || owner.id !== user.id) {
2011 return c.redirect(`/${ownerName}/${repoName}`);
2012 }
2013
2014 const [repo] = await db
2015 .select()
2016 .from(repositories)
2017 .where(and(eq(repositories.ownerId, owner.id), eq(repositories.name, repoName)))
2018 .limit(1);
2019 if (!repo) return c.notFound();
2020
2021 const pattern = String(body.pattern || "").trim();
2022 if (!pattern) {
2023 return c.redirect(
2024 `/${ownerName}/${repoName}/settings?error=Pattern+is+required#branch-protection`
2025 );
2026 }
2027
2028 const requiredApprovals = Math.max(0, parseInt(String(body.required_approvals || "1"), 10) || 0);
2029 const requireHumanReview = body.require_codeowner_review === "1";
2030 const dismissStaleReviews = body.dismiss_stale_reviews === "1";
2031
2032 try {
2033 await db
2034 .insert(branchProtection)
2035 .values({
2036 repositoryId: repo.id,
2037 pattern,
2038 requiredApprovals,
2039 requireHumanReview,
2040 dismissStaleReviews,
2041 })
2042 .onConflictDoUpdate({
2043 target: [branchProtection.repositoryId, branchProtection.pattern],
2044 set: {
2045 requiredApprovals,
2046 requireHumanReview,
2047 dismissStaleReviews,
2048 updatedAt: new Date(),
2049 },
2050 });
2051
2052 await audit({
2053 userId: user.id,
2054 repositoryId: repo.id,
2055 action: "branch_protection.updated",
2056 targetType: "repository",
2057 targetId: repo.id,
2058 metadata: { pattern, requiredApprovals, requireHumanReview, dismissStaleReviews },
2059 });
2060 } catch (err) {
2061 return c.redirect(
2062 `/${ownerName}/${repoName}/settings?error=${encodeURIComponent("Failed to save rule: " + String(err instanceof Error ? err.message : err))}#branch-protection`
2063 );
2064 }
2065
2066 return c.redirect(
2067 `/${ownerName}/${repoName}/settings?success=Branch+protection+rule+saved#branch-protection`
2068 );
2069 }
2070);
2071
2072// ─── Branch protection: delete rule ────────────────────────────────────────
2073repoSettings.post(
2074 "/:owner/:repo/settings/branch-protection/:ruleId/delete",
2075 requireAuth,
2076 requireRepoAccess("admin"),
2077 async (c) => {
2078 const { owner: ownerName, repo: repoName, ruleId } = c.req.param();
2079 const user = c.get("user")!;
2080
2081 const [owner] = await db
2082 .select()
2083 .from(users)
2084 .where(eq(users.username, ownerName))
2085 .limit(1);
2086 if (!owner || owner.id !== user.id) {
2087 return c.redirect(`/${ownerName}/${repoName}`);
2088 }
2089
2090 const [repo] = await db
2091 .select()
2092 .from(repositories)
2093 .where(and(eq(repositories.ownerId, owner.id), eq(repositories.name, repoName)))
2094 .limit(1);
2095 if (!repo) return c.notFound();
2096
2097 await db
2098 .delete(branchProtection)
2099 .where(
2100 and(
2101 eq(branchProtection.id, ruleId),
2102 eq(branchProtection.repositoryId, repo.id)
2103 )
2104 );
2105
2106 await audit({
2107 userId: user.id,
2108 repositoryId: repo.id,
2109 action: "branch_protection.deleted",
2110 targetType: "repository",
2111 targetId: repo.id,
2112 metadata: { ruleId },
2113 });
2114
2115 return c.redirect(
2116 `/${ownerName}/${repoName}/settings?success=Branch+protection+rule+deleted#branch-protection`
2117 );
2118 }
2119);
2120
79136bbClaude2121export default repoSettings;