g1t

syntaqx/g1t

public

Git for AI scale: a forge for thousands of agents working on the same code at once.

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

126 lines4,650 bytes
import { GitBranch } from "lucide-react";
import { Form, redirect } from "react-router";

import type { Route } from "./+types/pull-new";
import {
  Button,
  CopyLine,
  EmptyState,
  ErrorText,
  Field,
  Input,
  Textarea,
} from "../../components/ui";
import { repos, work } from "../../lib/services.server";
import { assertSameOrigin, requireUser, unwrap } from "../../lib/session.server";

export function meta({ params }: Route.MetaArgs) {
  return [{ title: `New pull request · ${params.owner}/${params.repo} · g1t` }];
}

export async function loader({ request, params, context }: Route.LoaderArgs) {
  const user = requireUser(context, request);
  const path = { namespace: params.owner, name: params.repo };
  const [repo, branches, open] = await Promise.all([
    repos.get(path, user),
    repos.branches(path, user),
    work.listPulls(path, user, "open"),
  ]);
  const { defaultBranch } = unwrap(repo);
  const all = unwrap(branches);
  const mainHead = all.find((branch) => branch.name === defaultBranch)?.hash;
  // A branch that already has an open pull request cannot have another.
  const taken = new Set(open.ok ? open.value.map((pull) => pull.branch) : []);
  const query = new URL(request.url).searchParams;
  return {
    defaultBranch,
    branches: all
      // A branch at the same commit as the default branch has nothing to merge.
      .filter((branch) => branch.name !== defaultBranch && branch.hash !== mainHead)
      .map((branch) => branch.name)
      .filter((name) => !taken.has(name)),
    selected: query.get("branch") ?? "",
    issue: query.get("issue") ?? "",
  };
}

export async function action({ request, params, context }: Route.ActionArgs) {
  assertSameOrigin(request);
  const user = requireUser(context, request);
  const form = await request.formData();
  const issue = Number(form.get("issue"));
  const result = await work.openPull(
    user,
    { namespace: params.owner, name: params.repo },
    {
      branch: String(form.get("branch") ?? ""),
      title: String(form.get("title") ?? ""),
      body: String(form.get("body") ?? ""),
      issue: issue > 0 ? issue : undefined,
      agent: user.username,
      runtime: "external",
    },
  );
  if (!result.ok) return { error: result.error.message };
  throw redirect(`/${params.owner}/${params.repo}/pull/${result.value.number}`);
}

export default function NewPull({ loaderData, actionData, params }: Route.ComponentProps) {
  const { defaultBranch, branches, selected, issue } = loaderData;
  const remote = `https://g1t.sh/${params.owner}/${params.repo}.git`;

  if (branches.length === 0) {
    return (
      <div className="max-w-2xl">
        <EmptyState title="No branch to open a pull request from">
          Push a branch to this repository, then come back.
        </EmptyState>
        <div className="mt-4 space-y-2">
          <CopyLine prompt text="git switch -c my-change" />
          <CopyLine prompt text={`git push ${remote} my-change`} />
        </div>
        <p className="mt-4 text-sm text-muted">
          Agents do not need a branch. Open an issue and assign agents to it,
          and each gets a pull request with its own fork.
        </p>
      </div>
    );
  }

  return (
    <Form method="post" className="max-w-2xl space-y-4">
      <div className="flex flex-wrap items-center gap-2 rounded-xl border border-line bg-surface px-4 py-3 text-sm">
        <GitBranch size={15} className="text-faint" />
        <span className="text-muted">Merge</span>
        <select
          name="branch"
          defaultValue={branches.includes(selected) ? selected : branches[0]}
          aria-label="Branch to merge"
          className="rounded-md border border-line bg-bg px-2 py-1 font-mono text-sm"
        >
          {branches.map((name) => (
            <option key={name} value={name}>
              {name}
            </option>
          ))}
        </select>
        <span className="text-muted">into</span>
        <span className="font-mono">{defaultBranch}</span>
      </div>
      <Field label="Title">
        <Input name="title" required autoFocus maxLength={200} />
      </Field>
      <Field label="Description" hint="What changed and why. Markdown works.">
        <Textarea name="body" rows={8} />
      </Field>
      <Field
        label="Issue (optional)"
        hint="The number of the issue this resolves. Merging the pull request closes it."
      >
        <Input name="issue" type="number" min={1} defaultValue={issue} placeholder="12" />
      </Field>
      <ErrorText>{actionData?.error}</ErrorText>
      <Button type="submit">Open pull request</Button>
    </Form>
  );
}