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

179 lines9,486 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.

Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1import { ChevronRight, ShieldCheck } from "lucide-react";
2import { Form, Link, useNavigation } from "react-router";
3
4import type { Route } from "./+types/settings-branches";
5import { page } from "../../lib/meta";
6import { RepoSettingsHeading } from "../../components/repo-settings-heading";
7import { SettingChoice as Choice, SettingsSection as Section, SettingToggle as Toggle } from "../../components/settings-section";
8import { Button, ErrorText, TimeAgo } from "../../components/ui";
9import { repos, work } from "../../lib/services.server";
10import { assertSameOrigin, getViewer, requireUser, unwrap } from "../../lib/session.server";
11import { requireCapability, requireInsider } from "../../lib/access.server";
12
13export function meta({ params, ...args }: Route.MetaArgs) {
14 return page(args, { title: `Branches and merging · ${params.owner}/${params.repo} · g1t` });
15}
16
17export async function loader({ params, context }: Route.LoaderArgs) {
18 const viewer = getViewer(context);
19 // Maintain and up; to anyone without a role here the page does not exist.
20 await requireInsider(context, params, "manage_protection");
21 const path = { namespace: params.owner, name: params.repo };
22 const [repo, settings] = await Promise.all([repos.get(path, viewer), work.getSettings(path, viewer)]);
23 return { repo: unwrap(repo), settings: unwrap(settings) };
24}
25
26/** A whole number from a form field, kept within `min` and `max`. */
27function count(value: FormDataEntryValue | null, min: number, max: number): number {
28 const number = Math.trunc(Number(value));
29 return Number.isFinite(number) ? Math.min(Math.max(number, min), max) : min;
30}
31
32export async function action({ request, params, context }: Route.ActionArgs) {
33 assertSameOrigin(request);
34 const user = requireUser(context, request);
35 await requireCapability(context, params, "manage_protection");
36 const form = await request.formData();
37 const path = { namespace: params.owner, name: params.repo };
38 const on = (name: string) => form.get(name) === "on";
39 // Protection belongs to the repository and how its pull requests are
40 // handled to the work service; the page is one form over both.
41 const repo = await repos.update(user, path, { protected: on("protected") });
42 if (!repo.ok) return { saved: false, error: repo.error.message };
43 const settings = await work.updateSettings(user, path, {
44 autoMerge: on("autoMerge"),
45 requireUpToDate: on("requireUpToDate"),
46 requiredApprovals: count(form.get("requiredApprovals"), 0, 6),
47 countAgentApprovals: on("countAgentApprovals"),
48 allowIgnoringChecks: !on("requireChecks"),
49 agentReview: on("agentReview"),
50 maxRevisions: count(form.get("maxRevisions"), 0, 5),
51 mergeQueue: on("mergeQueue"),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step52 holdLowConfidence: on("holdLowConfidence"),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look53 });
54 return settings.ok ? { saved: true, error: null } : { saved: false, error: settings.error.message };
55}
56
57export default function BranchSettings({ loaderData, actionData }: Route.ComponentProps) {
58 const { repo, settings } = loaderData;
59 const saving = useNavigation().state === "submitting";
60 const branch = repo.defaultBranch;
61 const base = `/${repo.namespace}/${repo.name}`;
62 const archived = Boolean(repo.archivedAt);
63 return (
64 <>
65 <RepoSettingsHeading base={base} />
66 <Form method="post" className="max-w-4xl">
67 <fieldset disabled={archived} className="min-w-0 space-y-8">
68 <Section title="Branch protection" about={`Rules for ${branch}, the branch everything lands on.`}>
69 <Toggle name="protected" on={repo.protected} title={`Require a pull request to change ${branch}`}>
70 Pushing to {branch} is refused, for members and agents alike, and git says why. Changes reach it only by
71 merging a pull request. The first push to an empty repository is still allowed.
72 </Toggle>
73 <Choice
74 name="requiredApprovals"
75 value={settings.requiredApprovals}
76 title="Required approvals"
77 options={[
78 [0, "None"],
79 [1, "1"],
80 [2, "2"],
81 [3, "3"],
82 ]}
83 >
84 How many reviewers must approve before a pull request can merge. A reviewer who has since asked for
85 changes blocks it, and nobody approves their own.
86 </Choice>
87 <Toggle name="countAgentApprovals" on={settings.countAgentApprovals} title="A g1t agent's approval counts">
88 With this off, required approvals have to come from people, and an agent's review is advice.
89 </Toggle>
90 <Toggle name="requireChecks" on={!settings.allowIgnoringChecks} title="Require acceptance checks to pass">
91 With this off, a member can choose to merge although the issue's checks failed or have not finished. With
92 it on, nobody can.
93 </Toggle>
94 <Toggle
95 name="requireUpToDate"
96 on={settings.requireUpToDate}
97 title="Require pull requests to be up to date before merging"
98 >
99 With this off, a pull request can be merged after {branch} has moved: g1t brings it up to date as part of
100 merging, and asks you only if there is a conflict it cannot resolve. With it on, it has to catch up first
101 and its checks run again on the result, so what lands is exactly what was checked.
102 </Toggle>
103 <Toggle name="mergeQueue" on={settings.mergeQueue} title="Merge through a queue">
104 Merging adds a pull request to the queue instead of changing {branch} at once. g1t tests it together with
105 every pull request ahead of it, several combinations at a time, and {branch} only ever moves to a
106 combination whose checks passed. One that fails leaves the queue and goes back to its author, and the ones
107 behind it are tested again without it.
108 </Toggle>
109 </Section>
110
111 <Section
112 title="g1t agents"
113 about="What happens to a pull request a g1t agent makes, from the moment it is ready."
114 >
115 <Toggle name="agentReview" on={settings.agentReview} title="Review by a second agent">
116 A different agent reads each change and posts comments on lines, a summary and a verdict. If it asks for
117 changes, the author is sent back to make them. With this off, review is left to people.
118 </Toggle>
119 <Choice
120 name="maxRevisions"
121 value={settings.maxRevisions}
122 title="Revisions before asking you"
123 options={[
124 [0, "None"],
125 [1, "1"],
126 [2, "2"],
127 [3, "3"],
128 [5, "5"],
129 ]}
130 >
131 How many times an agent is sent back to fix failed checks or address a review before g1t stops and the
132 pull request says it needs you.
133 </Choice>
134 <Toggle name="autoMerge" on={settings.autoMerge} title="Merge automatically when ready">
135 A g1t agent's pull request lands without anyone pressing merge once every rule above is met. With this
136 off, it waits for a member. Pull requests from people and from other agents always wait.
137 </Toggle>
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step138 <Toggle
139 name="holdLowConfidence"
140 on={settings.holdLowConfidence}
141 title="Ask a person before merging low-confidence changes"
142 >
143 g1t rates how sure it is of each change an agent finishes, from its checks, revisions, review, tests, size
144 and guardrails. One it rates low waits for a member to approve it, instead of merging by itself or joining
145 the queue, and shows on Mission control as needing you.
146 </Toggle>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look147 <Link
148 to={`${base}/settings/guardrails`}
149 className="group flex items-center gap-3 rounded-xl border border-line p-4 transition-colors hover:border-line-strong hover:bg-surface"
150 >
151 <ShieldCheck size={16} className="shrink-0 text-accent" />
152 <span className="min-w-0 grow">
153 <span className="block text-sm font-medium">Guardrails</span>
154 <span className="mt-0.5 block text-sm text-muted">
155 What agents may reach, run and spend while they work on this repository.
156 </span>
157 </span>
158 <ChevronRight size={16} className="shrink-0 text-faint transition-transform group-hover:translate-x-0.5" />
159 </Link>
160 </Section>
161
162 <div className="sticky bottom-0 -mx-4 flex flex-wrap items-center gap-4 border-t border-line bg-bg/90 px-4 py-4 backdrop-blur">
163 <Button type="submit" disabled={saving || archived}>
164 {saving ? "Saving…" : "Save settings"}
165 </Button>
166 {actionData?.saved && <span className="text-sm text-muted">Saved.</span>}
167 <ErrorText>{actionData?.error}</ErrorText>
168 {settings.updatedBy && settings.updatedAt && !actionData && (
169 <span className="text-xs text-faint">
170 Merge rules last changed by <span className="font-mono">{settings.updatedBy}</span>{" "}
171 <TimeAgo at={settings.updatedAt} />
172 </span>
173 )}
174 </div>
175 </fieldset>
176 </Form>
177 </>
178 );
179}