History

feat(source-control): add Bitbucket & Azure Devops providers (#2473)

Julius Marminge committed 6 months agov0.0.22-nightly.20260504.196

Commit 91a03e01 parent d501ebe59 files · +5834 −2327Browse files

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

Unified · Split

apps/server/src/sourceControl/SourceControlProviderRegistry.ts+104−22
@@ -1,40 +1,46 @@
1import { Cache, Context, Duration, Effect, Exit, Layer } from "effect";1import { Cache, Context, Duration, Effect, Exit, Layer } from "effect";
2import { SourceControlProviderError } from "@t3tools/contracts";2import {
3 SourceControlProviderError,
4 type SourceControlProviderDiscoveryItem,
5} from "@t3tools/contracts";
3import type { SourceControlProviderKind } from "@t3tools/contracts";6import type { SourceControlProviderKind } from "@t3tools/contracts";
4import { detectSourceControlProviderFromRemoteUrl } from "@t3tools/shared/sourceControl";7import { detectSourceControlProviderFromRemoteUrl } from "@t3tools/shared/sourceControl";
5 8
6import {9import * as AzureDevOpsSourceControlProvider from "./AzureDevOpsSourceControlProvider.ts";
7 SourceControlProvider,10import * as BitbucketSourceControlProvider from "./BitbucketSourceControlProvider.ts";
8 type SourceControlProviderContext,
9 type SourceControlProviderShape,
10} from "./SourceControlProvider.ts";
11import * as GitHubSourceControlProvider from "./GitHubSourceControlProvider.ts";11import * as GitHubSourceControlProvider from "./GitHubSourceControlProvider.ts";
12import * as GitLabSourceControlProvider from "./GitLabSourceControlProvider.ts";12import * as GitLabSourceControlProvider from "./GitLabSourceControlProvider.ts";
13import { VcsDriverRegistry } from "../vcs/VcsDriverRegistry.ts";13import * as SourceControlProvider from "./SourceControlProvider.ts";
14import * as SourceControlProviderDiscovery from "./SourceControlProviderDiscovery.ts";
15import { ServerConfig } from "../config.ts";
16import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts";
17import * as VcsProcess from "../vcs/VcsProcess.ts";
14 18
15const PROVIDER_DETECTION_CACHE_CAPACITY = 2_048;19const PROVIDER_DETECTION_CACHE_CAPACITY = 2_048;
16const PROVIDER_DETECTION_CACHE_TTL = Duration.seconds(5);20const PROVIDER_DETECTION_CACHE_TTL = Duration.seconds(5);
17 21
18export interface SourceControlProviderRegistration {22export 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
23export interface SourceControlProviderHandle {28export 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
28export interface SourceControlProviderRegistryShape {33export 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
40export class SourceControlProviderRegistry extends Context.Service<46export class SourceControlProviderRegistry extends Context.Service<
@@ -42,7 +48,9 @@ export class SourceControlProviderRegistry extends Context.Service< · 1 unmodified line
42 SourceControlProviderRegistryShape48 SourceControlProviderRegistryShape
43>()("t3/source-control/SourceControlProviderRegistry") {}49>()("t3/source-control/SourceControlProviderRegistry") {}
44 50
45function unsupportedProvider(kind: SourceControlProviderKind): SourceControlProviderShape {51function 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 = remotes90 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
111function 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
103export const makeWithProviders = Effect.fn("makeSourceControlProviderRegistryWithProviders")(155export 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 SourceControlProviderError185 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
154export const make = Effect.fn("makeSourceControlProviderRegistry")(function* () {221export 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});