Skip to content

g1t/apps/web/app/routes/repo/pull-new.tsx

146 lines5,756 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.

Pull requests from branches1import { GitBranch } from "lucide-react";
2import { Form, redirect } from "react-router";
3
4import type { Route } from "./+types/pull-new";
Social cards for every page: og.g1t.sh5import { page } from "../../lib/meta";
Merge branch 'worktree-agent-aaf03bdceac799c89'6import { cloneUrl, useAddresses } from "../../lib/addresses";
Workspace names and icons, and a component kit for every control7import { Combobox } from "../../components/ui/combobox";
Pull requests from branches8import {
Merge branch 'worktree-agent-a985c01f43538af01'9 SubmitButton,
Pull requests from branches10 CopyLine,
11 EmptyState,
12 ErrorText,
13 Field,
14 Input,
15 Textarea,
16} from "../../components/ui";
17import { repos, work } from "../../lib/services.server";
18import { assertSameOrigin, requireUser, unwrap } from "../../lib/session.server";
19
Social cards for every page: og.g1t.sh20export function meta({ params, ...args }: Route.MetaArgs) {
21 return page(args, { title: `New pull request · ${params.owner}/${params.repo} · g1t` });
Pull requests from branches22}
23
24export async function loader({ request, params, context }: Route.LoaderArgs) {
25 const user = requireUser(context, request);
26 const path = { namespace: params.owner, name: params.repo };
27 const [repo, branches, open] = await Promise.all([
28 repos.get(path, user),
29 repos.branches(path, user),
30 work.listPulls(path, user, "open"),
31 ]);
32 const { defaultBranch } = unwrap(repo);
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar33 const all = unwrap(branches).filter((branch) => !branch.name.startsWith("g1t-queue/"));
Pull requests from branches34 const query = new URL(request.url).searchParams;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar35 // Into the default branch unless another is asked for (Compare's base).
36 const asked = query.get("base") ?? "";
37 const base = all.some((branch) => branch.name === asked) ? asked : defaultBranch;
38 const baseHead = all.find((branch) => branch.name === base)?.hash;
39 // A branch that already has an open pull request into this base cannot
40 // have another.
41 const taken = new Set(open.ok ? open.value.filter((pull) => (pull.base ?? defaultBranch) === base).map((pull) => pull.branch) : []);
Pull requests from branches42 return {
43 defaultBranch,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar44 base,
45 bases: all.map((branch) => branch.name),
Pull requests from branches46 branches: all
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar47 // A branch at the same commit as the base has nothing to merge.
48 .filter((branch) => branch.name !== base && branch.hash !== baseHead)
Pull requests from branches49 .map((branch) => branch.name)
50 .filter((name) => !taken.has(name)),
51 selected: query.get("branch") ?? "",
52 issue: query.get("issue") ?? "",
53 };
54}
55
56export async function action({ request, params, context }: Route.ActionArgs) {
57 assertSameOrigin(request);
58 const user = requireUser(context, request);
59 const form = await request.formData();
60 const issue = Number(form.get("issue"));
61 const result = await work.openPull(
62 user,
63 { namespace: params.owner, name: params.repo },
64 {
65 branch: String(form.get("branch") ?? ""),
66 title: String(form.get("title") ?? ""),
67 body: String(form.get("body") ?? ""),
68 issue: issue > 0 ? issue : undefined,
69 agent: user.username,
70 runtime: "external",
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar71 base: String(form.get("base") ?? "") || undefined,
Pull requests from branches72 },
73 );
74 if (!result.ok) return { error: result.error.message };
75 throw redirect(`/${params.owner}/${params.repo}/pull/${result.value.number}`);
76}
77
78export default function NewPull({ loaderData, actionData, params }: Route.ComponentProps) {
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar79 const { defaultBranch, base, bases, branches, selected, issue } = loaderData;
Merge branch 'worktree-agent-aaf03bdceac799c89'80 const remote = cloneUrl(useAddresses(), `${params.owner}/${params.repo}`);
Pull requests from branches81
82 if (branches.length === 0) {
83 return (
84 <div className="max-w-2xl">
85 <EmptyState title="No branch to open a pull request from">
86 Push a branch to this repository, then come back.
87 </EmptyState>
88 <div className="mt-4 space-y-2">
89 <CopyLine prompt text="git switch -c my-change" />
90 <CopyLine prompt text={`git push ${remote} my-change`} />
91 </div>
92 <p className="mt-4 text-sm text-muted">
93 Agents do not need a branch. Open an issue and assign agents to it,
94 and each gets a pull request with its own fork.
95 </p>
96 </div>
97 );
98 }
99
100 return (
101 <Form method="post" className="max-w-2xl space-y-4">
102 <div className="flex flex-wrap items-center gap-2 rounded-xl border border-line bg-surface px-4 py-3 text-sm">
103 <GitBranch size={15} className="text-faint" />
104 <span className="text-muted">Merge</span>
Workspace names and icons, and a component kit for every control105 <Combobox
Pull requests from branches106 name="branch"
107 defaultValue={branches.includes(selected) ? selected : branches[0]}
108 aria-label="Branch to merge"
Workspace names and icons, and a component kit for every control109 searchPlaceholder="Find a branch"
110 emptyText="No branch by that name."
111 options={branches.map((name) => ({ value: name, label: name, icon: <GitBranch /> }))}
112 className="h-8 w-auto max-w-full min-w-40 font-mono"
113 />
Pull requests from branches114 <span className="text-muted">into</span>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar115 <Combobox
116 name="base"
117 defaultValue={base}
118 aria-label="Branch to merge into"
119 searchPlaceholder="Find a branch"
120 emptyText="No branch by that name."
121 options={bases.map((name) => ({
122 value: name,
123 label: name,
124 icon: <GitBranch />,
125 description: name === defaultBranch ? "default" : undefined,
126 }))}
127 className="h-8 w-auto max-w-full min-w-32 font-mono"
128 />
Pull requests from branches129 </div>
130 <Field label="Title">
131 <Input name="title" required autoFocus maxLength={200} />
132 </Field>
133 <Field label="Description" hint="What changed and why. Markdown works.">
134 <Textarea name="body" rows={8} />
135 </Field>
136 <Field
137 label="Issue (optional)"
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar138 hint={`The number of the issue this resolves. Merging the pull request into ${defaultBranch} closes it.`}
Pull requests from branches139 >
140 <Input name="issue" type="number" min={1} defaultValue={issue} placeholder="12" />
141 </Field>
142 <ErrorText>{actionData?.error}</ErrorText>
Merge branch 'worktree-agent-a985c01f43538af01'143 <SubmitButton pending="Opening…">Open pull request</SubmitButton>
Pull requests from branches144 </Form>
145 );
146}

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