663 lines
19 KiB
TypeScript
663 lines
19 KiB
TypeScript
// @generated — DO NOT EDIT. Source: packages/shared/pr-github.ts
|
|
/**
|
|
* GitHub-specific PR provider implementation.
|
|
*
|
|
* All functions use the `gh` CLI via the PRRuntime abstraction.
|
|
*/
|
|
|
|
import type { PRRuntime, PRMetadata, PRContext, PRReviewThread, PRThreadComment, PRReviewFileComment, CommandResult, PRStackTree, PRStackNode, PRListItem } from "./pr-provider";
|
|
import { encodeApiFilePath } from "./pr-provider";
|
|
|
|
// GitHub-specific PRRef shape (used internally)
|
|
interface GhPRRef {
|
|
platform: "github";
|
|
host: string;
|
|
owner: string;
|
|
repo: string;
|
|
number: number;
|
|
}
|
|
|
|
/** Build the --repo flag value: HOST/OWNER/REPO for GHE, OWNER/REPO for github.com */
|
|
function repoFlag(ref: GhPRRef): string {
|
|
if (ref.host !== "github.com") {
|
|
return `${ref.host}/${ref.owner}/${ref.repo}`;
|
|
}
|
|
return `${ref.owner}/${ref.repo}`;
|
|
}
|
|
|
|
/** Append --hostname to args for gh api / gh auth on GHE */
|
|
function hostnameArgs(host: string, args: string[]): string[] {
|
|
if (host !== "github.com") {
|
|
return [...args, "--hostname", host];
|
|
}
|
|
return args;
|
|
}
|
|
|
|
// --- Auth ---
|
|
|
|
export async function checkGhAuth(runtime: PRRuntime, host: string): Promise<void> {
|
|
const result = await runtime.runCommand("gh", hostnameArgs(host, ["auth", "status"]));
|
|
if (result.exitCode !== 0) {
|
|
const stderr = result.stderr.trim();
|
|
const hostHint = host !== "github.com" ? ` --hostname ${host}` : "";
|
|
throw new Error(
|
|
`GitHub CLI not authenticated. Run \`gh auth login${hostHint}\` first.\n${stderr}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
export async function getGhUser(runtime: PRRuntime, host: string): Promise<string | null> {
|
|
try {
|
|
const result = await runtime.runCommand("gh", hostnameArgs(host, ["api", "user", "--jq", ".login"]));
|
|
if (result.exitCode === 0 && result.stdout.trim()) {
|
|
return result.stdout.trim();
|
|
}
|
|
return null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// --- Fetch PR ---
|
|
|
|
export async function fetchGhPR(
|
|
runtime: PRRuntime,
|
|
ref: GhPRRef,
|
|
): Promise<{ metadata: PRMetadata; rawPatch: string }> {
|
|
const repo = repoFlag(ref);
|
|
|
|
// Fetch diff, metadata, and repository defaults in parallel.
|
|
const [diffResult, viewResult, repoResult] = await Promise.all([
|
|
runtime.runCommand("gh", [
|
|
"pr", "diff", String(ref.number),
|
|
"--repo", repo,
|
|
]),
|
|
runtime.runCommand("gh", [
|
|
"pr", "view", String(ref.number),
|
|
"--repo", repo,
|
|
"--json", "id,title,author,baseRefName,headRefName,baseRefOid,headRefOid,url",
|
|
]),
|
|
runtime.runCommand("gh", [
|
|
"repo", "view", repo,
|
|
"--json", "defaultBranchRef",
|
|
"--jq", ".defaultBranchRef.name",
|
|
]),
|
|
]);
|
|
|
|
if (diffResult.exitCode !== 0) {
|
|
throw new Error(
|
|
`Failed to fetch PR diff: ${diffResult.stderr.trim() || `exit code ${diffResult.exitCode}`}`,
|
|
);
|
|
}
|
|
|
|
if (viewResult.exitCode !== 0) {
|
|
throw new Error(
|
|
`Failed to fetch PR metadata: ${viewResult.stderr.trim() || `exit code ${viewResult.exitCode}`}`,
|
|
);
|
|
}
|
|
|
|
const raw = JSON.parse(viewResult.stdout) as {
|
|
id: string;
|
|
title: string;
|
|
author: { login: string };
|
|
baseRefName: string;
|
|
headRefName: string;
|
|
baseRefOid: string;
|
|
headRefOid: string;
|
|
url: string;
|
|
};
|
|
|
|
// Fetch the merge-base SHA — the common ancestor commit GitHub uses to compute the PR diff.
|
|
// baseSha (baseRefOid) is the tip of the base branch, which may have moved since the branch point.
|
|
// File contents must be fetched at the merge-base to match the diff hunks.
|
|
let mergeBaseSha: string | undefined;
|
|
try {
|
|
const compareResult = await runtime.runCommand("gh", hostnameArgs(ref.host, [
|
|
"api",
|
|
`repos/${ref.owner}/${ref.repo}/compare/${raw.baseRefOid}...${raw.headRefOid}`,
|
|
"--jq", ".merge_base_commit.sha",
|
|
]));
|
|
if (compareResult.exitCode === 0 && compareResult.stdout.trim()) {
|
|
mergeBaseSha = compareResult.stdout.trim();
|
|
}
|
|
} catch { /* fallback to baseSha if compare API fails */ }
|
|
|
|
const metadata: PRMetadata = {
|
|
platform: "github",
|
|
host: ref.host,
|
|
owner: ref.owner,
|
|
repo: ref.repo,
|
|
number: ref.number,
|
|
prNodeId: raw.id,
|
|
title: raw.title,
|
|
author: raw.author.login,
|
|
baseBranch: raw.baseRefName,
|
|
headBranch: raw.headRefName,
|
|
defaultBranch: repoResult.exitCode === 0 && repoResult.stdout.trim() && repoResult.stdout.trim() !== "null"
|
|
? repoResult.stdout.trim()
|
|
: undefined,
|
|
baseSha: raw.baseRefOid,
|
|
headSha: raw.headRefOid,
|
|
mergeBaseSha,
|
|
url: raw.url,
|
|
};
|
|
|
|
return { metadata, rawPatch: diffResult.stdout };
|
|
}
|
|
|
|
// --- PR Context ---
|
|
|
|
const GH_CONTEXT_FIELDS = [
|
|
"body", "state", "isDraft", "labels",
|
|
"comments", "reviews", "reviewDecision",
|
|
"mergeable", "mergeStateStatus",
|
|
"statusCheckRollup", "closingIssuesReferences",
|
|
].join(",");
|
|
|
|
function parseGhPRContext(raw: Record<string, unknown>): PRContext {
|
|
const arr = (v: unknown): unknown[] => (Array.isArray(v) ? v : []);
|
|
const str = (v: unknown): string => (typeof v === "string" ? v : "");
|
|
const login = (v: unknown): string =>
|
|
typeof v === "object" && v !== null && "login" in v
|
|
? String((v as { login: unknown }).login || "")
|
|
: "";
|
|
|
|
return {
|
|
body: str(raw.body),
|
|
state: str(raw.state),
|
|
isDraft: raw.isDraft === true,
|
|
labels: arr(raw.labels).map((l: any) => ({
|
|
name: str(l?.name),
|
|
color: str(l?.color),
|
|
})),
|
|
reviewDecision: str(raw.reviewDecision),
|
|
mergeable: str(raw.mergeable),
|
|
mergeStateStatus: str(raw.mergeStateStatus),
|
|
comments: arr(raw.comments).map((c: any) => ({
|
|
id: str(c?.id),
|
|
author: login(c?.author),
|
|
body: str(c?.body),
|
|
createdAt: str(c?.createdAt),
|
|
url: str(c?.url),
|
|
})),
|
|
reviews: arr(raw.reviews).map((r: any) => ({
|
|
id: str(r?.id),
|
|
author: login(r?.author),
|
|
state: str(r?.state),
|
|
body: str(r?.body),
|
|
submittedAt: str(r?.submittedAt),
|
|
...(r?.url ? { url: str(r.url) } : {}),
|
|
})),
|
|
reviewThreads: [], // populated via GraphQL after initial fetch
|
|
checks: arr(raw.statusCheckRollup).map((c: any) => ({
|
|
name: str(c?.name),
|
|
status: str(c?.status),
|
|
conclusion: typeof c?.conclusion === "string" ? c.conclusion : null,
|
|
workflowName: str(c?.workflowName),
|
|
detailsUrl: str(c?.detailsUrl),
|
|
})),
|
|
linkedIssues: arr(raw.closingIssuesReferences).map((i: any) => ({
|
|
number: typeof i?.number === "number" ? i.number : 0,
|
|
url: str(i?.url),
|
|
repo: i?.repository
|
|
? `${login(i.repository.owner)}/${str(i.repository.name)}`
|
|
: "",
|
|
})),
|
|
};
|
|
}
|
|
|
|
export async function fetchGhPRContext(
|
|
runtime: PRRuntime,
|
|
ref: GhPRRef,
|
|
): Promise<PRContext> {
|
|
const repo = repoFlag(ref);
|
|
|
|
const result = await runtime.runCommand("gh", [
|
|
"pr", "view", String(ref.number),
|
|
"--repo", repo,
|
|
"--json", GH_CONTEXT_FIELDS,
|
|
]);
|
|
|
|
if (result.exitCode !== 0) {
|
|
throw new Error(
|
|
`Failed to fetch PR context: ${result.stderr.trim() || `exit code ${result.exitCode}`}`,
|
|
);
|
|
}
|
|
|
|
const raw = JSON.parse(result.stdout) as Record<string, unknown>;
|
|
const context = parseGhPRContext(raw);
|
|
|
|
// Fetch inline review threads via GraphQL (parallel-safe, non-blocking failure)
|
|
try {
|
|
context.reviewThreads = await fetchGhReviewThreads(runtime, ref);
|
|
} catch {
|
|
// GraphQL may not be available or may fail — degrade gracefully
|
|
context.reviewThreads = [];
|
|
}
|
|
|
|
return context;
|
|
}
|
|
|
|
// --- Review Threads (GraphQL) ---
|
|
|
|
const REVIEW_THREADS_QUERY = `
|
|
query($owner: String!, $repo: String!, $number: Int!) {
|
|
repository(owner: $owner, name: $repo) {
|
|
pullRequest(number: $number) {
|
|
reviewThreads(first: 100) {
|
|
nodes {
|
|
id
|
|
isResolved
|
|
isOutdated
|
|
line
|
|
startLine
|
|
path
|
|
diffSide
|
|
comments(first: 50) {
|
|
nodes {
|
|
id
|
|
body
|
|
author { login }
|
|
createdAt
|
|
url
|
|
diffHunk
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}`;
|
|
|
|
async function fetchGhReviewThreads(
|
|
runtime: PRRuntime,
|
|
ref: GhPRRef,
|
|
): Promise<PRReviewThread[]> {
|
|
const result = await runtime.runCommand("gh", hostnameArgs(ref.host, [
|
|
"api", "graphql",
|
|
"-f", `query=${REVIEW_THREADS_QUERY}`,
|
|
"-f", `owner=${ref.owner}`,
|
|
"-f", `repo=${ref.repo}`,
|
|
"-F", `number=${ref.number}`,
|
|
]));
|
|
|
|
if (result.exitCode !== 0) return [];
|
|
|
|
const data = JSON.parse(result.stdout);
|
|
const threads = data?.data?.repository?.pullRequest?.reviewThreads?.nodes;
|
|
if (!Array.isArray(threads)) return [];
|
|
|
|
return threads.map((t: any): PRReviewThread => ({
|
|
id: String(t.id ?? ''),
|
|
isResolved: t.isResolved === true,
|
|
isOutdated: t.isOutdated === true,
|
|
path: String(t.path ?? ''),
|
|
line: typeof t.line === 'number' ? t.line : null,
|
|
startLine: typeof t.startLine === 'number' ? t.startLine : null,
|
|
diffSide: t.diffSide === 'LEFT' || t.diffSide === 'RIGHT' ? t.diffSide : null,
|
|
comments: Array.isArray(t.comments?.nodes)
|
|
? t.comments.nodes.map((c: any): PRThreadComment => ({
|
|
id: String(c.id ?? ''),
|
|
author: c.author?.login ? String(c.author.login) : '',
|
|
body: String(c.body ?? ''),
|
|
createdAt: String(c.createdAt ?? ''),
|
|
url: String(c.url ?? ''),
|
|
...(c.diffHunk ? { diffHunk: String(c.diffHunk) } : {}),
|
|
}))
|
|
: [],
|
|
}));
|
|
}
|
|
|
|
// --- File Content ---
|
|
|
|
export async function fetchGhPRFileContent(
|
|
runtime: PRRuntime,
|
|
ref: GhPRRef,
|
|
sha: string,
|
|
filePath: string,
|
|
): Promise<string | null> {
|
|
const result = await runtime.runCommand("gh", hostnameArgs(ref.host, [
|
|
"api",
|
|
`repos/${ref.owner}/${ref.repo}/contents/${encodeApiFilePath(filePath)}?ref=${sha}`,
|
|
"--jq", ".content",
|
|
]));
|
|
|
|
if (result.exitCode !== 0) return null;
|
|
|
|
const base64Content = result.stdout.trim();
|
|
if (!base64Content) return null;
|
|
|
|
// GitHub returns base64-encoded content with newlines
|
|
const cleaned = base64Content.replace(/\n/g, "");
|
|
try {
|
|
return Buffer.from(cleaned, "base64").toString("utf-8");
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// --- Viewed Files ---
|
|
|
|
/**
|
|
* Fetch the per-file "viewed" state for a GitHub PR via GraphQL.
|
|
* Returns a map of { filePath: isViewed } where isViewed is true for
|
|
* VIEWED or DISMISSED states (i.e., the file was reviewed but may need
|
|
* re-review after new commits).
|
|
*/
|
|
export async function fetchGhPRViewedFiles(
|
|
runtime: PRRuntime,
|
|
ref: GhPRRef,
|
|
): Promise<Record<string, boolean>> {
|
|
const query = `
|
|
query($owner: String!, $repo: String!, $number: Int!, $cursor: String) {
|
|
repository(owner: $owner, name: $repo) {
|
|
pullRequest(number: $number) {
|
|
files(first: 100, after: $cursor) {
|
|
nodes {
|
|
path
|
|
viewerViewedState
|
|
}
|
|
pageInfo {
|
|
hasNextPage
|
|
endCursor
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`;
|
|
|
|
const result: Record<string, boolean> = {};
|
|
let cursor: string | null = null;
|
|
|
|
// Paginate through all files (GitHub returns max 100 per page)
|
|
do {
|
|
const args = hostnameArgs(ref.host, [
|
|
"api", "graphql",
|
|
"-f", `query=${query}`,
|
|
"-F", `owner=${ref.owner}`,
|
|
"-F", `repo=${ref.repo}`,
|
|
"-F", `number=${ref.number}`,
|
|
]);
|
|
if (cursor) {
|
|
args.push("-F", `cursor=${cursor}`);
|
|
}
|
|
|
|
const res = await runtime.runCommand("gh", args);
|
|
if (res.exitCode !== 0) {
|
|
throw new Error(
|
|
`Failed to fetch PR viewed files: ${res.stderr.trim() || `exit code ${res.exitCode}`}`,
|
|
);
|
|
}
|
|
|
|
const data = JSON.parse(res.stdout) as {
|
|
data?: {
|
|
repository?: {
|
|
pullRequest?: {
|
|
files?: {
|
|
nodes: Array<{ path: string; viewerViewedState: string }>;
|
|
pageInfo: { hasNextPage: boolean; endCursor: string | null };
|
|
};
|
|
};
|
|
};
|
|
};
|
|
errors?: Array<{ message: string }>;
|
|
};
|
|
|
|
if (data.errors?.length) {
|
|
throw new Error(`GraphQL error: ${data.errors[0].message}`);
|
|
}
|
|
|
|
const files = data.data?.repository?.pullRequest?.files;
|
|
if (!files) break;
|
|
|
|
for (const node of files.nodes) {
|
|
// VIEWED = explicitly marked as viewed
|
|
// DISMISSED = was viewed but new commits arrived (still "was reviewed")
|
|
result[node.path] = node.viewerViewedState === "VIEWED" || node.viewerViewedState === "DISMISSED";
|
|
}
|
|
|
|
cursor = files.pageInfo.hasNextPage ? files.pageInfo.endCursor : null;
|
|
} while (cursor !== null);
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Mark or unmark a set of files as viewed in a GitHub PR via GraphQL mutations.
|
|
* Uses Promise.allSettled so a single file failure doesn't block the rest.
|
|
* Throws only if ALL mutations fail.
|
|
*/
|
|
export async function markGhFilesViewed(
|
|
runtime: PRRuntime,
|
|
ref: GhPRRef,
|
|
prNodeId: string,
|
|
filePaths: string[],
|
|
viewed: boolean,
|
|
): Promise<void> {
|
|
if (filePaths.length === 0) return;
|
|
|
|
const mutationName = viewed ? "markFileAsViewed" : "unmarkFileAsViewed";
|
|
const mutation = `
|
|
mutation($id: ID!, $path: String!) {
|
|
${mutationName}(input: { pullRequestId: $id, path: $path }) {
|
|
clientMutationId
|
|
}
|
|
}
|
|
`;
|
|
|
|
const results = await Promise.allSettled(
|
|
filePaths.map((path) =>
|
|
runtime.runCommandWithInput
|
|
? runtime.runCommand("gh", hostnameArgs(ref.host, [
|
|
"api", "graphql",
|
|
"-f", `query=${mutation}`,
|
|
"-F", `id=${prNodeId}`,
|
|
"-F", `path=${path}`,
|
|
]))
|
|
: Promise.reject(new Error("Runtime does not support commands")),
|
|
),
|
|
);
|
|
|
|
const failures = results.filter((r): r is PromiseRejectedResult => r.status === "rejected");
|
|
if (failures.length === filePaths.length) {
|
|
throw new Error(
|
|
`Failed to ${mutationName} all files: ${failures[0].reason}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
// --- Submit PR Review ---
|
|
|
|
export async function submitGhPRReview(
|
|
runtime: PRRuntime,
|
|
ref: GhPRRef,
|
|
headSha: string,
|
|
action: "approve" | "comment",
|
|
body: string,
|
|
fileComments: PRReviewFileComment[],
|
|
): Promise<void> {
|
|
const payload = JSON.stringify({
|
|
commit_id: headSha,
|
|
body,
|
|
event: action === "approve" ? "APPROVE" : "COMMENT",
|
|
comments: fileComments,
|
|
});
|
|
|
|
const endpoint = `repos/${ref.owner}/${ref.repo}/pulls/${ref.number}/reviews`;
|
|
|
|
let result: CommandResult;
|
|
|
|
if (runtime.runCommandWithInput) {
|
|
result = await runtime.runCommandWithInput(
|
|
"gh",
|
|
hostnameArgs(ref.host, ["api", endpoint, "--method", "POST", "--input", "-"]),
|
|
payload,
|
|
);
|
|
} else {
|
|
throw new Error("Runtime does not support stdin input; cannot submit PR review");
|
|
}
|
|
|
|
if (result.exitCode !== 0) {
|
|
const message = result.stderr.trim() || result.stdout.trim() || `exit code ${result.exitCode}`;
|
|
throw new Error(`Failed to submit PR review: ${message}`);
|
|
}
|
|
}
|
|
|
|
// --- Stack Tree (GraphQL) ---
|
|
|
|
type StackPRNode = { number: number; title: string; url: string; baseRefName: string; headRefName: string; state: string };
|
|
|
|
function stackPRQuery(kind: "head" | "base"): string {
|
|
const varName = kind === "head" ? "headRefName" : "baseRefName";
|
|
const first = kind === "head" ? 5 : 10;
|
|
return `
|
|
query($owner: String!, $repo: String!, $${varName}: String!) {
|
|
repository(owner: $owner, name: $repo) {
|
|
pullRequests(first: ${first}, ${varName}: $${varName}, states: [OPEN, MERGED]) {
|
|
nodes { number title url baseRefName headRefName state }
|
|
}
|
|
}
|
|
}`;
|
|
}
|
|
|
|
async function queryPRsByRef(
|
|
runtime: PRRuntime,
|
|
ref: GhPRRef,
|
|
kind: "head" | "base",
|
|
refName: string,
|
|
): Promise<StackPRNode[]> {
|
|
const varName = kind === "head" ? "headRefName" : "baseRefName";
|
|
const result = await runtime.runCommand("gh", hostnameArgs(ref.host, [
|
|
"api", "graphql",
|
|
"-f", `query=${stackPRQuery(kind)}`,
|
|
"-f", `owner=${ref.owner}`,
|
|
"-f", `repo=${ref.repo}`,
|
|
"-f", `${varName}=${refName}`,
|
|
]));
|
|
if (result.exitCode !== 0) return [];
|
|
const data = JSON.parse(result.stdout);
|
|
const prs = data?.data?.repository?.pullRequests?.nodes;
|
|
return Array.isArray(prs) ? prs : [];
|
|
}
|
|
|
|
/**
|
|
* Walk up and down the PR stack from the current PR, resolving
|
|
* PR numbers/titles for every node in the chain.
|
|
*
|
|
* Up: walk from currentPR.baseBranch → defaultBranch (ancestors)
|
|
* Down: walk from currentPR.headBranch → leaf PRs (descendants)
|
|
*/
|
|
export async function fetchGhPRStack(
|
|
runtime: PRRuntime,
|
|
ref: GhPRRef,
|
|
metadata: PRMetadata,
|
|
): Promise<PRStackTree | null> {
|
|
if (metadata.platform !== "github") return null;
|
|
const defaultBranch = metadata.defaultBranch;
|
|
if (!defaultBranch) return null;
|
|
|
|
const currentNode: PRStackNode = {
|
|
branch: metadata.headBranch,
|
|
number: metadata.number,
|
|
title: metadata.title,
|
|
url: metadata.url,
|
|
isCurrent: true,
|
|
isDefaultBranch: false,
|
|
};
|
|
|
|
// Walk up: find the PR whose headRefName === baseBranch, repeat
|
|
const ancestors: PRStackNode[] = [];
|
|
let nextHead = metadata.baseBranch;
|
|
const maxDepth = 10;
|
|
|
|
for (let i = 0; i < maxDepth; i++) {
|
|
if (nextHead === defaultBranch) break;
|
|
|
|
const prs = await queryPRsByRef(runtime, ref, "head", nextHead);
|
|
if (prs.length === 0) {
|
|
ancestors.push({ branch: nextHead, isCurrent: false, isDefaultBranch: false });
|
|
break;
|
|
}
|
|
|
|
const pr = prs[0];
|
|
ancestors.push({
|
|
branch: pr.headRefName,
|
|
number: pr.number,
|
|
title: pr.title,
|
|
url: pr.url,
|
|
isCurrent: false,
|
|
isDefaultBranch: false,
|
|
state: (pr.state === 'MERGED' ? 'merged' : pr.state === 'CLOSED' ? 'closed' : 'open') as PRStackNode['state'],
|
|
});
|
|
nextHead = pr.baseRefName;
|
|
}
|
|
|
|
// Walk down: find PRs whose baseRefName === current headBranch, repeat
|
|
const descendants: PRStackNode[] = [];
|
|
let nextBase = metadata.headBranch;
|
|
|
|
for (let i = 0; i < maxDepth; i++) {
|
|
const prs = await queryPRsByRef(runtime, ref, "base", nextBase);
|
|
if (prs.length === 0) break;
|
|
|
|
const pr = prs[0];
|
|
descendants.push({
|
|
branch: pr.headRefName,
|
|
number: pr.number,
|
|
title: pr.title,
|
|
url: pr.url,
|
|
isCurrent: false,
|
|
isDefaultBranch: false,
|
|
state: (pr.state === 'MERGED' ? 'merged' : pr.state === 'CLOSED' ? 'closed' : 'open') as PRStackNode['state'],
|
|
});
|
|
nextBase = pr.headRefName;
|
|
}
|
|
|
|
// Build tree: defaultBranch → ancestors (reversed) → current → descendants
|
|
const nodes: PRStackNode[] = [
|
|
{ branch: defaultBranch, isCurrent: false, isDefaultBranch: true },
|
|
...ancestors.reverse(),
|
|
currentNode,
|
|
...descendants,
|
|
];
|
|
|
|
return { nodes };
|
|
}
|
|
|
|
// --- PR List ---
|
|
|
|
export async function fetchGhPRList(
|
|
runtime: PRRuntime,
|
|
ref: GhPRRef,
|
|
): Promise<PRListItem[]> {
|
|
const result = await runtime.runCommand("gh", [
|
|
"pr", "list",
|
|
"--repo", repoFlag(ref),
|
|
"--json", "number,title,author,url,baseRefName,state",
|
|
"--limit", "30",
|
|
"--state", "all",
|
|
]);
|
|
|
|
if (result.exitCode !== 0) return [];
|
|
|
|
const raw = JSON.parse(result.stdout) as Array<{
|
|
number: number;
|
|
title: string;
|
|
author: { login: string };
|
|
url: string;
|
|
baseRefName: string;
|
|
state: string;
|
|
}>;
|
|
|
|
return raw.map((pr) => ({
|
|
id: String(pr.number),
|
|
number: pr.number,
|
|
title: pr.title,
|
|
author: pr.author.login,
|
|
url: pr.url,
|
|
baseBranch: pr.baseRefName,
|
|
state: (pr.state === "OPEN" ? "open" : pr.state === "MERGED" ? "merged" : "closed") as PRListItem["state"],
|
|
}));
|
|
}
|