* 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>
171 lines
4.6 KiB
TypeScript
171 lines
4.6 KiB
TypeScript
import { type Forge, forgeFromRemoteUrl, getForgePresentation } from "@/git/forge";
|
|
import type { DesktopOpenTarget, OpenDesktopTargetInput } from "@/workspace/desktop-open-targets";
|
|
import {
|
|
type ResolvedWorkspaceFilePaths,
|
|
resolveWorkspaceFilePaths,
|
|
type WorkspaceFileLocation,
|
|
} from "@/workspace/file-open";
|
|
|
|
interface CheckoutStatusForOpenTarget {
|
|
isGit: boolean;
|
|
remoteUrl?: string | null;
|
|
currentBranch?: string | null;
|
|
}
|
|
|
|
export interface PlannedDesktopOpenTarget {
|
|
source: "desktop";
|
|
id: string;
|
|
label: string;
|
|
editorId: string;
|
|
icon: DesktopOpenTarget["icon"];
|
|
openInput: OpenDesktopTargetInput;
|
|
}
|
|
|
|
export interface PlannedForgeOpenTarget {
|
|
source: "forge";
|
|
forge: Forge;
|
|
id: Forge;
|
|
label: string;
|
|
url: string;
|
|
}
|
|
|
|
export type PlannedWorkspaceOpenTarget = PlannedDesktopOpenTarget | PlannedForgeOpenTarget;
|
|
|
|
export interface PlanWorkspaceOpenTargetsInput {
|
|
workspaceDirectory: string;
|
|
activeFile?: WorkspaceFileLocation | null;
|
|
resolvedActiveFile?: ResolvedWorkspaceFilePaths | null;
|
|
desktopTargets: readonly DesktopOpenTarget[];
|
|
canUseDesktopBridge: boolean;
|
|
isLocalExecution: boolean;
|
|
checkoutStatus?: CheckoutStatusForOpenTarget | null;
|
|
forge?: Forge | null;
|
|
}
|
|
|
|
function resolveActiveFileForOpenTargets(
|
|
input: Pick<
|
|
PlanWorkspaceOpenTargetsInput,
|
|
"activeFile" | "resolvedActiveFile" | "workspaceDirectory"
|
|
>,
|
|
): ResolvedWorkspaceFilePaths | null {
|
|
if (input.resolvedActiveFile !== undefined) {
|
|
return input.resolvedActiveFile;
|
|
}
|
|
return input.activeFile
|
|
? resolveWorkspaceFilePaths({
|
|
path: input.activeFile.path,
|
|
workspaceRoot: input.workspaceDirectory,
|
|
})
|
|
: null;
|
|
}
|
|
|
|
function planDesktopOpenTargets(input: {
|
|
workspaceDirectory: string;
|
|
activeFile?: WorkspaceFileLocation | null;
|
|
resolvedFile: ResolvedWorkspaceFilePaths | null;
|
|
desktopTargets: readonly DesktopOpenTarget[];
|
|
canUseDesktopBridge: boolean;
|
|
isLocalExecution: boolean;
|
|
}): PlannedDesktopOpenTarget[] {
|
|
if (!input.canUseDesktopBridge || !input.isLocalExecution) {
|
|
return [];
|
|
}
|
|
|
|
return input.desktopTargets.map((target) => {
|
|
if (!input.resolvedFile) {
|
|
return {
|
|
source: "desktop",
|
|
id: target.id,
|
|
label: target.label,
|
|
editorId: target.id,
|
|
icon: target.icon,
|
|
openInput: { editorId: target.id, workspacePath: input.workspaceDirectory },
|
|
};
|
|
}
|
|
return {
|
|
source: "desktop",
|
|
id: target.id,
|
|
label: target.label,
|
|
editorId: target.id,
|
|
icon: target.icon,
|
|
openInput: {
|
|
editorId: target.id,
|
|
workspacePath: input.workspaceDirectory,
|
|
filePath: input.resolvedFile.absolutePath,
|
|
...(input.activeFile?.lineStart ? { line: input.activeFile.lineStart } : {}),
|
|
},
|
|
};
|
|
});
|
|
}
|
|
|
|
function buildForgeWebUrl(
|
|
forge: Forge,
|
|
input: {
|
|
remoteUrl: string | null | undefined;
|
|
branch: string | null | undefined;
|
|
path: string | null;
|
|
lineStart?: number;
|
|
lineEnd?: number;
|
|
},
|
|
): string | null {
|
|
const presentation = getForgePresentation(forge);
|
|
if (input.path) {
|
|
return (
|
|
presentation.buildBlobUrl?.({
|
|
remoteUrl: input.remoteUrl,
|
|
branch: input.branch,
|
|
path: input.path,
|
|
lineStart: input.lineStart,
|
|
lineEnd: input.lineEnd,
|
|
}) ?? null
|
|
);
|
|
}
|
|
return (
|
|
presentation.buildBranchTreeUrl?.({
|
|
remoteUrl: input.remoteUrl,
|
|
branch: input.branch,
|
|
}) ?? null
|
|
);
|
|
}
|
|
|
|
function planForgeOpenTarget(input: {
|
|
activeFile?: WorkspaceFileLocation | null;
|
|
resolvedFile: ResolvedWorkspaceFilePaths | null;
|
|
checkoutStatus?: CheckoutStatusForOpenTarget | null;
|
|
forge?: Forge | null;
|
|
}): PlannedForgeOpenTarget | null {
|
|
if (!input.checkoutStatus?.isGit) {
|
|
return null;
|
|
}
|
|
const forge = input.forge ?? forgeFromRemoteUrl(input.checkoutStatus.remoteUrl) ?? null;
|
|
if (!forge) {
|
|
return null;
|
|
}
|
|
const url = buildForgeWebUrl(forge, {
|
|
remoteUrl: input.checkoutStatus.remoteUrl,
|
|
branch: input.checkoutStatus.currentBranch,
|
|
path: input.resolvedFile?.relativePath ?? null,
|
|
lineStart: input.activeFile?.lineStart,
|
|
lineEnd: input.activeFile?.lineEnd,
|
|
});
|
|
if (!url) {
|
|
return null;
|
|
}
|
|
return {
|
|
source: "forge",
|
|
forge,
|
|
id: forge,
|
|
label: getForgePresentation(forge).brandLabel,
|
|
url,
|
|
};
|
|
}
|
|
|
|
export function planWorkspaceOpenTargets(
|
|
input: PlanWorkspaceOpenTargetsInput,
|
|
): PlannedWorkspaceOpenTarget[] {
|
|
const resolvedFile = resolveActiveFileForOpenTargets(input);
|
|
const desktopTargets = planDesktopOpenTargets({ ...input, resolvedFile });
|
|
const forgeTarget = planForgeOpenTarget({ ...input, resolvedFile });
|
|
return forgeTarget ? [...desktopTargets, forgeTarget] : desktopTargets;
|
|
}
|