import { Cache, Context, Duration, Effect, Exit, Layer } from "effect";import { SourceControlProviderError } from "@t3tools/contracts";import type { SourceControlProviderKind } from "@t3tools/contracts";import { detectSourceControlProviderFromGitRemoteUrl } from "@t3tools/shared/git"; import { SourceControlProvider, type SourceControlProviderShape } from "./SourceControlProvider.ts";import * as GitHubSourceControlProvider from "./GitHubSourceControlProvider.ts";import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; const PROVIDER_DETECTION_CACHE_CAPACITY = 2_048;const PROVIDER_DETECTION_CACHE_TTL = Duration.seconds(5); export interface SourceControlProviderRegistryShape { readonly resolve: (input: { readonly cwd: string; }) => Effect.Effect<SourceControlProviderShape, SourceControlProviderError>;} export class SourceControlProviderRegistry extends Context.Service< SourceControlProviderRegistry, SourceControlProviderRegistryShape>()("t3/source-control/SourceControlProviderRegistry") {} function unsupportedProvider(kind: SourceControlProviderKind): SourceControlProviderShape { const unsupported = (operation: string) => Effect.fail( new SourceControlProviderError({ provider: kind, operation, detail: `No ${kind} source control provider is registered.`, }), ); return SourceControlProvider.of({ kind, listChangeRequests: () => unsupported("listChangeRequests"), getChangeRequest: () => unsupported("getChangeRequest"), createChangeRequest: () => unsupported("createChangeRequest"), getRepositoryCloneUrls: () => unsupported("getRepositoryCloneUrls"), 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 firstRemoteUrlFromVerboseOutput(output: string): string | null { for (const line of output.split("\n")) { const trimmed = line.trim(); if (trimmed.length === 0) { continue; } const match = /^\S+\s+(\S+)\s+\((?:fetch|push)\)$/.exec(trimmed); const remoteUrl = match?.[1]?.trim() ?? ""; if (remoteUrl.length > 0) { return remoteUrl; } } return null;} export const make = Effect.fn("makeSourceControlProviderRegistry")(function* () { const github = yield* GitHubSourceControlProvider.make(); const git = yield* GitVcsDriver.GitVcsDriver; const providers: Partial<Record<SourceControlProviderKind, SourceControlProviderShape>> = { github, }; const detectProviderKind = Effect.fn("SourceControlProviderRegistry.detectProviderKind")( function* (cwd: string) { const originUrl = yield* git .readConfigValue(cwd, "remote.origin.url") .pipe(Effect.catch(() => Effect.succeed(null))); const remoteUrl = originUrl ?? (yield* git .execute({ operation: "SourceControlProviderRegistry.detectProvider.remoteVerbose", cwd, args: ["remote", "-v"], allowNonZeroExit: true, }) .pipe( Effect.map((result) => result.exitCode === 0 ? firstRemoteUrlFromVerboseOutput(result.stdout) : null, ), Effect.mapError((error) => providerDetectionError("detectProvider", cwd, error)), )); if (!remoteUrl) { return "unknown" as const; } return detectSourceControlProviderFromGitRemoteUrl(remoteUrl)?.kind ?? "unknown"; }, ); const providerKindCache = yield* Cache.makeWith< string, SourceControlProviderKind, SourceControlProviderError >(detectProviderKind, { capacity: PROVIDER_DETECTION_CACHE_CAPACITY, timeToLive: (exit) => (Exit.isSuccess(exit) ? PROVIDER_DETECTION_CACHE_TTL : Duration.zero), }); return SourceControlProviderRegistry.of({ resolve: (input) => Cache.get(providerKindCache, input.cwd).pipe( Effect.map((kind) => providers[kind] ?? unsupportedProvider(kind)), ), });}); export const layer = Layer.effect(SourceControlProviderRegistry, make());
import { Cache, Context, Duration, Effect, Exit, Layer } from "effect";
import { SourceControlProviderError } from "@t3tools/contracts";
import type { SourceControlProviderKind } from "@t3tools/contracts";
import { detectSourceControlProviderFromGitRemoteUrl } from "@t3tools/shared/git";
import { SourceControlProvider, type SourceControlProviderShape } from "./SourceControlProvider.ts";
import * as GitHubSourceControlProvider from "./GitHubSourceControlProvider.ts";
import * as GitVcsDriver from "../vcs/GitVcsDriver.ts";
const PROVIDER_DETECTION_CACHE_CAPACITY = 2_048;
const PROVIDER_DETECTION_CACHE_TTL = Duration.seconds(5);
export interface SourceControlProviderRegistryShape {
readonly resolve: (input: {
readonly cwd: string;
}) => Effect.Effect<SourceControlProviderShape, SourceControlProviderError>;
}
export class SourceControlProviderRegistry extends Context.Service<
SourceControlProviderRegistry,
SourceControlProviderRegistryShape
>()("t3/source-control/SourceControlProviderRegistry") {}
function unsupportedProvider(kind: SourceControlProviderKind): SourceControlProviderShape {
const unsupported = (operation: string) =>
Effect.fail(
new SourceControlProviderError({
provider: kind,
operation,
detail: `No ${kind} source control provider is registered.`,
}),
);
return SourceControlProvider.of({
kind,
listChangeRequests: () => unsupported("listChangeRequests"),
getChangeRequest: () => unsupported("getChangeRequest"),
createChangeRequest: () => unsupported("createChangeRequest"),
getRepositoryCloneUrls: () => unsupported("getRepositoryCloneUrls"),
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 firstRemoteUrlFromVerboseOutput(output: string): string | null {
for (const line of output.split("\n")) {
const trimmed = line.trim();
if (trimmed.length === 0) {
continue;
}
const match = /^\S+\s+(\S+)\s+\((?:fetch|push)\)$/.exec(trimmed);
const remoteUrl = match?.[1]?.trim() ?? "";
if (remoteUrl.length > 0) {
return remoteUrl;
}
}
return null;
}
export const make = Effect.fn("makeSourceControlProviderRegistry")(function* () {
const github = yield* GitHubSourceControlProvider.make();
const git = yield* GitVcsDriver.GitVcsDriver;
const providers: Partial<Record<SourceControlProviderKind, SourceControlProviderShape>> = {
github,
};
const detectProviderKind = Effect.fn("SourceControlProviderRegistry.detectProviderKind")(
function* (cwd: string) {
const originUrl = yield* git
.readConfigValue(cwd, "remote.origin.url")
.pipe(Effect.catch(() => Effect.succeed(null)));
const remoteUrl =
originUrl ??
(yield* git
.execute({
operation: "SourceControlProviderRegistry.detectProvider.remoteVerbose",
cwd,
args: ["remote", "-v"],
allowNonZeroExit: true,
})
.pipe(
Effect.map((result) =>
result.exitCode === 0 ? firstRemoteUrlFromVerboseOutput(result.stdout) : null,
),
Effect.mapError((error) => providerDetectionError("detectProvider", cwd, error)),
));
if (!remoteUrl) {
return "unknown" as const;
}
return detectSourceControlProviderFromGitRemoteUrl(remoteUrl)?.kind ?? "unknown";
},
);
const providerKindCache = yield* Cache.makeWith<
string,
SourceControlProviderKind,
SourceControlProviderError
>(detectProviderKind, {
capacity: PROVIDER_DETECTION_CACHE_CAPACITY,
timeToLive: (exit) => (Exit.isSuccess(exit) ? PROVIDER_DETECTION_CACHE_TTL : Duration.zero),
});
return SourceControlProviderRegistry.of({
resolve: (input) =>
Cache.get(providerKindCache, input.cwd).pipe(
Effect.map((kind) => providers[kind] ?? unsupportedProvider(kind)),
),
});
});
export const layer = Layer.effect(SourceControlProviderRegistry, make());