Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

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