import * as Cache from "effect/Cache"; import * as Context from "effect/Context"; import * as Duration from "effect/Duration"; import * as Effect from "effect/Effect"; import * as Exit from "effect/Exit"; import * as Layer from "effect/Layer"; import { SourceControlProviderError, type SourceControlProviderDiscoveryItem, } from "@t3tools/contracts"; import type { SourceControlProviderKind } from "@t3tools/contracts"; import { detectSourceControlProviderFromRemoteUrl } from "@t3tools/shared/sourceControl"; import * as AzureDevOpsSourceControlProvider from "./AzureDevOpsSourceControlProvider.ts"; import * as BitbucketSourceControlProvider from "./BitbucketSourceControlProvider.ts"; import * as GitHubSourceControlProvider from "./GitHubSourceControlProvider.ts"; import * as GitLabSourceControlProvider from "./GitLabSourceControlProvider.ts"; import * as SourceControlProvider from "./SourceControlProvider.ts"; import * as SourceControlProviderDiscovery from "./SourceControlProviderDiscovery.ts"; import { ServerConfig } from "../config.ts"; import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts"; import * as VcsProcess from "../vcs/VcsProcess.ts"; const PROVIDER_DETECTION_CACHE_CAPACITY = 2_048; const PROVIDER_DETECTION_CACHE_TTL = Duration.seconds(5); export interface SourceControlProviderRegistration { readonly kind: SourceControlProviderKind; readonly provider: SourceControlProvider.SourceControlProviderShape; readonly discovery: SourceControlProviderDiscovery.SourceControlProviderDiscoverySpec; } export interface SourceControlProviderHandle { readonly provider: SourceControlProvider.SourceControlProviderShape; readonly context: SourceControlProvider.SourceControlProviderContext | null; } export interface SourceControlProviderRegistryShape { readonly get: ( kind: SourceControlProviderKind, ) => Effect.Effect; readonly resolveHandle: (input: { readonly cwd: string; }) => Effect.Effect; readonly resolve: (input: { readonly cwd: string; }) => Effect.Effect; readonly discover: Effect.Effect>; } export class SourceControlProviderRegistry extends Context.Service< SourceControlProviderRegistry, SourceControlProviderRegistryShape >()("t3/sourceControl/SourceControlProviderRegistry") {} function unsupportedProvider( kind: SourceControlProviderKind, ): SourceControlProvider.SourceControlProviderShape { const unsupported = (operation: string) => Effect.fail( new SourceControlProviderError({ provider: kind, operation, detail: `No ${kind} source control provider is registered.`, }), ); return SourceControlProvider.SourceControlProvider.of({ kind, listChangeRequests: () => unsupported("listChangeRequests"), getChangeRequest: () => unsupported("getChangeRequest"), createChangeRequest: () => unsupported("createChangeRequest"), getRepositoryCloneUrls: () => unsupported("getRepositoryCloneUrls"), createRepository: () => unsupported("createRepository"), getDefaultBranch: () => unsupported("getDefaultBranch"), checkoutChangeRequest: () => unsupported("checkoutChangeRequest"), }); } function providerDetectionError(operation: string, cwd: string, cause: unknown) { return new SourceControlProviderError({ provider: "unknown", operation, detail: `Failed to detect source control provider for ${cwd}.`, cause, }); } function selectProviderContext( remotes: ReadonlyArray<{ readonly name: string; readonly url: string; }>, ): SourceControlProvider.SourceControlProviderContext | null { const candidates: Array = []; for (const remote of remotes) { const provider = detectSourceControlProviderFromRemoteUrl(remote.url); if (provider) { candidates.push({ provider, remoteName: remote.name, remoteUrl: remote.url, }); } } return ( candidates.find((candidate) => candidate.remoteName === "origin") ?? candidates.find((candidate) => candidate.provider.kind !== "unknown") ?? candidates[0] ?? null ); } function bindProviderContext( provider: SourceControlProvider.SourceControlProviderShape, context: SourceControlProvider.SourceControlProviderContext | null, ): SourceControlProvider.SourceControlProviderShape { if (context === null) { return provider; } return SourceControlProvider.SourceControlProvider.of({ kind: provider.kind, listChangeRequests: (input) => provider.listChangeRequests({ ...input, context: input.context ?? context, }), getChangeRequest: (input) => provider.getChangeRequest({ ...input, context: input.context ?? context, }), createChangeRequest: (input) => provider.createChangeRequest({ ...input, context: input.context ?? context, }), getRepositoryCloneUrls: (input) => provider.getRepositoryCloneUrls({ ...input, context: input.context ?? context, }), createRepository: (input) => provider.createRepository(input), getDefaultBranch: (input) => provider.getDefaultBranch({ ...input, context: input.context ?? context, }), checkoutChangeRequest: (input) => provider.checkoutChangeRequest({ ...input, context: input.context ?? context, }), }); } export const makeWithProviders = Effect.fn("makeSourceControlProviderRegistryWithProviders")( function* (registrations: ReadonlyArray) { const config = yield* ServerConfig; const process = yield* VcsProcess.VcsProcess; const vcsRegistry = yield* VcsDriverRegistry.VcsDriverRegistry; const providers = new Map< SourceControlProviderKind, SourceControlProvider.SourceControlProviderShape >(registrations.map((registration) => [registration.kind, registration.provider])); const discoverySpecs = registrations.map((registration) => registration.discovery); const get: SourceControlProviderRegistryShape["get"] = (kind) => Effect.succeed(providers.get(kind) ?? unsupportedProvider(kind)); const detectProviderContext = Effect.fn("SourceControlProviderRegistry.detectProviderContext")( function* (cwd: string) { const handle = yield* vcsRegistry .resolve({ cwd }) .pipe(Effect.mapError((error) => providerDetectionError("detectProvider", cwd, error))); const remotes = yield* handle.driver .listRemotes(cwd) .pipe(Effect.mapError((error) => providerDetectionError("detectProvider", cwd, error))); const context = selectProviderContext(remotes.remotes); return yield* SourceControlProviderDiscovery.refineUnknownRemoteProvider({ specs: discoverySpecs, process, cwd, context, }); }, ); const providerContextCache = yield* Cache.makeWith< string, SourceControlProvider.SourceControlProviderContext | null, SourceControlProviderError >(detectProviderContext, { capacity: PROVIDER_DETECTION_CACHE_CAPACITY, timeToLive: (exit) => (Exit.isSuccess(exit) ? PROVIDER_DETECTION_CACHE_TTL : Duration.zero), }); const resolveHandle: SourceControlProviderRegistryShape["resolveHandle"] = (input) => Cache.get(providerContextCache, input.cwd).pipe( Effect.map((context) => { const kind = context?.provider.kind ?? "unknown"; const provider = providers.get(kind) ?? unsupportedProvider(kind); return { provider: bindProviderContext(provider, context), context, } satisfies SourceControlProviderHandle; }), ); return SourceControlProviderRegistry.of({ get, resolveHandle, resolve: (input) => resolveHandle(input).pipe(Effect.map((handle) => handle.provider)), discover: Effect.all( discoverySpecs.map((spec) => SourceControlProviderDiscovery.probeSourceControlProvider({ spec, process, cwd: config.cwd, }), ), { concurrency: "unbounded" }, ), }); }, ); export const make = Effect.fn("makeSourceControlProviderRegistry")(function* () { const github = yield* GitHubSourceControlProvider.make(); const gitlab = yield* GitLabSourceControlProvider.make(); const bitbucket = yield* BitbucketSourceControlProvider.make(); const bitbucketDiscovery = yield* BitbucketSourceControlProvider.makeDiscovery(); const azureDevOps = yield* AzureDevOpsSourceControlProvider.make(); return yield* makeWithProviders([ { kind: "github", provider: github, discovery: GitHubSourceControlProvider.discovery, }, { kind: "gitlab", provider: gitlab, discovery: GitLabSourceControlProvider.discovery, }, { kind: "azure-devops", provider: azureDevOps, discovery: AzureDevOpsSourceControlProvider.discovery, }, { kind: "bitbucket", provider: bitbucket, discovery: bitbucketDiscovery, }, ]); }); export const layer = Layer.effect(SourceControlProviderRegistry, make());