import { all, first, run } from "./db"; import { randomId } from "./crypto"; import { assertBelowCap, CAPS } from "./limits"; import { toProtection } from "./protection"; import type { BranchProtection, BranchProtectionRow } from "./protection"; export type ProtectionInput = { pattern: string; requirePullRequest: boolean; requiredApprovals: number; requiredChecks: string[]; dismissStaleReviews: boolean; allowDeletions: boolean; allowForcePushes: boolean; enforceAdmins: boolean; }; /** Callers must have checked admin access on the repo. */ export async function listProtectionRules(repoId: string): Promise { const rows = await all( "SELECT * FROM branch_protections WHERE repo_id = ? ORDER BY pattern", repoId, ); return rows.map(toProtection); } export async function loadProtectionRule(repoId: string, id: string): Promise { const row = await first( "SELECT * FROM branch_protections WHERE id = ? AND repo_id = ?", id, repoId, ); return row ? toProtection(row) : null; } /** Creates (id null) or updates a rule; returns the rule id. Callers must have checked admin access. */ export async function saveProtectionRule(repoId: string, id: string | null, input: ProtectionInput): Promise { if (!id) assertBelowCap( (await first<{ count: number }>("SELECT COUNT(*) AS count FROM branch_protections WHERE repo_id = ?", repoId)) ?.count ?? 0, CAPS.protectionsPerRepo, "protection rules", ); const values = [ input.pattern, input.requirePullRequest ? 1 : 0, input.requiredApprovals, JSON.stringify(input.requiredChecks), input.dismissStaleReviews ? 1 : 0, input.allowDeletions ? 1 : 0, input.allowForcePushes ? 1 : 0, input.enforceAdmins ? 1 : 0, ] as const; if (id) { await run( `UPDATE branch_protections SET pattern = ?, require_pull_request = ?, required_approvals = ?, required_checks = ?, dismiss_stale_reviews = ?, allow_deletions = ?, allow_force_pushes = ?, enforce_admins = ? WHERE id = ? AND repo_id = ?`, ...values, id, repoId, ); return id; } const newId = randomId(); await run( `INSERT INTO branch_protections (id, repo_id, pattern, require_pull_request, required_approvals, required_checks, dismiss_stale_reviews, allow_deletions, allow_force_pushes, enforce_admins, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, newId, repoId, ...values, Date.now(), ); return newId; } /** Toggles "require review from code owners" on a rule. Callers must have checked admin access. */ export async function setCodeOwnerRequirement(repoId: string, id: string, enabled: boolean): Promise { await run( "UPDATE branch_protections SET require_code_owner_review = ? WHERE id = ? AND repo_id = ?", enabled ? 1 : 0, id, repoId, ); } /** Returns whether a rule was deleted. Callers must have checked admin access. */ export async function deleteProtectionRule(repoId: string, id: string): Promise { const result = await run("DELETE FROM branch_protections WHERE id = ? AND repo_id = ?", id, repoId); return result.meta.changes > 0; }