| 1 | /** |
| 2 | * Bitbucket's client definition. Browser- and React Native-safe. |
| 3 | * |
| 4 | * @module source-control-bitbucket/client/definition |
| 5 | */ |
| 6 | import { |
| 7 | makeProviderSettingsSchema, |
| 8 | SourceControlProviderKind, |
| 9 | TrimmedString, |
| 10 | } from "@t3tools/contracts"; |
| 11 | import * as Effect from "effect/Effect"; |
| 12 | import * as Schema from "effect/Schema"; |
| 13 | import { |
| 14 | defineSourceControlClient, |
| 15 | isChangeRequestInProjectRepository, |
| 16 | isChangeRequestOnProjectHost, |
| 17 | isChangeRequestPath, |
| 18 | } from "@t3tools/source-control-core/client/definition"; |
| 19 | |
| 20 | const safeShellArgument = /^[A-Za-z0-9._/@+=,-]+$/; |
| 21 | const repositoryName = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/; |
| 22 | |
| 23 | const KIND = SourceControlProviderKind.make("bitbucket"); |
| 24 | |
| 25 | const text = (annotations: Schema.Annotations.Key<string>) => |
| 26 | TrimmedString.pipe( |
| 27 | Schema.withDecodingDefault(Effect.succeed("")), |
| 28 | Schema.annotateKey(annotations), |
| 29 | ); |
| 30 | |
| 31 | /** |
| 32 | * Bitbucket API credentials, used before the `T3CODE_BITBUCKET_*` environment variables: an |
| 33 | * access token, or an Atlassian account email with an API token. The access token wins when both |
| 34 | * are saved. |
| 35 | */ |
| 36 | export const settings = makeProviderSettingsSchema( |
| 37 | { |
| 38 | accessToken: text({ |
| 39 | title: "Access token", |
| 40 | description: |
| 41 | "Scoped to one repository, project, or workspace. Create it in that item's Bitbucket settings.", |
| 42 | settingsSecret: true, |
| 43 | providerSettingsForm: { control: "password" }, |
| 44 | }), |
| 45 | email: text({ |
| 46 | title: "Atlassian account email", |
| 47 | description: "With an API token instead of an access token.", |
| 48 | providerSettingsForm: { placeholder: "you@example.com" }, |
| 49 | }), |
| 50 | apiToken: text({ |
| 51 | title: "API token", |
| 52 | description: |
| 53 | "Reaches every repository your Atlassian account can. Give it read and write access to repositories and pull requests, and read:user:bitbucket.", |
| 54 | settingsSecret: true, |
| 55 | providerSettingsForm: { control: "password" }, |
| 56 | }), |
| 57 | }, |
| 58 | { order: ["accessToken", "email", "apiToken"] }, |
| 59 | ); |
| 60 | export type BitbucketSettings = typeof settings.Type; |
| 61 | |
| 62 | export const definition = defineSourceControlClient({ |
| 63 | kind: KIND, |
| 64 | label: "Bitbucket", |
| 65 | pickerLabel: "Bitbucket", |
| 66 | icon: "bitbucket", |
| 67 | changeRequest: { shortLabel: "PR", singular: "pull request" }, |
| 68 | repositoryPathHint: "workspace/repository", |
| 69 | publicHost: "bitbucket.org", |
| 70 | publishDescription: "bitbucket.org", |
| 71 | publishHost: () => "bitbucket.org", |
| 72 | // A new repository belongs to a workspace, which the signed-in account does not name. |
| 73 | newRepositoryOwner: () => null, |
| 74 | defaultCloneTransport: "ssh", |
| 75 | changeRequestUrl: ({ host, repository, number }) => |
| 76 | `https://${host}/${repository}/pull-requests/${number}`, |
| 77 | // No endpoint reopens a declined pull request, and nothing documented moves one in or out of |
| 78 | // draft, so neither is offered rather than failing when pressed. |
| 79 | changeRequestActions: new Set(["merge", "close"] as const), |
| 80 | // Bitbucket has no checkout CLI, so clone the head branch from its own repository. |
| 81 | checkoutCommand: ({ number, headBranch, headRepositoryNameWithOwner }) => |
| 82 | headRepositoryNameWithOwner && |
| 83 | repositoryName.test(headRepositoryNameWithOwner) && |
| 84 | safeShellArgument.test(headBranch) |
| 85 | ? `git clone --single-branch --branch ${headBranch} https://bitbucket.org/${headRepositoryNameWithOwner}.git t3code-pr-${number}` |
| 86 | : null, |
| 87 | authorProfileUrl: () => null, |
| 88 | referenceAutolinkRepositoryUrl: () => null, |
| 89 | reviewSummaryRequired: () => false, |
| 90 | // No checkout CLI, and the reference field never accepted Bitbucket URLs. |
| 91 | checkoutCommandArgument: () => null, |
| 92 | isChangeRequestReference: () => false, |
| 93 | changeRequestUrlHost: (url) => url.hostname, |
| 94 | checkoutChangeRequestHost: () => null, |
| 95 | isChangeRequestInRepository: (identity, link) => |
| 96 | isChangeRequestInProjectRepository(KIND, identity, link), |
| 97 | canReadChangeRequestOnHost: (identity, link) => |
| 98 | isChangeRequestOnProjectHost(KIND, identity, link), |
| 99 | isChangeRequestUrl: (url) => isChangeRequestPath(url, "/pull-requests/"), |
| 100 | settings, |
| 101 | }); |