import * as Config from "effect/Config";import * as Context from "effect/Context";import * as Effect from "effect/Effect";import * as FileSystem from "effect/FileSystem";import * as Layer from "effect/Layer";import * as Option from "effect/Option";import * as Schema from "effect/Schema";import { TrimmedNonEmptyString, type SourceControlProviderAuth, type SourceControlRepositoryCloneUrls, type SourceControlRepositoryVisibility,} from "@t3tools/contracts";import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http";import { sanitizeBranchFragment } from "@t3tools/shared/git";import { detectSourceControlProviderFromRemoteUrl } from "@t3tools/shared/sourceControl"; import * as BitbucketPullRequests from "./bitbucketPullRequests.ts";import * as SourceControlProvider from "./SourceControlProvider.ts";import * as GitVcsDriver from "../vcs/GitVcsDriver.ts";import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts"; const DEFAULT_API_BASE_URL = "https://api.bitbucket.org/2.0"; const BitbucketApiEnvConfig = Config.all({ baseUrl: Config.string("T3CODE_BITBUCKET_API_BASE_URL").pipe( Config.withDefault(DEFAULT_API_BASE_URL), ), accessToken: Config.string("T3CODE_BITBUCKET_ACCESS_TOKEN").pipe(Config.option), email: Config.string("T3CODE_BITBUCKET_EMAIL").pipe(Config.option), apiToken: Config.string("T3CODE_BITBUCKET_API_TOKEN").pipe(Config.option),}); export class BitbucketApiError extends Schema.TaggedErrorClass<BitbucketApiError>()( "BitbucketApiError", { operation: Schema.String, detail: Schema.String, status: Schema.optional(Schema.Number), cause: Schema.optional(Schema.Defect()), },) { override get message(): string { return `Bitbucket API failed in ${this.operation}: ${this.detail}`; }}const isBitbucketApiErrorValue = Schema.is(BitbucketApiError); const RawBitbucketRepositorySchema = Schema.Struct({ full_name: TrimmedNonEmptyString, links: Schema.Struct({ html: Schema.optional( Schema.Struct({ href: TrimmedNonEmptyString, }), ), clone: Schema.optional( Schema.Array( Schema.Struct({ name: TrimmedNonEmptyString, href: TrimmedNonEmptyString, }), ), ), }), mainbranch: Schema.optional( Schema.NullOr( Schema.Struct({ name: TrimmedNonEmptyString, }), ), ),}); const RawBitbucketBranchingModelSchema = Schema.Struct({ development: Schema.optional( Schema.Struct({ branch: Schema.optional( Schema.NullOr( Schema.Struct({ name: Schema.optional(TrimmedNonEmptyString), }), ), ), is_valid: Schema.optional(Schema.Boolean), name: Schema.optional(Schema.NullOr(Schema.String)), use_mainbranch: Schema.optional(Schema.Boolean), }), ),}); const BitbucketUserSchema = Schema.Struct({ username: Schema.optional(TrimmedNonEmptyString), display_name: Schema.optional(TrimmedNonEmptyString), account_id: Schema.optional(TrimmedNonEmptyString),}); export interface BitbucketRepositoryLocator { readonly workspace: string; readonly repoSlug: string;} export interface BitbucketApiShape { readonly probeAuth: Effect.Effect<SourceControlProviderAuth, never>; readonly listPullRequests: (input: { readonly cwd: string; readonly context?: SourceControlProvider.SourceControlProviderContext; readonly headSelector: string; readonly source?: SourceControlProvider.SourceControlRefSelector; readonly state: "open" | "closed" | "merged" | "all"; readonly limit?: number; }) => Effect.Effect< ReadonlyArray<BitbucketPullRequests.NormalizedBitbucketPullRequestRecord>, BitbucketApiError >; readonly getPullRequest: (input: { readonly cwd: string; readonly context?: SourceControlProvider.SourceControlProviderContext; readonly reference: string; }) => Effect.Effect< BitbucketPullRequests.NormalizedBitbucketPullRequestRecord, BitbucketApiError >; readonly getRepositoryCloneUrls: (input: { readonly cwd: string; readonly context?: SourceControlProvider.SourceControlProviderContext; readonly repository: string; }) => Effect.Effect<SourceControlRepositoryCloneUrls, BitbucketApiError>; readonly createRepository: (input: { readonly cwd: string; readonly repository: string; readonly visibility: SourceControlRepositoryVisibility; }) => Effect.Effect<SourceControlRepositoryCloneUrls, BitbucketApiError>; readonly createPullRequest: (input: { readonly cwd: string; readonly context?: SourceControlProvider.SourceControlProviderContext; readonly baseBranch: string; readonly headSelector: string; readonly source?: SourceControlProvider.SourceControlRefSelector; readonly target?: SourceControlProvider.SourceControlRefSelector; readonly title: string; readonly bodyFile: string; }) => Effect.Effect<void, BitbucketApiError>; readonly getDefaultBranch: (input: { readonly cwd: string; readonly context?: SourceControlProvider.SourceControlProviderContext; }) => Effect.Effect<string | null, BitbucketApiError>; readonly checkoutPullRequest: (input: { readonly cwd: string; readonly context?: SourceControlProvider.SourceControlProviderContext; readonly reference: string; readonly force?: boolean; }) => Effect.Effect<void, BitbucketApiError>;} export class BitbucketApi extends Context.Service<BitbucketApi, BitbucketApiShape>()( "t3/sourceControl/BitbucketApi",) {} function nonEmpty(value: string | undefined): Option.Option<string> { const trimmed = value?.trim(); return trimmed === undefined || trimmed.length === 0 ? Option.none() : Option.some(trimmed);} function normalizeChangeRequestId(reference: string): string { const trimmed = reference.trim().replace(/^#/, ""); const urlMatch = /(?:pull-requests|pullrequests|pull-request|pull|pr)\/(\d+)(?:\D.*)?$/i.exec( trimmed, ); return urlMatch?.[1] ?? trimmed;} function sourceWorkspace(input: { readonly headSelector: string; readonly source?: SourceControlProvider.SourceControlRefSelector;}): string | undefined { if (input.source?.owner) return input.source.owner; return SourceControlProvider.parseSourceControlOwnerRef(input.headSelector)?.owner;} function toBitbucketStates(state: "open" | "closed" | "merged" | "all"): ReadonlyArray<string> { switch (state) { case "open": return ["OPEN"]; case "closed": return ["DECLINED", "SUPERSEDED"]; case "merged": return ["MERGED"]; case "all": return ["OPEN", "MERGED", "DECLINED", "SUPERSEDED"]; }} function bitbucketQueryString(filters: ReadonlyArray<string>): string { return filters.join(" AND ");} function bitbucketStateFilter(states: ReadonlyArray<string>): string { return states.length === 1 ? `state = "${states[0]}"` : `(${states.map((state) => `state = "${state}"`).join(" OR ")})`;} function parseBitbucketRepositorySlug(value: string): BitbucketRepositoryLocator | null { const normalized = value.trim().replace(/\.git$/u, ""); const parts = normalized.split("/").filter((part) => part.length > 0); if (parts.length < 2) return null; const workspace = parts.at(-2); const repoSlug = parts.at(-1); return workspace && repoSlug ? { workspace, repoSlug } : null;} function requireRepositoryLocator( operation: string, repository: string,): Effect.Effect<BitbucketRepositoryLocator, BitbucketApiError> { const locator = parseBitbucketRepositorySlug(repository); return locator ? Effect.succeed(locator) : Effect.fail( new BitbucketApiError({ operation, detail: "Bitbucket repositories must be specified as workspace/repository.", }), );} function parseBitbucketRemoteUrl(remoteUrl: string): BitbucketRepositoryLocator | null { const trimmed = remoteUrl.trim(); if (trimmed.startsWith("git@")) { const pathStart = trimmed.indexOf(":"); return pathStart < 0 ? null : parseBitbucketRepositorySlug(trimmed.slice(pathStart + 1)); } try { return parseBitbucketRepositorySlug(new URL(trimmed).pathname); } catch { return null; }} function normalizeRepositoryCloneUrls( raw: typeof RawBitbucketRepositorySchema.Type,): SourceControlRepositoryCloneUrls { const httpClone = raw.links.clone?.find((entry) => entry.name.toLowerCase() === "https")?.href ?? raw.links.html?.href; const sshClone = raw.links.clone?.find((entry) => entry.name.toLowerCase() === "ssh")?.href; return { nameWithOwner: raw.full_name, url: httpClone ?? raw.links.html?.href ?? raw.full_name, sshUrl: sshClone ?? httpClone ?? raw.full_name, };} function defaultChangeRequestTargetBranch(input: { readonly repository: typeof RawBitbucketRepositorySchema.Type; readonly branchingModel: typeof RawBitbucketBranchingModelSchema.Type | null;}): string | null { const repositoryMainBranch = input.repository.mainbranch?.name ?? null; const development = input.branchingModel?.development; if (!development || development.use_mainbranch === true || development.is_valid === false) { return repositoryMainBranch; } const developmentBranch = development.branch?.name?.trim() ?? development.name?.trim() ?? ""; if (developmentBranch.length === 0 || developmentBranch === "null") { return repositoryMainBranch; } return developmentBranch;} function shouldPreferSshRemote(originRemoteUrl: string | null): boolean { const trimmed = originRemoteUrl?.trim() ?? ""; return trimmed.startsWith("git@") || trimmed.startsWith("ssh://");} function selectCloneUrl(input: { readonly cloneUrls: SourceControlRepositoryCloneUrls; readonly originRemoteUrl: string | null;}): string { return shouldPreferSshRemote(input.originRemoteUrl) ? input.cloneUrls.sshUrl : input.cloneUrls.url;} function checkoutBranchName(input: { readonly pullRequestId: number; readonly headBranch: string; readonly isCrossRepository: boolean;}): string { if (!input.isCrossRepository) { return input.headBranch; } return `t3code/pr-${input.pullRequestId}/${sanitizeBranchFragment(input.headBranch)}`;} function repositoryNameWithOwner( repository: Schema.Schema.Type< typeof BitbucketPullRequests.BitbucketPullRequestSchema >["source"]["repository"],): string | null { const fullName = repository?.full_name?.trim() ?? ""; return fullName.length > 0 ? fullName : null;} function repositoryOwnerName(repositoryName: string): string { return repositoryName.split("/")[0]?.trim() || "bitbucket";} function authFromConfig( config: Config.Success<typeof BitbucketApiEnvConfig>,): SourceControlProviderAuth { if (Option.isSome(config.accessToken)) { return { status: "unknown", account: Option.none(), host: Option.some("bitbucket.org"), detail: Option.some("Bitbucket access token is configured."), }; } if (Option.isSome(config.email) && Option.isSome(config.apiToken)) { return { status: "unknown", account: config.email, host: Option.some("bitbucket.org"), detail: Option.some("Bitbucket API token is configured."), }; } return { status: "unauthenticated", account: Option.none(), host: Option.some("bitbucket.org"), detail: Option.some( "Set T3CODE_BITBUCKET_EMAIL and T3CODE_BITBUCKET_API_TOKEN, or T3CODE_BITBUCKET_ACCESS_TOKEN.", ), };} function requestError(operation: string, cause: unknown): BitbucketApiError { return new BitbucketApiError({ operation, detail: cause instanceof Error ? cause.message : String(cause), cause, });} function isBitbucketApiError(cause: unknown): cause is BitbucketApiError { return isBitbucketApiErrorValue(cause);} function responseError( operation: string, response: HttpClientResponse.HttpClientResponse,): Effect.Effect<never, BitbucketApiError> { return response.text.pipe( Effect.orElseSucceed(() => ""), Effect.flatMap((body) => Effect.fail( new BitbucketApiError({ operation, status: response.status, detail: body.trim().length > 0 ? `Bitbucket returned HTTP ${response.status}: ${body.trim()}` : `Bitbucket returned HTTP ${response.status}.`, }), ), ), );} export const make = Effect.fn("makeBitbucketApi")(function* () { const config = yield* BitbucketApiEnvConfig; const httpClient = yield* HttpClient.HttpClient; const fileSystem = yield* FileSystem.FileSystem; const git = yield* GitVcsDriver.GitVcsDriver; const vcsRegistry = yield* VcsDriverRegistry.VcsDriverRegistry; const apiUrl = (path: string) => `${config.baseUrl.replace(/\/+$/u, "")}${path}`; const withAuth = (request: HttpClientRequest.HttpClientRequest) => { if (Option.isSome(config.accessToken)) { return request.pipe(HttpClientRequest.bearerToken(config.accessToken.value)); } if (Option.isSome(config.email) && Option.isSome(config.apiToken)) { return request.pipe(HttpClientRequest.basicAuth(config.email.value, config.apiToken.value)); } return request; }; const decodeResponse = <S extends Schema.Top>( operation: string, schema: S, response: HttpClientResponse.HttpClientResponse, ): Effect.Effect<S["Type"], BitbucketApiError, S["DecodingServices"]> => HttpClientResponse.matchStatus({ "2xx": (success) => HttpClientResponse.schemaBodyJson(schema)(success).pipe( Effect.mapError( (cause) => new BitbucketApiError({ operation, detail: "Bitbucket returned invalid JSON for the requested resource.", cause, }), ), ), orElse: (failed) => responseError(operation, failed), })(response); const executeJson = <S extends Schema.Top>( operation: string, request: HttpClientRequest.HttpClientRequest, schema: S, ): Effect.Effect<S["Type"], BitbucketApiError, S["DecodingServices"]> => httpClient.execute(withAuth(request.pipe(HttpClientRequest.acceptJson))).pipe( Effect.mapError((cause) => requestError(operation, cause)), Effect.flatMap((response) => decodeResponse(operation, schema, response)), ); const resolveRepository = Effect.fn("BitbucketApi.resolveRepository")(function* (input: { readonly cwd: string; readonly context?: SourceControlProvider.SourceControlProviderContext; readonly repository?: string; }) { const fromRepository = input.repository !== undefined ? parseBitbucketRepositorySlug(input.repository) : null; if (fromRepository) return fromRepository; const fromContext = input.context?.provider.kind === "bitbucket" ? parseBitbucketRemoteUrl(input.context.remoteUrl) : null; if (fromContext) return fromContext; const handle = yield* vcsRegistry.resolve({ cwd: input.cwd }).pipe( Effect.mapError( (cause) => new BitbucketApiError({ operation: "resolveRepository", detail: `Failed to resolve VCS repository for ${input.cwd}.`, cause, }), ), ); const remotes = yield* handle.driver.listRemotes(input.cwd).pipe( Effect.mapError( (cause) => new BitbucketApiError({ operation: "resolveRepository", detail: `Failed to list remotes for ${input.cwd}.`, cause, }), ), ); for (const remote of remotes.remotes) { if (detectSourceControlProviderFromRemoteUrl(remote.url)?.kind !== "bitbucket") continue; const parsed = parseBitbucketRemoteUrl(remote.url); if (parsed) return parsed; } return yield* new BitbucketApiError({ operation: "resolveRepository", detail: `No Bitbucket repository remote was detected for ${input.cwd}.`, }); }); const getRepositoryFromLocator = (repository: BitbucketRepositoryLocator) => executeJson( "getRepository", HttpClientRequest.get( apiUrl( `/repositories/${encodeURIComponent(repository.workspace)}/${encodeURIComponent(repository.repoSlug)}`, ), ), RawBitbucketRepositorySchema, ); const getRepository = (input: { readonly cwd: string; readonly context?: SourceControlProvider.SourceControlProviderContext; readonly repository?: string; }) => resolveRepository(input).pipe(Effect.flatMap(getRepositoryFromLocator)); const getBranchingModelFromLocator = (repository: BitbucketRepositoryLocator) => executeJson( "getBranchingModel", HttpClientRequest.get( apiUrl( `/repositories/${encodeURIComponent(repository.workspace)}/${encodeURIComponent(repository.repoSlug)}/branching-model`, ), ), RawBitbucketBranchingModelSchema,Lines 1–500 of 769. Load more
import * as Config from "effect/Config"; import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as Schema from "effect/Schema"; import { TrimmedNonEmptyString, type SourceControlProviderAuth, type SourceControlRepositoryCloneUrls, type SourceControlRepositoryVisibility, } from "@t3tools/contracts"; import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"; import { sanitizeBranchFragment } from "@t3tools/shared/git"; import { detectSourceControlProviderFromRemoteUrl } from "@t3tools/shared/sourceControl"; import * as BitbucketPullRequests from "./bitbucketPullRequests.ts"; import * as SourceControlProvider from "./SourceControlProvider.ts"; import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts"; const DEFAULT_API_BASE_URL = "https://api.bitbucket.org/2.0"; const BitbucketApiEnvConfig = Config.all({ baseUrl: Config.string("T3CODE_BITBUCKET_API_BASE_URL").pipe( Config.withDefault(DEFAULT_API_BASE_URL), ), accessToken: Config.string("T3CODE_BITBUCKET_ACCESS_TOKEN").pipe(Config.option), email: Config.string("T3CODE_BITBUCKET_EMAIL").pipe(Config.option), apiToken: Config.string("T3CODE_BITBUCKET_API_TOKEN").pipe(Config.option), }); export class BitbucketApiError extends Schema.TaggedErrorClass<BitbucketApiError>()( "BitbucketApiError", { operation: Schema.String, detail: Schema.String, status: Schema.optional(Schema.Number), cause: Schema.optional(Schema.Defect()), }, ) { override get message(): string { return `Bitbucket API failed in ${this.operation}: ${this.detail}`; } } const isBitbucketApiErrorValue = Schema.is(BitbucketApiError); const RawBitbucketRepositorySchema = Schema.Struct({ full_name: TrimmedNonEmptyString, links: Schema.Struct({ html: Schema.optional( Schema.Struct({ href: TrimmedNonEmptyString, }), ), clone: Schema.optional( Schema.Array( Schema.Struct({ name: TrimmedNonEmptyString, href: TrimmedNonEmptyString, }), ), ), }), mainbranch: Schema.optional( Schema.NullOr( Schema.Struct({ name: TrimmedNonEmptyString, }), ), ), }); const RawBitbucketBranchingModelSchema = Schema.Struct({ development: Schema.optional( Schema.Struct({ branch: Schema.optional( Schema.NullOr( Schema.Struct({ name: Schema.optional(TrimmedNonEmptyString), }), ), ), is_valid: Schema.optional(Schema.Boolean), name: Schema.optional(Schema.NullOr(Schema.String)), use_mainbranch: Schema.optional(Schema.Boolean), }), ), }); const BitbucketUserSchema = Schema.Struct({ username: Schema.optional(TrimmedNonEmptyString), display_name: Schema.optional(TrimmedNonEmptyString), account_id: Schema.optional(TrimmedNonEmptyString), }); export interface BitbucketRepositoryLocator { readonly workspace: string; readonly repoSlug: string; } export interface BitbucketApiShape { readonly probeAuth: Effect.Effect<SourceControlProviderAuth, never>; readonly listPullRequests: (input: { readonly cwd: string; readonly context?: SourceControlProvider.SourceControlProviderContext; readonly headSelector: string; readonly source?: SourceControlProvider.SourceControlRefSelector; readonly state: "open" | "closed" | "merged" | "all"; readonly limit?: number; }) => Effect.Effect< ReadonlyArray<BitbucketPullRequests.NormalizedBitbucketPullRequestRecord>, BitbucketApiError >; readonly getPullRequest: (input: { readonly cwd: string; readonly context?: SourceControlProvider.SourceControlProviderContext; readonly reference: string; }) => Effect.Effect< BitbucketPullRequests.NormalizedBitbucketPullRequestRecord, BitbucketApiError >; readonly getRepositoryCloneUrls: (input: { readonly cwd: string; readonly context?: SourceControlProvider.SourceControlProviderContext; readonly repository: string; }) => Effect.Effect<SourceControlRepositoryCloneUrls, BitbucketApiError>; readonly createRepository: (input: { readonly cwd: string; readonly repository: string; readonly visibility: SourceControlRepositoryVisibility; }) => Effect.Effect<SourceControlRepositoryCloneUrls, BitbucketApiError>; readonly createPullRequest: (input: { readonly cwd: string; readonly context?: SourceControlProvider.SourceControlProviderContext; readonly baseBranch: string; readonly headSelector: string; readonly source?: SourceControlProvider.SourceControlRefSelector; readonly target?: SourceControlProvider.SourceControlRefSelector; readonly title: string; readonly bodyFile: string; }) => Effect.Effect<void, BitbucketApiError>; readonly getDefaultBranch: (input: { readonly cwd: string; readonly context?: SourceControlProvider.SourceControlProviderContext; }) => Effect.Effect<string | null, BitbucketApiError>; readonly checkoutPullRequest: (input: { readonly cwd: string; readonly context?: SourceControlProvider.SourceControlProviderContext; readonly reference: string; readonly force?: boolean; }) => Effect.Effect<void, BitbucketApiError>; } export class BitbucketApi extends Context.Service<BitbucketApi, BitbucketApiShape>()( "t3/sourceControl/BitbucketApi", ) {} function nonEmpty(value: string | undefined): Option.Option<string> { const trimmed = value?.trim(); return trimmed === undefined || trimmed.length === 0 ? Option.none() : Option.some(trimmed); } function normalizeChangeRequestId(reference: string): string { const trimmed = reference.trim().replace(/^#/, ""); const urlMatch = /(?:pull-requests|pullrequests|pull-request|pull|pr)\/(\d+)(?:\D.*)?$/i.exec( trimmed, ); return urlMatch?.[1] ?? trimmed; } function sourceWorkspace(input: { readonly headSelector: string; readonly source?: SourceControlProvider.SourceControlRefSelector; }): string | undefined { if (input.source?.owner) return input.source.owner; return SourceControlProvider.parseSourceControlOwnerRef(input.headSelector)?.owner; } function toBitbucketStates(state: "open" | "closed" | "merged" | "all"): ReadonlyArray<string> { switch (state) { case "open": return ["OPEN"]; case "closed": return ["DECLINED", "SUPERSEDED"]; case "merged": return ["MERGED"]; case "all": return ["OPEN", "MERGED", "DECLINED", "SUPERSEDED"]; } } function bitbucketQueryString(filters: ReadonlyArray<string>): string { return filters.join(" AND "); } function bitbucketStateFilter(states: ReadonlyArray<string>): string { return states.length === 1 ? `state = "${states[0]}"` : `(${states.map((state) => `state = "${state}"`).join(" OR ")})`; } function parseBitbucketRepositorySlug(value: string): BitbucketRepositoryLocator | null { const normalized = value.trim().replace(/\.git$/u, ""); const parts = normalized.split("/").filter((part) => part.length > 0); if (parts.length < 2) return null; const workspace = parts.at(-2); const repoSlug = parts.at(-1); return workspace && repoSlug ? { workspace, repoSlug } : null; } function requireRepositoryLocator( operation: string, repository: string, ): Effect.Effect<BitbucketRepositoryLocator, BitbucketApiError> { const locator = parseBitbucketRepositorySlug(repository); return locator ? Effect.succeed(locator) : Effect.fail( new BitbucketApiError({ operation, detail: "Bitbucket repositories must be specified as workspace/repository.", }), ); } function parseBitbucketRemoteUrl(remoteUrl: string): BitbucketRepositoryLocator | null { const trimmed = remoteUrl.trim(); if (trimmed.startsWith("git@")) { const pathStart = trimmed.indexOf(":"); return pathStart < 0 ? null : parseBitbucketRepositorySlug(trimmed.slice(pathStart + 1)); } try { return parseBitbucketRepositorySlug(new URL(trimmed).pathname); } catch { return null; } } function normalizeRepositoryCloneUrls( raw: typeof RawBitbucketRepositorySchema.Type, ): SourceControlRepositoryCloneUrls { const httpClone = raw.links.clone?.find((entry) => entry.name.toLowerCase() === "https")?.href ?? raw.links.html?.href; const sshClone = raw.links.clone?.find((entry) => entry.name.toLowerCase() === "ssh")?.href; return { nameWithOwner: raw.full_name, url: httpClone ?? raw.links.html?.href ?? raw.full_name, sshUrl: sshClone ?? httpClone ?? raw.full_name, }; } function defaultChangeRequestTargetBranch(input: { readonly repository: typeof RawBitbucketRepositorySchema.Type; readonly branchingModel: typeof RawBitbucketBranchingModelSchema.Type | null; }): string | null { const repositoryMainBranch = input.repository.mainbranch?.name ?? null; const development = input.branchingModel?.development; if (!development || development.use_mainbranch === true || development.is_valid === false) { return repositoryMainBranch; } const developmentBranch = development.branch?.name?.trim() ?? development.name?.trim() ?? ""; if (developmentBranch.length === 0 || developmentBranch === "null") { return repositoryMainBranch; } return developmentBranch; } function shouldPreferSshRemote(originRemoteUrl: string | null): boolean { const trimmed = originRemoteUrl?.trim() ?? ""; return trimmed.startsWith("git@") || trimmed.startsWith("ssh://"); } function selectCloneUrl(input: { readonly cloneUrls: SourceControlRepositoryCloneUrls; readonly originRemoteUrl: string | null; }): string { return shouldPreferSshRemote(input.originRemoteUrl) ? input.cloneUrls.sshUrl : input.cloneUrls.url; } function checkoutBranchName(input: { readonly pullRequestId: number; readonly headBranch: string; readonly isCrossRepository: boolean; }): string { if (!input.isCrossRepository) { return input.headBranch; } return `t3code/pr-${input.pullRequestId}/${sanitizeBranchFragment(input.headBranch)}`; } function repositoryNameWithOwner( repository: Schema.Schema.Type< typeof BitbucketPullRequests.BitbucketPullRequestSchema >["source"]["repository"], ): string | null { const fullName = repository?.full_name?.trim() ?? ""; return fullName.length > 0 ? fullName : null; } function repositoryOwnerName(repositoryName: string): string { return repositoryName.split("/")[0]?.trim() || "bitbucket"; } function authFromConfig( config: Config.Success<typeof BitbucketApiEnvConfig>, ): SourceControlProviderAuth { if (Option.isSome(config.accessToken)) { return { status: "unknown", account: Option.none(), host: Option.some("bitbucket.org"), detail: Option.some("Bitbucket access token is configured."), }; } if (Option.isSome(config.email) && Option.isSome(config.apiToken)) { return { status: "unknown", account: config.email, host: Option.some("bitbucket.org"), detail: Option.some("Bitbucket API token is configured."), }; } return { status: "unauthenticated", account: Option.none(), host: Option.some("bitbucket.org"), detail: Option.some( "Set T3CODE_BITBUCKET_EMAIL and T3CODE_BITBUCKET_API_TOKEN, or T3CODE_BITBUCKET_ACCESS_TOKEN.", ), }; } function requestError(operation: string, cause: unknown): BitbucketApiError { return new BitbucketApiError({ operation, detail: cause instanceof Error ? cause.message : String(cause), cause, }); } function isBitbucketApiError(cause: unknown): cause is BitbucketApiError { return isBitbucketApiErrorValue(cause); } function responseError( operation: string, response: HttpClientResponse.HttpClientResponse, ): Effect.Effect<never, BitbucketApiError> { return response.text.pipe( Effect.orElseSucceed(() => ""), Effect.flatMap((body) => Effect.fail( new BitbucketApiError({ operation, status: response.status, detail: body.trim().length > 0 ? `Bitbucket returned HTTP ${response.status}: ${body.trim()}` : `Bitbucket returned HTTP ${response.status}.`, }), ), ), ); } export const make = Effect.fn("makeBitbucketApi")(function* () { const config = yield* BitbucketApiEnvConfig; const httpClient = yield* HttpClient.HttpClient; const fileSystem = yield* FileSystem.FileSystem; const git = yield* GitVcsDriver.GitVcsDriver; const vcsRegistry = yield* VcsDriverRegistry.VcsDriverRegistry; const apiUrl = (path: string) => `${config.baseUrl.replace(/\/+$/u, "")}${path}`; const withAuth = (request: HttpClientRequest.HttpClientRequest) => { if (Option.isSome(config.accessToken)) { return request.pipe(HttpClientRequest.bearerToken(config.accessToken.value)); } if (Option.isSome(config.email) && Option.isSome(config.apiToken)) { return request.pipe(HttpClientRequest.basicAuth(config.email.value, config.apiToken.value)); } return request; }; const decodeResponse = <S extends Schema.Top>( operation: string, schema: S, response: HttpClientResponse.HttpClientResponse, ): Effect.Effect<S["Type"], BitbucketApiError, S["DecodingServices"]> => HttpClientResponse.matchStatus({ "2xx": (success) => HttpClientResponse.schemaBodyJson(schema)(success).pipe( Effect.mapError( (cause) => new BitbucketApiError({ operation, detail: "Bitbucket returned invalid JSON for the requested resource.", cause, }), ), ), orElse: (failed) => responseError(operation, failed), })(response); const executeJson = <S extends Schema.Top>( operation: string, request: HttpClientRequest.HttpClientRequest, schema: S, ): Effect.Effect<S["Type"], BitbucketApiError, S["DecodingServices"]> => httpClient.execute(withAuth(request.pipe(HttpClientRequest.acceptJson))).pipe( Effect.mapError((cause) => requestError(operation, cause)), Effect.flatMap((response) => decodeResponse(operation, schema, response)), ); const resolveRepository = Effect.fn("BitbucketApi.resolveRepository")(function* (input: { readonly cwd: string; readonly context?: SourceControlProvider.SourceControlProviderContext; readonly repository?: string; }) { const fromRepository = input.repository !== undefined ? parseBitbucketRepositorySlug(input.repository) : null; if (fromRepository) return fromRepository; const fromContext = input.context?.provider.kind === "bitbucket" ? parseBitbucketRemoteUrl(input.context.remoteUrl) : null; if (fromContext) return fromContext; const handle = yield* vcsRegistry.resolve({ cwd: input.cwd }).pipe( Effect.mapError( (cause) => new BitbucketApiError({ operation: "resolveRepository", detail: `Failed to resolve VCS repository for ${input.cwd}.`, cause, }), ), ); const remotes = yield* handle.driver.listRemotes(input.cwd).pipe( Effect.mapError( (cause) => new BitbucketApiError({ operation: "resolveRepository", detail: `Failed to list remotes for ${input.cwd}.`, cause, }), ), ); for (const remote of remotes.remotes) { if (detectSourceControlProviderFromRemoteUrl(remote.url)?.kind !== "bitbucket") continue; const parsed = parseBitbucketRemoteUrl(remote.url); if (parsed) return parsed; } return yield* new BitbucketApiError({ operation: "resolveRepository", detail: `No Bitbucket repository remote was detected for ${input.cwd}.`, }); }); const getRepositoryFromLocator = (repository: BitbucketRepositoryLocator) => executeJson( "getRepository", HttpClientRequest.get( apiUrl( `/repositories/${encodeURIComponent(repository.workspace)}/${encodeURIComponent(repository.repoSlug)}`, ), ), RawBitbucketRepositorySchema, ); const getRepository = (input: { readonly cwd: string; readonly context?: SourceControlProvider.SourceControlProviderContext; readonly repository?: string; }) => resolveRepository(input).pipe(Effect.flatMap(getRepositoryFromLocator)); const getBranchingModelFromLocator = (repository: BitbucketRepositoryLocator) => executeJson( "getBranchingModel", HttpClientRequest.get( apiUrl( `/repositories/${encodeURIComponent(repository.workspace)}/${encodeURIComponent(repository.repoSlug)}/branching-model`, ), ), RawBitbucketBranchingModelSchema,