1import { SourceControlProviderKind } from "@t3tools/contracts";
2import * as Cache from "effect/Cache";
3import * as Context from "effect/Context";
4import * as Duration from "effect/Duration";
5import * as Effect from "effect/Effect";
6import * as Exit from "effect/Exit";
7import * as Layer from "effect/Layer";
8import {
9 SourceControlProviderError,
10 type SourceControlProviderDiscoveryItem,
11} from "@t3tools/contracts";
12import type {} from "@t3tools/contracts";
13import { detectSourceControlProviderFromRemoteUrl } from "@t3tools/shared/sourceControl";
14
15import * as BuiltInDrivers from "./builtInDrivers.ts";
16import * as SourceControlProvider from "@t3tools/source-control-core/server/SourceControlProvider";
17import {
18 probeSourceControlProvider,
19 refineUnknownRemoteProvider,
20 type SourceControlProviderDiscoverySpec,
21} from "@t3tools/source-control-core/server/discovery";
22import * as SourceControlHost from "@t3tools/source-control-core/server/SourceControlHost";
23
24import * as ServerConfig from "../config.ts";
25import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts";
26
27const PROVIDER_DETECTION_CACHE_CAPACITY = 2_048;
28const PROVIDER_DETECTION_CACHE_TTL = Duration.seconds(5);
29
30export interface SourceControlProviderRegistration {
31 readonly kind: SourceControlProviderKind;
32 readonly provider: SourceControlProvider.SourceControlProvider["Service"];
33 readonly discovery: SourceControlProviderDiscoverySpec;
34}
35
36export interface SourceControlProviderHandle {
37 readonly provider: SourceControlProvider.SourceControlProvider["Service"];
38 readonly context: SourceControlProvider.SourceControlProviderContext | null;
39}
40
41export class SourceControlProviderRegistry extends Context.Service<
42 SourceControlProviderRegistry,
43 {
44 readonly resolveLink: SourceControlProvider.ResolveSourceControlLink;
45 readonly get: (
46 kind: SourceControlProviderKind,
47 ) => Effect.Effect<
48 SourceControlProvider.SourceControlProvider["Service"],
49 SourceControlProviderError
50 >;
51 readonly resolveHandle: (input: {
52 readonly cwd: string;
53 readonly context?: SourceControlProvider.SourceControlProviderContext;
54 }) => Effect.Effect<SourceControlProviderHandle, SourceControlProviderError>;
55 readonly resolve: (input: {
56 readonly cwd: string;
57 }) => Effect.Effect<
58 SourceControlProvider.SourceControlProvider["Service"],
59 SourceControlProviderError
60 >;
61 readonly discover: Effect.Effect<ReadonlyArray<SourceControlProviderDiscoveryItem>>;
62 }
63>()("t3/sourceControl/SourceControlProviderRegistry") {}
64
65function unsupportedProvider(
66 kind: SourceControlProviderKind,
67): SourceControlProvider.SourceControlProvider["Service"] {
68 return SourceControlProvider.SourceControlProvider.of({
69 kind,
70 listChangeRequests: (input) =>
71 new SourceControlProviderError({
72 provider: kind,
73 operation: "listChangeRequests",
74 cwd: input.cwd,
75 detail: `No ${kind} source control provider is registered.`,
76 }),
77 getChangeRequest: (input) =>
78 new SourceControlProviderError({
79 provider: kind,
80 operation: "getChangeRequest",
81 cwd: input.cwd,
82 reference: SourceControlProvider.transportSafeSourceControlErrorValue(input.reference),
83 detail: `No ${kind} source control provider is registered.`,
84 }),
85 createChangeRequest: (input) =>
86 new SourceControlProviderError({
87 provider: kind,
88 operation: "createChangeRequest",
89 cwd: input.cwd,
90 reference: SourceControlProvider.transportSafeSourceControlErrorValue(input.headSelector),
91 detail: `No ${kind} source control provider is registered.`,
92 }),
93 getRepositoryCloneUrls: (input) =>
94 new SourceControlProviderError({
95 provider: kind,
96 operation: "getRepositoryCloneUrls",
97 cwd: input.cwd,
98 repository: SourceControlProvider.transportSafeSourceControlErrorValue(input.repository),
99 detail: `No ${kind} source control provider is registered.`,
100 }),
101 createRepository: (input) =>
102 new SourceControlProviderError({
103 provider: kind,
104 operation: "createRepository",
105 cwd: input.cwd,
106 repository: SourceControlProvider.transportSafeSourceControlErrorValue(input.repository),
107 detail: `No ${kind} source control provider is registered.`,
108 }),
109 getDefaultBranch: (input) =>
110 new SourceControlProviderError({
111 provider: kind,
112 operation: "getDefaultBranch",
113 cwd: input.cwd,
114 detail: `No ${kind} source control provider is registered.`,
115 }),
116 checkoutChangeRequest: (input) =>
117 new SourceControlProviderError({
118 provider: kind,
119 operation: "checkoutChangeRequest",
120 cwd: input.cwd,
121 reference: SourceControlProvider.transportSafeSourceControlErrorValue(input.reference),
122 detail: `No ${kind} source control provider is registered.`,
123 }),
124 });
125}
126
127function selectProviderContext(
128 remotes: ReadonlyArray<{
129 readonly name: string;
130 readonly url: string;
131 }>,
132): SourceControlProvider.SourceControlProviderContext | null {
133 const candidates: Array<SourceControlProvider.SourceControlProviderContext> = [];
134 for (const remote of remotes) {
135 const provider = detectSourceControlProviderFromRemoteUrl(remote.url);
136 if (provider) {
137 candidates.push({
138 provider,
139 remoteName: remote.name,
140 remoteUrl: remote.url,
141 });
142 }
143 }
144
145 return (
146 candidates.find((candidate) => candidate.remoteName === "origin") ??
147 candidates.find((candidate) => candidate.provider.kind !== "unknown") ??
148 candidates[0] ??
149 null
150 );
151}
152
153function bindProviderContext(
154 provider: SourceControlProvider.SourceControlProvider["Service"],
155 context: SourceControlProvider.SourceControlProviderContext | null,
156): SourceControlProvider.SourceControlProvider["Service"] {
157 if (context === null) {
158 return provider;
159 }
160
161 return SourceControlProvider.SourceControlProvider.of({
162 ...provider,
163 listChangeRequests: (input) =>
164 provider.listChangeRequests({
165 ...input,
166 context: input.context ?? context,
167 }),
168 getChangeRequest: (input) =>
169 provider.getChangeRequest({
170 ...input,
171 context: input.context ?? context,
172 }),
173 createChangeRequest: (input) =>
174 provider.createChangeRequest({
175 ...input,
176 context: input.context ?? context,
177 }),
178 getRepositoryCloneUrls: (input) =>
179 provider.getRepositoryCloneUrls({
180 ...input,
181 context: input.context ?? context,
182 }),
183 createRepository: (input) => provider.createRepository(input),
184 getDefaultBranch: (input) =>
185 provider.getDefaultBranch({
186 ...input,
187 context: input.context ?? context,
188 }),
189 checkoutChangeRequest: (input) =>
190 provider.checkoutChangeRequest({
191 ...input,
192 context: input.context ?? context,
193 }),
194 });
195}
196
197/** @public Service construction is part of the canonical Effect module API. */
198export const makeWithProviders = Effect.fn("makeSourceControlProviderRegistryWithProviders")(
199 function* (registrations: ReadonlyArray<SourceControlProviderRegistration>) {
200 const config = yield* ServerConfig.ServerConfig;
201 const { process } = yield* SourceControlHost.SourceControlHost;
202 const vcsRegistry = yield* VcsDriverRegistry.VcsDriverRegistry;
203 const providers = new Map<
204 SourceControlProviderKind,
205 SourceControlProvider.SourceControlProvider["Service"]
206 >(registrations.map((registration) => [registration.kind, registration.provider]));
207 const discoverySpecs = registrations.map((registration) => registration.discovery);
208
209 const get: SourceControlProviderRegistry["Service"]["get"] = (kind) =>
210 Effect.succeed(providers.get(kind) ?? unsupportedProvider(kind));
211
212 const detectProviderContext = Effect.fn("SourceControlProviderRegistry.detectProviderContext")(
213 function* (cwd: string) {
214 const handle = yield* vcsRegistry.resolve({ cwd }).pipe(
215 Effect.mapError(
216 (error) =>
217 new SourceControlProviderError({
218 provider: SourceControlProviderKind.make("unknown"),
219 operation: "detectProvider",
220 cwd,
221 detail: "Failed to detect source control provider.",
222 cause: error,
223 }),
224 ),
225 );
226 const remotes = yield* handle.driver.listRemotes(cwd).pipe(
227 Effect.mapError(
228 (error) =>
229 new SourceControlProviderError({
230 provider: SourceControlProviderKind.make("unknown"),
231 operation: "detectProvider",
232 cwd,
233 detail: "Failed to detect source control provider.",
234 cause: error,
235 }),
236 ),
237 );
238 const context = selectProviderContext(remotes.remotes);
239
240 return yield* refineUnknownRemoteProvider({
241 specs: discoverySpecs,
242 process,
243 cwd,
244 context,
245 });
246 },
247 );
248
249 const providerContextCache = yield* Cache.makeWith<
250 string,
251 SourceControlProvider.SourceControlProviderContext | null,
252 SourceControlProviderError
253 >(detectProviderContext, {
254 capacity: PROVIDER_DETECTION_CACHE_CAPACITY,
255 timeToLive: (exit) => (Exit.isSuccess(exit) ? PROVIDER_DETECTION_CACHE_TTL : Duration.zero),
256 });
257
258 const resolveHandle: SourceControlProviderRegistry["Service"]["resolveHandle"] = (input) =>
259 (input.context === undefined
260 ? Cache.get(providerContextCache, input.cwd)
261 : refineUnknownRemoteProvider({
262 specs: discoverySpecs,
263 process,
264 cwd: input.cwd,
265 context: input.context,
266 })
267 ).pipe(
268 Effect.map((context) => {
269 const kind = context?.provider.kind ?? SourceControlProviderKind.make("unknown");
270 const provider = providers.get(kind) ?? unsupportedProvider(kind);
271 return {
272 provider: bindProviderContext(provider, context),
273 context,
274 } satisfies SourceControlProviderHandle;
275 }),
276 );
277
278 return SourceControlProviderRegistry.of({
279 resolveLink: (input) => {
280 if (input.url.protocol !== "https:" || input.url.username || input.url.password) {
281 return undefined;
282 }
283 const kind = detectSourceControlProviderFromRemoteUrl(input.url.href)?.kind;
284 return kind ? providers.get(kind)?.resolveLink?.(input) : undefined;
285 },
286 get,
287 resolveHandle,
288 resolve: (input) => resolveHandle(input).pipe(Effect.map((handle) => handle.provider)),
289 discover: Effect.forEach(
290 discoverySpecs,
291 (spec) =>
292 probeSourceControlProvider({
293 spec,
294 process,
295 cwd: config.cwd,
296 }),
297 { concurrency: "unbounded" },
298 ),
299 });
300 },
301);
302
303export const make = Effect.gen(function* () {
304 const drivers = yield* Effect.forEach(BuiltInDrivers.BUILT_IN_SOURCE_CONTROL_DRIVERS, (driver) =>
305 driver.make.pipe(
306 Effect.map((instance): SourceControlProviderRegistration => ({
307 kind: driver.kind,
308 provider: instance.sourceControl,
309 discovery: instance.discovery,
310 })),
311 ),
312 );
313 return yield* makeWithProviders(drivers);
314});
315
316export const layer = Layer.effect(SourceControlProviderRegistry, make);