import { z } from "zod"; import { badRequest, conflict, forbidden, notFoundError } from "../../errors"; import { requireHeadPush } from "../../pull-merge"; import { loadPull } from "../../pulls"; import { hasPermission } from "../../repos"; import { scopedRepoPermission } from "../../token-scope"; import { limitWrite } from "../../limits"; import { loadStack, restackTree, stackParent } from "../../stacks"; import { defineTool, repoArgs, scopedRepo } from "./base"; const number = z.number().int().positive(); const getStackTool = defineTool({ name: "get_pull_stack", title: "Get pull request stack", description: "The stack a pull request belongs to, bottom first. A pull request is stacked when its base is another open pull request's head branch; open one with open_pull using the parent's head branch as base.", input: z.object({ ...repoArgs, number }), write: false, async run({ caller }, args) { const { repo } = await scopedRepo(caller, args.owner, args.repo, "pulls", "read"); const pull = await loadPull(repo.id, args.number); if (!pull) throw notFoundError("Pull request not found"); const [stack, parent] = await Promise.all([loadStack(pull), stackParent(pull)]); return { structured: { stacked_on: parent ? parent.number : null, stack: stack.map((entry) => ({ number: entry.number, title: entry.title, state: entry.merged ? "merged" : entry.state, draft: entry.draft, head: entry.headRef, base: entry.baseRef, depth: entry.depth, current: entry.current, })), }, repoId: repo.id, }; }, }); const restackTool = defineTool({ name: "restack_pull", title: "Restack pull request", description: "Replay a stacked pull request's own commits onto its parent's current head (after the parent was amended or force-pushed), cascading to pull requests stacked above it.", input: z.object({ ...repoArgs, number, expected_head: z.string().length(40) }), write: true, async run({ caller }, args) { await limitWrite(caller.viewer); const { repo } = await scopedRepo(caller, args.owner, args.repo, "pulls", "write"); const pull = await loadPull(repo.id, args.number); if (!pull) throw notFoundError("Pull request not found"); if (pull.state !== "open") throw badRequest("Pull request is not open"); if (pull.head_sha !== args.expected_head) throw conflict("New commits were pushed; reload and try again"); if (pull.head_repo_id !== repo.id) throw badRequest("Only same-repository branches can be restacked"); if (!hasPermission(await scopedRepoPermission(repo, caller.credential, "contents"), "write")) { throw forbidden("This token cannot push to the head branch"); } await requireHeadPush(repo, pull.head_ref, caller.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: caller.viewer.id, committer: { name: caller.viewer.displayName ?? caller.viewer.name, email: caller.viewer.email }, parentNumber: parent.number, ontoRef: parent.head_ref, }); return { structured: { outcome, parent: parent.number }, repoId: repo.id }; }, }); export const stackTools = [getStackTool, restackTool];