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

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