Skip to content
Source
Sign inSign up

BitbucketSourceControlProvider.ts

226 lines8.6 KB

1import { SourceControlProviderKind } from "@t3tools/contracts";
2import * as Effect from "effect/Effect";
3import * as Option from "effect/Option";
4import * as Schema from "effect/Schema";
5import { SourceControlProviderError, type ChangeRequest } from "@t3tools/contracts";
6​
7import * as BitbucketApi from "./BitbucketApi.ts";
8import type { NormalizedBitbucketPullRequestRecord } from "./bitbucketPullRequests.ts";
9import * as SourceControlProvider from "@t3tools/source-control-core/server/SourceControlProvider";
10import type { SourceControlApiDiscoverySpec } from "@t3tools/source-control-core/server/discovery";
11​
12const isRepositoryLocatorError = Schema.is(BitbucketApi.BitbucketRepositoryLocatorError);
13​
14/** A repository named some other way than `workspace/repository` is the user's to fix. */
15const repositoryDetail = (error: BitbucketApi.BitbucketApiError, fallback: string) =>
16 isRepositoryLocatorError(error) ? error.detail : fallback;
17​
18function toChangeRequest(summary: NormalizedBitbucketPullRequestRecord): ChangeRequest {
19 return {
20 provider: SourceControlProviderKind.make("bitbucket"),
21 number: summary.number,
22 title: summary.title,
23 url: summary.url,
24 baseRefName: summary.baseRefName,
25 headRefName: summary.headRefName,
26 ...(summary.headSha !== undefined ? { headSha: summary.headSha } : {}),
27 state: summary.state,
28 ...(summary.isDraft === true ? { isDraft: true } : {}),
29 updatedAt: summary.updatedAt ?? Option.none(),
30 ...(summary.isCrossRepository !== undefined
31 ? { isCrossRepository: summary.isCrossRepository }
32 : {}),
33 ...(summary.headRepositoryNameWithOwner !== undefined
34 ? { headRepositoryNameWithOwner: summary.headRepositoryNameWithOwner }
35 : {}),
36 ...(summary.headRepositoryOwnerLogin !== undefined
37 ? { headRepositoryOwnerLogin: summary.headRepositoryOwnerLogin }
38 : {}),
39 };
41​
42export const make = Effect.gen(function* () {
43 const bitbucket = yield* BitbucketApi.BitbucketApi;
44​
45 return SourceControlProvider.SourceControlProvider.of({
46 kind: SourceControlProviderKind.make("bitbucket"),
47 listChangeRequests: Effect.fn("BitbucketSourceControlProvider.listChangeRequests")(
48 function (input) {
49 const source = SourceControlProvider.sourceControlRefFromInput(input);
50 return bitbucket
51 .listPullRequests({
52 cwd: input.cwd,
53 ...(input.context ? { context: input.context } : {}),
54 headSelector: input.headSelector,
55 ...(source ? { source } : {}),
56 state: input.state,
57 ...(input.limit !== undefined ? { limit: input.limit } : {}),
58 })
59 .pipe(
60 Effect.map((items) => items.map(toChangeRequest)),
61 Effect.mapError(
62 (error) =>
63 new SourceControlProviderError({
64 provider: SourceControlProviderKind.make("bitbucket"),
65 operation: "listChangeRequests",
66 cwd: input.cwd,
67 reference: SourceControlProvider.transportSafeSourceControlErrorValue(
68 input.headSelector,
69 ),
70 detail: "Failed to list change requests.",
71 cause: error,
72 }),
73 ),
74 );
75 },
76 ),
77 getChangeRequest: Effect.fn("BitbucketSourceControlProvider.getChangeRequest")(
78 function (input) {
79 return bitbucket.getPullRequest(input).pipe(
80 Effect.map(toChangeRequest),
81 Effect.mapError(
82 (error) =>
83 new SourceControlProviderError({
84 provider: SourceControlProviderKind.make("bitbucket"),
85 operation: "getChangeRequest",
86 cwd: input.cwd,
87 reference: SourceControlProvider.transportSafeSourceControlErrorValue(
88 input.reference,
89 ),
90 detail: "Failed to get change request.",
91 cause: error,
92 }),
93 ),
94 );
95 },
96 ),
97 createChangeRequest: Effect.fn("BitbucketSourceControlProvider.createChangeRequest")(
98 function (input) {
99 const source = SourceControlProvider.sourceControlRefFromInput(input);
100 return bitbucket
101 .createPullRequest({
102 cwd: input.cwd,
103 ...(input.context ? { context: input.context } : {}),
104 baseBranch: input.baseRefName,
105 headSelector: input.headSelector,
106 ...(source ? { source } : {}),
107 ...(input.target ? { target: input.target } : {}),
108 title: input.title,
109 bodyFile: input.bodyFile,
110 })
111 .pipe(
112 Effect.mapError(
113 (error) =>
114 new SourceControlProviderError({
115 provider: SourceControlProviderKind.make("bitbucket"),
116 operation: "createChangeRequest",
117 cwd: input.cwd,
118 reference: SourceControlProvider.transportSafeSourceControlErrorValue(
119 input.headSelector,
120 ),
121 detail: "Failed to create change request.",
122 cause: error,
123 }),
124 ),
125 );
126 },
127 ),
128 getRepositoryCloneUrls: Effect.fn("BitbucketSourceControlProvider.getRepositoryCloneUrls")(
129 function (input) {
130 return bitbucket.getRepositoryCloneUrls(input).pipe(
131 Effect.mapError(
132 (error) =>
133 new SourceControlProviderError({
134 provider: SourceControlProviderKind.make("bitbucket"),
135 operation: "getRepositoryCloneUrls",
136 cwd: input.cwd,
137 repository: SourceControlProvider.transportSafeSourceControlErrorValue(
138 input.repository,
139 ),
140 detail: repositoryDetail(error, "Failed to get repository clone URLs."),
141 cause: error,
142 }),
143 ),
144 );
145 },
146 ),
147 createRepository: Effect.fn("BitbucketSourceControlProvider.createRepository")(
148 function (input) {
149 return bitbucket.createRepository(input).pipe(
150 Effect.mapError(
151 (error) =>
152 new SourceControlProviderError({
153 provider: SourceControlProviderKind.make("bitbucket"),
154 operation: "createRepository",
155 cwd: input.cwd,
156 repository: SourceControlProvider.transportSafeSourceControlErrorValue(
157 input.repository,
158 ),
159 detail: repositoryDetail(error, "Failed to create repository."),
160 cause: error,
161 }),
162 ),
163 );
164 },
165 ),
166 getDefaultBranch: Effect.fn("BitbucketSourceControlProvider.getDefaultBranch")(
167 function (input) {
168 return bitbucket
169 .getDefaultBranch({
170 cwd: input.cwd,
171 ...(input.context ? { context: input.context } : {}),
172 })
173 .pipe(
174 Effect.mapError(
175 (error) =>
176 new SourceControlProviderError({
177 provider: SourceControlProviderKind.make("bitbucket"),
178 operation: "getDefaultBranch",
179 cwd: input.cwd,
180 detail: "Failed to get default branch.",
181 cause: error,
182 }),
183 ),
184 );
185 },
186 ),
187 checkoutChangeRequest: Effect.fn("BitbucketSourceControlProvider.checkoutChangeRequest")(
188 function (input) {
189 return bitbucket
190 .checkoutPullRequest({
191 cwd: input.cwd,
192 ...(input.context ? { context: input.context } : {}),
193 reference: input.reference,
194 ...(input.force !== undefined ? { force: input.force } : {}),
195 })
196 .pipe(
197 Effect.mapError(
198 (error) =>
199 new SourceControlProviderError({
200 provider: SourceControlProviderKind.make("bitbucket"),
201 operation: "checkoutChangeRequest",
202 cwd: input.cwd,
203 reference: SourceControlProvider.transportSafeSourceControlErrorValue(
204 input.reference,
205 ),
206 detail: "Failed to check out change request.",
207 cause: error,
208 }),
209 ),
210 );
211 },
212 ),
213 });
214});
215​
216export const makeDiscovery = Effect.gen(function* () {
217 const bitbucket = yield* BitbucketApi.BitbucketApi;
218​
219 return {
220 type: "api",
221 kind: SourceControlProviderKind.make("bitbucket"),
222 label: "Bitbucket",
223 installHint: "Add a Bitbucket token in Settings → Source Control.",
224 probeAuth: bitbucket.probeAuth,
225 } satisfies SourceControlApiDiscoverySpec;
226});