Showing the changes to apps/server/src/sourceControl/SourceControlProviderRegistry.ts (1 of 59 files). Show all
Unified · Split
| @@ -1,40 +1,46 @@ | ||
| 1 | 1 | import { Cache, Context, Duration, Effect, Exit, Layer } from "effect"; |
| 2 | −import { SourceControlProviderError } from "@t3tools/contracts"; | |
| 2 | +import { | |
| 3 | + SourceControlProviderError, | |
| 4 | + type SourceControlProviderDiscoveryItem, | |
| 5 | +} from "@t3tools/contracts"; | |
| 3 | 6 | import type { SourceControlProviderKind } from "@t3tools/contracts"; |
| 4 | 7 | import { detectSourceControlProviderFromRemoteUrl } from "@t3tools/shared/sourceControl"; |
| 5 | 8 | |
| 6 | −import { | |
| 7 | − SourceControlProvider, | |
| 8 | − type SourceControlProviderContext, | |
| 9 | − type SourceControlProviderShape, | |
| 10 | −} from "./SourceControlProvider.ts"; | |
| 9 | +import * as AzureDevOpsSourceControlProvider from "./AzureDevOpsSourceControlProvider.ts"; | |
| 10 | +import * as BitbucketSourceControlProvider from "./BitbucketSourceControlProvider.ts"; | |
| 11 | 11 | import * as GitHubSourceControlProvider from "./GitHubSourceControlProvider.ts"; |
| 12 | 12 | import * as GitLabSourceControlProvider from "./GitLabSourceControlProvider.ts"; |
| 13 | −import { VcsDriverRegistry } from "../vcs/VcsDriverRegistry.ts"; | |
| 13 | +import * as SourceControlProvider from "./SourceControlProvider.ts"; | |
| 14 | +import * as SourceControlProviderDiscovery from "./SourceControlProviderDiscovery.ts"; | |
| 15 | +import { ServerConfig } from "../config.ts"; | |
| 16 | +import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts"; | |
| 17 | +import * as VcsProcess from "../vcs/VcsProcess.ts"; | |
| 14 | 18 | |
| 15 | 19 | const PROVIDER_DETECTION_CACHE_CAPACITY = 2_048; |
| 16 | 20 | const PROVIDER_DETECTION_CACHE_TTL = Duration.seconds(5); |
| 17 | 21 | |
| 18 | 22 | export interface SourceControlProviderRegistration { |
| 19 | 23 | readonly kind: SourceControlProviderKind; |
| 20 | − readonly provider: SourceControlProviderShape; | |
| 24 | + readonly provider: SourceControlProvider.SourceControlProviderShape; | |
| 25 | + readonly discovery: SourceControlProviderDiscovery.SourceControlProviderDiscoverySpec; | |
| 21 | 26 | } |
| 22 | 27 | |
| 23 | 28 | export interface SourceControlProviderHandle { |
| 24 | − readonly provider: SourceControlProviderShape; | |
| 25 | − readonly context: SourceControlProviderContext | null; | |
| 29 | + readonly provider: SourceControlProvider.SourceControlProviderShape; | |
| 30 | + readonly context: SourceControlProvider.SourceControlProviderContext | null; | |
| 26 | 31 | } |
| 27 | 32 | |
| 28 | 33 | export interface SourceControlProviderRegistryShape { |
| 29 | 34 | readonly get: ( |
| 30 | 35 | kind: SourceControlProviderKind, |
| 31 | − ) => Effect.Effect<SourceControlProviderShape, SourceControlProviderError>; | |
| 36 | + ) => Effect.Effect<SourceControlProvider.SourceControlProviderShape, SourceControlProviderError>; | |
| 32 | 37 | readonly resolveHandle: (input: { |
| 33 | 38 | readonly cwd: string; |
| 34 | 39 | }) => Effect.Effect<SourceControlProviderHandle, SourceControlProviderError>; |
| 35 | 40 | readonly resolve: (input: { |
| 36 | 41 | readonly cwd: string; |
| 37 | − }) => Effect.Effect<SourceControlProviderShape, SourceControlProviderError>; | |
| 42 | + }) => Effect.Effect<SourceControlProvider.SourceControlProviderShape, SourceControlProviderError>; | |
| 43 | + readonly discover: Effect.Effect<ReadonlyArray<SourceControlProviderDiscoveryItem>>; | |
| 38 | 44 | } |
| 39 | 45 | |
| 40 | 46 | export class SourceControlProviderRegistry extends Context.Service< |
| @@ -42,7 +48,9 @@ export class SourceControlProviderRegistry extends Context.Service< · 1 unmodified line | ||
| 42 | 48 | SourceControlProviderRegistryShape |
| 43 | 49 | >()("t3/source-control/SourceControlProviderRegistry") {} |
| 44 | 50 | |
| 45 | −function unsupportedProvider(kind: SourceControlProviderKind): SourceControlProviderShape { | |
| 51 | +function unsupportedProvider( | |
| 52 | + kind: SourceControlProviderKind, | |
| 53 | +): SourceControlProvider.SourceControlProviderShape { | |
| 46 | 54 | const unsupported = (operation: string) => |
| 47 | 55 | Effect.fail( |
| 48 | 56 | new SourceControlProviderError({ |
| @@ -52,7 +60,7 @@ function unsupportedProvider(kind: SourceControlProviderKind): SourceControlProv · 3 unmodified lines | ||
| 52 | 60 | }), |
| 53 | 61 | ); |
| 54 | 62 | |
| 55 | − return SourceControlProvider.of({ | |
| 63 | + return SourceControlProvider.SourceControlProvider.of({ | |
| 56 | 64 | kind, |
| 57 | 65 | listChangeRequests: () => unsupported("listChangeRequests"), |
| 58 | 66 | getChangeRequest: () => unsupported("getChangeRequest"), |
| @@ -78,7 +86,7 @@ function selectProviderContext( · 19 unmodified lines | ||
| 78 | 86 | readonly name: string; |
| 79 | 87 | readonly url: string; |
| 80 | 88 | }>, |
| 81 | −): SourceControlProviderContext | null { | |
| 89 | +): SourceControlProvider.SourceControlProviderContext | null { | |
| 82 | 90 | const candidates = remotes |
| 83 | 91 | .map((remote) => { |
| 84 | 92 | const provider = detectSourceControlProviderFromRemoteUrl(remote.url); |
| @@ -90,7 +98,7 @@ function selectProviderContext( · 5 unmodified lines | ||
| 90 | 98 | } |
| 91 | 99 | : null; |
| 92 | 100 | }) |
| 93 | − .filter((value): value is SourceControlProviderContext => value !== null); | |
| 101 | + .filter((value): value is SourceControlProvider.SourceControlProviderContext => value !== null); | |
| 94 | 102 | |
| 95 | 103 | return ( |
| 96 | 104 | candidates.find((candidate) => candidate.remoteName === "origin") ?? |
| @@ -100,12 +108,60 @@ function selectProviderContext( · 3 unmodified lines | ||
| 100 | 108 | ); |
| 101 | 109 | } |
| 102 | 110 | |
| 111 | +function bindProviderContext( | |
| 112 | + provider: SourceControlProvider.SourceControlProviderShape, | |
| 113 | + context: SourceControlProvider.SourceControlProviderContext | null, | |
| 114 | +): SourceControlProvider.SourceControlProviderShape { | |
| 115 | + if (context === null) { | |
| 116 | + return provider; | |
| 117 | + } | |
| 118 | + | |
| 119 | + return SourceControlProvider.SourceControlProvider.of({ | |
| 120 | + kind: provider.kind, | |
| 121 | + listChangeRequests: (input) => | |
| 122 | + provider.listChangeRequests({ | |
| 123 | + ...input, | |
| 124 | + context: input.context ?? context, | |
| 125 | + }), | |
| 126 | + getChangeRequest: (input) => | |
| 127 | + provider.getChangeRequest({ | |
| 128 | + ...input, | |
| 129 | + context: input.context ?? context, | |
| 130 | + }), | |
| 131 | + createChangeRequest: (input) => | |
| 132 | + provider.createChangeRequest({ | |
| 133 | + ...input, | |
| 134 | + context: input.context ?? context, | |
| 135 | + }), | |
| 136 | + getRepositoryCloneUrls: (input) => | |
| 137 | + provider.getRepositoryCloneUrls({ | |
| 138 | + ...input, | |
| 139 | + context: input.context ?? context, | |
| 140 | + }), | |
| 141 | + createRepository: (input) => provider.createRepository(input), | |
| 142 | + getDefaultBranch: (input) => | |
| 143 | + provider.getDefaultBranch({ | |
| 144 | + ...input, | |
| 145 | + context: input.context ?? context, | |
| 146 | + }), | |
| 147 | + checkoutChangeRequest: (input) => | |
| 148 | + provider.checkoutChangeRequest({ | |
| 149 | + ...input, | |
| 150 | + context: input.context ?? context, | |
| 151 | + }), | |
| 152 | + }); | |
| 153 | +} | |
| 154 | + | |
| 103 | 155 | export const makeWithProviders = Effect.fn("makeSourceControlProviderRegistryWithProviders")( |
| 104 | 156 | function* (registrations: ReadonlyArray<SourceControlProviderRegistration>) { |
| 105 | − const vcsRegistry = yield* VcsDriverRegistry; | |
| 106 | − const providers = new Map<SourceControlProviderKind, SourceControlProviderShape>( | |
| 107 | − registrations.map((registration) => [registration.kind, registration.provider]), | |
| 108 | − ); | |
| 157 | + const config = yield* ServerConfig; | |
| 158 | + const process = yield* VcsProcess.VcsProcess; | |
| 159 | + const vcsRegistry = yield* VcsDriverRegistry.VcsDriverRegistry; | |
| 160 | + const providers = new Map< | |
| 161 | + SourceControlProviderKind, | |
| 162 | + SourceControlProvider.SourceControlProviderShape | |
| 163 | + >(registrations.map((registration) => [registration.kind, registration.provider])); | |
| 164 | + const discoverySpecs = registrations.map((registration) => registration.discovery); | |
| 109 | 165 | |
| 110 | 166 | const get: SourceControlProviderRegistryShape["get"] = (kind) => |
| 111 | 167 | Effect.succeed(providers.get(kind) ?? unsupportedProvider(kind)); |
| @@ -125,7 +181,7 @@ export const makeWithProviders = Effect.fn("makeSourceControlProviderRegistryWit · 13 unmodified lines | ||
| 125 | 181 | |
| 126 | 182 | const providerContextCache = yield* Cache.makeWith< |
| 127 | 183 | string, |
| 128 | − SourceControlProviderContext | null, | |
| 184 | + SourceControlProvider.SourceControlProviderContext | null, | |
| 129 | 185 | SourceControlProviderError |
| 130 | 186 | >(detectProviderContext, { |
| 131 | 187 | capacity: PROVIDER_DETECTION_CACHE_CAPACITY, |
| @@ -136,8 +192,9 @@ export const makeWithProviders = Effect.fn("makeSourceControlProviderRegistryWit · 4 unmodified lines | ||
| 136 | 192 | Cache.get(providerContextCache, input.cwd).pipe( |
| 137 | 193 | Effect.map((context) => { |
| 138 | 194 | const kind = context?.provider.kind ?? "unknown"; |
| 195 | + const provider = providers.get(kind) ?? unsupportedProvider(kind); | |
| 139 | 196 | return { |
| 140 | − provider: providers.get(kind) ?? unsupportedProvider(kind), | |
| 197 | + provider: bindProviderContext(provider, context), | |
| 141 | 198 | context, |
| 142 | 199 | } satisfies SourceControlProviderHandle; |
| 143 | 200 | }), |
| @@ -147,6 +204,16 @@ export const makeWithProviders = Effect.fn("makeSourceControlProviderRegistryWit · 3 unmodified lines | ||
| 147 | 204 | get, |
| 148 | 205 | resolveHandle, |
| 149 | 206 | resolve: (input) => resolveHandle(input).pipe(Effect.map((handle) => handle.provider)), |
| 207 | + discover: Effect.all( | |
| 208 | + discoverySpecs.map((spec) => | |
| 209 | + SourceControlProviderDiscovery.probeSourceControlProvider({ | |
| 210 | + spec, | |
| 211 | + process, | |
| 212 | + cwd: config.cwd, | |
| 213 | + }), | |
| 214 | + ), | |
| 215 | + { concurrency: "unbounded" }, | |
| 216 | + ), | |
| 150 | 217 | }); |
| 151 | 218 | }, |
| 152 | 219 | ); |
| @@ -154,14 +221,29 @@ export const makeWithProviders = Effect.fn("makeSourceControlProviderRegistryWit · 1 unmodified line | ||
| 154 | 221 | export const make = Effect.fn("makeSourceControlProviderRegistry")(function* () { |
| 155 | 222 | const github = yield* GitHubSourceControlProvider.make(); |
| 156 | 223 | const gitlab = yield* GitLabSourceControlProvider.make(); |
| 224 | + const bitbucket = yield* BitbucketSourceControlProvider.make(); | |
| 225 | + const bitbucketDiscovery = yield* BitbucketSourceControlProvider.makeDiscovery(); | |
| 226 | + const azureDevOps = yield* AzureDevOpsSourceControlProvider.make(); | |
| 157 | 227 | return yield* makeWithProviders([ |
| 158 | 228 | { |
| 159 | 229 | kind: "github", |
| 160 | 230 | provider: github, |
| 231 | + discovery: GitHubSourceControlProvider.discovery, | |
| 161 | 232 | }, |
| 162 | 233 | { |
| 163 | 234 | kind: "gitlab", |
| 164 | 235 | provider: gitlab, |
| 236 | + discovery: GitLabSourceControlProvider.discovery, | |
| 237 | + }, | |
| 238 | + { | |
| 239 | + kind: "azure-devops", | |
| 240 | + provider: azureDevOps, | |
| 241 | + discovery: AzureDevOpsSourceControlProvider.discovery, | |
| 242 | + }, | |
| 243 | + { | |
| 244 | + kind: "bitbucket", | |
| 245 | + provider: bitbucket, | |
| 246 | + discovery: bitbucketDiscovery, | |
| 165 | 247 | }, |
| 166 | 248 | ]); |
| 167 | 249 | }); |