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

208 lines10,877 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";
Merge branch 'worktree-agent-a985c01f43538af01'2import { Form, Link } from "react-router";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look3
4import type { Route } from "./+types/settings-branches";
5import { page } from "../../lib/meta";
Fast pages, required checks on the branch, self-hosted runners, honest incidents6import { AddCiPrompt } from "../../components/add-ci";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look7import { RepoSettingsHeading } from "../../components/repo-settings-heading";
Fast pages, required checks on the branch, self-hosted runners, honest incidents8import { RequiredChecksPicker } from "../../components/required-checks";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look9import { SettingChoice as Choice, SettingsSection as Section, SettingToggle as Toggle } from "../../components/settings-section";
Merge branch 'worktree-agent-a985c01f43538af01'10import { ErrorText, SubmitButton, TimeAgo } from "../../components/ui";
Fast pages, required checks on the branch, self-hosted runners, honest incidents11import { actions, repos, work } from "../../lib/services.server";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look12import { assertSameOrigin, getViewer, requireUser, unwrap } from "../../lib/session.server";
13import { requireCapability, requireInsider } from "../../lib/access.server";
14
15export function meta({ params, ...args }: Route.MetaArgs) {
16 return page(args, { title: `Branches and merging · ${params.owner}/${params.repo} · g1t` });
17}
18
19export async function loader({ params, context }: Route.LoaderArgs) {
20 const viewer = getViewer(context);
21 // Maintain and up; to anyone without a role here the page does not exist.
Fast pages, required checks on the branch, self-hosted runners, honest incidents22 const { access } = await requireInsider(context, params, "manage_protection");
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look23 const path = { namespace: params.owner, name: params.repo };
Fast pages, required checks on the branch, self-hosted runners, honest incidents24 const [repo, settings, seen, workflows] = await Promise.all([
25 repos.get(path, viewer),
26 work.getSettings(path, viewer),
27 work.seenChecks(path, viewer),
28 actions.workflows(path, viewer),
29 ]);
30 return {
31 repo: unwrap(repo),
32 settings: unwrap(settings),
33 // The names to choose required checks from: what reported lately.
34 seen: seen.ok ? seen.value : [],
35 // Nothing to require until something runs: the page offers to add CI.
36 noChecks: workflows.ok && workflows.value.length === 0 && seen.ok && seen.value.length === 0,
37 canPush: access.can.push,
38 };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look39}
40
41/** A whole number from a form field, kept within `min` and `max`. */
42function count(value: FormDataEntryValue | null, min: number, max: number): number {
43 const number = Math.trunc(Number(value));
44 return Number.isFinite(number) ? Math.min(Math.max(number, min), max) : min;
45}
46
47export async function action({ request, params, context }: Route.ActionArgs) {
48 assertSameOrigin(request);
49 const user = requireUser(context, request);
50 await requireCapability(context, params, "manage_protection");
51 const form = await request.formData();
52 const path = { namespace: params.owner, name: params.repo };
53 const on = (name: string) => form.get(name) === "on";
54 // Protection belongs to the repository and how its pull requests are
55 // handled to the work service; the page is one form over both.
56 const repo = await repos.update(user, path, { protected: on("protected") });
57 if (!repo.ok) return { saved: false, error: repo.error.message };
58 const settings = await work.updateSettings(user, path, {
59 autoMerge: on("autoMerge"),
Fast pages, required checks on the branch, self-hosted runners, honest incidents60 requiredChecks: form.getAll("requiredChecks").map(String),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look61 requireUpToDate: on("requireUpToDate"),
62 requiredApprovals: count(form.get("requiredApprovals"), 0, 6),
63 countAgentApprovals: on("countAgentApprovals"),
Fast pages, required checks on the branch, self-hosted runners, honest incidents64 allowIgnoringChecks: on("bypassChecks"),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look65 agentReview: on("agentReview"),
66 maxRevisions: count(form.get("maxRevisions"), 0, 5),
67 mergeQueue: on("mergeQueue"),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step68 holdLowConfidence: on("holdLowConfidence"),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look69 });
70 return settings.ok ? { saved: true, error: null } : { saved: false, error: settings.error.message };
71}
72
73export default function BranchSettings({ loaderData, actionData }: Route.ComponentProps) {
Fast pages, required checks on the branch, self-hosted runners, honest incidents74 const { repo, settings, seen, noChecks, canPush } = loaderData;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look75 const branch = repo.defaultBranch;
76 const base = `/${repo.namespace}/${repo.name}`;
77 const archived = Boolean(repo.archivedAt);
78 return (
79 <>
80 <RepoSettingsHeading base={base} />
Fast pages, required checks on the branch, self-hosted runners, honest incidents81 {/* Its own form, so outside the settings one. */}
82 {noChecks && (
83 <div className="mb-8 max-w-4xl">
84 <AddCiPrompt owner={repo.namespace} repo={repo.name} canAdd={canPush && !archived} />
85 </div>
86 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look87 <Form method="post" className="max-w-4xl">
88 <fieldset disabled={archived} className="min-w-0 space-y-8">
Fast pages, required checks on the branch, self-hosted runners, honest incidents89 <Section title="Branch protection" about={`Rules for ${branch}, the branch every pull request merges into. They hold for people and agents alike.`}>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look90 <Toggle name="protected" on={repo.protected} title={`Require a pull request to change ${branch}`}>
91 Pushing to {branch} is refused, for members and agents alike, and git says why. Changes reach it only by
92 merging a pull request. The first push to an empty repository is still allowed.
93 </Toggle>
Fast pages, required checks on the branch, self-hosted runners, honest incidents94 <RequiredChecksPicker
95 required={settings.requiredChecks ?? []}
96 seen={seen}
97 mergeQueue={settings.mergeQueue}
98 disabled={archived}
99 />
100 <Toggle name="bypassChecks" on={settings.allowIgnoringChecks} title="Allow bypassing required checks">
101 Someone who may merge can tick a box to merge although a required check failed or has not finished, and
102 the pull request says who did. With this off, nobody can, and auto-merge never does.
103 </Toggle>
104 <Toggle
105 name="requireUpToDate"
106 on={settings.requireUpToDate}
107 title="Require branches to be up to date before merging"
108 >
109 With this off, a pull request can be merged after {branch} has moved: g1t brings it up to date as part of
110 merging, and asks you only if there is a conflict it cannot resolve. With it on, it has to catch up first
111 and its required checks pass again on the result, so what lands is exactly what was checked.
112 </Toggle>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look113 <Choice
114 name="requiredApprovals"
115 value={settings.requiredApprovals}
116 title="Required approvals"
117 options={[
118 [0, "None"],
119 [1, "1"],
120 [2, "2"],
121 [3, "3"],
122 ]}
123 >
124 How many reviewers must approve before a pull request can merge. A reviewer who has since asked for
125 changes blocks it, and nobody approves their own.
126 </Choice>
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent127 <Toggle name="countAgentApprovals" on={settings.countAgentApprovals} title="g1t's approval counts">
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look128 With this off, required approvals have to come from people, and an agent's review is advice.
129 </Toggle>
130 <Toggle name="mergeQueue" on={settings.mergeQueue} title="Merge through a queue">
Fast pages, required checks on the branch, self-hosted runners, honest incidents131 Merging adds a pull request to the queue instead of changing {branch} at once. g1t builds it together with
132 every pull request ahead of it, several combinations at a time, and runs the workflows that run on{" "}
133 <code className="text-fg">merge_group</code> on each. {branch} only ever moves to a combination whose
134 required checks passed. One that fails leaves the queue and goes back to its author.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look135 </Toggle>
136 </Section>
137
138 <Section
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent139 title="g1t"
140 about="What happens to a pull request g1t makes, from the moment it is ready. Its checks are the same workflows, and the rules above hold."
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look141 >
142 <Toggle name="agentReview" on={settings.agentReview} title="Review by a second agent">
143 A different agent reads each change and posts comments on lines, a summary and a verdict. If it asks for
144 changes, the author is sent back to make them. With this off, review is left to people.
145 </Toggle>
146 <Choice
147 name="maxRevisions"
148 value={settings.maxRevisions}
149 title="Revisions before asking you"
150 options={[
151 [0, "None"],
152 [1, "1"],
153 [2, "2"],
154 [3, "3"],
155 [5, "5"],
156 ]}
157 >
Fast pages, required checks on the branch, self-hosted runners, honest incidents158 How many times an agent is sent back to fix a failed check, with what its jobs printed, or to address a
159 review, before g1t stops and the pull request says it needs you. After that, only a required check that
160 still fails holds it.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look161 </Choice>
162 <Toggle name="autoMerge" on={settings.autoMerge} title="Merge automatically when ready">
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent163 A pull request g1t made lands without anyone pressing merge once every rule above is met, its required
Fast pages, required checks on the branch, self-hosted runners, honest incidents164 checks included. With this off, it waits for a member. Pull requests from people and from other agents
165 always wait.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look166 </Toggle>
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step167 <Toggle
168 name="holdLowConfidence"
169 on={settings.holdLowConfidence}
170 title="Ask a person before merging low-confidence changes"
171 >
Fast pages, required checks on the branch, self-hosted runners, honest incidents172 g1t rates how sure it is of each change an agent finishes, from its required checks, revisions, review,
173 tests, size and guardrails. One it rates low waits for a member to approve it, instead of merging by itself
174 or joining the queue, and shows on Mission control as needing you.
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step175 </Toggle>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look176 <Link
177 to={`${base}/settings/guardrails`}
178 className="group flex items-center gap-3 rounded-xl border border-line p-4 transition-colors hover:border-line-strong hover:bg-surface"
179 >
180 <ShieldCheck size={16} className="shrink-0 text-accent" />
181 <span className="min-w-0 grow">
182 <span className="block text-sm font-medium">Guardrails</span>
183 <span className="mt-0.5 block text-sm text-muted">
184 What agents may reach, run and spend while they work on this repository.
185 </span>
186 </span>
187 <ChevronRight size={16} className="shrink-0 text-faint transition-transform group-hover:translate-x-0.5" />
188 </Link>
189 </Section>
190
191 <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">
Merge branch 'worktree-agent-a985c01f43538af01'192 <SubmitButton pending="Saving…" disabled={archived}>
193 Save settings
194 </SubmitButton>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look195 {actionData?.saved && <span className="text-sm text-muted">Saved.</span>}
196 <ErrorText>{actionData?.error}</ErrorText>
197 {settings.updatedBy && settings.updatedAt && !actionData && (
198 <span className="text-xs text-faint">
199 Merge rules last changed by <span className="font-mono">{settings.updatedBy}</span>{" "}
200 <TimeAgo at={settings.updatedAt} />
201 </span>
202 )}
203 </div>
204 </fieldset>
205 </Form>
206 </>
207 );
208}

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