import { createServerFn } from "@tanstack/react-start"; import { z } from "zod"; import { requireViewer, viewerMiddleware } from "./middleware"; import { pullInput, requirePullMutation } from "~/server/pull-access"; import { limitWrite } from "~/server/limits"; import { requireHeadPush } from "~/server/pull-merge"; import { MergeError } from "~/server/pulls"; import { kickBranch } from "~/server/merge-automation"; import { enableStackMerge, restackTree, stackParent, StackError } from "~/server/stacks"; import { badRequest, conflict } from "~/server/errors"; /** Replays a stacked pull request's own commits onto its parent's current head, cascading up the stack. */ export const restackPullRequest = createServerFn({ method: "POST" }) .middleware([viewerMiddleware]) .validator(pullInput.extend({ expectedHead: z.string().length(40) })) .handler(async ({ data, context }) => { const viewer = requireViewer(context.viewer); await limitWrite(viewer); const { pull, headRepo } = await requirePullMutation(data.owner, data.repo, data.number, viewer, "read"); if (pull.state !== "open") throw badRequest("Pull request is not open"); if (pull.head_sha !== data.expectedHead) throw conflict("The head branch changed; reload and try again"); if (headRepo.id !== pull.repo_id) throw badRequest("Only same-repository branches can be restacked here"); await requireHeadPush(headRepo, pull.head_ref, viewer); const parent = await stackParent(pull); if (!parent) throw badRequest("This pull request is not stacked on another one"); const outcome = await restackTree(pull, parent.head_sha, [pull.restack_upstream_sha, pull.base_sha], { actorId: viewer.id, committer: { name: viewer.displayName ?? viewer.name, email: viewer.email }, parentNumber: parent.number, ontoRef: parent.head_ref, }); if (outcome === "conflict") throw conflict("The commits conflict with the parent; restack locally and force-push"); if (outcome === "unsupported") throw conflict("This history can't be restacked on the server; restack locally"); if (outcome === "moved") throw conflict("The head branch moved; reload and try again"); return { outcome }; }); /** Merges every pull request from the bottom of the stack up to this one, each as its checks pass. */ export const mergeStack = createServerFn({ method: "POST" }) .middleware([viewerMiddleware]) .validator(pullInput.extend({ method: z.enum(["merge", "squash", "rebase"]) })) .handler(async ({ data, context }) => { const viewer = requireViewer(context.viewer); await limitWrite(viewer); const { pull } = await requirePullMutation(data.owner, data.repo, data.number, viewer, "write"); try { const { queued, baseRef } = await enableStackMerge(pull, viewer.id, data.method); await kickBranch(pull.repo_id, baseRef); return { queued }; } catch (error: unknown) { if (error instanceof StackError) throw badRequest(error.message); if (error instanceof MergeError) throw conflict(error.message); throw error; } });