import { repoDb, stmt } from "./scoped-db"; import { addIssueEvent } from "./pulls"; import type { PullRow } from "./pulls"; import { ownerAccountIds, ownersToRequest, pullCodeOwners } from "./codeowners"; import { accountsById } from "./people"; import { emitNotification } from "./events/emit"; import { pullUrl } from "./pull-access"; import type { RepoRow, Viewer } from "~/lib/types"; import { issuesChanged } from "./repo-state"; async function notifyRequested(repo: RepoRow, pull: PullRow, actor: Viewer, userIds: string[]) { await emitNotification({ repoId: repo.id, issueId: null, actorId: actor.id, reason: "review_requested", title: `${actor.name} requested your review on ${repo.owner_name}/${repo.name}#${pull.number}`, url: pullUrl(repo, pull.number), mentionText: null, directRecipients: userIds, }); } /** Auto-requests reviews from CODEOWNERS of the changed files who have not reviewed yet. */ export async function requestCodeOwnerReviews( pull: PullRow, baseRepo: RepoRow, headRepo: RepoRow, actor: Viewer, ): Promise { const { owned } = await pullCodeOwners(pull, baseRepo, headRepo); if (owned.size === 0) return; const candidates = ownersToRequest(owned, await ownerAccountIds(owned), pull.author_id); if (candidates.length === 0) return; const reviewed = new Set( ( await repoDb(pull.repo_id).all<{ author_id: string }>( "SELECT DISTINCT author_id FROM reviews WHERE issue_id = ? AND state != 'dismissed'", pull.issue_id, ) ).map((row) => row.author_id), ); const wanted = candidates.filter((id) => !reviewed.has(id)); if (wanted.length === 0) return; const now = Date.now(); await repoDb(pull.repo_id).batch( wanted.map((id) => stmt( "INSERT OR IGNORE INTO review_requests (issue_id, user_id, created_at, code_owner) VALUES (?, ?, ?, 1)", pull.issue_id, id, now, ), ), ); const people = await accountsById(wanted); await addIssueEvent(pull, actor.id, "review_requested", { usernames: wanted.map((id) => people.get(id)?.name ?? "ghost"), codeOwners: true, }); issuesChanged(baseRepo.id, [pull.issue_id]); await notifyRequested(baseRepo, pull, actor, wanted); } /** Asks a previous reviewer to look again after new changes. */ export async function rerequestReview(repo: RepoRow, pull: PullRow, actor: Viewer, userId: string): Promise { const db = repoDb(pull.repo_id); const reviewed = await db.first( "SELECT 1 FROM reviews WHERE issue_id = ? AND author_id = ? LIMIT 1", pull.issue_id, userId, ); if (!reviewed) return false; await db.run( `INSERT INTO review_requests (issue_id, user_id, created_at) VALUES (?, ?, ?) ON CONFLICT (issue_id, user_id) DO UPDATE SET created_at = excluded.created_at`, pull.issue_id, userId, Date.now(), ); issuesChanged(repo.id, [pull.issue_id]); const people = await accountsById([userId]); await addIssueEvent(pull, actor.id, "review_requested", { usernames: [people.get(userId)?.name ?? "ghost"], rerequested: true, }); await notifyRequested(repo, pull, actor, [userId]); return true; }