Skip to content

g1t/apps/web/app/routes/repo/settings-ruleset.tsx

97 lines4,432 bytesCodeBlame

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Merge rulesets: branch and tag rules, agent-first, enforced on push and merge1import { ArrowLeft } from "lucide-react";
2import { Link, data, redirect } from "react-router";
3
4import type { RulesetSpec } from "@g1t/contracts";
5
6import type { Route } from "./+types/settings-ruleset";
7import { page } from "../../lib/meta";
8import { EnforcementBadge, RulesetForm } from "../../components/rules";
9import { newRuleset } from "../../lib/rules";
10import { TimeAgo } from "../../components/ui";
11import { work } from "../../lib/services.server";
12import { assertSameOrigin, getViewer, requireUser, unwrap } from "../../lib/session.server";
13import { requireCapability, requireInsider } from "../../lib/access.server";
14
15export function meta({ params, loaderData, ...args }: Route.MetaArgs) {
16 const name = loaderData?.existing?.name ?? "New ruleset";
17 return page(args, { title: `${name} · Rules · ${params.owner}/${params.repo} · g1t` });
18}
19
20export async function loader({ params, context }: Route.LoaderArgs) {
21 const viewer = getViewer(context);
22 const { repo, access } = await requireInsider(context, params, "manage_protection");
23 const path = { namespace: params.owner, name: params.repo };
24 const seen = await work.seenChecks(path, viewer).catch(() => null);
25 const base = {
26 seen: seen?.ok ? seen.value : [],
27 editable: access.can.manage_protection && !repo.archivedAt,
28 repository: `${repo.namespace}/${repo.name}`,
29 };
30 if (params.id === "new") return { ...base, existing: null };
31 const found = await work.getRuleset({ repo: path }, params.id, viewer);
32 if (!found.ok && found.error.code === "not_found") throw data(null, { status: 404 });
33 const existing = unwrap(found);
34 // A workspace's ruleset is changed in the workspace's settings.
35 if (existing.level === "workspace") throw redirect(`/${params.owner}/-/rules/${existing.id}`);
36 return { ...base, existing };
37}
38
39export async function action({ request, params, context }: Route.ActionArgs) {
40 assertSameOrigin(request);
41 const user = requireUser(context, request);
42 await requireCapability(context, params, "manage_protection");
43 const form = await request.formData();
44 const owner = { repo: { namespace: params.owner, name: params.repo } };
45 const list = `/${params.owner}/${params.repo}/settings/rules`;
46 if (form.get("intent") === "delete" && params.id !== "new") {
47 const deleted = await work.deleteRuleset(user, owner, params.id);
48 return deleted.ok ? redirect(list) : { error: deleted.error.message };
49 }
50 let ruleset: RulesetSpec;
51 try {
52 ruleset = JSON.parse(String(form.get("ruleset") ?? "")) as RulesetSpec;
53 } catch {
54 return { error: "The ruleset could not be read. Reload the page and try again." };
55 }
56 const saved = await work.saveRuleset(user, owner, ruleset, params.id === "new" ? undefined : params.id);
57 return saved.ok ? redirect(list) : { error: saved.error.message };
58}
59
60export default function RepoRuleset({ loaderData, actionData, params }: Route.ComponentProps) {
61 const { existing, seen, editable, repository } = loaderData;
62 const list = `/${params.owner}/${params.repo}/settings/rules`;
63 return (
64 <div className="max-w-4xl">
65 <header className="mb-6 border-b border-line pb-5">
66 <Link to={list} className="mb-3 inline-flex items-center gap-1.5 text-sm text-muted hover:text-fg">
67 <ArrowLeft size={14} /> Rules
68 </Link>
69 <h1 className="flex flex-wrap items-center gap-2 text-lg font-semibold tracking-tight">
70 {existing ? existing.name : "New ruleset"}
71 {existing && <EnforcementBadge enforcement={existing.enforcement} />}
72 </h1>
73 <p className="mt-1 text-sm text-muted">
74 {existing ? (
75 <>
76 Changed <TimeAgo at={existing.updated_at} /> by <span className="font-mono">{existing.updated_by}</span>. Created by{" "}
77 <span className="font-mono">{existing.created_by}</span>.
78 </>
79 ) : (
80 "What may happen to this repository's branches or tags, and what a pull request needs before it merges."
81 )}
82 </p>
83 </header>
84 <RulesetForm
85 key={existing?.id ?? "new"}
86 initial={existing ?? newRuleset("repository")}
87 level="repository"
88 existing={existing ?? undefined}
89 seen={seen}
90 editable={editable}
91 error={actionData && "error" in actionData ? actionData.error : null}
92 backHref={list}
93 repositoryLabel={repository}
94 />
95 </div>
96 );
97}

This file's history is long; its oldest lines are credited to the oldest commit read.