History

Add pluggable source control provider detection

- Detect GitHub, GitLab, and Azure DevOps remotes in the shared parser - Cache provider resolution and return unsupported stubs for unregistered kinds - Remove deprecated git RPCs from the web IPC surface

Julius Marminge committed 6 months ago

Commit 506e0fc1 parent e8d67ff8 files · +214 −52Browse files

Showing the changes to apps/server/src/sourceControl/SourceControlProviderRegistry.ts (1 of 8 files). Show all

Unified · Split

apps/server/src/sourceControl/SourceControlProviderRegistry.ts+101−7
@@ -1,8 +1,14 @@
1−import { Context, Effect, Layer } from "effect";
2−import type { SourceControlProviderError } from "@t3tools/contracts";
1+import { Cache, Context, Duration, Effect, Exit, Layer } from "effect";
2+import { SourceControlProviderError } from "@t3tools/contracts";
3+import type { SourceControlProviderKind } from "@t3tools/contracts";
4+import { detectGitHostingProviderFromRemoteUrl } from "@t3tools/shared/git";
35
46 import { SourceControlProvider, type SourceControlProviderShape } from "./SourceControlProvider.ts";
57 import * as GitHubSourceControlProvider from "./GitHubSourceControlProvider.ts";
8+import * as GitVcsDriver from "../vcs/GitVcsDriver.ts";
9+
10+const PROVIDER_DETECTION_CACHE_CAPACITY = 2_048;
11+const PROVIDER_DETECTION_CACHE_TTL = Duration.seconds(5);
612
713 export interface SourceControlProviderRegistryShape {
814 readonly resolve: (input: {
@@ -15,14 +21,102 @@ export class SourceControlProviderRegistry extends Context.Service< · 6 unmodified lines
1521 SourceControlProviderRegistryShape
1622 >()("t3/source-control/SourceControlProviderRegistry") {}
1723
24+function unsupportedProvider(kind: SourceControlProviderKind): SourceControlProviderShape {
25+ const unsupported = (operation: string) =>
26+ Effect.fail(
27+ new SourceControlProviderError({
28+ provider: kind,
29+ operation,
30+ detail: `No ${kind} source control provider is registered.`,
31+ }),
32+ );
33+
34+ return SourceControlProvider.of({
35+ kind,
36+ listChangeRequests: () => unsupported("listChangeRequests"),
37+ getChangeRequest: () => unsupported("getChangeRequest"),
38+ createChangeRequest: () => unsupported("createChangeRequest"),
39+ getRepositoryCloneUrls: () => unsupported("getRepositoryCloneUrls"),
40+ getDefaultBranch: () => unsupported("getDefaultBranch"),
41+ checkoutChangeRequest: () => unsupported("checkoutChangeRequest"),
42+ });
43+}
44+
45+function providerDetectionError(operation: string, cwd: string, cause: unknown) {
46+ return new SourceControlProviderError({
47+ provider: "unknown",
48+ operation,
49+ detail: `Failed to detect source control provider for ${cwd}.`,
50+ cause,
51+ });
52+}
53+
54+function firstRemoteUrlFromVerboseOutput(output: string): string | null {
55+ for (const line of output.split("\n")) {
56+ const trimmed = line.trim();
57+ if (trimmed.length === 0) {
58+ continue;
59+ }
60+ const match = /^\S+\s+(\S+)\s+\((?:fetch|push)\)$/.exec(trimmed);
61+ const remoteUrl = match?.[1]?.trim() ?? "";
62+ if (remoteUrl.length > 0) {
63+ return remoteUrl;
64+ }
65+ }
66+ return null;
67+}
68+
1869 export const make = Effect.fn("makeSourceControlProviderRegistry")(function* () {
19− const github = yield* SourceControlProvider;
70+ const github = yield* GitHubSourceControlProvider.make();
71+ const git = yield* GitVcsDriver.GitVcsDriver;
72+ const providers: Partial<Record<SourceControlProviderKind, SourceControlProviderShape>> = {
73+ github,
74+ };
75+
76+ const detectProviderKind = Effect.fn("SourceControlProviderRegistry.detectProviderKind")(
77+ function* (cwd: string) {
78+ const originUrl = yield* git
79+ .readConfigValue(cwd, "remote.origin.url")
80+ .pipe(Effect.catch(() => Effect.succeed(null)));
81+ const remoteUrl =
82+ originUrl ??
83+ (yield* git
84+ .execute({
85+ operation: "SourceControlProviderRegistry.detectProvider.remoteVerbose",
86+ cwd,
87+ args: ["remote", "-v"],
88+ allowNonZeroExit: true,
89+ })
90+ .pipe(
91+ Effect.map((result) =>
92+ result.code === 0 ? firstRemoteUrlFromVerboseOutput(result.stdout) : null,
93+ ),
94+ Effect.mapError((error) => providerDetectionError("detectProvider", cwd, error)),
95+ ));
96+
97+ if (!remoteUrl) {
98+ return "unknown" as const;
99+ }
100+
101+ return detectGitHostingProviderFromRemoteUrl(remoteUrl)?.kind ?? "unknown";
102+ },
103+ );
104+
105+ const providerKindCache = yield* Cache.makeWith<
106+ string,
107+ SourceControlProviderKind,
108+ SourceControlProviderError
109+ >(detectProviderKind, {
110+ capacity: PROVIDER_DETECTION_CACHE_CAPACITY,
111+ timeToLive: (exit) => (Exit.isSuccess(exit) ? PROVIDER_DETECTION_CACHE_TTL : Duration.zero),
112+ });
20113
21114 return SourceControlProviderRegistry.of({
22− resolve: () => Effect.succeed(github),
115+ resolve: (input) =>
116+ Cache.get(providerKindCache, input.cwd).pipe(
117+ Effect.map((kind) => providers[kind] ?? unsupportedProvider(kind)),
118+ ),
23119 });
24120 });
25121
26−export const layer = Layer.effect(SourceControlProviderRegistry, make()).pipe(
27− Layer.provide(GitHubSourceControlProvider.layer),
28−);
122+export const layer = Layer.effect(SourceControlProviderRegistry, make());