| author | |
| committer | |
| log | 9ae9f2ba1f30e5fc14f69ba6dff6b08c7bfb401c |
| tree | acbec4d648cfcad7d65d13951d28f664b7aaa79d |
| parent | 4980ba282eb313b3be87a6b7df28f3b42dcb4ce9 |
| signature | Signed by PGP key B5690EEEBB952194 |
4 files changed, 248 insertions(+), 23 deletions(-)
apps/server/src/sourceControl/BitbucketApi.test.ts+120-17| ... | @@ -6,6 +6,7 @@ import * as Effect from "effect/Effect"; | ... | @@ -6,6 +6,7 @@ import * as Effect from "effect/Effect"; |
| 6 | import * as FileSystem from "effect/FileSystem"; | 6 | import * as FileSystem from "effect/FileSystem"; |
| 7 | import * as Layer from "effect/Layer"; | 7 | import * as Layer from "effect/Layer"; |
| 8 | import * as Option from "effect/Option"; | 8 | import * as Option from "effect/Option"; |
| 9 | import * as Schema from "effect/Schema"; | ||
| 9 | import * as TestClock from "effect/testing/TestClock"; | 10 | import * as TestClock from "effect/testing/TestClock"; |
| 10 | import { HttpClient, HttpClientError, HttpClientRequest, HttpClientResponse } from "effect/http"; | 11 | import { HttpClient, HttpClientError, HttpClientRequest, HttpClientResponse } from "effect/http"; |
| 11 | 12 | ||
| ... | @@ -16,6 +17,8 @@ import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; | ... | @@ -16,6 +17,8 @@ import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; |
| 16 | import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts"; | 17 | import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts"; |
| 17 | import type * as VcsDriver from "../vcs/VcsDriver.ts"; | 18 | import type * as VcsDriver from "../vcs/VcsDriver.ts"; |
| 18 | 19 | ||
| 20 | const isBitbucketRepositoryLocatorError = Schema.is(BitbucketApi.BitbucketRepositoryLocatorError); | ||
| 21 | |||
| 19 | const bitbucketPullRequest = { | 22 | const bitbucketPullRequest = { |
| 20 | id: 42, | 23 | id: 42, |
| 21 | title: "Add Bitbucket provider", | 24 | title: "Add Bitbucket provider", |
| ... | @@ -99,7 +102,7 @@ function makeLayer(input: { | ... | @@ -99,7 +102,7 @@ function makeLayer(input: { |
| 99 | } satisfies Partial<GitVcsDriver.GitVcsDriver["Service"]>; | 102 | } satisfies Partial<GitVcsDriver.GitVcsDriver["Service"]>; |
| 100 | 103 | ||
| 101 | const driver = { | 104 | const driver = { |
| 102 | listRemotes: () => | 105 | listRemotes: vi.fn(() => |
| 103 | Effect.succeed({ | 106 | Effect.succeed({ |
| 104 | remotes: [ | 107 | remotes: [ |
| 105 | { | 108 | { |
| ... | @@ -115,8 +118,26 @@ function makeLayer(input: { | ... | @@ -115,8 +118,26 @@ function makeLayer(input: { |
| 115 | expiresAt: Option.none(), | 118 | expiresAt: Option.none(), |
| 116 | }, | 119 | }, |
| 117 | }), | 120 | }), |
| 121 | ), | ||
| 118 | } satisfies Partial<VcsDriver.VcsDriver["Service"]>; | 122 | } satisfies Partial<VcsDriver.VcsDriver["Service"]>; |
| 119 | 123 | ||
| 124 | const resolve = vi.fn(() => | ||
| 125 | Effect.succeed({ | ||
| 126 | kind: "git" as const, | ||
| 127 | repository: { | ||
| 128 | kind: "git" as const, | ||
| 129 | rootPath: "/repo", | ||
| 130 | metadataPath: null, | ||
| 131 | freshness: { | ||
| 132 | source: "live-local" as const, | ||
| 133 | observedAt: DateTime.makeUnsafe("1970-01-01T00:00:00.000Z"), | ||
| 134 | expiresAt: Option.none(), | ||
| 135 | }, | ||
| 136 | }, | ||
| 137 | driver: driver as unknown as VcsDriver.VcsDriver["Service"], | ||
| 138 | }), | ||
| 139 | ); | ||
| 140 | |||
| 120 | const layer = BitbucketApi.layer.pipe( | 141 | const layer = BitbucketApi.layer.pipe( |
| 121 | Layer.provide( | 142 | Layer.provide( |
| 122 | Layer.succeed( | 143 | Layer.succeed( |
| ... | @@ -126,21 +147,7 @@ function makeLayer(input: { | ... | @@ -126,21 +147,7 @@ function makeLayer(input: { |
| 126 | ), | 147 | ), |
| 127 | Layer.provide( | 148 | Layer.provide( |
| 128 | Layer.mock(VcsDriverRegistry.VcsDriverRegistry)({ | 149 | Layer.mock(VcsDriverRegistry.VcsDriverRegistry)({ |
| 129 | resolve: () => | 150 | resolve, |
| 130 | Effect.succeed({ | ||
| 131 | kind: "git", | ||
| 132 | repository: { | ||
| 133 | kind: "git", | ||
| 134 | rootPath: "/repo", | ||
| 135 | metadataPath: null, | ||
| 136 | freshness: { | ||
| 137 | source: "live-local" as const, | ||
| 138 | observedAt: DateTime.makeUnsafe("1970-01-01T00:00:00.000Z"), | ||
| 139 | expiresAt: Option.none(), | ||
| 140 | }, | ||
| 141 | }, | ||
| 142 | driver: driver as unknown as VcsDriver.VcsDriver["Service"], | ||
| 143 | }), | ||
| 144 | }), | 151 | }), |
| 145 | ), | 152 | ), |
| 146 | Layer.provide(Layer.mock(GitVcsDriver.GitVcsDriver)(git)), | 153 | Layer.provide(Layer.mock(GitVcsDriver.GitVcsDriver)(git)), |
| ... | @@ -159,7 +166,7 @@ function makeLayer(input: { | ... | @@ -159,7 +166,7 @@ function makeLayer(input: { |
| 159 | Layer.provideMerge(NodeServices.layer), | 166 | Layer.provideMerge(NodeServices.layer), |
| 160 | ); | 167 | ); |
| 161 | 168 | ||
| 162 | return { execute, git: gitMock, layer }; | 169 | return { execute, git: gitMock, resolve, listRemotes: driver.listRemotes, layer }; |
| 163 | } | 170 | } |
| 164 | 171 | ||
| 165 | it.effect("parses pull request responses from the Bitbucket REST API", () => { | 172 | it.effect("parses pull request responses from the Bitbucket REST API", () => { |
| ... | @@ -333,6 +340,102 @@ it.effect("reads repository clone URLs and default branch", () => { | ... | @@ -333,6 +340,102 @@ it.effect("reads repository clone URLs and default branch", () => { |
| 333 | }).pipe(Effect.provide(layer)); | 340 | }).pipe(Effect.provide(layer)); |
| 334 | }); | 341 | }); |
| 335 | 342 | ||
| 343 | it.effect.each([false, true])( | ||
| 344 | "rejects invalid explicit repositories before fallback, context=%s", | ||
| 345 | (withContext) => { | ||
| 346 | const { execute, resolve, listRemotes, layer } = makeLayer({ | ||
| 347 | response: () => Response.json(repositoryJson), | ||
| 348 | }); | ||
| 349 | |||
| 350 | return Effect.gen(function* () { | ||
| 351 | const bitbucket = yield* BitbucketApi.BitbucketApi; | ||
| 352 | for (const repository of ["t3code", "", " \t "]) { | ||
| 353 | const error = yield* bitbucket | ||
| 354 | .getRepositoryCloneUrls({ | ||
| 355 | cwd: "/repo", | ||
| 356 | repository, | ||
| 357 | ...(withContext | ||
| 358 | ? { | ||
| 359 | context: { | ||
| 360 | provider: { | ||
| 361 | kind: "bitbucket" as const, | ||
| 362 | name: "Bitbucket", | ||
| 363 | baseUrl: "https://bitbucket.org", | ||
| 364 | }, | ||
| 365 | remoteName: "origin", | ||
| 366 | remoteUrl: "git@bitbucket.org:pingdotgg/t3code.git", | ||
| 367 | }, | ||
| 368 | } | ||
| 369 | : {}), | ||
| 370 | }) | ||
| 371 | .pipe(Effect.match({ onFailure: (error) => error, onSuccess: () => null })); | ||
| 372 | |||
| 373 | assert.instanceOf(error, BitbucketApi.BitbucketRepositoryLocatorError); | ||
| 374 | assert.strictEqual( | ||
| 375 | isBitbucketRepositoryLocatorError(error) ? error.repository : null, | ||
| 376 | repository, | ||
| 377 | ); | ||
| 378 | assert.strictEqual(resolve.mock.calls.length, 0); | ||
| 379 | assert.strictEqual(listRemotes.mock.calls.length, 0); | ||
| 380 | assert.strictEqual(execute.mock.calls.length, 0); | ||
| 381 | } | ||
| 382 | }).pipe(Effect.provide(layer)); | ||
| 383 | }, | ||
| 384 | ); | ||
| 385 | |||
| 386 | it.effect("prefers an explicit repository and uses context when the repository is omitted", () => { | ||
| 387 | const { execute, resolve, listRemotes, layer } = makeLayer({ | ||
| 388 | response: (request) => | ||
| 389 | Response.json(request.url.endsWith("/branching-model") ? {} : repositoryJson), | ||
| 390 | }); | ||
| 391 | |||
| 392 | return Effect.gen(function* () { | ||
| 393 | const bitbucket = yield* BitbucketApi.BitbucketApi; | ||
| 394 | const context = { | ||
| 395 | provider: { kind: "bitbucket" as const, name: "Bitbucket", baseUrl: "https://bitbucket.org" }, | ||
| 396 | remoteName: "origin", | ||
| 397 | remoteUrl: "git@bitbucket.org:another/context.git", | ||
| 398 | }; | ||
| 399 | for (const repository of ["pingdotgg/t3code", "https://bitbucket.org/pingdotgg/t3code.git"]) { | ||
| 400 | yield* bitbucket.getRepositoryCloneUrls({ cwd: "/repo", context, repository }); | ||
| 401 | } | ||
| 402 | yield* bitbucket.getDefaultBranch({ cwd: "/repo", context }); | ||
| 403 | |||
| 404 | assert.deepStrictEqual( | ||
| 405 | execute.mock.calls.map(([request]) => request.url).toSorted(), | ||
| 406 | [ | ||
| 407 | "https://api.test.local/2.0/repositories/pingdotgg/t3code", | ||
| 408 | "https://api.test.local/2.0/repositories/pingdotgg/t3code", | ||
| 409 | "https://api.test.local/2.0/repositories/another/context", | ||
| 410 | "https://api.test.local/2.0/repositories/another/context/branching-model", | ||
| 411 | ].toSorted(), | ||
| 412 | ); | ||
| 413 | assert.strictEqual(resolve.mock.calls.length, 0); | ||
| 414 | assert.strictEqual(listRemotes.mock.calls.length, 0); | ||
| 415 | }).pipe(Effect.provide(layer)); | ||
| 416 | }); | ||
| 417 | |||
| 418 | it.effect("discovers the repository from cwd remotes when the repository is omitted", () => { | ||
| 419 | const { execute, resolve, listRemotes, layer } = makeLayer({ | ||
| 420 | response: (request) => | ||
| 421 | Response.json(request.url.endsWith("/branching-model") ? {} : repositoryJson), | ||
| 422 | }); | ||
| 423 | |||
| 424 | return Effect.gen(function* () { | ||
| 425 | const bitbucket = yield* BitbucketApi.BitbucketApi; | ||
| 426 | const branch = yield* bitbucket.getDefaultBranch({ cwd: "/repo" }); | ||
| 427 | |||
| 428 | assert.strictEqual(branch, "main"); | ||
| 429 | assert.deepStrictEqual(resolve.mock.calls, [[{ cwd: "/repo" }]]); | ||
| 430 | assert.deepStrictEqual(listRemotes.mock.calls, [["/repo"]]); | ||
| 431 | assert.isTrue( | ||
| 432 | execute.mock.calls.every(([request]) => | ||
| 433 | request.url.startsWith("https://api.test.local/2.0/repositories/pingdotgg/t3code"), | ||
| 434 | ), | ||
| 435 | ); | ||
| 436 | }).pipe(Effect.provide(layer)); | ||
| 437 | }); | ||
| 438 | |||
| 336 | it.effect( | 439 | it.effect( |
| 337 | "prefers the Bitbucket branching model development branch as the default PR target", | 440 | "prefers the Bitbucket branching model development branch as the default PR target", |
| 338 | () => { | 441 | () => { |
apps/server/src/sourceControl/BitbucketApi.ts+7-5| ... | @@ -72,12 +72,14 @@ export class BitbucketRepositoryLocatorError extends Schema.TaggedError<Bitbucke | ... | @@ -72,12 +72,14 @@ export class BitbucketRepositoryLocatorError extends Schema.TaggedError<Bitbucke |
| 72 | repository: Schema.String, | 72 | repository: Schema.String, |
| 73 | }, | 73 | }, |
| 74 | ) { | 74 | ) { |
| 75 | static readonly detail = "Bitbucket repositories must be specified as workspace/repository."; | ||
| 76 | |||
| 75 | get detail(): string { | 77 | get detail(): string { |
| 76 | return "Bitbucket repositories must be specified as workspace/repository."; | 78 | return BitbucketRepositoryLocatorError.detail; |
| 77 | } | 79 | } |
| 78 | 80 | ||
| 79 | override get message(): string { | 81 | override get message(): string { |
| 80 | return `Bitbucket API failed in createRepository: ${this.detail}`; | 82 | return `Bitbucket API failed: ${this.detail}`; |
| 81 | } | 83 | } |
| 82 | } | 84 | } |
| 83 | 85 | ||
| ... | @@ -727,9 +729,9 @@ export const make = Effect.gen(function* () { | ... | @@ -727,9 +729,9 @@ export const make = Effect.gen(function* () { |
| 727 | readonly context?: SourceControlProvider.SourceControlProviderContext; | 729 | readonly context?: SourceControlProvider.SourceControlProviderContext; |
| 728 | readonly repository?: string; | 730 | readonly repository?: string; |
| 729 | }) { | 731 | }) { |
| 730 | const fromRepository = | 732 | if (input.repository !== undefined) { |
| 731 | input.repository !== undefined ? parseBitbucketRepositorySlug(input.repository) : null; | 733 | return yield* requireRepositoryLocator(input.repository); |
| 732 | if (fromRepository) return fromRepository; | 734 | } |
| 733 | 735 | ||
| 734 | const fromContext = | 736 | const fromContext = |
| 735 | input.context?.provider.kind === "bitbucket" | 737 | input.context?.provider.kind === "bitbucket" |
apps/server/src/sourceControl/SourceControlRepositoryService.test.ts+111| ... | @@ -6,16 +6,21 @@ import * as FileSystem from "effect/FileSystem"; | ... | @@ -6,16 +6,21 @@ import * as FileSystem from "effect/FileSystem"; |
| 6 | import * as Path from "effect/Path"; | 6 | import * as Path from "effect/Path"; |
| 7 | import * as Layer from "effect/Layer"; | 7 | import * as Layer from "effect/Layer"; |
| 8 | import * as PlatformError from "effect/PlatformError"; | 8 | import * as PlatformError from "effect/PlatformError"; |
| 9 | import * as Schema from "effect/Schema"; | ||
| 9 | import { ChildProcessSpawner } from "effect/process"; | 10 | import { ChildProcessSpawner } from "effect/process"; |
| 10 | 11 | ||
| 11 | import { GitCommandError, SourceControlProviderError } from "@t3tools/contracts"; | 12 | import { GitCommandError, SourceControlProviderError } from "@t3tools/contracts"; |
| 12 | 13 | ||
| 13 | import * as ServerConfig from "../config.ts"; | 14 | import * as ServerConfig from "../config.ts"; |
| 14 | import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; | 15 | import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; |
| 16 | import * as BitbucketApi from "./BitbucketApi.ts"; | ||
| 17 | import * as BitbucketSourceControlProvider from "./BitbucketSourceControlProvider.ts"; | ||
| 15 | import type * as SourceControlProvider from "./SourceControlProvider.ts"; | 18 | import type * as SourceControlProvider from "./SourceControlProvider.ts"; |
| 16 | import * as SourceControlProviderRegistry from "./SourceControlProviderRegistry.ts"; | 19 | import * as SourceControlProviderRegistry from "./SourceControlProviderRegistry.ts"; |
| 17 | import * as SourceControlRepositoryService from "./SourceControlRepositoryService.ts"; | 20 | import * as SourceControlRepositoryService from "./SourceControlRepositoryService.ts"; |
| 18 | 21 | ||
| 22 | const isSourceControlProviderError = Schema.is(SourceControlProviderError); | ||
| 23 | |||
| 19 | const CLONE_URLS = { | 24 | const CLONE_URLS = { |
| 20 | nameWithOwner: "octocat/t3code", | 25 | nameWithOwner: "octocat/t3code", |
| 21 | url: "https://github.com/octocat/t3code", | 26 | url: "https://github.com/octocat/t3code", |
| ... | @@ -152,6 +157,112 @@ it.effect("preserves provider failures without deriving the repository message f | ... | @@ -152,6 +157,112 @@ it.effect("preserves provider failures without deriving the repository message f |
| 152 | }).pipe(Effect.provide(layer({ provider }))); | 157 | }).pipe(Effect.provide(layer({ provider }))); |
| 153 | }); | 158 | }); |
| 154 | 159 | ||
| 160 | it.effect( | ||
| 161 | "reports the safe Bitbucket locator detail through repository lookup and publishing", | ||
| 162 | () => | ||
| 163 | Effect.gen(function* () { | ||
| 164 | const locatorCause = new BitbucketApi.BitbucketRepositoryLocatorError({ | ||
| 165 | repository: "credential-token-abc123", | ||
| 166 | }); | ||
| 167 | const provider = yield* BitbucketSourceControlProvider.make.pipe( | ||
| 168 | Effect.provide( | ||
| 169 | Layer.mock(BitbucketApi.BitbucketApi)({ | ||
| 170 | getRepositoryCloneUrls: () => Effect.fail(locatorCause), | ||
| 171 | createRepository: () => Effect.fail(locatorCause), | ||
| 172 | }), | ||
| 173 | ), | ||
| 174 | ); | ||
| 175 | |||
| 176 | yield* Effect.gen(function* () { | ||
| 177 | const service = yield* SourceControlRepositoryService.SourceControlRepositoryService; | ||
| 178 | const input = { | ||
| 179 | provider: "bitbucket" as const, | ||
| 180 | repository: locatorCause.repository, | ||
| 181 | cwd: "/workspace", | ||
| 182 | }; | ||
| 183 | for (const operation of [ | ||
| 184 | service.lookupRepository(input).pipe(Effect.asVoid), | ||
| 185 | service.publishRepository({ ...input, visibility: "private" }).pipe(Effect.asVoid), | ||
| 186 | ]) { | ||
| 187 | const error = yield* Effect.flip(operation); | ||
| 188 | |||
| 189 | assert.strictEqual(error.provider, "bitbucket"); | ||
| 190 | assert.strictEqual( | ||
| 191 | error.detail, | ||
| 192 | "Bitbucket repositories must be specified as workspace/repository.", | ||
| 193 | ); | ||
| 194 | assert.notInclude(error.message, locatorCause.repository); | ||
| 195 | assert.instanceOf(error.cause, SourceControlProviderError); | ||
| 196 | assert.strictEqual( | ||
| 197 | isSourceControlProviderError(error.cause) ? error.cause.cause : null, | ||
| 198 | locatorCause, | ||
| 199 | ); | ||
| 200 | } | ||
| 201 | }).pipe(Effect.provide(layer({ provider }))); | ||
| 202 | }), | ||
| 203 | ); | ||
| 204 | |||
| 205 | it.effect("keeps a plain Bitbucket locator lookalike out of the repository message", () => { | ||
| 206 | const provider = makeProvider({ | ||
| 207 | kind: "bitbucket", | ||
| 208 | getRepositoryCloneUrls: () => | ||
| 209 | Effect.fail( | ||
| 210 | new SourceControlProviderError({ | ||
| 211 | provider: "bitbucket", | ||
| 212 | operation: "getRepositoryCloneUrls", | ||
| 213 | cwd: "/workspace", | ||
| 214 | detail: "credential token abc123 was rejected", | ||
| 215 | cause: { | ||
| 216 | _tag: "BitbucketRepositoryLocatorError", | ||
| 217 | repository: "t3code", | ||
| 218 | detail: "credential token abc123 was rejected", | ||
| 219 | }, | ||
| 220 | }), | ||
| 221 | ), | ||
| 222 | }); | ||
| 223 | |||
| 224 | return Effect.gen(function* () { | ||
| 225 | const service = yield* SourceControlRepositoryService.SourceControlRepositoryService; | ||
| 226 | const error = yield* Effect.flip( | ||
| 227 | service.lookupRepository({ provider: "bitbucket", repository: "t3code" }), | ||
| 228 | ); | ||
| 229 | |||
| 230 | assert.strictEqual(error.detail, "The source control operation could not be completed."); | ||
| 231 | assert.notInclude(error.message, "abc123"); | ||
| 232 | }).pipe(Effect.provide(layer({ provider }))); | ||
| 233 | }); | ||
| 234 | |||
| 235 | it.effect("keeps arbitrary Bitbucket request failures out of the repository message", () => | ||
| 236 | Effect.gen(function* () { | ||
| 237 | const cause = new BitbucketApi.BitbucketRequestError({ | ||
| 238 | operation: "getRepository", | ||
| 239 | cause: new Error("credential token abc123 was rejected"), | ||
| 240 | }); | ||
| 241 | const provider = yield* BitbucketSourceControlProvider.make.pipe( | ||
| 242 | Effect.provide( | ||
| 243 | Layer.mock(BitbucketApi.BitbucketApi)({ | ||
| 244 | getRepositoryCloneUrls: () => Effect.fail(cause), | ||
| 245 | }), | ||
| 246 | ), | ||
| 247 | ); | ||
| 248 | |||
| 249 | yield* Effect.gen(function* () { | ||
| 250 | const service = yield* SourceControlRepositoryService.SourceControlRepositoryService; | ||
| 251 | const error = yield* Effect.flip( | ||
| 252 | service.lookupRepository({ | ||
| 253 | provider: "bitbucket", | ||
| 254 | repository: "pingdotgg/t3code", | ||
| 255 | cwd: "/workspace", | ||
| 256 | }), | ||
| 257 | ); | ||
| 258 | |||
| 259 | assert.strictEqual(error.detail, "The source control operation could not be completed."); | ||
| 260 | assert.notInclude(error.message, "abc123"); | ||
| 261 | assert.notInclude(error.message, cause.detail); | ||
| 262 | }).pipe(Effect.provide(layer({ provider }))); | ||
| 263 | }), | ||
| 264 | ); | ||
| 265 | |||
| 155 | it.effect("clones a looked-up repository into the requested destination", () => | 266 | it.effect("clones a looked-up repository into the requested destination", () => |
| 156 | Effect.gen(function* () { | 267 | Effect.gen(function* () { |
| 157 | const fs = yield* FileSystem.FileSystem; | 268 | const fs = yield* FileSystem.FileSystem; |
apps/server/src/sourceControl/SourceControlRepositoryService.ts+10-1| ... | @@ -7,6 +7,7 @@ import * as Schedule from "effect/Schedule"; | ... | @@ -7,6 +7,7 @@ import * as Schedule from "effect/Schedule"; |
| 7 | import * as Schema from "effect/Schema"; | 7 | import * as Schema from "effect/Schema"; |
| 8 | 8 | ||
| 9 | import { | 9 | import { |
| 10 | SourceControlProviderError, | ||
| 10 | SourceControlRepositoryError, | 11 | SourceControlRepositoryError, |
| 11 | type SourceControlCloneRepositoryInput, | 12 | type SourceControlCloneRepositoryInput, |
| 12 | type SourceControlCloneRepositoryResult, | 13 | type SourceControlCloneRepositoryResult, |
| ... | @@ -26,8 +27,11 @@ import { | ... | @@ -26,8 +27,11 @@ import { |
| 26 | type GitCloneProgressLine, | 27 | type GitCloneProgressLine, |
| 27 | } from "../project/gitCloneProgress.ts"; | 28 | } from "../project/gitCloneProgress.ts"; |
| 28 | import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; | 29 | import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; |
| 30 | import * as BitbucketApi from "./BitbucketApi.ts"; | ||
| 29 | import * as SourceControlProviderRegistry from "./SourceControlProviderRegistry.ts"; | 31 | import * as SourceControlProviderRegistry from "./SourceControlProviderRegistry.ts"; |
| 30 | const isSourceControlRepositoryError = Schema.is(SourceControlRepositoryError); | 32 | const isSourceControlRepositoryError = Schema.is(SourceControlRepositoryError); |
| 33 | const isSourceControlProviderError = Schema.is(SourceControlProviderError); | ||
| 34 | const isBitbucketRepositoryLocatorError = Schema.is(BitbucketApi.BitbucketRepositoryLocatorError); | ||
| 31 | 35 | ||
| 32 | export class SourceControlRepositoryService extends Context.Service< | 36 | export class SourceControlRepositoryService extends Context.Service< |
| 33 | SourceControlRepositoryService, | 37 | SourceControlRepositoryService, |
| ... | @@ -92,7 +96,12 @@ function mapRepositoryError(operation: string, provider: SourceControlProviderKi | ... | @@ -92,7 +96,12 @@ function mapRepositoryError(operation: string, provider: SourceControlProviderKi |
| 92 | : new SourceControlRepositoryError({ | 96 | : new SourceControlRepositoryError({ |
| 93 | operation, | 97 | operation, |
| 94 | provider, | 98 | provider, |
| 95 | detail: "The source control operation could not be completed.", | 99 | detail: |
| 100 | isSourceControlProviderError(cause) && | ||
| 101 | cause.provider === "bitbucket" && | ||
| 102 | isBitbucketRepositoryLocatorError(cause.cause) | ||
| 103 | ? BitbucketApi.BitbucketRepositoryLocatorError.detail | ||
| 104 | : "The source control operation could not be completed.", | ||
| 96 | cause, | 105 | cause, |
| 97 | }), | 106 | }), |
| 98 | ); | 107 | ); |