Files
paseo/packages/app/src/git/pr-hint.ts
T
a8ebd390fa feat(forge): pluggable forge abstraction + GitLab and Gitea/Forgejo/Codeberg (#1913)
* refactor(forge): forge-neutral foundation (GitHub-only)

Decouple git-hosting from GitHub behind a neutral abstraction (issue #1616), GitHub-only for now; existing GitHub behaviour is unchanged.

- Forge manifest, neutral ForgeService contract, forge registry + resolver, and a client forge-module registry.
- GitHub code renamed to the neutral shape; PR/Issue attachment wording preserved.
- forge.search.response enums parse tolerantly (unknown kind/auth state degrade instead of breaking the client).
- createPullRequest reports typed CLI/auth errors instead of a generic message.
- forge-resolver host/remote caches are LRU-bounded.
- Forge host trust is explicit: only a known cloud host or a CLI-authenticated host is ever talked to; an unauthenticated GitHub Enterprise host fails resolution instead of routing to github.com.
- Docs: forge-providers guide, glossary and i18n forge-copy conventions, architecture and rpc-namespacing terminology.
- Vitest React Native mocks (unistyles, svg, linking, lucide) consolidated into shared aliased test-stubs.

* feat(forge): GitLab adapter, forge-aware UI, pipelines and approvals

GitLab adapter over the glab CLI on the neutral contracts: MR status, forge-aware UI, pipeline tree, and N-of-M approvals.

- threadIsResolved is part of the neutral timeline item.
- Pipeline load failures show an error instead of an empty section.
- Manual pipeline jobs render as pending.
- Fork/detached MR head pipelines are fetched by MR iid (glab ci get --merge-request).

* feat(forge): Gitea family adapter (Gitea, Forgejo, Codeberg)

One adapter over the tea CLI serving Gitea, Forgejo, and Codeberg on the neutral contracts.

- CI status aggregates commit statuses and Actions runs together.
- Gitea's terminal "warning" state maps to failure on server and client.
- Gitea Actions check details are reachable from the PR pane by workflowRunId.

* refactor(forge): localize compatibility handling

* test(forge): expect normalized GitLab facts

---------

Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
2026-07-17 15:03:26 +08:00

69 lines
1.9 KiB
TypeScript

import { normalizeForge, type Forge } from "@/git/forge";
export interface PrHint {
url: string;
number: number;
state: "open" | "merged" | "closed";
/** Forge backing this change request, so badges render the right brand mark. */
forge: Forge;
checks?: Array<{ name: string; status: string; url: string | null }>;
checksStatus?: "none" | "pending" | "success" | "failure";
reviewDecision?: "approved" | "changes_requested" | "pending" | null;
}
interface PrStatusLike {
url: string;
state: string;
isMerged: boolean;
checks?: Array<{ name: string; status: string; url: string | null }>;
checksStatus?: string;
reviewDecision?: string | null;
forge?: string;
}
function parsePullRequestNumber(url: string): number | null {
try {
const pathname = new URL(url).pathname;
// GitHub uses /pull/N, Gitea/Forgejo /pulls/N, GitLab /-/merge_requests/N.
// Match any so a non-GitHub change-request summary yields a hint (and brand mark).
const match = pathname.match(/\/(?:pull|pulls|merge_requests)\/(\d+)(?:\/|$)/);
if (!match) {
return null;
}
const number = Number.parseInt(match[1], 10);
return Number.isFinite(number) ? number : null;
} catch {
return null;
}
}
export function selectPrHintFromStatus(
status: PrStatusLike | null | undefined,
forge?: string | null,
): PrHint | null {
if (!status?.url) {
return null;
}
const number = parsePullRequestNumber(status.url);
if (number === null) {
return null;
}
let state: "merged" | "open" | "closed";
if (status.isMerged || status.state === "merged") state = "merged";
else if (status.state === "open") state = "open";
else state = "closed";
return {
url: status.url,
number,
state,
forge: normalizeForge(forge ?? status.forge),
checks: status.checks,
checksStatus: status.checksStatus as PrHint["checksStatus"],
reviewDecision: status.reviewDecision as PrHint["reviewDecision"],
};
}