import { repoDb } from "./scoped-db"; import { badRequest, conflict, forbidden } from "./errors"; import { branchSha, listRefs } from "./git/refs"; import { kickBranch } from "./merge-automation"; import { dequeuePull, enqueuePull } from "./merge-queue"; import { afterPullChange } from "./pull-events"; import { stackParent } from "./stacks"; import { addIssueEvent, MergeError } from "./pulls"; import type { MergeMethod, Viewer } from "~/lib/types"; import type { PullAccess } from "./reviews-write"; function rethrowMerge(error: unknown): never { if (error instanceof MergeError) throw conflict(error.message); throw error; } /** Requires write access. Enabling pins the current head; a missing or stale `expectedHead` is a 409. */ export async function setAutoMerge( access: PullAccess, viewer: Viewer, input: { enabled: boolean; method: MergeMethod; message: string; expectedHead?: string }, ): Promise { const { pull } = access; if (!input.enabled) { await repoDb(pull.repo_id).run("DELETE FROM pull_auto_merges WHERE issue_id = ?", pull.issue_id); await addIssueEvent(pull, viewer.id, "auto_merge_disabled", {}); return; } if (pull.state !== "open") throw badRequest("Pull request is not open"); if (pull.head_sha !== input.expectedHead) throw conflict("The head branch changed; reload and try again"); await repoDb(pull.repo_id).run( `INSERT INTO pull_auto_merges (issue_id, repo_id, method, message, enabled_by, created_at, head_sha) VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT (issue_id) DO UPDATE SET method = excluded.method, message = excluded.message, enabled_by = excluded.enabled_by, head_sha = excluded.head_sha`, pull.issue_id, pull.repo_id, input.method, input.message, viewer.id, Date.now(), pull.head_sha, ); await addIssueEvent(pull, viewer.id, "auto_merge_enabled", { method: input.method }); await kickBranch(pull.repo_id, pull.base_ref); } /** Requires write access. Queueing pins the current head; a missing or stale `expectedHead` is a 409. */ export async function setMergeQueue( access: PullAccess, viewer: Viewer, input: { queued: boolean; method: "merge" | "squash"; expectedHead?: string }, ): Promise { const { pull } = access; if (input.queued) { if (pull.state !== "open" || pull.draft === 1) throw badRequest("Only open, ready pull requests can be queued"); if (pull.head_sha !== input.expectedHead) throw conflict("The head branch changed; reload and try again"); const parent = await stackParent(pull); if (parent) throw badRequest(`This pull request is stacked on #${parent.number}; merge the stack instead`); await enqueuePull(pull, input.method, viewer).catch(rethrowMerge); } else { await dequeuePull(pull, viewer.id, null); } } /** Requires read access; only the author or a collaborator may change base or draft state. */ export async function updatePull( access: PullAccess, viewer: Viewer, input: { base?: string; draft?: boolean }, ): Promise { const { repo, permission, pull } = access; if (pull.author_id !== viewer.id && permission === "read") throw forbidden(); if (input.base && input.base !== pull.base_ref) { const sha = branchSha(await listRefs(repo), input.base); if (!sha) throw badRequest("Base branch not found"); await repoDb(pull.repo_id).run( `UPDATE pull_requests SET base_ref = ?, base_sha = ?, mergeable = 'unknown', stacked_on_issue_id = NULL, restack_upstream_sha = NULL WHERE issue_id = ?`, input.base, sha, pull.issue_id, ); await addIssueEvent(pull, viewer.id, "base_changed", { from: pull.base_ref, to: input.base }); } if (input.draft !== undefined && input.draft !== (pull.draft === 1)) { await repoDb(pull.repo_id).run( "UPDATE pull_requests SET draft = ? WHERE issue_id = ?", input.draft ? 1 : 0, pull.issue_id, ); await addIssueEvent(pull, viewer.id, input.draft ? "converted_to_draft" : "ready_for_review", {}); if (!input.draft) afterPullChange(pull, "ready_for_review", viewer, pull.head_sha); } }