433 lines
11 KiB
TypeScript
433 lines
11 KiB
TypeScript
// @generated — DO NOT EDIT. Source: packages/shared/pr-provider.ts
|
|
/**
|
|
* Runtime-agnostic PR provider shared by Bun runtimes and Pi.
|
|
*
|
|
* Dispatches to platform-specific implementations (GitHub, GitLab)
|
|
* based on the `platform` field in PRRef/PRMetadata.
|
|
*
|
|
* Same pattern as review-core.ts: a runtime interface abstracts subprocess
|
|
* execution so the logic is reusable across Bun and Node/jiti.
|
|
*/
|
|
|
|
import { checkGhAuth, getGhUser, fetchGhPR, fetchGhPRContext, fetchGhPRFileContent, submitGhPRReview, fetchGhPRViewedFiles, markGhFilesViewed, fetchGhPRStack, fetchGhPRList } from "./pr-github";
|
|
import { checkGlAuth, getGlUser, fetchGlMR, fetchGlMRContext, fetchGlFileContent, submitGlMRReview } from "./pr-gitlab";
|
|
|
|
// --- Runtime Types ---
|
|
|
|
export interface CommandResult {
|
|
stdout: string;
|
|
stderr: string;
|
|
exitCode: number;
|
|
}
|
|
|
|
export interface PRRuntime {
|
|
runCommand: (
|
|
cmd: string,
|
|
args: string[],
|
|
) => Promise<CommandResult>;
|
|
runCommandWithInput?: (
|
|
cmd: string,
|
|
args: string[],
|
|
input: string,
|
|
) => Promise<CommandResult>;
|
|
}
|
|
|
|
// --- Platform Types ---
|
|
|
|
export type Platform = "github" | "gitlab";
|
|
|
|
/** GitHub PR reference */
|
|
export interface GithubPRRef {
|
|
platform: "github";
|
|
host: string;
|
|
owner: string;
|
|
repo: string;
|
|
number: number;
|
|
}
|
|
|
|
/** GitLab MR reference */
|
|
export interface GitlabMRRef {
|
|
platform: "gitlab";
|
|
host: string;
|
|
projectPath: string;
|
|
iid: number;
|
|
}
|
|
|
|
/** Discriminated union — auto-detected from URL */
|
|
export type PRRef = GithubPRRef | GitlabMRRef;
|
|
|
|
/** GitHub PR metadata */
|
|
export interface GithubPRMetadata {
|
|
platform: "github";
|
|
host: string;
|
|
owner: string;
|
|
repo: string;
|
|
number: number;
|
|
/** GraphQL node ID for the PR — used for markFileAsViewed mutations */
|
|
prNodeId?: string;
|
|
title: string;
|
|
author: string;
|
|
baseBranch: string;
|
|
headBranch: string;
|
|
/** Repository default branch, used to infer whether this PR targets another PR branch. */
|
|
defaultBranch?: string;
|
|
baseSha: string;
|
|
headSha: string;
|
|
/** Merge-base SHA — the common ancestor commit used to compute the PR diff. Differs from baseSha when the base branch has moved. */
|
|
mergeBaseSha?: string;
|
|
url: string;
|
|
}
|
|
|
|
/** GitLab MR metadata */
|
|
export interface GitlabMRMetadata {
|
|
platform: "gitlab";
|
|
host: string;
|
|
projectPath: string;
|
|
iid: number;
|
|
title: string;
|
|
author: string;
|
|
baseBranch: string;
|
|
headBranch: string;
|
|
/** Project default branch, used to infer whether this MR targets another MR branch. */
|
|
defaultBranch?: string;
|
|
baseSha: string;
|
|
headSha: string;
|
|
/** Merge-base SHA — the common ancestor commit used to compute the MR diff. */
|
|
mergeBaseSha?: string;
|
|
url: string;
|
|
}
|
|
|
|
/** Discriminated union — downstream gets type narrowing for free */
|
|
export type PRMetadata = GithubPRMetadata | GitlabMRMetadata;
|
|
|
|
// --- PR Context Types (platform-agnostic) ---
|
|
|
|
export interface PRComment {
|
|
id: string;
|
|
author: string;
|
|
body: string;
|
|
createdAt: string;
|
|
url: string;
|
|
}
|
|
|
|
export interface PRReview {
|
|
id: string;
|
|
author: string;
|
|
state: string;
|
|
body: string;
|
|
submittedAt: string;
|
|
url?: string;
|
|
}
|
|
|
|
export interface PRCheck {
|
|
name: string;
|
|
status: string;
|
|
conclusion: string | null;
|
|
workflowName: string;
|
|
detailsUrl: string;
|
|
}
|
|
|
|
export interface PRLinkedIssue {
|
|
number: number;
|
|
url: string;
|
|
repo: string;
|
|
}
|
|
|
|
export interface PRThreadComment {
|
|
id: string;
|
|
author: string;
|
|
body: string;
|
|
createdAt: string;
|
|
url: string;
|
|
diffHunk?: string;
|
|
}
|
|
|
|
export interface PRReviewThread {
|
|
id: string;
|
|
isResolved: boolean;
|
|
isOutdated: boolean;
|
|
path: string;
|
|
line: number | null;
|
|
startLine: number | null;
|
|
diffSide: 'LEFT' | 'RIGHT' | null;
|
|
comments: PRThreadComment[];
|
|
}
|
|
|
|
export interface PRContext {
|
|
body: string;
|
|
state: string;
|
|
isDraft: boolean;
|
|
labels: Array<{ name: string; color: string }>;
|
|
reviewDecision: string;
|
|
mergeable: string;
|
|
mergeStateStatus: string;
|
|
comments: PRComment[];
|
|
reviews: PRReview[];
|
|
reviewThreads: PRReviewThread[];
|
|
checks: PRCheck[];
|
|
linkedIssues: PRLinkedIssue[];
|
|
}
|
|
|
|
export interface PRReviewFileComment {
|
|
path: string;
|
|
line: number;
|
|
side: "LEFT" | "RIGHT";
|
|
body: string;
|
|
start_line?: number;
|
|
start_side?: "LEFT" | "RIGHT";
|
|
}
|
|
|
|
export type PRDiffScope = "layer" | "full-stack";
|
|
|
|
export interface PRDiffScopeOption {
|
|
id: PRDiffScope;
|
|
label: string;
|
|
description: string;
|
|
enabled: boolean;
|
|
}
|
|
|
|
export interface PRStackInfo {
|
|
isStacked: boolean;
|
|
baseBranch: string;
|
|
defaultBranch?: string;
|
|
label: string;
|
|
source: "branch-inferred" | "tree-discovered" | "github-native" | "gitlab-native" | "graphite" | "ghstack";
|
|
}
|
|
|
|
export interface PRStackNode {
|
|
branch: string;
|
|
number?: number;
|
|
title?: string;
|
|
url?: string;
|
|
isCurrent: boolean;
|
|
isDefaultBranch: boolean;
|
|
state?: 'open' | 'merged' | 'closed';
|
|
}
|
|
|
|
export interface PRStackTree {
|
|
nodes: PRStackNode[];
|
|
}
|
|
|
|
export interface PRListItem {
|
|
id: string;
|
|
number: number;
|
|
title: string;
|
|
author: string;
|
|
url: string;
|
|
baseBranch: string;
|
|
state: 'open' | 'closed' | 'merged';
|
|
}
|
|
|
|
// --- Label Helpers ---
|
|
// Accept either PRRef or PRMetadata (both have `platform` discriminant)
|
|
|
|
type HasPlatform = PRRef | PRMetadata;
|
|
|
|
/** "GitHub" or "GitLab" */
|
|
export function getPlatformLabel(m: HasPlatform): string {
|
|
return m.platform === "github" ? "GitHub" : "GitLab";
|
|
}
|
|
|
|
/** "PR" or "MR" */
|
|
export function getMRLabel(m: HasPlatform): string {
|
|
return m.platform === "github" ? "PR" : "MR";
|
|
}
|
|
|
|
/** "#123" or "!42" */
|
|
export function getMRNumberLabel(m: HasPlatform): string {
|
|
if (m.platform === "github") return `#${m.number}`;
|
|
return `!${m.iid}`;
|
|
}
|
|
|
|
/** "owner/repo" or "group/project" */
|
|
export function getDisplayRepo(m: HasPlatform): string {
|
|
if (m.platform === "github") return `${m.owner}/${m.repo}`;
|
|
return m.projectPath;
|
|
}
|
|
|
|
/** Reconstruct a PRRef from metadata */
|
|
export function prRefFromMetadata(m: PRMetadata): PRRef {
|
|
if (m.platform === "github") {
|
|
return { platform: "github", host: m.host, owner: m.owner, repo: m.repo, number: m.number };
|
|
}
|
|
return { platform: "gitlab", host: m.host, projectPath: m.projectPath, iid: m.iid };
|
|
}
|
|
|
|
export function isSameProject(a: PRRef, b: PRRef): boolean {
|
|
if (a.platform !== b.platform) return false;
|
|
if (a.platform === "github" && b.platform === "github") {
|
|
return a.host === b.host && a.owner === b.owner && a.repo === b.repo;
|
|
}
|
|
if (a.platform === "gitlab" && b.platform === "gitlab") {
|
|
return a.host === b.host && a.projectPath === b.projectPath;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/** CLI tool name for the platform */
|
|
export function getCliName(ref: PRRef): string {
|
|
return ref.platform === "github" ? "gh" : "glab";
|
|
}
|
|
|
|
/** Install URL for the platform CLI */
|
|
export function getCliInstallUrl(ref: PRRef): string {
|
|
return ref.platform === "github"
|
|
? "https://cli.github.com"
|
|
: "https://gitlab.com/gitlab-org/cli";
|
|
}
|
|
|
|
/** Encode a file path for use in platform API URLs */
|
|
export function encodeApiFilePath(filePath: string): string {
|
|
return encodeURIComponent(filePath);
|
|
}
|
|
|
|
// --- URL Parsing ---
|
|
|
|
/**
|
|
* Parse a PR/MR URL into its components. Auto-detects platform.
|
|
*
|
|
* Handles:
|
|
* - GitHub: https://github.com/owner/repo/pull/123[/files|/commits]
|
|
* - GitHub Enterprise: https://ghe.company.com/owner/repo/pull/123
|
|
* - GitLab: https://gitlab.com/group/subgroup/project/-/merge_requests/42[/diffs]
|
|
* - Self-hosted GitLab: https://gitlab.mycompany.com/group/project/-/merge_requests/42
|
|
*
|
|
* GitLab is checked first because `/-/merge_requests/` is unambiguous,
|
|
* while `/pull/` could theoretically appear on any host.
|
|
*/
|
|
export function parsePRUrl(url: string): PRRef | null {
|
|
if (!url) return null;
|
|
|
|
// GitLab: https://{host}/{projectPath}/-/merge_requests/{iid}[/...]
|
|
// Checked first — `/-/merge_requests/` is the most specific pattern.
|
|
const glMatch = url.match(
|
|
/^https?:\/\/([^/]+)\/(.+?)\/-\/merge_requests\/(\d+)/,
|
|
);
|
|
if (glMatch) {
|
|
return {
|
|
platform: "gitlab",
|
|
host: glMatch[1],
|
|
projectPath: glMatch[2],
|
|
iid: parseInt(glMatch[3], 10),
|
|
};
|
|
}
|
|
|
|
// GitHub (including GHE): https://{host}/{owner}/{repo}/pull/{number}[/...]
|
|
const ghMatch = url.match(
|
|
/^https?:\/\/([^/]+)\/([^/]+)\/([^/]+)\/pull\/(\d+)/,
|
|
);
|
|
if (ghMatch) {
|
|
return {
|
|
platform: "github",
|
|
host: ghMatch[1],
|
|
owner: ghMatch[2],
|
|
repo: ghMatch[3],
|
|
number: parseInt(ghMatch[4], 10),
|
|
};
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// --- Dispatch Functions ---
|
|
|
|
export async function checkAuth(runtime: PRRuntime, ref: PRRef): Promise<void> {
|
|
if (ref.platform === "github") return checkGhAuth(runtime, ref.host);
|
|
return checkGlAuth(runtime, ref.host);
|
|
}
|
|
|
|
export async function getUser(runtime: PRRuntime, ref: PRRef): Promise<string | null> {
|
|
if (ref.platform === "github") return getGhUser(runtime, ref.host);
|
|
return getGlUser(runtime, ref.host);
|
|
}
|
|
|
|
export async function fetchPR(
|
|
runtime: PRRuntime,
|
|
ref: PRRef,
|
|
): Promise<{ metadata: PRMetadata; rawPatch: string }> {
|
|
if (ref.platform === "github") return fetchGhPR(runtime, ref);
|
|
return fetchGlMR(runtime, ref);
|
|
}
|
|
|
|
export async function fetchPRContext(
|
|
runtime: PRRuntime,
|
|
ref: PRRef,
|
|
): Promise<PRContext> {
|
|
if (ref.platform === "github") return fetchGhPRContext(runtime, ref);
|
|
return fetchGlMRContext(runtime, ref);
|
|
}
|
|
|
|
export async function fetchPRFileContent(
|
|
runtime: PRRuntime,
|
|
ref: PRRef,
|
|
sha: string,
|
|
filePath: string,
|
|
): Promise<string | null> {
|
|
if (ref.platform === "github") return fetchGhPRFileContent(runtime, ref, sha, filePath);
|
|
return fetchGlFileContent(runtime, ref, sha, filePath);
|
|
}
|
|
|
|
export async function submitPRReview(
|
|
runtime: PRRuntime,
|
|
ref: PRRef,
|
|
headSha: string,
|
|
action: "approve" | "comment",
|
|
body: string,
|
|
fileComments: PRReviewFileComment[],
|
|
): Promise<void> {
|
|
if (ref.platform === "github") return submitGhPRReview(runtime, ref, headSha, action, body, fileComments);
|
|
return submitGlMRReview(runtime, ref, headSha, action, body, fileComments);
|
|
}
|
|
|
|
/**
|
|
* Fetch per-file "viewed" state for a PR.
|
|
* GitHub: returns { filePath: isViewed } map.
|
|
* GitLab: always returns {} (no server-side viewed state API).
|
|
*/
|
|
export async function fetchPRViewedFiles(
|
|
runtime: PRRuntime,
|
|
ref: PRRef,
|
|
): Promise<Record<string, boolean>> {
|
|
if (ref.platform === "github") return fetchGhPRViewedFiles(runtime, ref);
|
|
return {}; // GitLab has no server-side viewed state
|
|
}
|
|
|
|
/**
|
|
* Mark or unmark files as viewed in a PR.
|
|
* GitHub: fires markFileAsViewed / unmarkFileAsViewed GraphQL mutations.
|
|
* GitLab: no-op (no server-side viewed state API).
|
|
*/
|
|
export async function markPRFilesViewed(
|
|
runtime: PRRuntime,
|
|
ref: PRRef,
|
|
prNodeId: string,
|
|
filePaths: string[],
|
|
viewed: boolean,
|
|
): Promise<void> {
|
|
if (ref.platform === "github") return markGhFilesViewed(runtime, ref, prNodeId, filePaths, viewed);
|
|
// GitLab: no-op
|
|
}
|
|
|
|
/**
|
|
* Fetch the full stack tree for a stacked PR.
|
|
* Walks up from the current PR to the default branch, resolving
|
|
* PR numbers and titles for each intermediate branch.
|
|
* Returns null if the PR is not stacked or the API call fails.
|
|
*/
|
|
export async function fetchPRStack(
|
|
runtime: PRRuntime,
|
|
ref: PRRef,
|
|
metadata: PRMetadata,
|
|
): Promise<PRStackTree | null> {
|
|
if (ref.platform === "github") return fetchGhPRStack(runtime, ref, metadata);
|
|
return null; // GitLab: not yet implemented
|
|
}
|
|
|
|
export async function fetchPRList(
|
|
runtime: PRRuntime,
|
|
ref: PRRef,
|
|
): Promise<PRListItem[]> {
|
|
if (ref.platform === "github") return fetchGhPRList(runtime, ref);
|
|
return []; // GitLab: not yet implemented
|
|
}
|