Showing the changes to apps/server/src/sourceControl/SourceControlProviderRegistry.ts (1 of 59 files). Show all
Unified · Split
| @@ -1,40 +1,46 @@ | |||
| 1 | import { Cache, Context, Duration, Effect, Exit, Layer } from "effect"; | 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 | import type { SourceControlProviderKind } from "@t3tools/contracts"; | 6 | import type { SourceControlProviderKind } from "@t3tools/contracts"; |
| 4 | import { detectSourceControlProviderFromRemoteUrl } from "@t3tools/shared/sourceControl"; | 7 | import { detectSourceControlProviderFromRemoteUrl } from "@t3tools/shared/sourceControl"; |
| 5 | 8 | ||
| 6 | import { | 9 | import * as AzureDevOpsSourceControlProvider from "./AzureDevOpsSourceControlProvider.ts"; |
| 7 | SourceControlProvider, | 10 | import * as BitbucketSourceControlProvider from "./BitbucketSourceControlProvider.ts"; |
| 8 | type SourceControlProviderContext, | ||
| 9 | type SourceControlProviderShape, | ||
| 10 | } from "./SourceControlProvider.ts"; | ||
| 11 | import * as GitHubSourceControlProvider from "./GitHubSourceControlProvider.ts"; | 11 | import * as GitHubSourceControlProvider from "./GitHubSourceControlProvider.ts"; |
| 12 | import * as GitLabSourceControlProvider from "./GitLabSourceControlProvider.ts"; | 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 | const PROVIDER_DETECTION_CACHE_CAPACITY = 2_048; | 19 | const PROVIDER_DETECTION_CACHE_CAPACITY = 2_048; |
| 16 | const PROVIDER_DETECTION_CACHE_TTL = Duration.seconds(5); | 20 | const PROVIDER_DETECTION_CACHE_TTL = Duration.seconds(5); |
| 17 | 21 | ||
| 18 | export interface SourceControlProviderRegistration { | 22 | export interface SourceControlProviderRegistration { |
| 19 | readonly kind: SourceControlProviderKind; | 23 | readonly kind: SourceControlProviderKind; |
| 20 | readonly provider: SourceControlProviderShape; | 24 | readonly provider: SourceControlProvider.SourceControlProviderShape; |
| 25 | readonly discovery: SourceControlProviderDiscovery.SourceControlProviderDiscoverySpec; | ||
| 21 | } | 26 | } |
| 22 | 27 | ||
| 23 | export interface SourceControlProviderHandle { | 28 | export interface SourceControlProviderHandle { |
| 24 | readonly provider: SourceControlProviderShape; | 29 | readonly provider: SourceControlProvider.SourceControlProviderShape; |
| 25 | readonly context: SourceControlProviderContext | null; | 30 | readonly context: SourceControlProvider.SourceControlProviderContext | null; |
| 26 | } | 31 | } |
| 27 | 32 | ||
| 28 | export interface SourceControlProviderRegistryShape { | 33 | export interface SourceControlProviderRegistryShape { |
| 29 | readonly get: ( | 34 | readonly get: ( |
| 30 | kind: SourceControlProviderKind, | 35 | kind: SourceControlProviderKind, |
| 31 | ) => Effect.Effect<SourceControlProviderShape, SourceControlProviderError>; | 36 | ) => Effect.Effect<SourceControlProvider.SourceControlProviderShape, SourceControlProviderError>; |
| 32 | readonly resolveHandle: (input: { | 37 | readonly resolveHandle: (input: { |
| 33 | readonly cwd: string; | 38 | readonly cwd: string; |
| 34 | }) => Effect.Effect<SourceControlProviderHandle, SourceControlProviderError>; | 39 | }) => Effect.Effect<SourceControlProviderHandle, SourceControlProviderError>; |
| 35 | readonly resolve: (input: { | 40 | readonly resolve: (input: { |
| 36 | readonly cwd: string; | 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 | export class SourceControlProviderRegistry extends Context.Service< | 46 | export class SourceControlProviderRegistry extends Context.Service< |
| @@ -42,7 +48,9 @@ export class SourceControlProviderRegistry extends Context.Service< · 1 unmodified line | |||
| 42 | SourceControlProviderRegistryShape | 48 | SourceControlProviderRegistryShape |
| 43 | >()("t3/source-control/SourceControlProviderRegistry") {} | 49 | >()("t3/source-control/SourceControlProviderRegistry") {} |
| 44 | 50 | ||
| 45 | function unsupportedProvider(kind: SourceControlProviderKind): SourceControlProviderShape { | 51 | function unsupportedProvider( |
| 52 | kind: SourceControlProviderKind, | ||
| 53 | ): SourceControlProvider.SourceControlProviderShape { | ||
| 46 | const unsupported = (operation: string) => | 54 | const unsupported = (operation: string) => |
| 47 | Effect.fail( | 55 | Effect.fail( |
| 48 | new SourceControlProviderError({ | 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 | kind, | 64 | kind, |
| 57 | listChangeRequests: () => unsupported("listChangeRequests"), | 65 | listChangeRequests: () => unsupported("listChangeRequests"), |
| 58 | getChangeRequest: () => unsupported("getChangeRequest"), | 66 | getChangeRequest: () => unsupported("getChangeRequest"), |
| @@ -78,7 +86,7 @@ function selectProviderContext( · 19 unmodified lines | |||
| 78 | readonly name: string; | 86 | readonly name: string; |
| 79 | readonly url: string; | 87 | readonly url: string; |
| 80 | }>, | 88 | }>, |
| 81 | ): SourceControlProviderContext | null { | 89 | ): SourceControlProvider.SourceControlProviderContext | null { |
| 82 | const candidates = remotes | 90 | const candidates = remotes |
| 83 | .map((remote) => { | 91 | .map((remote) => { |
| 84 | const provider = detectSourceControlProviderFromRemoteUrl(remote.url); | 92 | const provider = detectSourceControlProviderFromRemoteUrl(remote.url); |
| @@ -90,7 +98,7 @@ function selectProviderContext( · 5 unmodified lines | |||
| 90 | } | 98 | } |
| 91 | : null; | 99 | : null; |
| 92 | }) | 100 | }) |
| 93 | .filter((value): value is SourceControlProviderContext => value !== null); | 101 | .filter((value): value is SourceControlProvider.SourceControlProviderContext => value !== null); |
| 94 | 102 | ||
| 95 | return ( | 103 | return ( |
| 96 | candidates.find((candidate) => candidate.remoteName === "origin") ?? | 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 | export const makeWithProviders = Effect.fn("makeSourceControlProviderRegistryWithProviders")( | 155 | export const makeWithProviders = Effect.fn("makeSourceControlProviderRegistryWithProviders")( |
| 104 | function* (registrations: ReadonlyArray<SourceControlProviderRegistration>) { | 156 | function* (registrations: ReadonlyArray<SourceControlProviderRegistration>) { |
| 105 | const vcsRegistry = yield* VcsDriverRegistry; | 157 | const config = yield* ServerConfig; |
| 106 | const providers = new Map<SourceControlProviderKind, SourceControlProviderShape>( | 158 | const process = yield* VcsProcess.VcsProcess; |
| 107 | registrations.map((registration) => [registration.kind, registration.provider]), | 159 | const vcsRegistry = yield* VcsDriverRegistry.VcsDriverRegistry; |
| 108 | ); | 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 | const get: SourceControlProviderRegistryShape["get"] = (kind) => | 166 | const get: SourceControlProviderRegistryShape["get"] = (kind) => |
| 111 | Effect.succeed(providers.get(kind) ?? unsupportedProvider(kind)); | 167 | Effect.succeed(providers.get(kind) ?? unsupportedProvider(kind)); |
| @@ -125,7 +181,7 @@ export const makeWithProviders = Effect.fn("makeSourceControlProviderRegistryWit · 13 unmodified lines | |||
| 125 | 181 | ||
| 126 | const providerContextCache = yield* Cache.makeWith< | 182 | const providerContextCache = yield* Cache.makeWith< |
| 127 | string, | 183 | string, |
| 128 | SourceControlProviderContext | null, | 184 | SourceControlProvider.SourceControlProviderContext | null, |
| 129 | SourceControlProviderError | 185 | SourceControlProviderError |
| 130 | >(detectProviderContext, { | 186 | >(detectProviderContext, { |
| 131 | capacity: PROVIDER_DETECTION_CACHE_CAPACITY, | 187 | capacity: PROVIDER_DETECTION_CACHE_CAPACITY, |
| @@ -136,8 +192,9 @@ export const makeWithProviders = Effect.fn("makeSourceControlProviderRegistryWit · 4 unmodified lines | |||
| 136 | Cache.get(providerContextCache, input.cwd).pipe( | 192 | Cache.get(providerContextCache, input.cwd).pipe( |
| 137 | Effect.map((context) => { | 193 | Effect.map((context) => { |
| 138 | const kind = context?.provider.kind ?? "unknown"; | 194 | const kind = context?.provider.kind ?? "unknown"; |
| 195 | const provider = providers.get(kind) ?? unsupportedProvider(kind); | ||
| 139 | return { | 196 | return { |
| 140 | provider: providers.get(kind) ?? unsupportedProvider(kind), | 197 | provider: bindProviderContext(provider, context), |
| 141 | context, | 198 | context, |
| 142 | } satisfies SourceControlProviderHandle; | 199 | } satisfies SourceControlProviderHandle; |
| 143 | }), | 200 | }), |
| @@ -147,6 +204,16 @@ export const makeWithProviders = Effect.fn("makeSourceControlProviderRegistryWit · 3 unmodified lines | |||
| 147 | get, | 204 | get, |
| 148 | resolveHandle, | 205 | resolveHandle, |
| 149 | resolve: (input) => resolveHandle(input).pipe(Effect.map((handle) => handle.provider)), | 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 | export const make = Effect.fn("makeSourceControlProviderRegistry")(function* () { | 221 | export const make = Effect.fn("makeSourceControlProviderRegistry")(function* () { |
| 155 | const github = yield* GitHubSourceControlProvider.make(); | 222 | const github = yield* GitHubSourceControlProvider.make(); |
| 156 | const gitlab = yield* GitLabSourceControlProvider.make(); | 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 | return yield* makeWithProviders([ | 227 | return yield* makeWithProviders([ |
| 158 | { | 228 | { |
| 159 | kind: "github", | 229 | kind: "github", |
| 160 | provider: github, | 230 | provider: github, |
| 231 | discovery: GitHubSourceControlProvider.discovery, | ||
| 161 | }, | 232 | }, |
| 162 | { | 233 | { |
| 163 | kind: "gitlab", | 234 | kind: "gitlab", |
| 164 | provider: gitlab, | 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 | }); |