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 @@
1import { Context, Effect, Layer } from "effect";1import { Cache, Context, Duration, Effect, Exit, Layer } from "effect";
2import type { SourceControlProviderError } from "@t3tools/contracts";2import { SourceControlProviderError } from "@t3tools/contracts";
3import type { SourceControlProviderKind } from "@t3tools/contracts";
4import { detectGitHostingProviderFromRemoteUrl } from "@t3tools/shared/git";
3 5
4import { SourceControlProvider, type SourceControlProviderShape } from "./SourceControlProvider.ts";6import { SourceControlProvider, type SourceControlProviderShape } from "./SourceControlProvider.ts";
5import * as GitHubSourceControlProvider from "./GitHubSourceControlProvider.ts";7import * as GitHubSourceControlProvider from "./GitHubSourceControlProvider.ts";
8import * as GitVcsDriver from "../vcs/GitVcsDriver.ts";
9
10const PROVIDER_DETECTION_CACHE_CAPACITY = 2_048;
11const PROVIDER_DETECTION_CACHE_TTL = Duration.seconds(5);
6 12
7export interface SourceControlProviderRegistryShape {13export interface SourceControlProviderRegistryShape {
8 readonly resolve: (input: {14 readonly resolve: (input: {
@@ -15,14 +21,102 @@ export class SourceControlProviderRegistry extends Context.Service< · 6 unmodified lines
15 SourceControlProviderRegistryShape21 SourceControlProviderRegistryShape
16>()("t3/source-control/SourceControlProviderRegistry") {}22>()("t3/source-control/SourceControlProviderRegistry") {}
17 23
24function 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
45function 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
54function 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
18export const make = Effect.fn("makeSourceControlProviderRegistry")(function* () {69export 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 });
20 113
21 return SourceControlProviderRegistry.of({114 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 ),
23 });119 });
24});120});
25 121
26export const layer = Layer.effect(SourceControlProviderRegistry, make()).pipe(122export const layer = Layer.effect(SourceControlProviderRegistry, make());
27 Layer.provide(GitHubSourceControlProvider.layer),
28);