lib/bulk-pull-request-links.ts
Rama main · réplica de lectura del repositorio original.
import type { IssuePr, PullRequestListItem } from "@/lib/agent-api";
import type { BulkPullRequestOption } from "@/lib/bulk-actions-context";
interface SelectedIssue {
id: string;
project_id: string;
}
const isLive = (pr: IssuePr | null): pr is IssuePr =>
!!pr && (pr.state === "open" || pr.state === "draft");
/**
* What a selection can do with pull requests.
*
* A pull request only takes issues of the project that links its repository
* (the server's rule), so every target list is restricted to that project.
* `partial` lists the live pull requests some selected issues already have,
* when other selected issues of the same project do not have it yet.
*/
export function bulkPullRequestLinks(
issues: readonly SelectedIssue[],
prOf: (issueId: string) => IssuePr | null,
openPullRequests: readonly PullRequestListItem[],
linkedPullRequestIds: (issueId: string) => readonly string[] = (issueId) => {
const pr = prOf(issueId);
return pr ? [pr.prId] : [];
},
) {
const projects = new Set(issues.map((issue) => issue.project_id));
const projectOfPr = new Map<string, string>();
const options: BulkPullRequestOption[] = [];
for (const pr of openPullRequests) {
if (pr.pr_state !== "open" && pr.pr_state !== "draft") continue;
if (!pr.project || !projects.has(pr.project.id)) continue;
projectOfPr.set(pr.prId, pr.project.id);
options.push({ prId: pr.prId, prNumber: pr.pr_number, state: pr.pr_state, title: pr.title ?? pr.head_branch ?? "" });
}
// Live PRs already on the selection; their project is the linked issue's.
const live = new Map<string, BulkPullRequestOption & { projectId: string }>();
for (const issue of issues) {
const pr = prOf(issue.id);
if (!isLive(pr) || live.has(pr.prId)) continue;
const projectId = projectOfPr.get(pr.prId) ?? issue.project_id;
projectOfPr.set(pr.prId, projectId);
const title = options.find((option) => option.prId === pr.prId)?.title ?? "";
live.set(pr.prId, { prId: pr.prId, prNumber: pr.prNumber, state: pr.state, title, projectId });
}
const targets = (prId: string): string[] => {
const projectId = projectOfPr.get(prId);
if (!projectId) return [];
// An issue keeps one live PR: one already holding another is refused by
// the server, so it is left out rather than linked and rolled back.
return issues
.filter((issue) => issue.project_id === projectId && !isLive(prOf(issue.id)))
.map((issue) => issue.id);
};
// Every link of every selected issue, for "unlink from pull requests".
const unlinkable = issues
.map((issue) => ({ issueId: issue.id, prIds: linkedPullRequestIds(issue.id) }))
.filter((link) => link.prIds.length > 0);
// Picking a pull request follows the single-issue rule: only a live pull
// request blocks a new link (MIN-939). A merged or closed link stays in the
// history without hiding the choice, and a pull request is offered only
// while one selected issue of its project can still take it.
const linkable = options.filter((pr) => targets(pr.prId).length > 0);
const partial = [...live.values()]
.map(({ projectId: _projectId, ...pr }) => ({ ...pr, missing: targets(pr.prId).length }))
.filter((pr) => pr.missing > 0)
.sort((left, right) => left.prNumber - right.prNumber);
return { options: linkable, partial, targets, unlinkable };
}