import { forbidden, notFoundError } from "../errors"; import { limitHeavy, limitWrite } from "../limits"; import { latestRuns } from "../mergefix/store"; import { startMergeFix } from "../mergefix/service"; import type { FixPlan } from "../mergefix/plan"; import { loadPull } from "../pulls"; import { hasPermission, loadRepoById } from "../repos"; import { scopedRepoPermission } from "../token-scope"; import { requireScopedRepo } from "./http"; import type { ApiContext } from "./http"; const HTTP_ACCEPTED = 202; async function loadScopedPull(ctx: ApiContext, owner: string, name: string, number: number, needed: "read" | "write") { const { repo } = await requireScopedRepo(ctx, owner, name, "pulls", needed); const pull = await loadPull(repo.id, number); if (!pull) throw notFoundError("Pull request not found"); return { repo, pull }; } /** Starts an AI conflict fix; like update-branch, the token must also be able to push to the head repo. */ export async function startPullAiFix(ctx: ApiContext, owner: string, name: string, number: number) { await limitWrite(ctx.viewer); const { repo, pull } = await loadScopedPull(ctx, owner, name, number, "write"); const headRepo = pull.head_repo_id === repo.id ? repo : await loadRepoById(pull.head_repo_id); if (!headRepo) throw notFoundError("Head repository was deleted"); if (!hasPermission(await scopedRepoPermission(headRepo, ctx.credential, "contents"), "write")) { throw forbidden("This token cannot push to the head branch"); } const id = await startMergeFix(pull, ctx.viewer, "button"); return Response.json({ id }, { status: HTTP_ACCEPTED }); } export async function getPullAiFix(ctx: ApiContext, owner: string, name: string, number: number) { await limitHeavy(ctx.viewer); const { pull } = await loadScopedPull(ctx, owner, name, number, "read"); const [latest] = await latestRuns(pull.issue_id, 1); if (!latest) throw notFoundError("No AI fix has run for this pull request"); const plan = latest.result_json ? (JSON.parse(latest.result_json) as FixPlan) : null; return Response.json({ id: latest.id, status: latest.status, error: latest.error, head_sha: latest.head_sha, commit_sha: latest.commit_sha, calls: latest.calls, requeue: latest.requeue_status ? { status: latest.requeue_status, error: latest.requeue_error } : null, auto_apply: plan?.autoApply ?? false, files: (plan?.files ?? []).map((file) => file.status === "skipped" ? { path: file.path, status: file.status, reason: file.reason } : { path: file.path, status: file.status, risk: file.decision.risk.level, auto_apply: file.decision.autoApply, reasons: file.decision.reasons, hunks: file.hunks.map((hunk) => ({ selected: hunk.selected, probabilities: hunk.probabilities })), }, ), }); }