91 lines · 3.4 KB
import { placeholders } from "./db";
import { repoDb } from "./scoped-db";
import { loadIssue, setIssueState } from "./issues";
import type { PullRow } from "./pulls";
import { emitWebhook, publicUrl } from "./events/emit";
import { assertBelowCap } from "./limits";
import type { RepoRow } from "~/lib/types";

const CLOSING_KEYWORDS = /\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+#(\d+)\b/gi;
const MAX_KEYWORD_LINKS = 10;
export const MAX_LINKED_ISSUES = 20;

export type LinkedIssue = { number: number; title: string; state: "open" | "closed"; source: "manual" | "keyword" };

type LinkPull = Pick<PullRow, "repo_id" | "issue_id">;

export async function linkIssue(pull: LinkPull, issueId: string, actorId: string): Promise<void> {
  const db = repoDb(pull.repo_id);
  const count = await db.first<{ count: number }>(
    "SELECT COUNT(*) AS count FROM pull_issue_links WHERE pull_issue_id = ?",
    pull.issue_id,
  );
  assertBelowCap(count?.count ?? 0, MAX_LINKED_ISSUES, "linked issues per pull request");
  await db.run(
    `INSERT INTO pull_issue_links (pull_issue_id, issue_id, source, created_by, created_at) VALUES (?, ?, 'manual', ?, ?)
     ON CONFLICT (pull_issue_id, issue_id) DO UPDATE SET source = 'manual', created_by = excluded.created_by`,
    pull.issue_id,
    issueId,
    actorId,
    Date.now(),
  );
}

export async function unlinkIssue(pull: LinkPull, issueId: string): Promise<void> {
  await repoDb(pull.repo_id).run(
    "DELETE FROM pull_issue_links WHERE pull_issue_id = ? AND issue_id = ?",
    pull.issue_id,
    issueId,
  );
}