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

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