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.
| Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97 | 1 | import { Form, data, redirect } from "react-router"; |
| 2 | ||
| 3 | import { MAX_RELEASE_BODY_CHARS, MAX_RELEASE_NAME_CHARS } from "@g1t/contracts"; | |
| 4 | ||
| 5 | import type { Route } from "./+types/release-new"; | |
| 6 | import { encodeTag } from "../../components/releases"; | |
| 7 | import { ErrorText, Field, Input, SubmitButton, Textarea } from "../../components/ui"; | |
| 8 | import { requireRepo } from "../../lib/access.server"; | |
| 9 | import { page } from "../../lib/meta"; | |
| 10 | import { repos } from "../../lib/services.server"; | |
| 11 | import { assertSameOrigin, requireUser } from "../../lib/session.server"; | |
| 12 | ||
| 13 | export function meta({ params, ...args }: Route.MetaArgs) { | |
| 14 | return page(args, { title: `New release · ${params.owner}/${params.repo} · g1t` }); | |
| 15 | } | |
| 16 | ||
| 17 | export async function loader({ params, context }: Route.LoaderArgs) { | |
| 18 | const { viewer, repo } = await requireRepo(context, params, "push"); | |
| 19 | const path = { namespace: params.owner, name: params.repo }; | |
| 20 | const [tags, branches] = await Promise.all([ | |
| 21 | repos.tags(path, viewer).then((found) => (found.ok ? found.value.map((tag) => tag.name) : [])).catch(() => []), | |
| 22 | repos.branches(path, viewer).then((found) => (found.ok ? found.value.map((branch) => branch.name) : [])).catch(() => []), | |
| 23 | ]); | |
| 24 | return { tags, branches, defaultBranch: repo.defaultBranch }; | |
| 25 | } | |
| 26 | ||
| 27 | export async function action({ request, params, context }: Route.ActionArgs) { | |
| 28 | assertSameOrigin(request); | |
| 29 | const user = requireUser(context, request); | |
| 30 | const form = await request.formData(); | |
| 31 | const text = (key: string) => String(form.get(key) ?? "").trim(); | |
| 32 | const tagName = text("tag"); | |
| 33 | if (!tagName) return data({ error: "Choose a tag, or name a new one." }, { status: 400 }); | |
| 34 | const created = await repos.createRelease(user, { namespace: params.owner, name: params.repo }, { | |
| 35 | tagName, | |
| 36 | target: text("target") || null, | |
| 37 | name: text("name") || null, | |
| 38 | body: String(form.get("body") ?? ""), | |
| 39 | draft: form.get("intent") === "draft", | |
| 40 | prerelease: form.get("prerelease") === "on", | |
| 41 | }); | |
| 42 | if (!created.ok) return data({ error: created.error.message }, { status: 400 }); | |
| 43 | throw redirect(`/${params.owner}/${params.repo}/releases/tag/${encodeTag(created.value.tagName)}`); | |
| 44 | } | |
| 45 | ||
| 46 | export default function NewRelease({ loaderData, actionData }: Route.ComponentProps) { | |
| 47 | const { tags, branches, defaultBranch } = loaderData; | |
| 48 | return ( | |
| 49 | <div className="mx-auto max-w-3xl space-y-5"> | |
| 50 | <div> | |
| 51 | <h2 className="text-xl font-semibold tracking-tight">New release</h2> | |
| 52 | <p className="mt-1 text-sm text-muted"> | |
| 53 | Publish a tag with a title and notes. A tag that does not exist yet is made at the branch or commit you choose. | |
| 54 | </p> | |
| 55 | </div> | |
| 56 | <Form method="post" className="space-y-4 rounded-xl border border-line bg-surface p-5"> | |
| 57 | <div className="grid gap-4 sm:grid-cols-2"> | |
| 58 | <Field label="Tag" hint={tags.length > 0 ? "An existing tag, or a new one such as v1.0.0." : "A new tag, such as v1.0.0."}> | |
| 59 | <Input name="tag" list="release-tags" required placeholder="v1.0.0" /> | |
| 60 | </Field> | |
| 61 | <Field label="Target" hint="Where a new tag is made. Ignored for a tag that exists."> | |
| 62 | <Input name="target" list="release-targets" placeholder={defaultBranch} /> | |
| 63 | </Field> | |
| 64 | </div> | |
| 65 | <datalist id="release-tags"> | |
| 66 | {tags.map((tag) => ( | |
| 67 | <option key={tag} value={tag} /> | |
| 68 | ))} | |
| 69 | </datalist> | |
| 70 | <datalist id="release-targets"> | |
| 71 | {branches.map((branch) => ( | |
| 72 | <option key={branch} value={branch} /> | |
| 73 | ))} | |
| 74 | </datalist> | |
| 75 | <Field label="Title"> | |
| 76 | <Input name="name" maxLength={MAX_RELEASE_NAME_CHARS} placeholder="What this release is" /> | |
| 77 | </Field> | |
| 78 | <Field label="Notes" hint="Markdown: what changed, and anything people need to do."> | |
| 79 | <Textarea name="body" rows={12} maxLength={MAX_RELEASE_BODY_CHARS} placeholder={"## What changed\n\n- "} /> | |
| 80 | </Field> | |
| 81 | <label className="flex items-center gap-2 text-sm"> | |
| 82 | <input type="checkbox" name="prerelease" className="size-4" /> | |
| 83 | Set as a pre-release: not ready for everyone, never the latest | |
| 84 | </label> | |
| 85 | {actionData?.error && <ErrorText>{actionData.error}</ErrorText>} | |
| 86 | <div className="flex flex-wrap items-center gap-2"> | |
| 87 | <SubmitButton name="intent" value="publish" pending="Publishing…"> | |
| 88 | Publish release | |
| 89 | </SubmitButton> | |
| 90 | <SubmitButton variant="quiet" name="intent" value="draft" pending="Saving…"> | |
| 91 | Save draft | |
| 92 | </SubmitButton> | |
| 93 | </div> | |
| 94 | </Form> | |
| 95 | </div> | |
| 96 | ); | |
| 97 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.