flagon-io/g1t

public

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

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

125 lines4,775 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";
Workspace names and icons, and a component kit for every control6import { Combobox } from "../../components/ui/combobox";
Pull requests from branches7import {
8 Button,
9 CopyLine,
10 EmptyState,
11 ErrorText,
12 Field,
13 Input,
14 Textarea,
15} from "../../components/ui";
16import { repos, work } from "../../lib/services.server";
17import { assertSameOrigin, requireUser, unwrap } from "../../lib/session.server";
18
Social cards for every page: og.g1t.sh19export function meta({ params, ...args }: Route.MetaArgs) {
20 return page(args, { title: `New pull request · ${params.owner}/${params.repo} · g1t` });
Pull requests from branches21}
22
23export async function loader({ request, params, context }: Route.LoaderArgs) {
24 const user = requireUser(context, request);
25 const path = { namespace: params.owner, name: params.repo };
26 const [repo, branches, open] = await Promise.all([
27 repos.get(path, user),
28 repos.branches(path, user),
29 work.listPulls(path, user, "open"),
30 ]);
31 const { defaultBranch } = unwrap(repo);
32 const all = unwrap(branches);
33 const mainHead = all.find((branch) => branch.name === defaultBranch)?.hash;
34 // A branch that already has an open pull request cannot have another.
35 const taken = new Set(open.ok ? open.value.map((pull) => pull.branch) : []);
36 const query = new URL(request.url).searchParams;
37 return {
38 defaultBranch,
39 branches: all
40 // A branch at the same commit as the default branch has nothing to merge.
41 .filter((branch) => branch.name !== defaultBranch && branch.hash !== mainHead)
42 .map((branch) => branch.name)
43 .filter((name) => !taken.has(name)),
44 selected: query.get("branch") ?? "",
45 issue: query.get("issue") ?? "",
46 };
47}
48
49export async function action({ request, params, context }: Route.ActionArgs) {
50 assertSameOrigin(request);
51 const user = requireUser(context, request);
52 const form = await request.formData();
53 const issue = Number(form.get("issue"));
54 const result = await work.openPull(
55 user,
56 { namespace: params.owner, name: params.repo },
57 {
58 branch: String(form.get("branch") ?? ""),
59 title: String(form.get("title") ?? ""),
60 body: String(form.get("body") ?? ""),
61 issue: issue > 0 ? issue : undefined,
62 agent: user.username,
63 runtime: "external",
64 },
65 );
66 if (!result.ok) return { error: result.error.message };
67 throw redirect(`/${params.owner}/${params.repo}/pull/${result.value.number}`);
68}
69
70export default function NewPull({ loaderData, actionData, params }: Route.ComponentProps) {
71 const { defaultBranch, branches, selected, issue } = loaderData;
72 const remote = `https://g1t.sh/${params.owner}/${params.repo}.git`;
73
74 if (branches.length === 0) {
75 return (
76 <div className="max-w-2xl">
77 <EmptyState title="No branch to open a pull request from">
78 Push a branch to this repository, then come back.
79 </EmptyState>
80 <div className="mt-4 space-y-2">
81 <CopyLine prompt text="git switch -c my-change" />
82 <CopyLine prompt text={`git push ${remote} my-change`} />
83 </div>
84 <p className="mt-4 text-sm text-muted">
85 Agents do not need a branch. Open an issue and assign agents to it,
86 and each gets a pull request with its own fork.
87 </p>
88 </div>
89 );
90 }
91
92 return (
93 <Form method="post" className="max-w-2xl space-y-4">
94 <div className="flex flex-wrap items-center gap-2 rounded-xl border border-line bg-surface px-4 py-3 text-sm">
95 <GitBranch size={15} className="text-faint" />
96 <span className="text-muted">Merge</span>
Workspace names and icons, and a component kit for every control97 <Combobox
Pull requests from branches98 name="branch"
99 defaultValue={branches.includes(selected) ? selected : branches[0]}
100 aria-label="Branch to merge"
Workspace names and icons, and a component kit for every control101 searchPlaceholder="Find a branch"
102 emptyText="No branch by that name."
103 options={branches.map((name) => ({ value: name, label: name, icon: <GitBranch /> }))}
104 className="h-8 w-auto max-w-full min-w-40 font-mono"
105 />
Pull requests from branches106 <span className="text-muted">into</span>
107 <span className="font-mono">{defaultBranch}</span>
108 </div>
109 <Field label="Title">
110 <Input name="title" required autoFocus maxLength={200} />
111 </Field>
112 <Field label="Description" hint="What changed and why. Markdown works.">
113 <Textarea name="body" rows={8} />
114 </Field>
115 <Field
116 label="Issue (optional)"
117 hint="The number of the issue this resolves. Merging the pull request closes it."
118 >
119 <Input name="issue" type="number" min={1} defaultValue={issue} placeholder="12" />
120 </Field>
121 <ErrorText>{actionData?.error}</ErrorText>
122 <Button type="submit">Open pull request</Button>
123 </Form>
124 );
125}