| author | |
| committer | |
| log | fd3549c8ffa5eac8230a8b5e1a87e59d28ba488e |
| tree | 5177c0522b7004cfd5bc7ec8a139624b222d3599 |
| parent | dacd2cb649eea06a00244bd181eb400735e923d5 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>74 files changed, 816 insertions(+), 551 deletions(-)
apps/mobile/src/features/projects/AddProjectScreen.tsx+3-2| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | |
| 1 | 2 | import { MaterialListRow } from "../../components/MaterialListRow"; |
| 2 | 3 | import { SettingsScreen } from "../settings/components/SettingsScreen"; |
| 3 | 4 | import { ScreenScrollView as ScrollView } from "../../components/ScreenScrollView"; |
| ... | ... | @@ -812,7 +813,7 @@ export function AddProjectRepositoryScreen(props: { |
| 812 | 813 | const result = await lookupRepositoryQuery({ |
| 813 | 814 | environmentId: environment.environmentId, |
| 814 | 815 | input: { |
| 815 | provider, | |
| 816 | provider: SourceControlProviderKind.make(provider), | |
| 816 | 817 | repository: repositoryInput.trim(), |
| 817 | 818 | }, |
| 818 | 819 | }); |
| ... | ... | @@ -1079,7 +1080,7 @@ export function AddProjectNewScreen(props: { readonly environmentId?: string | s |
| 1079 | 1080 | environmentId: environment.environmentId, |
| 1080 | 1081 | input: { |
| 1081 | 1082 | cwd: workspaceRoot, |
| 1082 | provider: "github", | |
| 1083 | provider: SourceControlProviderKind.make("github"), | |
| 1083 | 1084 | repository: getNewProjectGitHubRepository(githubTarget, workspaceRoot), |
| 1084 | 1085 | visibility: "private", |
| 1085 | 1086 | }, |
apps/mobile/src/state/use-thread-pr.test.ts+2-1| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | |
| 1 | 2 | import { ProjectId, type ThreadPullRequestLink, type VcsStatusResult } from "@t3tools/contracts"; |
| 2 | 3 | import { describe, expect, it } from "vite-plus/test"; |
| 3 | 4 | |
| ... | ... | @@ -28,7 +29,7 @@ describe("presentThreadPr", () => { |
| 28 | 29 | it("uses merge-request terminology for GitLab", () => { |
| 29 | 30 | expect( |
| 30 | 31 | presentThreadPr(pullRequest, { |
| 31 | kind: "gitlab", | |
| 32 | kind: SourceControlProviderKind.make("gitlab"), | |
| 32 | 33 | name: "GitLab", |
| 33 | 34 | baseUrl: "https://gitlab.com", |
| 34 | 35 | }), |
apps/server/src/git/GitManager.test.ts+15-5| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | |
| 1 | 2 | // @effect-diagnostics nodeBuiltinImport:off |
| 2 | 3 | import * as NodeFS from "node:fs"; |
| 3 | 4 | import * as NodePath from "node:path"; |
| ... | ... | @@ -410,7 +411,13 @@ function createGitHubProviderWithFakeGh(scenario: FakeGhScenario = {}): { |
| 410 | 411 | const ghCalls: string[] = []; |
| 411 | 412 | |
| 412 | 413 | const fail = (cwd: string, detail: string, cause?: unknown, operation = "fakeGh") => |
| 413 | new SourceControlProviderFailure({ provider: "github", operation, cwd, detail, cause }); | |
| 414 | new SourceControlProviderFailure({ | |
| 415 | provider: SourceControlProviderKind.make("github"), | |
| 416 | operation, | |
| 417 | cwd, | |
| 418 | detail, | |
| 419 | cause, | |
| 420 | }); | |
| 414 | 421 | |
| 415 | 422 | /** `operation` is the provider method the call answers, which a failure reports. */ |
| 416 | 423 | const execute = (input: { |
| ... | ... | @@ -534,7 +541,7 @@ function createGitHubProviderWithFakeGh(scenario: FakeGhScenario = {}): { |
| 534 | 541 | /** What GitHubSourceControlProvider makes of a pull request it read. */ |
| 535 | 542 | const toChangeRequest = (summary: FakePullRequestSummary): ChangeRequest => ({ |
| 536 | 543 | ...summary, |
| 537 | provider: "github", | |
| 544 | provider: SourceControlProviderKind.make("github"), | |
| 538 | 545 | state: summary.state ?? "open", |
| 539 | 546 | closedAt: summary.closedAt ?? null, |
| 540 | 547 | mergedAt: summary.mergedAt ?? null, |
| ... | ... | @@ -546,7 +553,7 @@ function createGitHubProviderWithFakeGh(scenario: FakeGhScenario = {}): { |
| 546 | 553 | |
| 547 | 554 | return { |
| 548 | 555 | service: { |
| 549 | kind: "github", | |
| 556 | kind: SourceControlProviderKind.make("github"), | |
| 550 | 557 | // The GitHub provider's own lookup rule and template convention, which GitManager reads |
| 551 | 558 | // instead of the kind. |
| 552 | 559 | headBranchProbe: ({ headSelectors }) => ({ |
| ... | ... | @@ -602,7 +609,10 @@ function createGitHubProviderWithFakeGh(scenario: FakeGhScenario = {}): { |
| 602 | 609 | if (raw.length === 0) return []; |
| 603 | 610 | const decoded = decodeGitHubPullRequestListJson(raw); |
| 604 | 611 | return Result.isSuccess(decoded) |
| 605 | ? decoded.success.map((record) => ({ provider: "github" as const, ...record })) | |
| 612 | ? decoded.success.map((record) => ({ | |
| 613 | provider: SourceControlProviderKind.make("github"), | |
| 614 | ...record, | |
| 615 | })) | |
| 606 | 616 | : []; |
| 607 | 617 | }), |
| 608 | 618 | ), |
| ... | ... | @@ -2841,7 +2851,7 @@ it.layer(layerGitManagerTest)("GitManager", (it) => { |
| 2841 | 2851 | operation: "lookupStatusPr", |
| 2842 | 2852 | branch: "feature/status-rate-limited", |
| 2843 | 2853 | errorTag: "SourceControlProviderError", |
| 2844 | provider: "github", | |
| 2854 | provider: SourceControlProviderKind.make("github"), | |
| 2845 | 2855 | providerOperation: "listChangeRequests", |
| 2846 | 2856 | errorDetail: "GitHub API rate limit exceeded. Requests resume when the limit resets.", |
| 2847 | 2857 | }); |
apps/server/src/git/GitManager.ts+7-2| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | |
| 1 | 2 | import * as Arr from "effect/Array"; |
| 2 | 3 | import * as Cache from "effect/Cache"; |
| 3 | 4 | import * as Context from "effect/Context"; |
| ... | ... | @@ -1787,7 +1788,9 @@ export const make = Effect.gen(function* () { |
| 1787 | 1788 | ) { |
| 1788 | 1789 | const terms = yield* sourceControlProvider(cwd).pipe( |
| 1789 | 1790 | Effect.map((provider) => getChangeRequestTerminologyForKind(provider.kind)), |
| 1790 | Effect.orElseSucceed(() => getChangeRequestTerminologyForKind("unknown")), | |
| 1791 | Effect.orElseSucceed(() => | |
| 1792 | getChangeRequestTerminologyForKind(SourceControlProviderKind.make("unknown")), | |
| 1793 | ), | |
| 1791 | 1794 | ); |
| 1792 | 1795 | const summary = summarizeGitActionResult(result, terms); |
| 1793 | 1796 | let latestOpenPr: PullRequestInfo | null = null; |
| ... | ... | @@ -2853,7 +2856,9 @@ export const make = Effect.gen(function* () { |
| 2853 | 2856 | const changeRequestTerms = wantsPr |
| 2854 | 2857 | ? yield* sourceControlProvider(input.cwd).pipe( |
| 2855 | 2858 | Effect.map((provider) => getChangeRequestTerminologyForKind(provider.kind)), |
| 2856 | Effect.orElseSucceed(() => getChangeRequestTerminologyForKind("unknown")), | |
| 2859 | Effect.orElseSucceed(() => | |
| 2860 | getChangeRequestTerminologyForKind(SourceControlProviderKind.make("unknown")), | |
| 2861 | ), | |
| 2857 | 2862 | ) |
| 2858 | 2863 | : null; |
| 2859 | 2864 |
apps/server/src/orchestration-v2/PullRequestSyncReactor.test.ts+4-3| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | |
| 1 | 2 | import * as Stream from "effect/Stream"; |
| 2 | 3 | import { |
| 3 | 4 | EventId, |
| ... | ... | @@ -148,7 +149,7 @@ function makeSummary( |
| 148 | 149 | overrides: Partial<PullRequestSummary> = {}, |
| 149 | 150 | ): PullRequestSummary { |
| 150 | 151 | return { |
| 151 | provider: "github", | |
| 152 | provider: SourceControlProviderKind.make("github"), | |
| 152 | 153 | projectId: input.projectId, |
| 153 | 154 | repository: input.repository, |
| 154 | 155 | number: input.number, |
| ... | ... | @@ -1009,7 +1010,7 @@ describe("PullRequestSyncReactor", () => { |
| 1009 | 1010 | Effect.gen(function* () { |
| 1010 | 1011 | if (input.host === "github.com" && (yield* Clock.currentTimeMillis) < retryAt) { |
| 1011 | 1012 | const paused = new PullRequestProviderError({ |
| 1012 | provider: "github", | |
| 1013 | provider: SourceControlProviderKind.make("github"), | |
| 1013 | 1014 | operation: "getChangeRequestSummary", |
| 1014 | 1015 | reason: "rate-limited", |
| 1015 | 1016 | detail: "paused", |
| ... | ... | @@ -1079,7 +1080,7 @@ describe("PullRequestSyncReactor", () => { |
| 1079 | 1080 | operation: "stack", |
| 1080 | 1081 | detail: "paused", |
| 1081 | 1082 | cause: new PullRequestProviderError({ |
| 1082 | provider: "github", | |
| 1083 | provider: SourceControlProviderKind.make("github"), | |
| 1083 | 1084 | operation: "getChangeRequestStack", |
| 1084 | 1085 | reason: "rate-limited", |
| 1085 | 1086 | detail: "paused", |
apps/server/src/orchestration-v2/ThreadSettlementService.test.ts+2-1| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | |
| 1 | 2 | import { assert, describe, expect, it } from "@effect/vitest"; |
| 2 | 3 | import * as DateTime from "effect/DateTime"; |
| 3 | 4 | import { |
| ... | ... | @@ -472,7 +473,7 @@ function makePullRequestSummary(input: { |
| 472 | 473 | readonly updatedAt?: string; |
| 473 | 474 | }): PullRequestSummary { |
| 474 | 475 | return { |
| 475 | provider: "github", | |
| 476 | provider: SourceControlProviderKind.make("github"), | |
| 476 | 477 | projectId: input.projectId, |
| 477 | 478 | repository: input.repository, |
| 478 | 479 | number: input.number, |
apps/server/src/orchestration-v2/runtimeLayer.test.ts+4-3| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | |
| 1 | 2 | import { limitRecoveryCommand } from "./UsageLimitRecoveryWorker.ts"; |
| 2 | 3 | import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions"; |
| 3 | 4 | import * as SourceControlProviderRegistry from "../sourceControl/SourceControlProviderRegistry.ts"; |
| ... | ... | @@ -169,7 +170,7 @@ const watchedPullRequestDetail = (input: { |
| 169 | 170 | readonly number: number; |
| 170 | 171 | readonly at: string; |
| 171 | 172 | }): PullRequestDetail => ({ |
| 172 | provider: "github", | |
| 173 | provider: SourceControlProviderKind.make("github"), | |
| 173 | 174 | capabilities: { |
| 174 | 175 | diff: true, |
| 175 | 176 | comment: true, |
| ... | ... | @@ -2572,7 +2573,7 @@ it.layer(layerTest)("OrchestrationV2LayerLive lifecycle", (it) => { |
| 2572 | 2573 | operation: "getChangeRequest", |
| 2573 | 2574 | detail: "github requests are paused until the rate limit resets", |
| 2574 | 2575 | cause: new PullRequestProviderError({ |
| 2575 | provider: "github", | |
| 2576 | provider: SourceControlProviderKind.make("github"), | |
| 2576 | 2577 | operation: "getChangeRequest", |
| 2577 | 2578 | reason: "rate-limited", |
| 2578 | 2579 | detail: "paused", |
| ... | ... | @@ -2831,7 +2832,7 @@ it.layer(layerTest)("OrchestrationV2LayerLive lifecycle", (it) => { |
| 2831 | 2832 | operation: "watchFingerprint", |
| 2832 | 2833 | detail: "paused", |
| 2833 | 2834 | cause: new PullRequestProviderError({ |
| 2834 | provider: "github", | |
| 2835 | provider: SourceControlProviderKind.make("github"), | |
| 2835 | 2836 | operation: "getChangeRequestWatchFingerprint", |
| 2836 | 2837 | reason: "rate-limited", |
| 2837 | 2838 | detail: "paused", |
apps/server/src/project/ProjectCloneTracker.test.ts+3-2| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | |
| 1 | 2 | import { describe, expect, it } from "@effect/vitest"; |
| 2 | 3 | import { |
| 3 | 4 | OrchestrationDispatchCommandError, |
| ... | ... | @@ -116,7 +117,7 @@ describe("ProjectCloneTracker", () => { |
| 116 | 117 | ? Effect.fail( |
| 117 | 118 | new SourceControlRepositoryError({ |
| 118 | 119 | operation: "cloneRepository", |
| 119 | provider: "unknown", | |
| 120 | provider: SourceControlProviderKind.make("unknown"), | |
| 120 | 121 | detail: "fatal: repository not found", |
| 121 | 122 | }), |
| 122 | 123 | ) |
| ... | ... | @@ -255,7 +256,7 @@ describe("ProjectCloneTracker", () => { |
| 255 | 256 | Effect.fail( |
| 256 | 257 | new SourceControlRepositoryError({ |
| 257 | 258 | operation: "cloneRepository", |
| 258 | provider: "unknown", | |
| 259 | provider: SourceControlProviderKind.make("unknown"), | |
| 259 | 260 | detail: "Destination path already exists and is not empty.", |
| 260 | 261 | }), |
| 261 | 262 | ), |
apps/server/src/project/ProjectCloneTracker.ts+2-1| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | |
| 1 | 2 | import type { |
| 2 | 3 | ProjectCloneSnapshot, |
| 3 | 4 | ProjectCloneStage, |
| ... | ... | @@ -301,7 +302,7 @@ export const make = Effect.gen(function* () { |
| 301 | 302 | if (!claimed) { |
| 302 | 303 | return yield* new SourceControlRepositoryError({ |
| 303 | 304 | operation: "cloneRepository", |
| 304 | provider: input.provider ?? "unknown", | |
| 305 | provider: input.provider ?? SourceControlProviderKind.make("unknown"), | |
| 305 | 306 | detail: "A clone into this destination is already in progress.", |
| 306 | 307 | }); |
| 307 | 308 | } |
apps/server/src/project/RepositoryIdentityResolver.test.ts+2-1| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | |
| 1 | 2 | // @effect-diagnostics nodeBuiltinImport:off - realpathSync.native resolves Windows 8.3 short names, which the Effect realPath does not. |
| 2 | 3 | import * as NodeFS from "node:fs"; |
| 3 | 4 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| ... | ... | @@ -71,7 +72,7 @@ it.layer(NodeServices.layer)("RepositoryIdentityResolverLive", (it) => { |
| 71 | 72 | if (refinementFails) |
| 72 | 73 | return Effect.fail( |
| 73 | 74 | new SourceControlProviderError({ |
| 74 | provider: "forgejo", | |
| 75 | provider: SourceControlProviderKind.make("forgejo"), | |
| 75 | 76 | operation: "detectProvider", |
| 76 | 77 | cwd: rootPath, |
| 77 | 78 | detail: "account unavailable", |
apps/server/src/pullRequest/PullRequestProviderRateLimit.test.ts+2-1| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | |
| 1 | 2 | import { assert, it } from "@effect/vitest"; |
| 2 | 3 | |
| 3 | 4 | import * as AzureDevOpsCli from "@t3tools/source-control-azure-devops/server/AzureDevOpsCli"; |
| ... | ... | @@ -59,7 +60,7 @@ it("keeps GitHub's exact retry time", () => { |
| 59 | 60 | assert.deepStrictEqual( |
| 60 | 61 | gitHubProviderFailure( |
| 61 | 62 | new SourceControlRateLimit.SourceControlRateLimitPausedError({ |
| 62 | provider: "github", | |
| 63 | provider: SourceControlProviderKind.make("github"), | |
| 63 | 64 | host: "github.com", |
| 64 | 65 | retryAt: 1_786_802_400_000, |
| 65 | 66 | }), |
apps/server/src/pullRequest/PullRequestService.test.ts+295-238| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | |
| 1 | 2 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 2 | 3 | import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"; |
| 3 | 4 | import * as KeyValueStore from "effect/persistence/KeyValueStore"; |
| ... | ... | @@ -17,7 +18,6 @@ import type { |
| 17 | 18 | ProjectId, |
| 18 | 19 | PullRequestReviewCapabilities, |
| 19 | 20 | PullRequestReviewerCapabilities, |
| 20 | SourceControlProviderKind, | |
| 21 | 21 | } from "@t3tools/contracts"; |
| 22 | 22 | import { DEFAULT_SERVER_SETTINGS } from "@t3tools/contracts/settings"; |
| 23 | 23 | import type { ServerSettings } from "@t3tools/contracts"; |
| ... | ... | @@ -129,7 +129,7 @@ it.effect("caches narrow previews and invalidates them after refresh or mutation |
| 129 | 129 | const service = yield* makeService({ |
| 130 | 130 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/w", repository: "acme/web" })], |
| 131 | 131 | providers: [ |
| 132 | fakeProvider("github", { | |
| 132 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 133 | 133 | getViewer: () => Effect.die("preview must not read the viewer"), |
| 134 | 134 | getChangeRequestPreview: () => |
| 135 | 135 | Effect.sync(() => { |
| ... | ... | @@ -175,13 +175,13 @@ it.effect("keeps cached previews available and pauses uncached previews until qu |
| 175 | 175 | const service = yield* makeService({ |
| 176 | 176 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/w", repository: "acme/web" })], |
| 177 | 177 | providers: [ |
| 178 | fakeProvider("github", { | |
| 178 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 179 | 179 | getChangeRequestPreview: ({ number }) => |
| 180 | 180 | Effect.gen(function* () { |
| 181 | 181 | reads++; |
| 182 | 182 | if (reads === 2) |
| 183 | 183 | return yield* new PullRequestProviderError({ |
| 184 | provider: "github", | |
| 184 | provider: SourceControlProviderKind.make("github"), | |
| 185 | 185 | operation: "getChangeRequestPreview", |
| 186 | 186 | reason: "rate-limited", |
| 187 | 187 | detail: "GitHub requests are paused until the rate limit resets.", |
| ... | ... | @@ -218,7 +218,7 @@ it.effect("reuses only unexpired detail for previews", () => |
| 218 | 218 | const service = yield* makeService({ |
| 219 | 219 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/w", repository: "acme/web" })], |
| 220 | 220 | providers: [ |
| 221 | fakeProvider("github", { | |
| 221 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 222 | 222 | getChangeRequest: () => Effect.succeed(hostedChangeRequest("Description")), |
| 223 | 223 | getChangeRequestPreview: () => |
| 224 | 224 | Effect.sync(() => { |
| ... | ... | @@ -250,7 +250,7 @@ it.effect("shares detail and activity for a minute, and for ten once merged", () |
| 250 | 250 | const service = yield* makeService({ |
| 251 | 251 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/w", repository: "acme/web" })], |
| 252 | 252 | providers: [ |
| 253 | fakeProvider("github", { | |
| 253 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 254 | 254 | getChangeRequest: ({ number }) => |
| 255 | 255 | Effect.sync(() => { |
| 256 | 256 | reads.push(`detail #${number}`); |
| ... | ... | @@ -314,7 +314,7 @@ it.effect("does not wait for an in-flight detail read to display a preview", () |
| 314 | 314 | const service = yield* makeService({ |
| 315 | 315 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/w", repository: "acme/web" })], |
| 316 | 316 | providers: [ |
| 317 | fakeProvider("github", { | |
| 317 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 318 | 318 | getChangeRequest: () => |
| 319 | 319 | Deferred.succeed(detailStarted, undefined).pipe( |
| 320 | 320 | Effect.andThen(Deferred.await(releaseDetail)), |
| ... | ... | @@ -342,7 +342,7 @@ it.effect("keeps previews warm when another project finishes a turn", () => |
| 342 | 342 | project({ id: "p2", title: "docs", workspaceRoot: "/d", repository: "acme/docs" }), |
| 343 | 343 | ], |
| 344 | 344 | providers: [ |
| 345 | fakeProvider("github", { | |
| 345 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 346 | 346 | getChangeRequestPreview: (input) => |
| 347 | 347 | Effect.sync(() => { |
| 348 | 348 | reads.push(input.repository); |
| ... | ... | @@ -374,11 +374,11 @@ it.effect("uses full detail for hosts without a narrow preview", () => |
| 374 | 374 | title: "web", |
| 375 | 375 | workspaceRoot: "/w", |
| 376 | 376 | repository: "acme/web", |
| 377 | provider: "gitlab", | |
| 377 | provider: SourceControlProviderKind.make("gitlab"), | |
| 378 | 378 | }), |
| 379 | 379 | ], |
| 380 | 380 | providers: [ |
| 381 | fakeProvider("gitlab", { | |
| 381 | fakeProvider(SourceControlProviderKind.make("gitlab"), { | |
| 382 | 382 | getChangeRequest: () => |
| 383 | 383 | Effect.sync(() => { |
| 384 | 384 | reads += 1; |
| ... | ... | @@ -405,7 +405,7 @@ function unusable(provider: SourceControlProviderKind, reason: "missing-tool" | |
| 405 | 405 | } |
| 406 | 406 | |
| 407 | 407 | const requestFailed = new PullRequestProviderError({ |
| 408 | provider: "github", | |
| 408 | provider: SourceControlProviderKind.make("github"), | |
| 409 | 409 | operation: "listChangeRequests", |
| 410 | 410 | reason: "failed", |
| 411 | 411 | detail: "HTTP 404", |
| ... | ... | @@ -530,12 +530,12 @@ it.effect("lists GitHub Enterprise PRs for a stored unknown repository after hos |
| 530 | 530 | title: "enterprise", |
| 531 | 531 | workspaceRoot: "/repo", |
| 532 | 532 | repository: "team/project", |
| 533 | provider: "unknown", | |
| 533 | provider: SourceControlProviderKind.make("unknown"), | |
| 534 | 534 | host: "code.example.test", |
| 535 | 535 | }), |
| 536 | 536 | ], |
| 537 | 537 | providers: [ |
| 538 | fakeProvider("github", { | |
| 538 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 539 | 539 | /** Supplies a PR only after verifying discovery retained the Enterprise repository target. */ |
| 540 | 540 | listChangeRequests: ({ host, repository }) => { |
| 541 | 541 | assert.strictEqual(host, "code.example.test"); |
| ... | ... | @@ -554,7 +554,11 @@ it.effect("lists GitHub Enterprise PRs for a stored unknown repository after hos |
| 554 | 554 | return Effect.succeed({ |
| 555 | 555 | context: { |
| 556 | 556 | ...context, |
| 557 | provider: { ...context.provider, kind: "github", name: "GitHub Self-Hosted" }, | |
| 557 | provider: { | |
| 558 | ...context.provider, | |
| 559 | kind: SourceControlProviderKind.make("github"), | |
| 560 | name: "GitHub Self-Hosted", | |
| 561 | }, | |
| 558 | 562 | }, |
| 559 | 563 | provider: undefined as never, |
| 560 | 564 | }); |
| ... | ... | @@ -576,7 +580,7 @@ it.effect("refines unknown self-hosted GitLab projects before listing merge requ |
| 576 | 580 | title: "self-hosted", |
| 577 | 581 | workspaceRoot: "/gitlab", |
| 578 | 582 | repository: "group/project", |
| 579 | provider: "unknown", | |
| 583 | provider: SourceControlProviderKind.make("unknown"), | |
| 580 | 584 | host: "code.example.test", |
| 581 | 585 | }); |
| 582 | 586 | const service = yield* makeService({ |
| ... | ... | @@ -584,12 +588,15 @@ it.effect("refines unknown self-hosted GitLab projects before listing merge requ |
| 584 | 588 | selfHosted, |
| 585 | 589 | { ...selfHosted, id: "p2" as ProjectId, workspaceRoot: "/gitlab-worktree" }, |
| 586 | 590 | ], |
| 587 | providers: [fakeProvider("gitlab")], | |
| 591 | providers: [fakeProvider(SourceControlProviderKind.make("gitlab"))], | |
| 588 | 592 | resolveHandle: ({ context }) => { |
| 589 | 593 | refinementCalls += 1; |
| 590 | 594 | assert.strictEqual(context?.remoteUrl, "https://code.example.test/group/project.git"); |
| 591 | 595 | return Effect.succeed({ |
| 592 | context: { ...context!, provider: { ...context!.provider, kind: "gitlab" } }, | |
| 596 | context: { | |
| 597 | ...context!, | |
| 598 | provider: { ...context!.provider, kind: SourceControlProviderKind.make("gitlab") }, | |
| 599 | }, | |
| 593 | 600 | provider: undefined as never, |
| 594 | 601 | }); |
| 595 | 602 | }, |
| ... | ... | @@ -610,7 +617,7 @@ it.effect("derives a legacy repository host after refining its provider", () => |
| 610 | 617 | title: "legacy self-hosted", |
| 611 | 618 | workspaceRoot: "/gitlab", |
| 612 | 619 | repository: "group/project", |
| 613 | provider: "unknown", | |
| 620 | provider: SourceControlProviderKind.make("unknown"), | |
| 614 | 621 | host: "code.example.test", |
| 615 | 622 | }); |
| 616 | 623 | const identity = current.repositoryIdentity!; |
| ... | ... | @@ -625,10 +632,13 @@ it.effect("derives a legacy repository host after refining its provider", () => |
| 625 | 632 | } as unknown as OrchestrationProjectShell; |
| 626 | 633 | const service = yield* makeService({ |
| 627 | 634 | projects: [legacy], |
| 628 | providers: [fakeProvider("gitlab")], | |
| 635 | providers: [fakeProvider(SourceControlProviderKind.make("gitlab"))], | |
| 629 | 636 | resolveHandle: ({ context }) => |
| 630 | 637 | Effect.succeed({ |
| 631 | context: { ...context!, provider: { ...context!.provider, kind: "gitlab" } }, | |
| 638 | context: { | |
| 639 | ...context!, | |
| 640 | provider: { ...context!.provider, kind: SourceControlProviderKind.make("gitlab") }, | |
| 641 | }, | |
| 632 | 642 | provider: undefined as never, |
| 633 | 643 | }), |
| 634 | 644 | }); |
| ... | ... | @@ -648,18 +658,21 @@ it.effect("tries another checkout when provider refinement remains unknown", () |
| 648 | 658 | title: "self-hosted", |
| 649 | 659 | workspaceRoot: "/gone", |
| 650 | 660 | repository: "group/project", |
| 651 | provider: "unknown", | |
| 661 | provider: SourceControlProviderKind.make("unknown"), | |
| 652 | 662 | host: "code.example.test", |
| 653 | 663 | }); |
| 654 | 664 | const service = yield* makeService({ |
| 655 | 665 | projects: [selfHosted, { ...selfHosted, id: "p2" as ProjectId, workspaceRoot: "/healthy" }], |
| 656 | providers: [fakeProvider("gitlab")], | |
| 666 | providers: [fakeProvider(SourceControlProviderKind.make("gitlab"))], | |
| 657 | 667 | resolveHandle: ({ cwd, context }) => { |
| 658 | 668 | asked.push(cwd); |
| 659 | 669 | return cwd === "/gone" |
| 660 | 670 | ? Effect.succeed({ context: context!, provider: undefined as never }) |
| 661 | 671 | : Effect.succeed({ |
| 662 | context: { ...context!, provider: { ...context!.provider, kind: "gitlab" } }, | |
| 672 | context: { | |
| 673 | ...context!, | |
| 674 | provider: { ...context!.provider, kind: SourceControlProviderKind.make("gitlab") }, | |
| 675 | }, | |
| 663 | 676 | provider: undefined as never, |
| 664 | 677 | }); |
| 665 | 678 | }, |
| ... | ... | @@ -689,11 +702,11 @@ it.effect("reads nothing from a host with no implementation, but reports it", () |
| 689 | 702 | title: "on gitlab", |
| 690 | 703 | workspaceRoot: "/c", |
| 691 | 704 | repository: "group/project", |
| 692 | provider: "gitlab", | |
| 705 | provider: SourceControlProviderKind.make("gitlab"), | |
| 693 | 706 | }), |
| 694 | 707 | ], |
| 695 | 708 | providers: [ |
| 696 | fakeProvider("github", { | |
| 709 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 697 | 710 | listChangeRequests: (input) => { |
| 698 | 711 | listed.push(input.repository); |
| 699 | 712 | return Effect.succeed({ |
| ... | ... | @@ -718,8 +731,8 @@ it.effect("reads nothing from a host with no implementation, but reports it", () |
| 718 | 731 | projectCount: summary.projectCount, |
| 719 | 732 | })), |
| 720 | 733 | [ |
| 721 | { kind: "github", configured: true, projectCount: 1 }, | |
| 722 | { kind: "gitlab", configured: false, projectCount: 1 }, | |
| 734 | { kind: SourceControlProviderKind.make("github"), configured: true, projectCount: 1 }, | |
| 735 | { kind: SourceControlProviderKind.make("gitlab"), configured: false, projectCount: 1 }, | |
| 723 | 736 | ], |
| 724 | 737 | ); |
| 725 | 738 | }), |
| ... | ... | @@ -733,7 +746,7 @@ it.effect("asks for a whole page of a host, and for the reader's own size when g |
| 733 | 746 | project({ id: "p1", title: "t3code", workspaceRoot: "/a", repository: "pingdotgg/t3code" }), |
| 734 | 747 | ], |
| 735 | 748 | providers: [ |
| 736 | fakeProvider("github", { | |
| 749 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 737 | 750 | listChangeRequests: (input) => { |
| 738 | 751 | limits.push(input.limit); |
| 739 | 752 | return Effect.succeed({ items: [], truncated: false, continues: true }); |
| ... | ... | @@ -759,7 +772,7 @@ it.effect("says where each repository carries on, and from nothing it has run ou |
| 759 | 772 | project({ id: "p2", title: "web", workspaceRoot: "/b", repository: "acme/web" }), |
| 760 | 773 | ], |
| 761 | 774 | providers: [ |
| 762 | fakeProvider("github", { | |
| 775 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 763 | 776 | listChangeRequests: ({ repository }) => |
| 764 | 777 | Effect.succeed({ |
| 765 | 778 | items: [changeRequest(1, "2026-07-02T00:00:00Z")], |
| ... | ... | @@ -787,7 +800,7 @@ it.effect("offers no continuation for a host that cannot be carried on from", () |
| 787 | 800 | project({ id: "p1", title: "t3code", workspaceRoot: "/a", repository: "pingdotgg/t3code" }), |
| 788 | 801 | ], |
| 789 | 802 | providers: [ |
| 790 | fakeProvider("github", { | |
| 803 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 791 | 804 | listChangeRequests: () => |
| 792 | 805 | Effect.succeed({ |
| 793 | 806 | items: [changeRequest(1, "2026-07-02T00:00:00Z")], |
| ... | ... | @@ -815,12 +828,12 @@ it.effect("uses a provider's raw cursor advance when it consumed malformed rows" |
| 815 | 828 | title: "web", |
| 816 | 829 | workspaceRoot: "/a", |
| 817 | 830 | repository: "acme/web", |
| 818 | provider: "azure-devops", | |
| 831 | provider: SourceControlProviderKind.make("azure-devops"), | |
| 819 | 832 | host: "dev.azure.com", |
| 820 | 833 | }), |
| 821 | 834 | ], |
| 822 | 835 | providers: [ |
| 823 | fakeProvider("azure-devops", { | |
| 836 | fakeProvider(SourceControlProviderKind.make("azure-devops"), { | |
| 824 | 837 | listChangeRequests: () => |
| 825 | 838 | Effect.succeed({ |
| 826 | 839 | items: [changeRequest(7, "2026-07-02T00:00:00Z")], |
| ... | ... | @@ -851,7 +864,7 @@ it.effect("reads only the repositories it was asked to carry on with", () => |
| 851 | 864 | project({ id: "p2", title: "web", workspaceRoot: "/b", repository: "acme/web" }), |
| 852 | 865 | ], |
| 853 | 866 | providers: [ |
| 854 | fakeProvider("github", { | |
| 867 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 855 | 868 | listChangeRequests: (input) => { |
| 856 | 869 | listed.push(input.repository); |
| 857 | 870 | cursors.push(input.cursor); |
| ... | ... | @@ -882,7 +895,7 @@ it.effect("keeps a row already sent at the boundary instant from arriving twice" |
| 882 | 895 | project({ id: "p1", title: "t3code", workspaceRoot: "/a", repository: "pingdotgg/t3code" }), |
| 883 | 896 | ], |
| 884 | 897 | providers: [ |
| 885 | fakeProvider("github", { | |
| 898 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 886 | 899 | // The boundary instant is asked for inclusively, so the host hands back the rows |
| 887 | 900 | // already sent at it alongside the ones beside them — which a strictly-older read |
| 888 | 901 | // would have lost instead. |
| ... | ... | @@ -922,7 +935,7 @@ it.effect("keeps the earlier exclusions when a slice ends on the instant it bega |
| 922 | 935 | project({ id: "p1", title: "t3code", workspaceRoot: "/a", repository: "pingdotgg/t3code" }), |
| 923 | 936 | ], |
| 924 | 937 | providers: [ |
| 925 | fakeProvider("github", { | |
| 938 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 926 | 939 | listChangeRequests: () => |
| 927 | 940 | Effect.succeed({ |
| 928 | 941 | items: [ |
| ... | ... | @@ -960,7 +973,9 @@ it.effect("refuses a continuation it did not issue, before asking any host anyth |
| 960 | 973 | project({ id: "p1", title: "t3code", workspaceRoot: "/a", repository: "pingdotgg/t3code" }), |
| 961 | 974 | ], |
| 962 | 975 | providers: [ |
| 963 | fakeProvider("github", { listChangeRequests: () => Effect.die("should not be read") }), | |
| 976 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 977 | listChangeRequests: () => Effect.die("should not be read"), | |
| 978 | }), | |
| 964 | 979 | ], |
| 965 | 980 | }); |
| 966 | 981 | |
| ... | ... | @@ -983,11 +998,11 @@ it.effect("calls a transient viewer failure a failed operation, not a signed-out |
| 983 | 998 | project({ id: "p1", title: "t3code", workspaceRoot: "/a", repository: "pingdotgg/t3code" }), |
| 984 | 999 | ], |
| 985 | 1000 | providers: [ |
| 986 | fakeProvider("github", { | |
| 1001 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 987 | 1002 | getViewer: () => |
| 988 | 1003 | Effect.fail( |
| 989 | 1004 | new PullRequestProviderError({ |
| 990 | provider: "github", | |
| 1005 | provider: SourceControlProviderKind.make("github"), | |
| 991 | 1006 | operation: "getViewer", |
| 992 | 1007 | reason: "failed", |
| 993 | 1008 | detail: "HTTP 500", |
| ... | ... | @@ -1014,23 +1029,24 @@ it.effect("reports an unusable host over a merely failing one", () => |
| 1014 | 1029 | title: "on gitlab", |
| 1015 | 1030 | workspaceRoot: "/c", |
| 1016 | 1031 | repository: "group/project", |
| 1017 | provider: "gitlab", | |
| 1032 | provider: SourceControlProviderKind.make("gitlab"), | |
| 1018 | 1033 | }), |
| 1019 | 1034 | ], |
| 1020 | 1035 | providers: [ |
| 1021 | fakeProvider("github", { | |
| 1036 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 1022 | 1037 | getViewer: () => |
| 1023 | 1038 | Effect.fail( |
| 1024 | 1039 | new PullRequestProviderError({ |
| 1025 | provider: "github", | |
| 1040 | provider: SourceControlProviderKind.make("github"), | |
| 1026 | 1041 | operation: "getViewer", |
| 1027 | 1042 | reason: "failed", |
| 1028 | 1043 | detail: "HTTP 500", |
| 1029 | 1044 | }), |
| 1030 | 1045 | ), |
| 1031 | 1046 | }), |
| 1032 | fakeProvider("gitlab", { | |
| 1033 | getViewer: () => Effect.fail(unusable("gitlab", "missing-tool")), | |
| 1047 | fakeProvider(SourceControlProviderKind.make("gitlab"), { | |
| 1048 | getViewer: () => | |
| 1049 | Effect.fail(unusable(SourceControlProviderKind.make("gitlab"), "missing-tool")), | |
| 1034 | 1050 | }), |
| 1035 | 1051 | ], |
| 1036 | 1052 | }); |
| ... | ... | @@ -1052,11 +1068,11 @@ it.effect("lists every host that has an implementation", () => |
| 1052 | 1068 | title: "on gitlab", |
| 1053 | 1069 | workspaceRoot: "/b", |
| 1054 | 1070 | repository: "group/sub/project", |
| 1055 | provider: "gitlab", | |
| 1071 | provider: SourceControlProviderKind.make("gitlab"), | |
| 1056 | 1072 | }), |
| 1057 | 1073 | ], |
| 1058 | 1074 | providers: [ |
| 1059 | fakeProvider("github", { | |
| 1075 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 1060 | 1076 | listChangeRequests: () => |
| 1061 | 1077 | Effect.succeed({ |
| 1062 | 1078 | items: [changeRequest(1, "2026-07-01T00:00:00Z")], |
| ... | ... | @@ -1064,7 +1080,7 @@ it.effect("lists every host that has an implementation", () => |
| 1064 | 1080 | continues: true, |
| 1065 | 1081 | }), |
| 1066 | 1082 | }), |
| 1067 | fakeProvider("gitlab", { | |
| 1083 | fakeProvider(SourceControlProviderKind.make("gitlab"), { | |
| 1068 | 1084 | listChangeRequests: (input) => |
| 1069 | 1085 | // Nested groups need the full path, not the last two segments. |
| 1070 | 1086 | input.repository === "group/sub/project" |
| ... | ... | @@ -1100,12 +1116,14 @@ it.effect("narrows the listing to one host when asked", () => |
| 1100 | 1116 | title: "on gitlab", |
| 1101 | 1117 | workspaceRoot: "/b", |
| 1102 | 1118 | repository: "group/project", |
| 1103 | provider: "gitlab", | |
| 1119 | provider: SourceControlProviderKind.make("gitlab"), | |
| 1104 | 1120 | }), |
| 1105 | 1121 | ], |
| 1106 | 1122 | providers: [ |
| 1107 | fakeProvider("github", { listChangeRequests: () => Effect.die("should not be read") }), | |
| 1108 | fakeProvider("gitlab", { | |
| 1123 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 1124 | listChangeRequests: () => Effect.die("should not be read"), | |
| 1125 | }), | |
| 1126 | fakeProvider(SourceControlProviderKind.make("gitlab"), { | |
| 1109 | 1127 | listChangeRequests: () => |
| 1110 | 1128 | Effect.succeed({ |
| 1111 | 1129 | items: [changeRequest(2, "2026-07-05T00:00:00Z")], |
| ... | ... | @@ -1139,7 +1157,7 @@ it.effect("tells two hosts of one kind apart in the switcher and the filter", () |
| 1139 | 1157 | }), |
| 1140 | 1158 | ], |
| 1141 | 1159 | providers: [ |
| 1142 | fakeProvider("github", { | |
| 1160 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 1143 | 1161 | listChangeRequests: ({ host }) => |
| 1144 | 1162 | Effect.succeed({ |
| 1145 | 1163 | items: host === "ghe.example.com" ? [changeRequest(2, "2026-07-05T00:00:00Z")] : [], |
| ... | ... | @@ -1179,11 +1197,11 @@ it.effect("keeps one host listed when another is not set up", () => |
| 1179 | 1197 | title: "on gitlab", |
| 1180 | 1198 | workspaceRoot: "/b", |
| 1181 | 1199 | repository: "group/project", |
| 1182 | provider: "gitlab", | |
| 1200 | provider: SourceControlProviderKind.make("gitlab"), | |
| 1183 | 1201 | }), |
| 1184 | 1202 | ], |
| 1185 | 1203 | providers: [ |
| 1186 | fakeProvider("github", { | |
| 1204 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 1187 | 1205 | listChangeRequests: () => |
| 1188 | 1206 | Effect.succeed({ |
| 1189 | 1207 | items: [changeRequest(1, "2026-07-01T00:00:00Z")], |
| ... | ... | @@ -1191,8 +1209,9 @@ it.effect("keeps one host listed when another is not set up", () => |
| 1191 | 1209 | continues: true, |
| 1192 | 1210 | }), |
| 1193 | 1211 | }), |
| 1194 | fakeProvider("gitlab", { | |
| 1195 | getViewer: () => Effect.fail(unusable("gitlab", "missing-tool")), | |
| 1212 | fakeProvider(SourceControlProviderKind.make("gitlab"), { | |
| 1213 | getViewer: () => | |
| 1214 | Effect.fail(unusable(SourceControlProviderKind.make("gitlab"), "missing-tool")), | |
| 1196 | 1215 | }), |
| 1197 | 1216 | ], |
| 1198 | 1217 | }); |
| ... | ... | @@ -1220,8 +1239,9 @@ it.effect("fails as unavailable only when no host can be read", () => |
| 1220 | 1239 | project({ id: "p1", title: "t3code", workspaceRoot: "/a", repository: "pingdotgg/t3code" }), |
| 1221 | 1240 | ], |
| 1222 | 1241 | providers: [ |
| 1223 | fakeProvider("github", { | |
| 1224 | getViewer: () => Effect.fail(unusable("github", "missing-tool")), | |
| 1242 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 1243 | getViewer: () => | |
| 1244 | Effect.fail(unusable(SourceControlProviderKind.make("github"), "missing-tool")), | |
| 1225 | 1245 | }), |
| 1226 | 1246 | ], |
| 1227 | 1247 | }); |
| ... | ... | @@ -1250,7 +1270,7 @@ it.effect("reads a repository once when several worktrees share it", () => |
| 1250 | 1270 | }), |
| 1251 | 1271 | ], |
| 1252 | 1272 | providers: [ |
| 1253 | fakeProvider("github", { | |
| 1273 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 1254 | 1274 | listChangeRequests: () => { |
| 1255 | 1275 | calls += 1; |
| 1256 | 1276 | return Effect.succeed({ |
| ... | ... | @@ -1278,7 +1298,7 @@ it.effect("keeps healthy repositories when one of them cannot be read", () => |
| 1278 | 1298 | project({ id: "p2", title: "broken", workspaceRoot: "/b", repository: "pingdotgg/broken" }), |
| 1279 | 1299 | ], |
| 1280 | 1300 | providers: [ |
| 1281 | fakeProvider("github", { | |
| 1301 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 1282 | 1302 | listChangeRequests: (input) => |
| 1283 | 1303 | input.repository === "pingdotgg/broken" |
| 1284 | 1304 | ? Effect.fail(requestFailed) |
| ... | ... | @@ -1309,11 +1329,11 @@ it.effect("tries another workspace on the same host for the viewer", () => |
| 1309 | 1329 | project({ id: "p2", title: "healthy", workspaceRoot: "/healthy", repository: "acme/two" }), |
| 1310 | 1330 | ], |
| 1311 | 1331 | providers: [ |
| 1312 | fakeProvider("github", { | |
| 1332 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 1313 | 1333 | getViewer: (input) => |
| 1314 | 1334 | input.cwd === "/healthy" |
| 1315 | 1335 | ? Effect.succeed("bilal") |
| 1316 | : Effect.fail(unusable("github", "missing-tool")), | |
| 1336 | : Effect.fail(unusable(SourceControlProviderKind.make("github"), "missing-tool")), | |
| 1317 | 1337 | listChangeRequests: () => |
| 1318 | 1338 | Effect.succeed({ |
| 1319 | 1339 | items: [changeRequest(1, "2026-07-02T00:00:00Z")], |
| ... | ... | @@ -1345,7 +1365,7 @@ it.effect("routing verifies the current account on the requested host without ca |
| 1345 | 1365 | }), |
| 1346 | 1366 | ], |
| 1347 | 1367 | providers: [ |
| 1348 | fakeProvider("github", { | |
| 1368 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 1349 | 1369 | getRoutingIdentity: (input) => { |
| 1350 | 1370 | assert.deepStrictEqual(input, { cwd: "/a", host: "github.example.test" }); |
| 1351 | 1371 | return Effect.succeed({ |
| ... | ... | @@ -1359,7 +1379,7 @@ it.effect("routing verifies the current account on the requested host without ca |
| 1359 | 1379 | const ref = { projectId: "p1" as ProjectId, repository: "acme/web", number: 1 }; |
| 1360 | 1380 | assert.deepStrictEqual(yield* service.routing(ref), { |
| 1361 | 1381 | host: "github.example.test", |
| 1362 | provider: "github", | |
| 1382 | provider: SourceControlProviderKind.make("github"), | |
| 1363 | 1383 | viewer: "first-account", |
| 1364 | 1384 | accountId: "123", |
| 1365 | 1385 | projectTitle: "web", |
| ... | ... | @@ -1382,7 +1402,7 @@ it.effect("refuses an action the host never claimed it could run", () => |
| 1382 | 1402 | const service = yield* makeService({ |
| 1383 | 1403 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 1384 | 1404 | providers: [ |
| 1385 | fakeProvider("github", { | |
| 1405 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 1386 | 1406 | capabilities: { |
| 1387 | 1407 | diff: true, |
| 1388 | 1408 | comment: true, |
| ... | ... | @@ -1428,12 +1448,12 @@ it.effect("publishes a merge for immediate settlement only after host confirmati |
| 1428 | 1448 | project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" }), |
| 1429 | 1449 | ], |
| 1430 | 1450 | providers: [ |
| 1431 | fakeProvider("github", { | |
| 1451 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 1432 | 1452 | getChangeRequestSummary: () => |
| 1433 | 1453 | confirmationFails |
| 1434 | 1454 | ? Effect.fail( |
| 1435 | 1455 | new PullRequestProviderError({ |
| 1436 | provider: "github", | |
| 1456 | provider: SourceControlProviderKind.make("github"), | |
| 1437 | 1457 | operation: "getChangeRequestSummary", |
| 1438 | 1458 | reason: "failed", |
| 1439 | 1459 | detail: "HTTP 504", |
| ... | ... | @@ -1486,7 +1506,7 @@ it.effect("refreshes every reader before a queued merge confirmation finishes", |
| 1486 | 1506 | project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" }), |
| 1487 | 1507 | ], |
| 1488 | 1508 | providers: [ |
| 1489 | fakeProvider("github", { | |
| 1509 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 1490 | 1510 | getChangeRequestSummary: () => |
| 1491 | 1511 | Effect.gen(function* () { |
| 1492 | 1512 | yield* Deferred.succeed(confirmationStarted, undefined); |
| ... | ... | @@ -1528,7 +1548,7 @@ it.effect("refuses an action this viewer may not take, and says what access it t |
| 1528 | 1548 | const service = yield* makeService({ |
| 1529 | 1549 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 1530 | 1550 | providers: [ |
| 1531 | fakeProvider("github", { | |
| 1551 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 1532 | 1552 | // The host merges; this account only reads it, and opened the change request — which |
| 1533 | 1553 | // is every contributor to a repository they do not own. |
| 1534 | 1554 | getViewerPermissions: () => |
| ... | ... | @@ -1565,7 +1585,7 @@ it.effect("gates arming a merge for later exactly as it gates merging now", () = |
| 1565 | 1585 | const service = yield* makeService({ |
| 1566 | 1586 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 1567 | 1587 | providers: [ |
| 1568 | fakeProvider("github", { | |
| 1588 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 1569 | 1589 | capabilities: { |
| 1570 | 1590 | diff: true, |
| 1571 | 1591 | comment: true, |
| ... | ... | @@ -1620,7 +1640,7 @@ it.effect("hands the host the strategy an armed merge was asked for", () => |
| 1620 | 1640 | const service = yield* makeService({ |
| 1621 | 1641 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 1622 | 1642 | providers: [ |
| 1623 | fakeProvider("github", { | |
| 1643 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 1624 | 1644 | capabilities: { |
| 1625 | 1645 | diff: true, |
| 1626 | 1646 | comment: true, |
| ... | ... | @@ -1666,7 +1686,7 @@ it.effect("refuses an auto-merge the host never claimed, without asking it", () |
| 1666 | 1686 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 1667 | 1687 | providers: [ |
| 1668 | 1688 | // Bitbucket's shape: it merges, and has nothing that merges later on its own. |
| 1669 | fakeProvider("github", { | |
| 1689 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 1670 | 1690 | runAction: () => { |
| 1671 | 1691 | ran = true; |
| 1672 | 1692 | return Effect.void; |
| ... | ... | @@ -1694,7 +1714,7 @@ it.effect("refuses to resolve a conversation this viewer may not, without asking |
| 1694 | 1714 | const service = yield* makeService({ |
| 1695 | 1715 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 1696 | 1716 | providers: [ |
| 1697 | fakeProvider("github", { | |
| 1717 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 1698 | 1718 | getViewerPermissions: () => |
| 1699 | 1719 | Effect.succeed({ |
| 1700 | 1720 | actions: ["merge", "ready", "draft", "close", "reopen"], |
| ... | ... | @@ -1729,7 +1749,7 @@ it.effect("asks nobody what the viewer may do when the host cannot do it at all" |
| 1729 | 1749 | const service = yield* makeService({ |
| 1730 | 1750 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 1731 | 1751 | providers: [ |
| 1732 | fakeProvider("github", { | |
| 1752 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 1733 | 1753 | capabilities: { |
| 1734 | 1754 | diff: true, |
| 1735 | 1755 | comment: true, |
| ... | ... | @@ -1768,7 +1788,7 @@ it.effect("refuses a comment on a host that cannot post one", () => |
| 1768 | 1788 | const service = yield* makeService({ |
| 1769 | 1789 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 1770 | 1790 | providers: [ |
| 1771 | fakeProvider("github", { | |
| 1791 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 1772 | 1792 | capabilities: { |
| 1773 | 1793 | diff: false, |
| 1774 | 1794 | comment: false, |
| ... | ... | @@ -1817,7 +1837,7 @@ it.effect("keeps two hosts of one provider kind as two accounts", () => |
| 1817 | 1837 | }), |
| 1818 | 1838 | ], |
| 1819 | 1839 | providers: [ |
| 1820 | fakeProvider("github", { | |
| 1840 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 1821 | 1841 | getViewer: (input) => Effect.succeed(viewerFor[input.cwd] ?? "unknown"), |
| 1822 | 1842 | listChangeRequests: () => |
| 1823 | 1843 | Effect.succeed({ |
| ... | ... | @@ -1858,11 +1878,11 @@ it.effect("reports repositories on a host that could not be read", () => |
| 1858 | 1878 | }), |
| 1859 | 1879 | ], |
| 1860 | 1880 | providers: [ |
| 1861 | fakeProvider("github", { | |
| 1881 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 1862 | 1882 | getViewer: (input) => |
| 1863 | 1883 | input.cwd === "/cloud" |
| 1864 | 1884 | ? Effect.succeed("bilal") |
| 1865 | : Effect.fail(unusable("github", "unauthenticated")), | |
| 1885 | : Effect.fail(unusable(SourceControlProviderKind.make("github"), "unauthenticated")), | |
| 1866 | 1886 | listChangeRequests: () => |
| 1867 | 1887 | Effect.succeed({ |
| 1868 | 1888 | items: [changeRequest(1, "2026-07-02T00:00:00Z")], |
| ... | ... | @@ -1893,7 +1913,7 @@ it.effect("stops new reads after a rate limit while leaving manual actions avail |
| 1893 | 1913 | project({ id: "p1", title: "cloud", workspaceRoot: "/cloud", repository: "acme/web" }), |
| 1894 | 1914 | ], |
| 1895 | 1915 | providers: [ |
| 1896 | fakeProvider("github", { | |
| 1916 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 1897 | 1917 | listChangeRequests: () => |
| 1898 | 1918 | Effect.sync(() => { |
| 1899 | 1919 | listCalls += 1; |
| ... | ... | @@ -1901,7 +1921,7 @@ it.effect("stops new reads after a rate limit while leaving manual actions avail |
| 1901 | 1921 | Effect.andThen( |
| 1902 | 1922 | Effect.fail( |
| 1903 | 1923 | new PullRequestProviderError({ |
| 1904 | provider: "github", | |
| 1924 | provider: SourceControlProviderKind.make("github"), | |
| 1905 | 1925 | operation: "listChangeRequests", |
| 1906 | 1926 | reason: "rate-limited", |
| 1907 | 1927 | detail: "GitHub API rate limit exceeded.", |
| ... | ... | @@ -1933,13 +1953,14 @@ it.effect("stops new reads after a rate limit while leaving manual actions avail |
| 1933 | 1953 | }), |
| 1934 | 1954 | ); |
| 1935 | 1955 | |
| 1936 | for (const [provider, host] of [ | |
| 1956 | for (const [providerName, host] of [ | |
| 1937 | 1957 | ["github", "github.com"], |
| 1938 | 1958 | ["gitlab", "gitlab.com"], |
| 1939 | 1959 | ["forgejo", "code.example.test"], |
| 1940 | 1960 | ["bitbucket", "bitbucket.org"], |
| 1941 | 1961 | ["azure-devops", "dev.azure.com"], |
| 1942 | 1962 | ] as const) { |
| 1963 | const provider = SourceControlProviderKind.make(providerName); | |
| 1943 | 1964 | it.effect.each(["detail", "checks"] as const)( |
| 1944 | 1965 | `shares fresh ${provider} %s reads and respects rate-limit resets`, |
| 1945 | 1966 | (read) => |
| ... | ... | @@ -2006,7 +2027,7 @@ it.effect("does not fall back to full detail when checks are unsupported", () => |
| 2006 | 2027 | project({ id: "p1", title: "web", workspaceRoot: "/repo", repository: "acme/web" }), |
| 2007 | 2028 | ], |
| 2008 | 2029 | providers: [ |
| 2009 | fakeProvider("github", { | |
| 2030 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 2010 | 2031 | getChangeRequest: () => Effect.die("Unexpected full detail read"), |
| 2011 | 2032 | }), |
| 2012 | 2033 | ], |
| ... | ... | @@ -2025,7 +2046,7 @@ it.effect("uses a manual rate limit to pause later reads", () => |
| 2025 | 2046 | project({ id: "p1", title: "cloud", workspaceRoot: "/cloud", repository: "acme/web" }), |
| 2026 | 2047 | ], |
| 2027 | 2048 | providers: [ |
| 2028 | fakeProvider("github", { | |
| 2049 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 2029 | 2050 | listChangeRequests: () => |
| 2030 | 2051 | Effect.sync(() => { |
| 2031 | 2052 | listCalls += 1; |
| ... | ... | @@ -2034,7 +2055,7 @@ it.effect("uses a manual rate limit to pause later reads", () => |
| 2034 | 2055 | runAction: () => |
| 2035 | 2056 | Effect.fail( |
| 2036 | 2057 | new PullRequestProviderError({ |
| 2037 | provider: "github", | |
| 2058 | provider: SourceControlProviderKind.make("github"), | |
| 2038 | 2059 | operation: "runAction", |
| 2039 | 2060 | reason: "rate-limited", |
| 2040 | 2061 | detail: "GitHub API rate limit exceeded.", |
| ... | ... | @@ -2066,7 +2087,7 @@ it.effect("flags a review request for the viewer but not on their own change req |
| 2066 | 2087 | project({ id: "p1", title: "t3code", workspaceRoot: "/a", repository: "pingdotgg/t3code" }), |
| 2067 | 2088 | ], |
| 2068 | 2089 | providers: [ |
| 2069 | fakeProvider("github", { | |
| 2090 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 2070 | 2091 | listChangeRequests: () => |
| 2071 | 2092 | Effect.succeed({ |
| 2072 | 2093 | items: [ |
| ... | ... | @@ -2099,7 +2120,7 @@ it.effect("refuses a repository that does not belong to the requested project", |
| 2099 | 2120 | projects: [ |
| 2100 | 2121 | project({ id: "p1", title: "t3code", workspaceRoot: "/a", repository: "pingdotgg/t3code" }), |
| 2101 | 2122 | ], |
| 2102 | providers: [fakeProvider("github")], | |
| 2123 | providers: [fakeProvider(SourceControlProviderKind.make("github"))], | |
| 2103 | 2124 | }); |
| 2104 | 2125 | |
| 2105 | 2126 | const error = yield* service |
| ... | ... | @@ -2116,7 +2137,7 @@ it.effect("caches stack membership separately from action details", () => |
| 2116 | 2137 | const service = yield* makeService({ |
| 2117 | 2138 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 2118 | 2139 | providers: [ |
| 2119 | fakeProvider("github", { | |
| 2140 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 2120 | 2141 | getChangeRequestStack: (input) => |
| 2121 | 2142 | Effect.sync(() => { |
| 2122 | 2143 | reads.push(input.includeDetails === true); |
| ... | ... | @@ -2157,7 +2178,7 @@ it.effect("reads a host-native stack through the provider and null where it has |
| 2157 | 2178 | const service = yield* makeService({ |
| 2158 | 2179 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 2159 | 2180 | providers: [ |
| 2160 | fakeProvider("github", { | |
| 2181 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 2161 | 2182 | getChangeRequestStack: () => |
| 2162 | 2183 | Effect.succeed({ |
| 2163 | 2184 | id: "9", |
| ... | ... | @@ -2185,7 +2206,7 @@ it.effect("reads a host-native stack through the provider and null where it has |
| 2185 | 2206 | |
| 2186 | 2207 | const withoutStacks = yield* makeService({ |
| 2187 | 2208 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 2188 | providers: [fakeProvider("github")], | |
| 2209 | providers: [fakeProvider(SourceControlProviderKind.make("github"))], | |
| 2189 | 2210 | }); |
| 2190 | 2211 | assert.isNull( |
| 2191 | 2212 | yield* withoutStacks.stack({ |
| ... | ... | @@ -2215,7 +2236,7 @@ it.effect("routes explicit Forgejo HTTP authorities through SSH checkouts after |
| 2215 | 2236 | }), |
| 2216 | 2237 | ], |
| 2217 | 2238 | providers: [ |
| 2218 | fakeProvider("forgejo", { | |
| 2239 | fakeProvider(SourceControlProviderKind.make("forgejo"), { | |
| 2219 | 2240 | getViewer: (input) => { |
| 2220 | 2241 | viewers.push(input.host); |
| 2221 | 2242 | assert.strictEqual(input.host, "code.example:3000"); |
| ... | ... | @@ -2244,7 +2265,11 @@ it.effect("routes explicit Forgejo HTTP authorities through SSH checkouts after |
| 2244 | 2265 | return Effect.succeed({ |
| 2245 | 2266 | context: { |
| 2246 | 2267 | ...context, |
| 2247 | provider: { kind: "forgejo", name: "Forgejo", baseUrl: "http://code.example:3000" }, | |
| 2268 | provider: { | |
| 2269 | kind: SourceControlProviderKind.make("forgejo"), | |
| 2270 | name: "Forgejo", | |
| 2271 | baseUrl: "http://code.example:3000", | |
| 2272 | }, | |
| 2248 | 2273 | }, |
| 2249 | 2274 | provider: undefined as never, |
| 2250 | 2275 | }); |
| ... | ... | @@ -2294,12 +2319,12 @@ it.effect("rejects a different Forgejo HTTP port for an HTTP checkout", () => |
| 2294 | 2319 | title: "http", |
| 2295 | 2320 | workspaceRoot: "/http", |
| 2296 | 2321 | repository: "team/repo", |
| 2297 | provider: "forgejo", | |
| 2322 | provider: SourceControlProviderKind.make("forgejo"), | |
| 2298 | 2323 | host: "code.example", |
| 2299 | 2324 | remoteUrl: "http://code.example:4000/team/repo.git", |
| 2300 | 2325 | }), |
| 2301 | 2326 | ], |
| 2302 | providers: [fakeProvider("forgejo")], | |
| 2327 | providers: [fakeProvider(SourceControlProviderKind.make("forgejo"))], | |
| 2303 | 2328 | }); |
| 2304 | 2329 | const failure = yield* service |
| 2305 | 2330 | .summary( |
| ... | ... | @@ -2327,7 +2352,7 @@ it.effect("resolves a project's repository identity when its shell has none cach |
| 2327 | 2352 | const service = yield* makeService({ |
| 2328 | 2353 | projects: [{ ...resolved, repositoryIdentity: null }], |
| 2329 | 2354 | providers: [ |
| 2330 | fakeProvider("github", { | |
| 2355 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 2331 | 2356 | getChangeRequestSummary: () => Effect.succeed(changeRequest(7, "2026-07-02T00:00:00Z")), |
| 2332 | 2357 | }), |
| 2333 | 2358 | ], |
| ... | ... | @@ -2351,7 +2376,7 @@ it.effect("routes a hosted reference to another repository through a project on |
| 2351 | 2376 | project({ id: "frontend", title: "web", workspaceRoot: "/web", repository: "acme/web" }), |
| 2352 | 2377 | ], |
| 2353 | 2378 | providers: [ |
| 2354 | fakeProvider("github", { | |
| 2379 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 2355 | 2380 | getChangeRequestSummary: (input) => |
| 2356 | 2381 | Effect.sync(() => { |
| 2357 | 2382 | seen.push({ cwd: input.cwd, repository: input.repository, host: input.host }); |
| ... | ... | @@ -2381,12 +2406,12 @@ it.effect("routes Azure reads and writes through the requested organization's ch |
| 2381 | 2406 | title: organization, |
| 2382 | 2407 | workspaceRoot: `/${organization}`, |
| 2383 | 2408 | repository: `${organization}/project/_git/web`, |
| 2384 | provider: "azure-devops", | |
| 2409 | provider: SourceControlProviderKind.make("azure-devops"), | |
| 2385 | 2410 | host: "dev.azure.com", |
| 2386 | 2411 | }), |
| 2387 | 2412 | ), |
| 2388 | 2413 | providers: [ |
| 2389 | fakeProvider("azure-devops", { | |
| 2414 | fakeProvider(SourceControlProviderKind.make("azure-devops"), { | |
| 2390 | 2415 | getChangeRequestSummary: (input) => |
| 2391 | 2416 | Effect.sync(() => { |
| 2392 | 2417 | seen.push(`read ${input.cwd} ${input.repository}`); |
| ... | ... | @@ -2434,7 +2459,7 @@ it.effect.each([ |
| 2434 | 2459 | id: "target", |
| 2435 | 2460 | title: "target", |
| 2436 | 2461 | workspaceRoot: "/target", |
| 2437 | provider: "azure-devops", | |
| 2462 | provider: SourceControlProviderKind.make("azure-devops"), | |
| 2438 | 2463 | ...checkout, |
| 2439 | 2464 | }); |
| 2440 | 2465 | const service = yield* makeService({ |
| ... | ... | @@ -2444,7 +2469,7 @@ it.effect.each([ |
| 2444 | 2469 | id: repository, |
| 2445 | 2470 | title: repository, |
| 2446 | 2471 | workspaceRoot: `/${repository}`, |
| 2447 | provider: "azure-devops", | |
| 2472 | provider: SourceControlProviderKind.make("azure-devops"), | |
| 2448 | 2473 | host: "dev.azure.com", |
| 2449 | 2474 | repository, |
| 2450 | 2475 | }), |
| ... | ... | @@ -2452,7 +2477,7 @@ it.effect.each([ |
| 2452 | 2477 | target, |
| 2453 | 2478 | ], |
| 2454 | 2479 | providers: [ |
| 2455 | fakeProvider("azure-devops", { | |
| 2480 | fakeProvider(SourceControlProviderKind.make("azure-devops"), { | |
| 2456 | 2481 | getChangeRequestSummary: (input) => |
| 2457 | 2482 | Effect.sync(() => { |
| 2458 | 2483 | seen.push(`read ${input.cwd} ${input.repository}`); |
| ... | ... | @@ -2486,12 +2511,12 @@ it.effect("refuses Azure cross-organization reads and writes without its checkou |
| 2486 | 2511 | title: "org-a", |
| 2487 | 2512 | workspaceRoot: "/org-a", |
| 2488 | 2513 | repository: "org-a/project/_git/web", |
| 2489 | provider: "azure-devops", | |
| 2514 | provider: SourceControlProviderKind.make("azure-devops"), | |
| 2490 | 2515 | host: "dev.azure.com", |
| 2491 | 2516 | }), |
| 2492 | 2517 | ], |
| 2493 | 2518 | providers: [ |
| 2494 | fakeProvider("azure-devops", { | |
| 2519 | fakeProvider(SourceControlProviderKind.make("azure-devops"), { | |
| 2495 | 2520 | getChangeRequestSummary: () => Effect.die("must not read the wrong organization"), |
| 2496 | 2521 | runAction: () => Effect.die("must not modify the wrong organization"), |
| 2497 | 2522 | }), |
| ... | ... | @@ -2518,7 +2543,7 @@ it.effect("refuses a hosted reference when nothing is checked out from that host |
| 2518 | 2543 | projects: [ |
| 2519 | 2544 | project({ id: "frontend", title: "web", workspaceRoot: "/web", repository: "acme/web" }), |
| 2520 | 2545 | ], |
| 2521 | providers: [fakeProvider("github")], | |
| 2546 | providers: [fakeProvider(SourceControlProviderKind.make("github"))], | |
| 2522 | 2547 | }); |
| 2523 | 2548 | |
| 2524 | 2549 | const error = yield* service |
| ... | ... | @@ -2546,11 +2571,11 @@ it.effect("refuses a diff on a host that cannot produce one", () => |
| 2546 | 2571 | title: "on azure", |
| 2547 | 2572 | workspaceRoot: "/a", |
| 2548 | 2573 | repository: "org/project", |
| 2549 | provider: "azure-devops", | |
| 2574 | provider: SourceControlProviderKind.make("azure-devops"), | |
| 2550 | 2575 | }), |
| 2551 | 2576 | ], |
| 2552 | 2577 | providers: [ |
| 2553 | fakeProvider("azure-devops", { | |
| 2578 | fakeProvider(SourceControlProviderKind.make("azure-devops"), { | |
| 2554 | 2579 | capabilities: { |
| 2555 | 2580 | diff: false, |
| 2556 | 2581 | comment: true, |
| ... | ... | @@ -2580,7 +2605,11 @@ it.effect("rejects an empty comment before reaching the host", () => |
| 2580 | 2605 | projects: [ |
| 2581 | 2606 | project({ id: "p1", title: "t3code", workspaceRoot: "/a", repository: "pingdotgg/t3code" }), |
| 2582 | 2607 | ], |
| 2583 | providers: [fakeProvider("github", { comment: () => Effect.die("must not be called") })], | |
| 2608 | providers: [ | |
| 2609 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 2610 | comment: () => Effect.die("must not be called"), | |
| 2611 | }), | |
| 2612 | ], | |
| 2584 | 2613 | }); |
| 2585 | 2614 | |
| 2586 | 2615 | const error = yield* service |
| ... | ... | @@ -2606,11 +2635,11 @@ it.effect("refuses a verdict the host never claimed, without asking the provider |
| 2606 | 2635 | title: "on gitlab", |
| 2607 | 2636 | workspaceRoot: "/a", |
| 2608 | 2637 | repository: "group/project", |
| 2609 | provider: "gitlab", | |
| 2638 | provider: SourceControlProviderKind.make("gitlab"), | |
| 2610 | 2639 | }), |
| 2611 | 2640 | ], |
| 2612 | 2641 | providers: [ |
| 2613 | fakeProvider("gitlab", { | |
| 2642 | fakeProvider(SourceControlProviderKind.make("gitlab"), { | |
| 2614 | 2643 | capabilities: { |
| 2615 | 2644 | diff: true, |
| 2616 | 2645 | comment: true, |
| ... | ... | @@ -2658,7 +2687,7 @@ it.effect("refuses line comments on a host that takes only a summary", () => |
| 2658 | 2687 | project({ id: "p1", title: "t3code", workspaceRoot: "/a", repository: "pingdotgg/t3code" }), |
| 2659 | 2688 | ], |
| 2660 | 2689 | providers: [ |
| 2661 | fakeProvider("github", { | |
| 2690 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 2662 | 2691 | capabilities: { |
| 2663 | 2692 | diff: true, |
| 2664 | 2693 | comment: true, |
| ... | ... | @@ -2704,7 +2733,7 @@ it.effect( |
| 2704 | 2733 | }), |
| 2705 | 2734 | ], |
| 2706 | 2735 | providers: [ |
| 2707 | fakeProvider("github", { | |
| 2736 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 2708 | 2737 | submitReview: () => { |
| 2709 | 2738 | approved = true; |
| 2710 | 2739 | return Effect.void; |
| ... | ... | @@ -2736,7 +2765,7 @@ it.effect("refuses to resolve a conversation on a host that cannot", () => |
| 2736 | 2765 | project({ id: "p1", title: "t3code", workspaceRoot: "/a", repository: "pingdotgg/t3code" }), |
| 2737 | 2766 | ], |
| 2738 | 2767 | providers: [ |
| 2739 | fakeProvider("github", { | |
| 2768 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 2740 | 2769 | capabilities: { |
| 2741 | 2770 | diff: true, |
| 2742 | 2771 | comment: true, |
| ... | ... | @@ -2777,7 +2806,7 @@ it.effect("refuses to react on a host with no reactions", () => |
| 2777 | 2806 | project({ id: "p1", title: "t3code", workspaceRoot: "/a", repository: "pingdotgg/t3code" }), |
| 2778 | 2807 | ], |
| 2779 | 2808 | providers: [ |
| 2780 | fakeProvider("github", { | |
| 2809 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 2781 | 2810 | capabilities: { |
| 2782 | 2811 | diff: true, |
| 2783 | 2812 | comment: true, |
| ... | ... | @@ -2814,7 +2843,7 @@ it.effect("refuses to react on a host whose capabilities omit reactions entirely |
| 2814 | 2843 | project({ id: "p1", title: "t3code", workspaceRoot: "/a", repository: "pingdotgg/t3code" }), |
| 2815 | 2844 | ], |
| 2816 | 2845 | providers: [ |
| 2817 | fakeProvider("github", { | |
| 2846 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 2818 | 2847 | capabilities: { |
| 2819 | 2848 | diff: true, |
| 2820 | 2849 | comment: true, |
| ... | ... | @@ -2855,7 +2884,7 @@ it.effect("passes a reaction through with its subject id on a host that has them |
| 2855 | 2884 | project({ id: "p1", title: "t3code", workspaceRoot: "/a", repository: "pingdotgg/t3code" }), |
| 2856 | 2885 | ], |
| 2857 | 2886 | providers: [ |
| 2858 | fakeProvider("github", { | |
| 2887 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 2859 | 2888 | setReaction: (input) => { |
| 2860 | 2889 | received = { |
| 2861 | 2890 | subjectId: input.subjectId, |
| ... | ... | @@ -2888,7 +2917,7 @@ it.effect("invalidates the cached activity after reacting, like the other mutati |
| 2888 | 2917 | const service = yield* makeService({ |
| 2889 | 2918 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 2890 | 2919 | providers: [ |
| 2891 | fakeProvider("github", { | |
| 2920 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 2892 | 2921 | getChangeRequestActivity: () => { |
| 2893 | 2922 | activityCalls += 1; |
| 2894 | 2923 | return Effect.succeed({ |
| ... | ... | @@ -2926,7 +2955,9 @@ it.effect("refuses an empty reply before it reaches the host", () => |
| 2926 | 2955 | project({ id: "p1", title: "t3code", workspaceRoot: "/a", repository: "pingdotgg/t3code" }), |
| 2927 | 2956 | ], |
| 2928 | 2957 | providers: [ |
| 2929 | fakeProvider("github", { replyToThread: () => Effect.die("must not be called") }), | |
| 2958 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 2959 | replyToThread: () => Effect.die("must not be called"), | |
| 2960 | }), | |
| 2930 | 2961 | ], |
| 2931 | 2962 | }); |
| 2932 | 2963 | |
| ... | ... | @@ -2952,7 +2983,7 @@ it.effect("refuses a merge strategy the host does not offer", () => |
| 2952 | 2983 | project({ id: "p1", title: "t3code", workspaceRoot: "/a", repository: "pingdotgg/t3code" }), |
| 2953 | 2984 | ], |
| 2954 | 2985 | providers: [ |
| 2955 | fakeProvider("github", { | |
| 2986 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 2956 | 2987 | capabilities: { |
| 2957 | 2988 | diff: true, |
| 2958 | 2989 | comment: true, |
| ... | ... | @@ -3005,7 +3036,7 @@ it.effect("hands the provider the host its repository lives on", () => |
| 3005 | 3036 | }), |
| 3006 | 3037 | ], |
| 3007 | 3038 | providers: [ |
| 3008 | fakeProvider("github", { | |
| 3039 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 3009 | 3040 | listChangeRequests: (input) => { |
| 3010 | 3041 | hosts.push(input.host); |
| 3011 | 3042 | return Effect.succeed({ items: [], truncated: false, continues: true }); |
| ... | ... | @@ -3037,12 +3068,12 @@ it.effect("asks every host the reader's search, rather than filtering what came |
| 3037 | 3068 | title: "on gitlab", |
| 3038 | 3069 | workspaceRoot: "/b", |
| 3039 | 3070 | repository: "group/project", |
| 3040 | provider: "gitlab", | |
| 3071 | provider: SourceControlProviderKind.make("gitlab"), | |
| 3041 | 3072 | }), |
| 3042 | 3073 | ], |
| 3043 | 3074 | providers: [ |
| 3044 | fakeProvider("github", { listChangeRequests: listing }), | |
| 3045 | fakeProvider("gitlab", { listChangeRequests: listing }), | |
| 3075 | fakeProvider(SourceControlProviderKind.make("github"), { listChangeRequests: listing }), | |
| 3076 | fakeProvider(SourceControlProviderKind.make("gitlab"), { listChangeRequests: listing }), | |
| 3046 | 3077 | ], |
| 3047 | 3078 | }); |
| 3048 | 3079 | |
| ... | ... | @@ -3062,7 +3093,7 @@ it.effect("asks for no search when the reader has typed nothing", () => |
| 3062 | 3093 | project({ id: "p1", title: "t3code", workspaceRoot: "/a", repository: "pingdotgg/t3code" }), |
| 3063 | 3094 | ], |
| 3064 | 3095 | providers: [ |
| 3065 | fakeProvider("github", { | |
| 3096 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 3066 | 3097 | listChangeRequests: (input) => { |
| 3067 | 3098 | asked.push(input.query); |
| 3068 | 3099 | return Effect.succeed({ items: [], truncated: false, continues: true }); |
| ... | ... | @@ -3098,13 +3129,13 @@ it.effect("asks another checkout who is signed in when the first one cannot answ |
| 3098 | 3129 | }), |
| 3099 | 3130 | ], |
| 3100 | 3131 | providers: [ |
| 3101 | fakeProvider("github", { | |
| 3132 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 3102 | 3133 | getViewer: (input) => { |
| 3103 | 3134 | asked.push(input.cwd); |
| 3104 | 3135 | return input.cwd === "/gone" |
| 3105 | 3136 | ? Effect.fail( |
| 3106 | 3137 | new PullRequestProviderError({ |
| 3107 | provider: "github", | |
| 3138 | provider: SourceControlProviderKind.make("github"), | |
| 3108 | 3139 | operation: "getViewer", |
| 3109 | 3140 | reason: "failed", |
| 3110 | 3141 | detail: "not a git repository", |
| ... | ... | @@ -3138,7 +3169,7 @@ it.effect("refuses to ask for a review on a host that cannot, before any call is |
| 3138 | 3169 | const service = yield* makeService({ |
| 3139 | 3170 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 3140 | 3171 | providers: [ |
| 3141 | fakeProvider("github", { | |
| 3172 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 3142 | 3173 | capabilities: { |
| 3143 | 3174 | diff: true, |
| 3144 | 3175 | comment: true, |
| ... | ... | @@ -3179,7 +3210,7 @@ it.effect("refuses the candidate list on a host that has no such list to give", |
| 3179 | 3210 | const service = yield* makeService({ |
| 3180 | 3211 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 3181 | 3212 | providers: [ |
| 3182 | fakeProvider("github", { | |
| 3213 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 3183 | 3214 | capabilities: { |
| 3184 | 3215 | diff: false, |
| 3185 | 3216 | comment: false, |
| ... | ... | @@ -3215,7 +3246,7 @@ it.effect("refuses a review request this viewer may not make, and says what acce |
| 3215 | 3246 | const service = yield* makeService({ |
| 3216 | 3247 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 3217 | 3248 | providers: [ |
| 3218 | fakeProvider("github", { | |
| 3249 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 3219 | 3250 | // The host asks for reviews; this account only reads the repository. |
| 3220 | 3251 | getViewerPermissions: () => |
| 3221 | 3252 | Effect.succeed({ |
| ... | ... | @@ -3254,7 +3285,7 @@ it.effect("keeps the menu from a viewer who may not ask, which is all it is for" |
| 3254 | 3285 | const service = yield* makeService({ |
| 3255 | 3286 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 3256 | 3287 | providers: [ |
| 3257 | fakeProvider("github", { | |
| 3288 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 3258 | 3289 | getViewerPermissions: () => |
| 3259 | 3290 | Effect.succeed({ |
| 3260 | 3291 | actions: [], |
| ... | ... | @@ -3286,7 +3317,7 @@ it.effect("hands the host's own candidate list back, and asks for it with the ch |
| 3286 | 3317 | const service = yield* makeService({ |
| 3287 | 3318 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 3288 | 3319 | providers: [ |
| 3289 | fakeProvider("github", { | |
| 3320 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 3290 | 3321 | listReviewerCandidates: (input) => { |
| 3291 | 3322 | askedFor = input.number; |
| 3292 | 3323 | return Effect.succeed({ |
| ... | ... | @@ -3328,7 +3359,7 @@ it.effect("refuses a label change on a host that has not said it takes one", () |
| 3328 | 3359 | const service = yield* makeService({ |
| 3329 | 3360 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 3330 | 3361 | providers: [ |
| 3331 | fakeProvider("github", { | |
| 3362 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 3332 | 3363 | // The method is there; the capability that would let it be called is not. |
| 3333 | 3364 | setLabels: () => { |
| 3334 | 3365 | changed = true; |
| ... | ... | @@ -3360,8 +3391,11 @@ it.effect("refuses a label change this viewer may not make, and says what access |
| 3360 | 3391 | const service = yield* makeService({ |
| 3361 | 3392 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 3362 | 3393 | providers: [ |
| 3363 | fakeProvider("github", { | |
| 3364 | capabilities: { ...fakeProvider("github").capabilities, labels: true }, | |
| 3394 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 3395 | capabilities: { | |
| 3396 | ...fakeProvider(SourceControlProviderKind.make("github")).capabilities, | |
| 3397 | labels: true, | |
| 3398 | }, | |
| 3365 | 3399 | getViewerPermissions: () => |
| 3366 | 3400 | Effect.succeed({ |
| 3367 | 3401 | actions: [], |
| ... | ... | @@ -3405,8 +3439,11 @@ it.effect("hands a label change to the host, and reads the labels back for the m |
| 3405 | 3439 | const service = yield* makeService({ |
| 3406 | 3440 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 3407 | 3441 | providers: [ |
| 3408 | fakeProvider("github", { | |
| 3409 | capabilities: { ...fakeProvider("github").capabilities, labels: true }, | |
| 3442 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 3443 | capabilities: { | |
| 3444 | ...fakeProvider(SourceControlProviderKind.make("github")).capabilities, | |
| 3445 | labels: true, | |
| 3446 | }, | |
| 3410 | 3447 | listLabelCandidates: () => |
| 3411 | 3448 | Effect.succeed({ |
| 3412 | 3449 | candidates: [{ name: "bug", color: null, description: null, isApplied: false }], |
| ... | ... | @@ -3447,7 +3484,7 @@ it.effect("answers a repeated listing from cache, and concurrent readers share o |
| 3447 | 3484 | const service = yield* makeService({ |
| 3448 | 3485 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 3449 | 3486 | providers: [ |
| 3450 | fakeProvider("github", { | |
| 3487 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 3451 | 3488 | listChangeRequests: () => { |
| 3452 | 3489 | hostCalls += 1; |
| 3453 | 3490 | return Effect.succeed({ |
| ... | ... | @@ -3496,7 +3533,7 @@ it.effect.each(["list", "listStats"] as const)( |
| 3496 | 3533 | project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" }), |
| 3497 | 3534 | ], |
| 3498 | 3535 | providers: [ |
| 3499 | fakeProvider("github", { | |
| 3536 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 3500 | 3537 | listChangeRequests: () => |
| 3501 | 3538 | lookup.pipe( |
| 3502 | 3539 | Effect.as({ |
| ... | ... | @@ -3545,7 +3582,7 @@ it.effect("shares one cold viewer lookup across distinct concurrent lists", () = |
| 3545 | 3582 | const service = yield* makeService({ |
| 3546 | 3583 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 3547 | 3584 | providers: [ |
| 3548 | fakeProvider("github", { | |
| 3585 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 3549 | 3586 | getViewer: () => |
| 3550 | 3587 | Effect.sync(() => { |
| 3551 | 3588 | viewerCalls += 1; |
| ... | ... | @@ -3583,7 +3620,7 @@ it.effect("uses five host reads for the normal indexed-repository page workflow" |
| 3583 | 3620 | const service = yield* makeService({ |
| 3584 | 3621 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 3585 | 3622 | providers: [ |
| 3586 | fakeProvider("github", { | |
| 3623 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 3587 | 3624 | getViewer: () => |
| 3588 | 3625 | Effect.sync(() => { |
| 3589 | 3626 | viewerCalls += 1; |
| ... | ... | @@ -3643,7 +3680,7 @@ it.effect("returns the refreshed listing on the first read after its cache expir |
| 3643 | 3680 | const service = yield* makeService({ |
| 3644 | 3681 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 3645 | 3682 | providers: [ |
| 3646 | fakeProvider("github", { | |
| 3683 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 3647 | 3684 | listChangeRequests: () => { |
| 3648 | 3685 | hostCalls += 1; |
| 3649 | 3686 | return Effect.succeed({ |
| ... | ... | @@ -3682,7 +3719,7 @@ it.effect("a listing narrowed to some projects is its own cache entry", () => |
| 3682 | 3719 | project({ id: "p2", title: "docs", workspaceRoot: "/b", repository: "acme/docs" }), |
| 3683 | 3720 | ], |
| 3684 | 3721 | providers: [ |
| 3685 | fakeProvider("github", { | |
| 3722 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 3686 | 3723 | listChangeRequestsAcross: (input) => { |
| 3687 | 3724 | asked.push(input.repositories); |
| 3688 | 3725 | return Effect.succeed({ |
| ... | ... | @@ -3725,7 +3762,7 @@ it.effect( |
| 3725 | 3762 | project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" }), |
| 3726 | 3763 | ], |
| 3727 | 3764 | providers: [ |
| 3728 | fakeProvider("github", { | |
| 3765 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 3729 | 3766 | listChangeRequests: ({ filters }) => |
| 3730 | 3767 | Effect.gen(function* () { |
| 3731 | 3768 | reads += 1; |
| ... | ... | @@ -3782,7 +3819,7 @@ it.effect("keeps unrelated PRs warm after a mutation, explicit refresh, and proj |
| 3782 | 3819 | project({ id: "p2", title: "docs", workspaceRoot: "/b", repository: "acme/docs" }), |
| 3783 | 3820 | ], |
| 3784 | 3821 | providers: [ |
| 3785 | fakeProvider("github", { | |
| 3822 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 3786 | 3823 | getChangeRequest: (input) => |
| 3787 | 3824 | Effect.sync(() => { |
| 3788 | 3825 | calls.push(`${input.repository}/${input.number}`); |
| ... | ... | @@ -3827,7 +3864,7 @@ it.effect( |
| 3827 | 3864 | }), |
| 3828 | 3865 | ], |
| 3829 | 3866 | providers: [ |
| 3830 | fakeProvider("github", { | |
| 3867 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 3831 | 3868 | getChangeRequest: (input) => |
| 3832 | 3869 | Effect.sync(() => { |
| 3833 | 3870 | hosts.push(input.host); |
| ... | ... | @@ -3858,7 +3895,7 @@ it.effect("does not revive old summaries when project epochs are evicted", () => |
| 3858 | 3895 | const service = yield* makeService({ |
| 3859 | 3896 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 3860 | 3897 | providers: [ |
| 3861 | fakeProvider("github", { | |
| 3898 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 3862 | 3899 | getChangeRequest: () => Effect.succeed({ ...hostedChangeRequest("body"), title }), |
| 3863 | 3900 | }), |
| 3864 | 3901 | ], |
| ... | ... | @@ -3882,7 +3919,7 @@ it.effect("explicit and turn invalidations make the next listing ask the host ag |
| 3882 | 3919 | const service = yield* makeService({ |
| 3883 | 3920 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 3884 | 3921 | providers: [ |
| 3885 | fakeProvider("github", { | |
| 3922 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 3886 | 3923 | getViewer: () => { |
| 3887 | 3924 | viewerCalls += 1; |
| 3888 | 3925 | return Effect.succeed("bilal"); |
| ... | ... | @@ -3924,7 +3961,7 @@ it.effect("close and reopen notify subscribed readers after invalidating their c |
| 3924 | 3961 | project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" }), |
| 3925 | 3962 | ], |
| 3926 | 3963 | providers: [ |
| 3927 | fakeProvider("github", { | |
| 3964 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 3928 | 3965 | getChangeRequestSummary: () => |
| 3929 | 3966 | Effect.succeed({ ...changeRequest(1, "2026-09-16T00:00:00.000Z"), state }), |
| 3930 | 3967 | runAction: (input) => |
| ... | ... | @@ -3976,7 +4013,7 @@ it.effect("explicit invalidation refreshes origin readers after a routed host mu |
| 3976 | 4013 | project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" }), |
| 3977 | 4014 | ], |
| 3978 | 4015 | providers: [ |
| 3979 | fakeProvider("github", { | |
| 4016 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 3980 | 4017 | getChangeRequestSummary: () => |
| 3981 | 4018 | Effect.succeed({ ...changeRequest(1, "2026-09-16T00:00:00.000Z"), state }), |
| 3982 | 4019 | }), |
| ... | ... | @@ -4021,7 +4058,7 @@ it.effect("does not cache a failed listing", () => |
| 4021 | 4058 | const service = yield* makeService({ |
| 4022 | 4059 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 4023 | 4060 | providers: [ |
| 4024 | fakeProvider("github", { | |
| 4061 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 4025 | 4062 | // The viewer lookup is what fails the whole listing rather than one repository. |
| 4026 | 4063 | getViewer: () => { |
| 4027 | 4064 | hostCalls += 1; |
| ... | ... | @@ -4052,11 +4089,11 @@ it.effect("reads a host's repositories in one search, and files the rows back un |
| 4052 | 4089 | title: "on gitlab", |
| 4053 | 4090 | workspaceRoot: "/c", |
| 4054 | 4091 | repository: "group/project", |
| 4055 | provider: "gitlab", | |
| 4092 | provider: SourceControlProviderKind.make("gitlab"), | |
| 4056 | 4093 | }), |
| 4057 | 4094 | ], |
| 4058 | 4095 | providers: [ |
| 4059 | fakeProvider("github", { | |
| 4096 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 4060 | 4097 | listChangeRequests: ({ repository }) => { |
| 4061 | 4098 | separately.push(repository); |
| 4062 | 4099 | return Effect.succeed({ items: [], truncated: false, continues: true }); |
| ... | ... | @@ -4073,7 +4110,7 @@ it.effect("reads a host's repositories in one search, and files the rows back un |
| 4073 | 4110 | }, |
| 4074 | 4111 | }), |
| 4075 | 4112 | // A host with no search across repositories keeps being asked one at a time. |
| 4076 | fakeProvider("gitlab", { | |
| 4113 | fakeProvider(SourceControlProviderKind.make("gitlab"), { | |
| 4077 | 4114 | listChangeRequests: ({ repository }) => { |
| 4078 | 4115 | separately.push(repository); |
| 4079 | 4116 | return Effect.succeed({ |
| ... | ... | @@ -4111,7 +4148,7 @@ it.effect("carries every repository of a slice on from the oldest row in it", () |
| 4111 | 4148 | project({ id: "p3", title: "docs", workspaceRoot: "/c", repository: "acme/docs" }), |
| 4112 | 4149 | ], |
| 4113 | 4150 | providers: [ |
| 4114 | fakeProvider("github", { | |
| 4151 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 4115 | 4152 | listChangeRequestsAcross: () => |
| 4116 | 4153 | Effect.succeed({ |
| 4117 | 4154 | items: [ |
| ... | ... | @@ -4144,7 +4181,7 @@ it.effect("carries a slice on without sending the rows it already sent", () => |
| 4144 | 4181 | const service = yield* makeService({ |
| 4145 | 4182 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 4146 | 4183 | providers: [ |
| 4147 | fakeProvider("github", { | |
| 4184 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 4148 | 4185 | listChangeRequestsAcross: (input) => { |
| 4149 | 4186 | cursors.push(input.cursor); |
| 4150 | 4187 | return Effect.succeed({ |
| ... | ... | @@ -4189,7 +4226,7 @@ it.effect("reads a workspace larger than one search in chunks, and merges them", |
| 4189 | 4226 | }), |
| 4190 | 4227 | ), |
| 4191 | 4228 | providers: [ |
| 4192 | fakeProvider("github", { | |
| 4229 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 4193 | 4230 | listChangeRequestsAcross: (input) => { |
| 4194 | 4231 | asked.push(input.repositories.length); |
| 4195 | 4232 | return Effect.succeed({ |
| ... | ... | @@ -4218,7 +4255,7 @@ it.effect("asks on its own for a repository a search answered nothing for", () = |
| 4218 | 4255 | project({ id: "p2", title: "docs", workspaceRoot: "/b", repository: "acme/docs" }), |
| 4219 | 4256 | ], |
| 4220 | 4257 | providers: [ |
| 4221 | fakeProvider("github", { | |
| 4258 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 4222 | 4259 | listChangeRequests: ({ repository }) => { |
| 4223 | 4260 | separately.push(repository); |
| 4224 | 4261 | return repository === "acme/docs" |
| ... | ... | @@ -4262,7 +4299,7 @@ it.effect("reads the repositories one at a time when the search itself fails", ( |
| 4262 | 4299 | project({ id: "p2", title: "docs", workspaceRoot: "/b", repository: "acme/docs" }), |
| 4263 | 4300 | ], |
| 4264 | 4301 | providers: [ |
| 4265 | fakeProvider("github", { | |
| 4302 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 4266 | 4303 | listChangeRequests: ({ repository }) => { |
| 4267 | 4304 | separately.push(repository); |
| 4268 | 4305 | return Effect.succeed({ |
| ... | ... | @@ -4295,11 +4332,11 @@ it.effect("fills in the line counts for the rows it is given", () => |
| 4295 | 4332 | title: "on gitlab", |
| 4296 | 4333 | workspaceRoot: "/b", |
| 4297 | 4334 | repository: "group/project", |
| 4298 | provider: "gitlab", | |
| 4335 | provider: SourceControlProviderKind.make("gitlab"), | |
| 4299 | 4336 | }), |
| 4300 | 4337 | ], |
| 4301 | 4338 | providers: [ |
| 4302 | fakeProvider("github", { | |
| 4339 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 4303 | 4340 | listChangeRequestStats: (input) => { |
| 4304 | 4341 | asked.push(input.changeRequests); |
| 4305 | 4342 | return Effect.succeed([ |
| ... | ... | @@ -4308,7 +4345,7 @@ it.effect("fills in the line counts for the rows it is given", () => |
| 4308 | 4345 | }, |
| 4309 | 4346 | }), |
| 4310 | 4347 | // Its listing carries the counts already, so it has nothing to be asked. |
| 4311 | fakeProvider("gitlab"), | |
| 4348 | fakeProvider(SourceControlProviderKind.make("gitlab")), | |
| 4312 | 4349 | ], |
| 4313 | 4350 | }); |
| 4314 | 4351 | |
| ... | ... | @@ -4355,7 +4392,7 @@ it.effect( |
| 4355 | 4392 | project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" }), |
| 4356 | 4393 | ], |
| 4357 | 4394 | providers: [ |
| 4358 | fakeProvider("github", { | |
| 4395 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 4359 | 4396 | listChangeRequestStats: (input) => { |
| 4360 | 4397 | asked.push(input.changeRequests.map((ref) => ref.number)); |
| 4361 | 4398 | return Effect.succeed( |
| ... | ... | @@ -4404,7 +4441,7 @@ it.effect("reads the fresh diff when detail or summary discovers a changed revis |
| 4404 | 4441 | const service = yield* makeService({ |
| 4405 | 4442 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 4406 | 4443 | providers: [ |
| 4407 | fakeProvider("github", { | |
| 4444 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 4408 | 4445 | getChangeRequest: () => |
| 4409 | 4446 | Effect.sync(() => ({ ...hostedChangeRequest("body"), updatedAt: revision })), |
| 4410 | 4447 | getChangeRequestSummary: () => |
| ... | ... | @@ -4453,7 +4490,9 @@ it.effect("keeps the rows when the line counts cannot be read", () => |
| 4453 | 4490 | const service = yield* makeService({ |
| 4454 | 4491 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 4455 | 4492 | providers: [ |
| 4456 | fakeProvider("github", { listChangeRequestStats: () => Effect.fail(requestFailed) }), | |
| 4493 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 4494 | listChangeRequestStats: () => Effect.fail(requestFailed), | |
| 4495 | }), | |
| 4457 | 4496 | ], |
| 4458 | 4497 | }); |
| 4459 | 4498 | |
| ... | ... | @@ -4478,7 +4517,7 @@ it.effect( |
| 4478 | 4517 | project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" }), |
| 4479 | 4518 | ], |
| 4480 | 4519 | providers: [ |
| 4481 | fakeProvider("github", { | |
| 4520 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 4482 | 4521 | listChangeRequestStats: () => { |
| 4483 | 4522 | statsCalls += 1; |
| 4484 | 4523 | return Effect.succeed([]); |
| ... | ... | @@ -4556,7 +4595,7 @@ it.effect("shares linked summaries and reuses them for display without asking th |
| 4556 | 4595 | const service = yield* makeService({ |
| 4557 | 4596 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 4558 | 4597 | providers: [ |
| 4559 | fakeProvider("github", { | |
| 4598 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 4560 | 4599 | getChangeRequestSummary: () => |
| 4561 | 4600 | Effect.sync(() => { |
| 4562 | 4601 | calls += 1; |
| ... | ... | @@ -4567,7 +4606,7 @@ it.effect("shares linked summaries and reuses them for display without asking th |
| 4567 | 4606 | shouldFail |
| 4568 | 4607 | ? Effect.fail( |
| 4569 | 4608 | new PullRequestProviderError({ |
| 4570 | provider: "github", | |
| 4609 | provider: SourceControlProviderKind.make("github"), | |
| 4571 | 4610 | operation: "getChangeRequestSummary", |
| 4572 | 4611 | reason: "failed", |
| 4573 | 4612 | detail: "HTTP 504", |
| ... | ... | @@ -4622,8 +4661,11 @@ it.effect("keeps routed reads separate when the GitHub account changes", () => |
| 4622 | 4661 | project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" }), |
| 4623 | 4662 | ], |
| 4624 | 4663 | providers: [ |
| 4625 | fakeProvider("github", { | |
| 4626 | capabilities: { ...fakeProvider("github").capabilities, viewedFiles: "host" }, | |
| 4664 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 4665 | capabilities: { | |
| 4666 | ...fakeProvider(SourceControlProviderKind.make("github")).capabilities, | |
| 4667 | viewedFiles: "host", | |
| 4668 | }, | |
| 4627 | 4669 | getFilesViewed: () => |
| 4628 | 4670 | read().pipe( |
| 4629 | 4671 | Effect.as({ |
| ... | ... | @@ -4685,8 +4727,11 @@ it.effect("isolates routed caches for two credentials belonging to the same acco |
| 4685 | 4727 | project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" }), |
| 4686 | 4728 | ], |
| 4687 | 4729 | providers: [ |
| 4688 | fakeProvider("github", { | |
| 4689 | capabilities: { ...fakeProvider("github").capabilities, viewedFiles: "host" }, | |
| 4730 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 4731 | capabilities: { | |
| 4732 | ...fakeProvider(SourceControlProviderKind.make("github")).capabilities, | |
| 4733 | viewedFiles: "host", | |
| 4734 | }, | |
| 4690 | 4735 | getFilesViewed: () => |
| 4691 | 4736 | read().pipe( |
| 4692 | 4737 | Effect.as({ |
| ... | ... | @@ -4744,7 +4789,7 @@ it.effect("rejects mismatched routing credentials before use and preserves actio |
| 4744 | 4789 | const service = yield* makeService({ |
| 4745 | 4790 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 4746 | 4791 | providers: [ |
| 4747 | fakeProvider("github", { | |
| 4792 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 4748 | 4793 | getRoutingIdentity: () => Effect.succeed({ accountId: "101", viewer: "octocat" }), |
| 4749 | 4794 | withVerifiedCredential: (_, use) => |
| 4750 | 4795 | use({ |
| ... | ... | @@ -4798,7 +4843,7 @@ it.effect("answers a known pull request immediately while the host refreshes", ( |
| 4798 | 4843 | const service = yield* makeService({ |
| 4799 | 4844 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 4800 | 4845 | providers: [ |
| 4801 | fakeProvider("github", { | |
| 4846 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 4802 | 4847 | getChangeRequest: () => |
| 4803 | 4848 | Effect.gen(function* () { |
| 4804 | 4849 | calls += 1; |
| ... | ... | @@ -4829,7 +4874,7 @@ it.effect("does not ask the host again for a linked summary it already holds", ( |
| 4829 | 4874 | const service = yield* makeService({ |
| 4830 | 4875 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 4831 | 4876 | providers: [ |
| 4832 | fakeProvider("github", { | |
| 4877 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 4833 | 4878 | getChangeRequestSummary: () => |
| 4834 | 4879 | Effect.sync(() => { |
| 4835 | 4880 | calls += 1; |
| ... | ... | @@ -4858,7 +4903,7 @@ it.effect( |
| 4858 | 4903 | project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" }), |
| 4859 | 4904 | ], |
| 4860 | 4905 | providers: [ |
| 4861 | fakeProvider("github", { | |
| 4906 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 4862 | 4907 | getChangeRequestSummary: () => |
| 4863 | 4908 | Effect.succeed({ |
| 4864 | 4909 | ...changeRequest(1, "2026-07-02T00:00:00Z"), |
| ... | ... | @@ -4896,7 +4941,7 @@ it.effect("reuses an observed merged state for strict settlement reads", () => |
| 4896 | 4941 | const service = yield* makeService({ |
| 4897 | 4942 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 4898 | 4943 | providers: [ |
| 4899 | fakeProvider("github", { | |
| 4944 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 4900 | 4945 | getChangeRequest: () => |
| 4901 | 4946 | Effect.succeed({ |
| 4902 | 4947 | ...hostedChangeRequest("merged body", 4), |
| ... | ... | @@ -4927,7 +4972,7 @@ it.effect("announces state a detail read sees first or newly", () => |
| 4927 | 4972 | const service = yield* makeService({ |
| 4928 | 4973 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 4929 | 4974 | providers: [ |
| 4930 | fakeProvider("github", { | |
| 4975 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 4931 | 4976 | getChangeRequest: () => Effect.succeed({ ...hostedChangeRequest("body"), ...detail }), |
| 4932 | 4977 | getChangeRequestSummary: () => |
| 4933 | 4978 | Effect.succeed({ |
| ... | ... | @@ -4979,7 +5024,7 @@ it.effect("announces a reported state without trusting it", () => |
| 4979 | 5024 | const service = yield* makeService({ |
| 4980 | 5025 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 4981 | 5026 | providers: [ |
| 4982 | fakeProvider("github", { | |
| 5027 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 4983 | 5028 | getChangeRequest: () => Effect.succeed({ ...hostedChangeRequest("body"), ...detail }), |
| 4984 | 5029 | }), |
| 4985 | 5030 | ], |
| ... | ... | @@ -5024,7 +5069,7 @@ it.effect("does not let a stale detail reopen overwrite a fresher linked summary |
| 5024 | 5069 | const service = yield* makeService({ |
| 5025 | 5070 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 5026 | 5071 | providers: [ |
| 5027 | fakeProvider("github", { | |
| 5072 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 5028 | 5073 | getChangeRequest: () => |
| 5029 | 5074 | Effect.gen(function* () { |
| 5030 | 5075 | detailCalls += 1; |
| ... | ... | @@ -5078,7 +5123,7 @@ it.effect("does not let a still-cached detail overwrite a fresher linked summary |
| 5078 | 5123 | const service = yield* makeService({ |
| 5079 | 5124 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 5080 | 5125 | providers: [ |
| 5081 | fakeProvider("github", { | |
| 5126 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 5082 | 5127 | getChangeRequest: () => Effect.succeed(hostedChangeRequest("old body", 4)), |
| 5083 | 5128 | getChangeRequestSummary: () => |
| 5084 | 5129 | Effect.succeed({ |
| ... | ... | @@ -5114,11 +5159,11 @@ it.effect("tells the client when the host has no pull request under that number" |
| 5114 | 5159 | const service = yield* makeService({ |
| 5115 | 5160 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 5116 | 5161 | providers: [ |
| 5117 | fakeProvider("github", { | |
| 5162 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 5118 | 5163 | getChangeRequest: () => |
| 5119 | 5164 | Effect.fail( |
| 5120 | 5165 | new PullRequestProviderError({ |
| 5121 | provider: "github", | |
| 5166 | provider: SourceControlProviderKind.make("github"), | |
| 5122 | 5167 | operation: "getChangeRequest", |
| 5123 | 5168 | reason: "not-found", |
| 5124 | 5169 | detail: "Pull request not found. Check the PR number or URL and try again.", |
| ... | ... | @@ -5142,12 +5187,12 @@ it.effect("keeps recent detail on a transient refresh failure but not after inva |
| 5142 | 5187 | const service = yield* makeService({ |
| 5143 | 5188 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 5144 | 5189 | providers: [ |
| 5145 | fakeProvider("github", { | |
| 5190 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 5146 | 5191 | getChangeRequest: () => |
| 5147 | 5192 | failing |
| 5148 | 5193 | ? Effect.fail( |
| 5149 | 5194 | new PullRequestProviderError({ |
| 5150 | provider: "github", | |
| 5195 | provider: SourceControlProviderKind.make("github"), | |
| 5151 | 5196 | operation: "getChangeRequest", |
| 5152 | 5197 | reason: "failed", |
| 5153 | 5198 | detail: "spawn gh EAGAIN", |
| ... | ... | @@ -5197,7 +5242,7 @@ it.effect("carries an armed auto-merge through to the detail, and silence as sil |
| 5197 | 5242 | project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" }), |
| 5198 | 5243 | ], |
| 5199 | 5244 | providers: [ |
| 5200 | fakeProvider("github", { | |
| 5245 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 5201 | 5246 | getChangeRequest: () => |
| 5202 | 5247 | Effect.succeed({ |
| 5203 | 5248 | ...changeRequest(1, "2026-07-02T00:00:00Z"), |
| ... | ... | @@ -5243,13 +5288,13 @@ it.effect("narrows the rows of a host that ignored the filters it was handed", ( |
| 5243 | 5288 | title: "web", |
| 5244 | 5289 | workspaceRoot: "/a", |
| 5245 | 5290 | repository: "acme/web", |
| 5246 | provider: "gitlab", | |
| 5291 | provider: SourceControlProviderKind.make("gitlab"), | |
| 5247 | 5292 | }), |
| 5248 | 5293 | ], |
| 5249 | 5294 | providers: [ |
| 5250 | 5295 | // Only GitHub narrows a listing for itself; every other host answers unnarrowed, and |
| 5251 | 5296 | // sending it a draft filter it quietly ignores used to put drafts on a filtered page. |
| 5252 | fakeProvider("gitlab", { | |
| 5297 | fakeProvider(SourceControlProviderKind.make("gitlab"), { | |
| 5253 | 5298 | listChangeRequests: () => |
| 5254 | 5299 | Effect.succeed({ |
| 5255 | 5300 | items: [ |
| ... | ... | @@ -5285,11 +5330,11 @@ it.effect("keeps a row of a host that ignored the filters if any name of a label |
| 5285 | 5330 | title: "web", |
| 5286 | 5331 | workspaceRoot: "/a", |
| 5287 | 5332 | repository: "acme/web", |
| 5288 | provider: "gitlab", | |
| 5333 | provider: SourceControlProviderKind.make("gitlab"), | |
| 5289 | 5334 | }), |
| 5290 | 5335 | ], |
| 5291 | 5336 | providers: [ |
| 5292 | fakeProvider("gitlab", { | |
| 5337 | fakeProvider(SourceControlProviderKind.make("gitlab"), { | |
| 5293 | 5338 | listChangeRequests: () => |
| 5294 | 5339 | Effect.succeed({ |
| 5295 | 5340 | items: [ |
| ... | ... | @@ -5328,13 +5373,13 @@ it.effect('resolves an author filter of "me" to the viewer before narrowing a ho |
| 5328 | 5373 | title: "web", |
| 5329 | 5374 | workspaceRoot: "/a", |
| 5330 | 5375 | repository: "acme/web", |
| 5331 | provider: "gitlab", | |
| 5376 | provider: SourceControlProviderKind.make("gitlab"), | |
| 5332 | 5377 | }), |
| 5333 | 5378 | ], |
| 5334 | 5379 | providers: [ |
| 5335 | 5380 | // Only GitHub narrows a listing for itself, so this fixture's "me" has to be resolved |
| 5336 | 5381 | // locally too — the same helper both call sites lean on. |
| 5337 | fakeProvider("gitlab", { | |
| 5382 | fakeProvider(SourceControlProviderKind.make("gitlab"), { | |
| 5338 | 5383 | listChangeRequests: () => |
| 5339 | 5384 | Effect.succeed({ |
| 5340 | 5385 | items: [ |
| ... | ... | @@ -5395,7 +5440,7 @@ it.effect.each([false, true])( |
| 5395 | 5440 | }), |
| 5396 | 5441 | ], |
| 5397 | 5442 | providers: [ |
| 5398 | fakeProvider("github", { | |
| 5443 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 5399 | 5444 | capabilities, |
| 5400 | 5445 | getViewerPermissions: () => |
| 5401 | 5446 | Effect.succeed({ |
| ... | ... | @@ -5463,7 +5508,7 @@ it.effect("refuses a way of updating a branch that the host or the viewer does n |
| 5463 | 5508 | const service = yield* makeService({ |
| 5464 | 5509 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 5465 | 5510 | providers: [ |
| 5466 | fakeProvider("github", { | |
| 5511 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 5467 | 5512 | capabilities: { |
| 5468 | 5513 | diff: true, |
| 5469 | 5514 | comment: true, |
| ... | ... | @@ -5516,11 +5561,11 @@ it.effect("refuses to merge a target branch into a source branch on a host that |
| 5516 | 5561 | title: "on gitlab", |
| 5517 | 5562 | workspaceRoot: "/a", |
| 5518 | 5563 | repository: "group/project", |
| 5519 | provider: "gitlab", | |
| 5564 | provider: SourceControlProviderKind.make("gitlab"), | |
| 5520 | 5565 | }), |
| 5521 | 5566 | ], |
| 5522 | 5567 | providers: [ |
| 5523 | fakeProvider("gitlab", { | |
| 5568 | fakeProvider(SourceControlProviderKind.make("gitlab"), { | |
| 5524 | 5569 | capabilities: { |
| 5525 | 5570 | diff: true, |
| 5526 | 5571 | comment: true, |
| ... | ... | @@ -5575,12 +5620,12 @@ it.effect("judges the review filter only on a host that summarises its reviews", |
| 5575 | 5620 | title: "on gitlab", |
| 5576 | 5621 | workspaceRoot: "/b", |
| 5577 | 5622 | repository: "group/project", |
| 5578 | provider: "gitlab", | |
| 5623 | provider: SourceControlProviderKind.make("gitlab"), | |
| 5579 | 5624 | }), |
| 5580 | 5625 | ], |
| 5581 | 5626 | providers: [ |
| 5582 | 5627 | // GitHub answers with the field on every row: null is "nobody has decided yet". |
| 5583 | fakeProvider("github", { | |
| 5628 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 5584 | 5629 | listChangeRequests: () => |
| 5585 | 5630 | Effect.succeed({ |
| 5586 | 5631 | items: [ |
| ... | ... | @@ -5595,7 +5640,7 @@ it.effect("judges the review filter only on a host that summarises its reviews", |
| 5595 | 5640 | }), |
| 5596 | 5641 | }), |
| 5597 | 5642 | // GitLab never supplies the field, so its rows are not the filter's to judge. |
| 5598 | fakeProvider("gitlab", { | |
| 5643 | fakeProvider(SourceControlProviderKind.make("gitlab"), { | |
| 5599 | 5644 | listChangeRequests: () => |
| 5600 | 5645 | Effect.succeed({ |
| 5601 | 5646 | items: [changeRequest(3, "2026-07-02T00:00:00Z")], |
| ... | ... | @@ -5621,7 +5666,7 @@ it.effect("sends only the words a rewrite carries", () => |
| 5621 | 5666 | const service = yield* makeService({ |
| 5622 | 5667 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 5623 | 5668 | providers: [ |
| 5624 | fakeProvider("github", { | |
| 5669 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 5625 | 5670 | updateChangeRequest: (input) => { |
| 5626 | 5671 | received.push({ title: input.title, body: input.body }); |
| 5627 | 5672 | return Effect.void; |
| ... | ... | @@ -5647,7 +5692,9 @@ it.effect("refuses a rewrite that changes nothing, before any call is made", () |
| 5647 | 5692 | const service = yield* makeService({ |
| 5648 | 5693 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 5649 | 5694 | providers: [ |
| 5650 | fakeProvider("github", { updateChangeRequest: () => Effect.die("must not be called") }), | |
| 5695 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 5696 | updateChangeRequest: () => Effect.die("must not be called"), | |
| 5697 | }), | |
| 5651 | 5698 | ], |
| 5652 | 5699 | }); |
| 5653 | 5700 | |
| ... | ... | @@ -5665,7 +5712,7 @@ it.effect("refuses to rewrite anything on a host that never claimed it", () => |
| 5665 | 5712 | const service = yield* makeService({ |
| 5666 | 5713 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 5667 | 5714 | providers: [ |
| 5668 | fakeProvider("github", { | |
| 5715 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 5669 | 5716 | capabilities: { |
| 5670 | 5717 | diff: true, |
| 5671 | 5718 | comment: true, |
| ... | ... | @@ -5704,7 +5751,7 @@ it.effect("passes a rewritten remark through with the id and kind it arrived und |
| 5704 | 5751 | const service = yield* makeService({ |
| 5705 | 5752 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 5706 | 5753 | providers: [ |
| 5707 | fakeProvider("github", { | |
| 5754 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 5708 | 5755 | updateComment: (input) => { |
| 5709 | 5756 | received = { id: input.commentId, kind: input.kind, body: input.body }; |
| 5710 | 5757 | return Effect.void; |
| ... | ... | @@ -5735,7 +5782,9 @@ it.effect("refuses a remark rewritten into nothing but whitespace", () => |
| 5735 | 5782 | const service = yield* makeService({ |
| 5736 | 5783 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 5737 | 5784 | providers: [ |
| 5738 | fakeProvider("github", { updateComment: () => Effect.die("must not be called") }), | |
| 5785 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 5786 | updateComment: () => Effect.die("must not be called"), | |
| 5787 | }), | |
| 5739 | 5788 | ], |
| 5740 | 5789 | }); |
| 5741 | 5790 | |
| ... | ... | @@ -5761,7 +5810,7 @@ it.effect("forgets the cached detail after a rewrite or terminal turn", () => |
| 5761 | 5810 | const service = yield* makeService({ |
| 5762 | 5811 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 5763 | 5812 | providers: [ |
| 5764 | fakeProvider("github", { | |
| 5813 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 5765 | 5814 | getChangeRequest: () => { |
| 5766 | 5815 | coreCalls += 1; |
| 5767 | 5816 | return Effect.succeed({ |
| ... | ... | @@ -5813,7 +5862,7 @@ it.effect("names the signed-in account in the detail, and says nothing where the |
| 5813 | 5862 | number: 1, |
| 5814 | 5863 | }); |
| 5815 | 5864 | }); |
| 5816 | const readable = fakeProvider("github", { | |
| 5865 | const readable = fakeProvider(SourceControlProviderKind.make("github"), { | |
| 5817 | 5866 | getChangeRequest: () => |
| 5818 | 5867 | Effect.succeed({ |
| 5819 | 5868 | ...changeRequest(1, "2026-07-02T00:00:00Z"), |
| ... | ... | @@ -5837,7 +5886,8 @@ it.effect("names the signed-in account in the detail, and says nothing where the |
| 5837 | 5886 | const named = yield* detailFrom(readable); |
| 5838 | 5887 | const unnamed = yield* detailFrom({ |
| 5839 | 5888 | ...readable, |
| 5840 | getViewer: () => Effect.fail(unusable("github", "unauthenticated")), | |
| 5889 | getViewer: () => | |
| 5890 | Effect.fail(unusable(SourceControlProviderKind.make("github"), "unauthenticated")), | |
| 5841 | 5891 | }); |
| 5842 | 5892 | |
| 5843 | 5893 | assert.strictEqual(named.viewer, "bilal"); |
| ... | ... | @@ -5855,7 +5905,7 @@ it.effect("keeps the diff cached across a file being ticked off", () => |
| 5855 | 5905 | project({ id: "p1", title: "t3code", workspaceRoot: "/a", repository: "pingdotgg/t3code" }), |
| 5856 | 5906 | ], |
| 5857 | 5907 | providers: [ |
| 5858 | fakeProvider("github", { | |
| 5908 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 5859 | 5909 | capabilities: { |
| 5860 | 5910 | diff: true, |
| 5861 | 5911 | comment: true, |
| ... | ... | @@ -5912,7 +5962,7 @@ it.effect("returns large diff slices intact without retaining them in either cac |
| 5912 | 5962 | const service = yield* makeService({ |
| 5913 | 5963 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 5914 | 5964 | providers: [ |
| 5915 | fakeProvider("github", { | |
| 5965 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 5916 | 5966 | getDiff: () => |
| 5917 | 5967 | Effect.sync(() => { |
| 5918 | 5968 | reads += 1; |
| ... | ... | @@ -5950,7 +6000,7 @@ it.effect("caches a small replacement after releasing a large diff", () => |
| 5950 | 6000 | const service = yield* makeService({ |
| 5951 | 6001 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 5952 | 6002 | providers: [ |
| 5953 | fakeProvider("github", { | |
| 6003 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 5954 | 6004 | getDiff: () => |
| 5955 | 6005 | Effect.sync(() => { |
| 5956 | 6006 | reads += 1; |
| ... | ... | @@ -5976,7 +6026,7 @@ const environmentViewedProvider = ( |
| 5976 | 6026 | asked: Array<ReadonlyArray<string>>, |
| 5977 | 6027 | unreadable: ReadonlySet<string> = new Set(), |
| 5978 | 6028 | ) => |
| 5979 | fakeProvider("gitlab", { | |
| 6029 | fakeProvider(SourceControlProviderKind.make("gitlab"), { | |
| 5980 | 6030 | capabilities: { |
| 5981 | 6031 | diff: true, |
| 5982 | 6032 | comment: true, |
| ... | ... | @@ -6018,7 +6068,7 @@ const environmentViewedService = ( |
| 6018 | 6068 | title: "on gitlab", |
| 6019 | 6069 | workspaceRoot: "/a", |
| 6020 | 6070 | repository: "group/project", |
| 6021 | provider: "gitlab", | |
| 6071 | provider: SourceControlProviderKind.make("gitlab"), | |
| 6022 | 6072 | }), |
| 6023 | 6073 | ], |
| 6024 | 6074 | providers: [environmentViewedProvider(revisions, asked, unreadable)], |
| ... | ... | @@ -6083,7 +6133,7 @@ it.effect("tracks Forgejo viewed files through its diff and refuses truncated ba |
| 6083 | 6133 | title: "on forgejo", |
| 6084 | 6134 | workspaceRoot: "/forgejo", |
| 6085 | 6135 | repository: "reviewer/project", |
| 6086 | provider: "forgejo", | |
| 6136 | provider: SourceControlProviderKind.make("forgejo"), | |
| 6087 | 6137 | host: "forge.example:3000", |
| 6088 | 6138 | remoteUrl: "http://forge.example:3000/reviewer/project.git", |
| 6089 | 6139 | }), |
| ... | ... | @@ -6145,7 +6195,7 @@ it.effect("keeps hosted Forgejo marks with their repository instead of the servi |
| 6145 | 6195 | title: "first repository", |
| 6146 | 6196 | workspaceRoot: "/first", |
| 6147 | 6197 | repository: "reviewer/first", |
| 6148 | provider: "forgejo", | |
| 6198 | provider: SourceControlProviderKind.make("forgejo"), | |
| 6149 | 6199 | host: "forge.example", |
| 6150 | 6200 | remoteUrl: "https://forge.example:3000/reviewer/first.git", |
| 6151 | 6201 | }), |
| ... | ... | @@ -6153,13 +6203,20 @@ it.effect("keeps hosted Forgejo marks with their repository instead of the servi |
| 6153 | 6203 | const service = yield* makeService({ |
| 6154 | 6204 | projects, |
| 6155 | 6205 | providers: [ |
| 6156 | { ...environmentViewedProvider(new Map([["same.ts", "blob"]]), []), kind: "forgejo" }, | |
| 6206 | { | |
| 6207 | ...environmentViewedProvider(new Map([["same.ts", "blob"]]), []), | |
| 6208 | kind: SourceControlProviderKind.make("forgejo"), | |
| 6209 | }, | |
| 6157 | 6210 | ], |
| 6158 | 6211 | resolveHandle: ({ context }) => |
| 6159 | 6212 | Effect.succeed({ |
| 6160 | 6213 | context: { |
| 6161 | 6214 | ...context!, |
| 6162 | provider: { kind: "forgejo", name: "Forgejo", baseUrl: "https://forge.example:3000" }, | |
| 6215 | provider: { | |
| 6216 | kind: SourceControlProviderKind.make("forgejo"), | |
| 6217 | name: "Forgejo", | |
| 6218 | baseUrl: "https://forge.example:3000", | |
| 6219 | }, | |
| 6163 | 6220 | }, |
| 6164 | 6221 | provider: undefined as never, |
| 6165 | 6222 | }), |
| ... | ... | @@ -6186,7 +6243,7 @@ it.effect("keeps hosted Forgejo marks with their repository instead of the servi |
| 6186 | 6243 | title: "second repository", |
| 6187 | 6244 | workspaceRoot: "/second", |
| 6188 | 6245 | repository: "reviewer/second", |
| 6189 | provider: "forgejo", | |
| 6246 | provider: SourceControlProviderKind.make("forgejo"), | |
| 6190 | 6247 | host: "forge.example", |
| 6191 | 6248 | remoteUrl: "https://forge.example:3000/reviewer/second.git", |
| 6192 | 6249 | }), |
| ... | ... | @@ -6201,7 +6258,7 @@ it.effect("keeps hosted Forgejo marks with their repository instead of the servi |
| 6201 | 6258 | title: "second repository over SSH", |
| 6202 | 6259 | workspaceRoot: "/ssh", |
| 6203 | 6260 | repository: "reviewer/second", |
| 6204 | provider: "forgejo", | |
| 6261 | provider: SourceControlProviderKind.make("forgejo"), | |
| 6205 | 6262 | host: "ssh.forge.example", |
| 6206 | 6263 | remoteUrl: "git@ssh.forge.example:reviewer/second.git", |
| 6207 | 6264 | }), |
| ... | ... | @@ -6269,7 +6326,7 @@ it.effect("holds what a whole-change answer carried, so the next tick reads noth |
| 6269 | 6326 | title: "on gitlab", |
| 6270 | 6327 | workspaceRoot: "/a", |
| 6271 | 6328 | repository: "group/project", |
| 6272 | provider: "gitlab", | |
| 6329 | provider: SourceControlProviderKind.make("gitlab"), | |
| 6273 | 6330 | }), |
| 6274 | 6331 | ], |
| 6275 | 6332 | providers: [ |
| ... | ... | @@ -6643,11 +6700,11 @@ it.effect("still reports its own marks when the host will not say what the head |
| 6643 | 6700 | title: "on gitlab", |
| 6644 | 6701 | workspaceRoot: "/a", |
| 6645 | 6702 | repository: "group/project", |
| 6646 | provider: "gitlab", | |
| 6703 | provider: SourceControlProviderKind.make("gitlab"), | |
| 6647 | 6704 | }), |
| 6648 | 6705 | ], |
| 6649 | 6706 | providers: [ |
| 6650 | fakeProvider("gitlab", { | |
| 6707 | fakeProvider(SourceControlProviderKind.make("gitlab"), { | |
| 6651 | 6708 | capabilities: { |
| 6652 | 6709 | diff: true, |
| 6653 | 6710 | comment: true, |
| ... | ... | @@ -6699,11 +6756,11 @@ it.effect("finishes two presses on one file in the order they were made", () => |
| 6699 | 6756 | title: "on gitlab", |
| 6700 | 6757 | workspaceRoot: "/a", |
| 6701 | 6758 | repository: "group/project", |
| 6702 | provider: "gitlab", | |
| 6759 | provider: SourceControlProviderKind.make("gitlab"), | |
| 6703 | 6760 | }), |
| 6704 | 6761 | ], |
| 6705 | 6762 | providers: [ |
| 6706 | fakeProvider("gitlab", { | |
| 6763 | fakeProvider(SourceControlProviderKind.make("gitlab"), { | |
| 6707 | 6764 | capabilities: { |
| 6708 | 6765 | diff: true, |
| 6709 | 6766 | comment: true, |
| ... | ... | @@ -6755,7 +6812,7 @@ const azureViewedService = (projects: ReadonlyArray<OrchestrationProjectShell>) |
| 6755 | 6812 | makeService({ |
| 6756 | 6813 | projects, |
| 6757 | 6814 | providers: [ |
| 6758 | fakeProvider("azure-devops", { | |
| 6815 | fakeProvider(SourceControlProviderKind.make("azure-devops"), { | |
| 6759 | 6816 | capabilities: { |
| 6760 | 6817 | diff: true, |
| 6761 | 6818 | comment: true, |
| ... | ... | @@ -6781,7 +6838,7 @@ const AZURE_PAIR = [ |
| 6781 | 6838 | title: "platform web", |
| 6782 | 6839 | workspaceRoot: "/a", |
| 6783 | 6840 | repository: "acme/platform/_git/web", |
| 6784 | provider: "azure-devops", | |
| 6841 | provider: SourceControlProviderKind.make("azure-devops"), | |
| 6785 | 6842 | host: "dev.azure.com", |
| 6786 | 6843 | }), |
| 6787 | 6844 | project({ |
| ... | ... | @@ -6789,7 +6846,7 @@ const AZURE_PAIR = [ |
| 6789 | 6846 | title: "other web", |
| 6790 | 6847 | workspaceRoot: "/b", |
| 6791 | 6848 | repository: "acme/other/_git/web", |
| 6792 | provider: "azure-devops", | |
| 6849 | provider: SourceControlProviderKind.make("azure-devops"), | |
| 6793 | 6850 | host: "dev.azure.com", |
| 6794 | 6851 | }), |
| 6795 | 6852 | ]; |
| ... | ... | @@ -6880,7 +6937,7 @@ it.effect("keeps the marked paths when a whole-change answer is wider than the c |
| 6880 | 6937 | title: "on gitlab", |
| 6881 | 6938 | workspaceRoot: "/a", |
| 6882 | 6939 | repository: "group/project", |
| 6883 | provider: "gitlab", | |
| 6940 | provider: SourceControlProviderKind.make("gitlab"), | |
| 6884 | 6941 | }), |
| 6885 | 6942 | ], |
| 6886 | 6943 | providers: [ |
| ... | ... | @@ -6949,7 +7006,7 @@ const environmentViewedServiceWithViewer = ( |
| 6949 | 7006 | title: "on gitlab", |
| 6950 | 7007 | workspaceRoot: "/a", |
| 6951 | 7008 | repository: "group/project", |
| 6952 | provider: "gitlab", | |
| 7009 | provider: SourceControlProviderKind.make("gitlab"), | |
| 6953 | 7010 | }), |
| 6954 | 7011 | ], |
| 6955 | 7012 | providers: [{ ...environmentViewedProvider(revisions, []), getViewer }], |
| ... | ... | @@ -6983,7 +7040,7 @@ it.effect("puts a listing and a press for one host on a single viewer lookup", ( |
| 6983 | 7040 | title: "on gitlab", |
| 6984 | 7041 | workspaceRoot: "/a", |
| 6985 | 7042 | repository: "group/project", |
| 6986 | provider: "gitlab", | |
| 7043 | provider: SourceControlProviderKind.make("gitlab"), | |
| 6987 | 7044 | }), |
| 6988 | 7045 | ], |
| 6989 | 7046 | providers: [ |
| ... | ... | @@ -7048,7 +7105,7 @@ it.effect("records a press while the host is backing off", () => |
| 7048 | 7105 | title: "on gitlab", |
| 7049 | 7106 | workspaceRoot: "/a", |
| 7050 | 7107 | repository: "group/project", |
| 7051 | provider: "gitlab", | |
| 7108 | provider: SourceControlProviderKind.make("gitlab"), | |
| 7052 | 7109 | }), |
| 7053 | 7110 | ], |
| 7054 | 7111 | providers: [ |
| ... | ... | @@ -7064,7 +7121,7 @@ it.effect("records a press while the host is backing off", () => |
| 7064 | 7121 | getFileRevisions: () => |
| 7065 | 7122 | Effect.fail( |
| 7066 | 7123 | new PullRequestProviderError({ |
| 7067 | provider: "gitlab", | |
| 7124 | provider: SourceControlProviderKind.make("gitlab"), | |
| 7068 | 7125 | operation: "getFileRevisions", |
| 7069 | 7126 | reason: "rate-limited", |
| 7070 | 7127 | detail: "API rate limit exceeded.", |
| ... | ... | @@ -7111,7 +7168,7 @@ it.effect("asks who is reading through a pause only for the press that is waitin |
| 7111 | 7168 | title: "on gitlab", |
| 7112 | 7169 | workspaceRoot: "/a", |
| 7113 | 7170 | repository: "group/project", |
| 7114 | provider: "gitlab", | |
| 7171 | provider: SourceControlProviderKind.make("gitlab"), | |
| 7115 | 7172 | }), |
| 7116 | 7173 | ], |
| 7117 | 7174 | providers: [ |
| ... | ... | @@ -7129,7 +7186,7 @@ it.effect("asks who is reading through a pause only for the press that is waitin |
| 7129 | 7186 | getFileRevisions: () => |
| 7130 | 7187 | Effect.fail( |
| 7131 | 7188 | new PullRequestProviderError({ |
| 7132 | provider: "gitlab", | |
| 7189 | provider: SourceControlProviderKind.make("gitlab"), | |
| 7133 | 7190 | operation: "getFileRevisions", |
| 7134 | 7191 | reason: "rate-limited", |
| 7135 | 7192 | detail: "API rate limit exceeded.", |
| ... | ... | @@ -7174,7 +7231,7 @@ it.effect("does not ask a paused host who is reading again after the ask failed" |
| 7174 | 7231 | title: "on gitlab", |
| 7175 | 7232 | workspaceRoot: "/a", |
| 7176 | 7233 | repository: "group/project", |
| 7177 | provider: "gitlab", | |
| 7234 | provider: SourceControlProviderKind.make("gitlab"), | |
| 7178 | 7235 | }), |
| 7179 | 7236 | ], |
| 7180 | 7237 | providers: [ |
| ... | ... | @@ -7185,7 +7242,7 @@ it.effect("does not ask a paused host who is reading again after the ask failed" |
| 7185 | 7242 | viewerLookups += 1; |
| 7186 | 7243 | return Effect.fail( |
| 7187 | 7244 | new PullRequestProviderError({ |
| 7188 | provider: "gitlab", | |
| 7245 | provider: SourceControlProviderKind.make("gitlab"), | |
| 7189 | 7246 | operation: "getViewer", |
| 7190 | 7247 | reason: "failed", |
| 7191 | 7248 | detail: "glab exited with status 1", |
| ... | ... | @@ -7197,7 +7254,7 @@ it.effect("does not ask a paused host who is reading again after the ask failed" |
| 7197 | 7254 | runAction: () => |
| 7198 | 7255 | Effect.fail( |
| 7199 | 7256 | new PullRequestProviderError({ |
| 7200 | provider: "gitlab", | |
| 7257 | provider: SourceControlProviderKind.make("gitlab"), | |
| 7201 | 7258 | operation: "runAction", |
| 7202 | 7259 | reason: "rate-limited", |
| 7203 | 7260 | detail: "API rate limit exceeded.", |
| ... | ... | @@ -7231,7 +7288,7 @@ it.effect("refuses the marks when the host could not be asked who is reading", ( |
| 7231 | 7288 | ? Effect.succeed("bilal") |
| 7232 | 7289 | : Effect.fail( |
| 7233 | 7290 | new PullRequestProviderError({ |
| 7234 | provider: "gitlab", | |
| 7291 | provider: SourceControlProviderKind.make("gitlab"), | |
| 7235 | 7292 | operation: "getViewer", |
| 7236 | 7293 | reason: "failed", |
| 7237 | 7294 | detail: "glab exited with status 1", |
| ... | ... | @@ -7276,11 +7333,11 @@ it.effect("refuses to track viewed files on a host that does not", () => |
| 7276 | 7333 | title: "on gitlab", |
| 7277 | 7334 | workspaceRoot: "/a", |
| 7278 | 7335 | repository: "group/project", |
| 7279 | provider: "gitlab", | |
| 7336 | provider: SourceControlProviderKind.make("gitlab"), | |
| 7280 | 7337 | }), |
| 7281 | 7338 | ], |
| 7282 | 7339 | providers: [ |
| 7283 | fakeProvider("gitlab", { | |
| 7340 | fakeProvider(SourceControlProviderKind.make("gitlab"), { | |
| 7284 | 7341 | getFilesViewed: () => Effect.die("must not be called"), |
| 7285 | 7342 | setFilesViewed: () => Effect.die("must not be called"), |
| 7286 | 7343 | }), |
| ... | ... | @@ -7308,12 +7365,12 @@ it.effect("keeps Azure continuation cursors separate for repositories with the s |
| 7308 | 7365 | title: organization, |
| 7309 | 7366 | workspaceRoot: `/${organization}`, |
| 7310 | 7367 | repository: `${organization}/project/_git/web`, |
| 7311 | provider: "azure-devops", | |
| 7368 | provider: SourceControlProviderKind.make("azure-devops"), | |
| 7312 | 7369 | host: "dev.azure.com", |
| 7313 | 7370 | }), |
| 7314 | 7371 | ), |
| 7315 | 7372 | providers: [ |
| 7316 | fakeProvider("azure-devops", { | |
| 7373 | fakeProvider(SourceControlProviderKind.make("azure-devops"), { | |
| 7317 | 7374 | listChangeRequests: (input) => |
| 7318 | 7375 | Effect.sync(() => { |
| 7319 | 7376 | seen.push(input.cwd); |
| ... | ... | @@ -7363,9 +7420,9 @@ it.effect.each([ |
| 7363 | 7420 | }, |
| 7364 | 7421 | }, |
| 7365 | 7422 | providers: [ |
| 7366 | fakeProvider("github", { | |
| 7423 | fakeProvider(SourceControlProviderKind.make("github"), { | |
| 7367 | 7424 | capabilities: { |
| 7368 | ...fakeProvider("github").capabilities, | |
| 7425 | ...fakeProvider(SourceControlProviderKind.make("github")).capabilities, | |
| 7369 | 7426 | actions: ["merge", "enable-auto-merge"], |
| 7370 | 7427 | }, |
| 7371 | 7428 | getChangeRequestSummary: () => Effect.succeed(changeRequest(1, "2026-07-02T00:00:00Z")), |
apps/server/src/pullRequest/PullRequestService.ts+7-4| ... | ... | @@ -72,7 +72,7 @@ import { |
| 72 | 72 | type PullRequestThreadCommentsResult, |
| 73 | 73 | type PullRequestUpdateInput, |
| 74 | 74 | type SourceControlProviderInfo, |
| 75 | type SourceControlProviderKind, | |
| 75 | SourceControlProviderKind, | |
| 76 | 76 | type ThreadPullRequestKey, |
| 77 | 77 | } from "@t3tools/contracts"; |
| 78 | 78 | import { resolveProjectSettings } from "@t3tools/shared/projectSettings"; |
| ... | ... | @@ -736,14 +736,15 @@ export const make = Effect.gen(function* () { |
| 736 | 736 | sourceControlRepositorySelector(project.repositoryIdentity) === null |
| 737 | 737 | ) |
| 738 | 738 | continue; |
| 739 | const host = pullRequestHostOf(identity, "unknown"); | |
| 739 | const host = pullRequestHostOf(identity, SourceControlProviderKind.make("unknown")); | |
| 740 | 740 | // A legacy identity has no canonical host until its provider is refined, so it must reach |
| 741 | 741 | // the refinement before a host filter can decide whether it belongs in the result. |
| 742 | 742 | if ( |
| 743 | 743 | filter.host !== undefined && |
| 744 | 744 | host !== "unknown" && |
| 745 | 745 | host !== filter.host.toLowerCase() && |
| 746 | pullRequestHostOf(identity, "forgejo") !== filter.host.toLowerCase() && | |
| 746 | pullRequestHostOf(identity, SourceControlProviderKind.make("forgejo")) !== | |
| 747 | filter.host.toLowerCase() && | |
| 747 | 748 | !isSshRemoteUrl(identity.locator.remoteUrl) |
| 748 | 749 | ) { |
| 749 | 750 | continue; |
| ... | ... | @@ -768,7 +769,9 @@ export const make = Effect.gen(function* () { |
| 768 | 769 | cwd: project.workspaceRoot, |
| 769 | 770 | context: { |
| 770 | 771 | provider: |
| 771 | provider.kind === "forgejo" ? { ...provider, kind: "unknown" } : provider, | |
| 772 | provider.kind === "forgejo" | |
| 773 | ? { ...provider, kind: SourceControlProviderKind.make("unknown") } | |
| 774 | : provider, | |
| 772 | 775 | remoteName, |
| 773 | 776 | remoteUrl, |
| 774 | 777 | ...(filter.host !== undefined && isSshRemoteUrl(remoteUrl) |
apps/server/src/pullRequest/pullRequestSyncKey.ts+3-2| ... | ... | @@ -2,7 +2,7 @@ import { |
| 2 | 2 | pullRequestHostOf, |
| 3 | 3 | type PullRequestRef, |
| 4 | 4 | type RepositoryIdentity, |
| 5 | type SourceControlProviderKind, | |
| 5 | SourceControlProviderKind, | |
| 6 | 6 | type ThreadPullRequestKey, |
| 7 | 7 | } from "@t3tools/contracts"; |
| 8 | 8 | import { sourceControlRepositorySelector } from "@t3tools/shared/sourceControl"; |
| ... | ... | @@ -18,7 +18,8 @@ export function pullRequestSyncKey( |
| 18 | 18 | reference.repository.toLowerCase() !== |
| 19 | 19 | sourceControlRepositorySelector(identity)?.toLowerCase() || |
| 20 | 20 | (reference.host !== undefined && |
| 21 | reference.host.toLowerCase() !== pullRequestHostOf(identity, "azure-devops")) | |
| 21 | reference.host.toLowerCase() !== | |
| 22 | pullRequestHostOf(identity, SourceControlProviderKind.make("azure-devops"))) | |
| 22 | 23 | ) |
| 23 | 24 | return null; |
| 24 | 25 | const [host, ...repository] = identity.canonicalKey.split("/"); |
apps/server/src/sourceControl/BitbucketApi.test.ts+8-3| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | |
| 1 | 2 | import { assert, it, vi } from "@effect/vitest"; |
| 2 | 3 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 3 | 4 | import * as ConfigProvider from "effect/ConfigProvider"; |
| ... | ... | @@ -362,7 +363,7 @@ it.effect.each([false, true])( |
| 362 | 363 | ? { |
| 363 | 364 | context: { |
| 364 | 365 | provider: { |
| 365 | kind: "bitbucket" as const, | |
| 366 | kind: SourceControlProviderKind.make("bitbucket"), | |
| 366 | 367 | name: "Bitbucket", |
| 367 | 368 | baseUrl: "https://bitbucket.org", |
| 368 | 369 | }, |
| ... | ... | @@ -396,7 +397,11 @@ it.effect("prefers an explicit repository and uses context when the repository i |
| 396 | 397 | return Effect.gen(function* () { |
| 397 | 398 | const bitbucket = yield* BitbucketApi.BitbucketApi; |
| 398 | 399 | const context = { |
| 399 | provider: { kind: "bitbucket" as const, name: "Bitbucket", baseUrl: "https://bitbucket.org" }, | |
| 400 | provider: { | |
| 401 | kind: SourceControlProviderKind.make("bitbucket"), | |
| 402 | name: "Bitbucket", | |
| 403 | baseUrl: "https://bitbucket.org", | |
| 404 | }, | |
| 400 | 405 | remoteName: "origin", |
| 401 | 406 | remoteUrl: "git@bitbucket.org:another/context.git", |
| 402 | 407 | }; |
| ... | ... | @@ -838,7 +843,7 @@ it.effect("checks out same-repository pull requests with the existing Bitbucket |
| 838 | 843 | cwd: "/repo", |
| 839 | 844 | context: { |
| 840 | 845 | provider: { |
| 841 | kind: "bitbucket", | |
| 846 | kind: SourceControlProviderKind.make("bitbucket"), | |
| 842 | 847 | name: "Bitbucket", |
| 843 | 848 | baseUrl: "https://bitbucket.org", |
| 844 | 849 | }, |
apps/server/src/sourceControl/SourceControlDiscovery.test.ts+18-13| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | |
| 1 | 2 | import { assert, it } from "@effect/vitest"; |
| 2 | 3 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 3 | 4 | import * as Effect from "effect/Effect"; |
| ... | ... | @@ -176,37 +177,37 @@ it.effect("reports implemented tools separately from locally available executabl |
| 176 | 177 | })), |
| 177 | 178 | [ |
| 178 | 179 | { |
| 179 | kind: "github", | |
| 180 | kind: SourceControlProviderKind.make("github"), | |
| 180 | 181 | status: "available", |
| 181 | 182 | auth: "authenticated", |
| 182 | 183 | account: Option.some("juliusmarminge"), |
| 183 | 184 | }, |
| 184 | 185 | { |
| 185 | kind: "gitlab", | |
| 186 | kind: SourceControlProviderKind.make("gitlab"), | |
| 186 | 187 | status: "missing", |
| 187 | 188 | auth: "unknown", |
| 188 | 189 | account: Option.none(), |
| 189 | 190 | }, |
| 190 | 191 | { |
| 191 | kind: "azure-devops", | |
| 192 | kind: SourceControlProviderKind.make("azure-devops"), | |
| 192 | 193 | status: "missing", |
| 193 | 194 | auth: "unknown", |
| 194 | 195 | account: Option.none(), |
| 195 | 196 | }, |
| 196 | 197 | { |
| 197 | kind: "bitbucket", | |
| 198 | kind: SourceControlProviderKind.make("bitbucket"), | |
| 198 | 199 | status: "available", |
| 199 | 200 | auth: "unauthenticated", |
| 200 | 201 | account: Option.none(), |
| 201 | 202 | }, |
| 202 | 203 | { |
| 203 | kind: "forgejo", | |
| 204 | kind: SourceControlProviderKind.make("forgejo"), | |
| 204 | 205 | status: "missing", |
| 205 | 206 | auth: "unknown", |
| 206 | 207 | account: Option.none(), |
| 207 | 208 | }, |
| 208 | 209 | { |
| 209 | kind: "gitcafe", | |
| 210 | kind: SourceControlProviderKind.make("gitcafe"), | |
| 210 | 211 | status: "missing", |
| 211 | 212 | auth: "unknown", |
| 212 | 213 | account: Option.none(), |
| ... | ... | @@ -327,37 +328,37 @@ Logged in to gitlab.com as gitlab-user |
| 327 | 328 | })), |
| 328 | 329 | [ |
| 329 | 330 | { |
| 330 | kind: "github", | |
| 331 | kind: SourceControlProviderKind.make("github"), | |
| 331 | 332 | auth: "authenticated", |
| 332 | 333 | account: Option.some("octocat"), |
| 333 | 334 | detail: Option.none(), |
| 334 | 335 | }, |
| 335 | 336 | { |
| 336 | kind: "gitlab", | |
| 337 | kind: SourceControlProviderKind.make("gitlab"), | |
| 337 | 338 | auth: "authenticated", |
| 338 | 339 | account: Option.some("gitlab-user"), |
| 339 | 340 | detail: Option.none(), |
| 340 | 341 | }, |
| 341 | 342 | { |
| 342 | kind: "azure-devops", | |
| 343 | kind: SourceControlProviderKind.make("azure-devops"), | |
| 343 | 344 | auth: "authenticated", |
| 344 | 345 | account: Option.some("azure-user@example.com"), |
| 345 | 346 | detail: Option.none(), |
| 346 | 347 | }, |
| 347 | 348 | { |
| 348 | kind: "bitbucket", | |
| 349 | kind: SourceControlProviderKind.make("bitbucket"), | |
| 349 | 350 | auth: "authenticated", |
| 350 | 351 | account: Option.some("bitbucket-user"), |
| 351 | 352 | detail: Option.none(), |
| 352 | 353 | }, |
| 353 | 354 | { |
| 354 | kind: "forgejo", | |
| 355 | kind: SourceControlProviderKind.make("forgejo"), | |
| 355 | 356 | auth: "authenticated", |
| 356 | 357 | account: Option.some("forgejo-user"), |
| 357 | 358 | detail: Option.none(), |
| 358 | 359 | }, |
| 359 | 360 | { |
| 360 | kind: "gitcafe", | |
| 361 | kind: SourceControlProviderKind.make("gitcafe"), | |
| 361 | 362 | auth: "authenticated", |
| 362 | 363 | account: Option.some("cafe-user"), |
| 363 | 364 | detail: Option.none(), |
| ... | ... | @@ -528,7 +529,11 @@ it.effect( |
| 528 | 529 | reference: "42", |
| 529 | 530 | force: true, |
| 530 | 531 | context: { |
| 531 | provider: { kind: "forgejo", name: "Forgejo", baseUrl: "https://forgejo.test" }, | |
| 532 | provider: { | |
| 533 | kind: SourceControlProviderKind.make("forgejo"), | |
| 534 | name: "Forgejo", | |
| 535 | baseUrl: "https://forgejo.test", | |
| 536 | }, | |
| 532 | 537 | remoteName: "origin", |
| 533 | 538 | remoteUrl: "git@forgejo.test:maria/project.git", |
| 534 | 539 | }, |
apps/server/src/sourceControl/SourceControlProviderRegistry.test.ts+6-5| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | |
| 1 | 2 | import { assert, it } from "@effect/vitest"; |
| 2 | 3 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 3 | 4 | import * as DateTime from "effect/DateTime"; |
| ... | ... | @@ -154,7 +155,7 @@ it.effect("routes directly by provider kind for remote-first workflows", () => |
| 154 | 155 | remotes: [], |
| 155 | 156 | }); |
| 156 | 157 | |
| 157 | const provider = yield* registry.get("github"); | |
| 158 | const provider = yield* registry.get(SourceControlProviderKind.make("github")); | |
| 158 | 159 | |
| 159 | 160 | assert.strictEqual(provider.kind, "github"); |
| 160 | 161 | }), |
| ... | ... | @@ -163,7 +164,7 @@ it.effect("routes directly by provider kind for remote-first workflows", () => |
| 163 | 164 | it.effect("includes the request cwd when an unregistered provider is used", () => |
| 164 | 165 | Effect.gen(function* () { |
| 165 | 166 | const registry = yield* makeRegistry({ remotes: [] }); |
| 166 | const provider = yield* registry.get("unknown"); | |
| 167 | const provider = yield* registry.get(SourceControlProviderKind.make("unknown")); | |
| 167 | 168 | |
| 168 | 169 | const error = yield* provider |
| 169 | 170 | .getChangeRequest({ cwd: "/repo", reference: "#42" }) |
| ... | ... | @@ -257,7 +258,7 @@ it.effect("refines the caller-selected remote instead of choosing another config |
| 257 | 258 | cwd: "/repo", |
| 258 | 259 | context: { |
| 259 | 260 | provider: { |
| 260 | kind: "unknown", | |
| 261 | kind: SourceControlProviderKind.make("unknown"), | |
| 261 | 262 | name: "self-hosted.example.test", |
| 262 | 263 | baseUrl: "https://self-hosted.example.test", |
| 263 | 264 | }, |
| ... | ... | @@ -412,7 +413,7 @@ it.effect.each([ |
| 412 | 413 | }, |
| 413 | 414 | }); |
| 414 | 415 | const handle = yield* registry.resolveHandle({ cwd: "/repo" }); |
| 415 | assert.strictEqual(handle.provider.kind, scenario.expected); | |
| 416 | assert.strictEqual(handle.provider.kind, SourceControlProviderKind.make(scenario.expected)); | |
| 416 | 417 | assert.strictEqual(handle.context?.provider.baseUrl, "https://code.example.test"); |
| 417 | 418 | assert.deepStrictEqual(hosts, ["code.example.test"]); |
| 418 | 419 | // Recognition goes through the credential layer; gh is only one of its sources. |
| ... | ... | @@ -437,7 +438,7 @@ it.effect("skips GitHub discovery for the identity resolver's empty base URL", ( |
| 437 | 438 | }, |
| 438 | 439 | }); |
| 439 | 440 | const context = { |
| 440 | provider: { kind: "unknown" as const, name: "Unknown", baseUrl: "" }, | |
| 441 | provider: { kind: SourceControlProviderKind.make("unknown"), name: "Unknown", baseUrl: "" }, | |
| 441 | 442 | remoteName: "origin", |
| 442 | 443 | remoteUrl, |
| 443 | 444 | }; |
apps/server/src/sourceControl/SourceControlProviderRegistry.ts+5-4| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | |
| 1 | 2 | import * as Cache from "effect/Cache"; |
| 2 | 3 | import * as Context from "effect/Context"; |
| 3 | 4 | import * as Duration from "effect/Duration"; |
| ... | ... | @@ -8,7 +9,7 @@ import { |
| 8 | 9 | SourceControlProviderError, |
| 9 | 10 | type SourceControlProviderDiscoveryItem, |
| 10 | 11 | } from "@t3tools/contracts"; |
| 11 | import type { SourceControlProviderKind } from "@t3tools/contracts"; | |
| 12 | import type {} from "@t3tools/contracts"; | |
| 12 | 13 | import { detectSourceControlProviderFromRemoteUrl } from "@t3tools/shared/sourceControl"; |
| 13 | 14 | |
| 14 | 15 | import * as BuiltInDrivers from "./builtInDrivers.ts"; |
| ... | ... | @@ -214,7 +215,7 @@ export const makeWithProviders = Effect.fn("makeSourceControlProviderRegistryWit |
| 214 | 215 | Effect.mapError( |
| 215 | 216 | (error) => |
| 216 | 217 | new SourceControlProviderError({ |
| 217 | provider: "unknown", | |
| 218 | provider: SourceControlProviderKind.make("unknown"), | |
| 218 | 219 | operation: "detectProvider", |
| 219 | 220 | cwd, |
| 220 | 221 | detail: "Failed to detect source control provider.", |
| ... | ... | @@ -226,7 +227,7 @@ export const makeWithProviders = Effect.fn("makeSourceControlProviderRegistryWit |
| 226 | 227 | Effect.mapError( |
| 227 | 228 | (error) => |
| 228 | 229 | new SourceControlProviderError({ |
| 229 | provider: "unknown", | |
| 230 | provider: SourceControlProviderKind.make("unknown"), | |
| 230 | 231 | operation: "detectProvider", |
| 231 | 232 | cwd, |
| 232 | 233 | detail: "Failed to detect source control provider.", |
| ... | ... | @@ -265,7 +266,7 @@ export const makeWithProviders = Effect.fn("makeSourceControlProviderRegistryWit |
| 265 | 266 | }) |
| 266 | 267 | ).pipe( |
| 267 | 268 | Effect.map((context) => { |
| 268 | const kind = context?.provider.kind ?? "unknown"; | |
| 269 | const kind = context?.provider.kind ?? SourceControlProviderKind.make("unknown"); | |
| 269 | 270 | const provider = providers.get(kind) ?? unsupportedProvider(kind); |
| 270 | 271 | return { |
| 271 | 272 | provider: bindProviderContext(provider, context), |
apps/server/src/sourceControl/SourceControlRepositoryService.test.ts+24-17| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | |
| 1 | 2 | import * as NodePath from "@effect/platform-node/NodePath"; |
| 2 | 3 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 3 | 4 | import { assert, it } from "@effect/vitest"; |
| ... | ... | @@ -37,7 +38,7 @@ function makeProvider( |
| 37 | 38 | >; |
| 38 | 39 | |
| 39 | 40 | return { |
| 40 | kind: "github", | |
| 41 | kind: SourceControlProviderKind.make("github"), | |
| 41 | 42 | listChangeRequests: () => unsupported("listChangeRequests"), |
| 42 | 43 | getChangeRequest: () => unsupported("getChangeRequest"), |
| 43 | 44 | createChangeRequest: () => unsupported("createChangeRequest"), |
| ... | ... | @@ -114,19 +115,22 @@ it.effect("looks up repositories through the requested provider without search", |
| 114 | 115 | return Effect.gen(function* () { |
| 115 | 116 | const service = yield* SourceControlRepositoryService.SourceControlRepositoryService; |
| 116 | 117 | const result = yield* service.lookupRepository({ |
| 117 | provider: "github", | |
| 118 | provider: SourceControlProviderKind.make("github"), | |
| 118 | 119 | repository: "octocat/t3code", |
| 119 | 120 | cwd: "/workspace", |
| 120 | 121 | }); |
| 121 | 122 | |
| 122 | assert.deepStrictEqual(result, { provider: "github", ...CLONE_URLS }); | |
| 123 | assert.deepStrictEqual(result, { | |
| 124 | provider: SourceControlProviderKind.make("github"), | |
| 125 | ...CLONE_URLS, | |
| 126 | }); | |
| 123 | 127 | assert.deepStrictEqual(calls, [{ cwd: "/workspace", repository: "octocat/t3code" }]); |
| 124 | 128 | }).pipe(Effect.provide(layer({ provider }))); |
| 125 | 129 | }); |
| 126 | 130 | |
| 127 | 131 | it.effect("preserves provider failures without deriving the repository message from them", () => { |
| 128 | 132 | const providerCause = new SourceControlProviderError({ |
| 129 | provider: "github", | |
| 133 | provider: SourceControlProviderKind.make("github"), | |
| 130 | 134 | operation: "getRepositoryCloneUrls", |
| 131 | 135 | cwd: "/workspace", |
| 132 | 136 | repository: "octocat/t3code", |
| ... | ... | @@ -140,7 +144,7 @@ it.effect("preserves provider failures without deriving the repository message f |
| 140 | 144 | const service = yield* SourceControlRepositoryService.SourceControlRepositoryService; |
| 141 | 145 | const error = yield* Effect.flip( |
| 142 | 146 | service.lookupRepository({ |
| 143 | provider: "github", | |
| 147 | provider: SourceControlProviderKind.make("github"), | |
| 144 | 148 | repository: "octocat/t3code", |
| 145 | 149 | cwd: "/workspace", |
| 146 | 150 | }), |
| ... | ... | @@ -176,7 +180,7 @@ it.effect( |
| 176 | 180 | yield* Effect.gen(function* () { |
| 177 | 181 | const service = yield* SourceControlRepositoryService.SourceControlRepositoryService; |
| 178 | 182 | const input = { |
| 179 | provider: "bitbucket" as const, | |
| 183 | provider: SourceControlProviderKind.make("bitbucket"), | |
| 180 | 184 | repository: locatorCause.repository, |
| 181 | 185 | cwd: "/workspace", |
| 182 | 186 | }; |
| ... | ... | @@ -204,11 +208,11 @@ it.effect( |
| 204 | 208 | |
| 205 | 209 | it.effect("keeps a plain Bitbucket locator lookalike out of the repository message", () => { |
| 206 | 210 | const provider = makeProvider({ |
| 207 | kind: "bitbucket", | |
| 211 | kind: SourceControlProviderKind.make("bitbucket"), | |
| 208 | 212 | getRepositoryCloneUrls: () => |
| 209 | 213 | Effect.fail( |
| 210 | 214 | new SourceControlProviderError({ |
| 211 | provider: "bitbucket", | |
| 215 | provider: SourceControlProviderKind.make("bitbucket"), | |
| 212 | 216 | operation: "getRepositoryCloneUrls", |
| 213 | 217 | cwd: "/workspace", |
| 214 | 218 | detail: "credential token abc123 was rejected", |
| ... | ... | @@ -224,7 +228,10 @@ it.effect("keeps a plain Bitbucket locator lookalike out of the repository messa |
| 224 | 228 | return Effect.gen(function* () { |
| 225 | 229 | const service = yield* SourceControlRepositoryService.SourceControlRepositoryService; |
| 226 | 230 | const error = yield* Effect.flip( |
| 227 | service.lookupRepository({ provider: "bitbucket", repository: "t3code" }), | |
| 231 | service.lookupRepository({ | |
| 232 | provider: SourceControlProviderKind.make("bitbucket"), | |
| 233 | repository: "t3code", | |
| 234 | }), | |
| 228 | 235 | ); |
| 229 | 236 | |
| 230 | 237 | assert.strictEqual(error.detail, "The source control operation could not be completed."); |
| ... | ... | @@ -250,7 +257,7 @@ it.effect("keeps arbitrary Bitbucket request failures out of the repository mess |
| 250 | 257 | const service = yield* SourceControlRepositoryService.SourceControlRepositoryService; |
| 251 | 258 | const error = yield* Effect.flip( |
| 252 | 259 | service.lookupRepository({ |
| 253 | provider: "bitbucket", | |
| 260 | provider: SourceControlProviderKind.make("bitbucket"), | |
| 254 | 261 | repository: "pingdotgg/t3code", |
| 255 | 262 | cwd: "/workspace", |
| 256 | 263 | }), |
| ... | ... | @@ -276,7 +283,7 @@ it.effect("clones a looked-up repository into the requested destination", () => |
| 276 | 283 | yield* Effect.gen(function* () { |
| 277 | 284 | const service = yield* SourceControlRepositoryService.SourceControlRepositoryService; |
| 278 | 285 | const result = yield* service.cloneRepository({ |
| 279 | provider: "github", | |
| 286 | provider: SourceControlProviderKind.make("github"), | |
| 280 | 287 | repository: "octocat/t3code", |
| 281 | 288 | destinationPath, |
| 282 | 289 | protocol: "https", |
| ... | ... | @@ -285,7 +292,7 @@ it.effect("clones a looked-up repository into the requested destination", () => |
| 285 | 292 | assert.deepStrictEqual(result, { |
| 286 | 293 | cwd: destinationPath, |
| 287 | 294 | remoteUrl: CLONE_URLS.url, |
| 288 | repository: { provider: "github", ...CLONE_URLS }, | |
| 295 | repository: { provider: SourceControlProviderKind.make("github"), ...CLONE_URLS }, | |
| 289 | 296 | }); |
| 290 | 297 | assert.deepStrictEqual(cloneCalls, [ |
| 291 | 298 | { |
| ... | ... | @@ -546,7 +553,7 @@ it.effect("publishes by creating the repository, adding a remote, and pushing up |
| 546 | 553 | const service = yield* SourceControlRepositoryService.SourceControlRepositoryService; |
| 547 | 554 | const result = yield* service.publishRepository({ |
| 548 | 555 | cwd: "/workspace", |
| 549 | provider: "github", | |
| 556 | provider: SourceControlProviderKind.make("github"), | |
| 550 | 557 | repository: "octocat/t3code", |
| 551 | 558 | visibility: "private", |
| 552 | 559 | remoteName: "origin", |
| ... | ... | @@ -554,7 +561,7 @@ it.effect("publishes by creating the repository, adding a remote, and pushing up |
| 554 | 561 | }); |
| 555 | 562 | |
| 556 | 563 | assert.deepStrictEqual(result, { |
| 557 | repository: { provider: "github", ...CLONE_URLS }, | |
| 564 | repository: { provider: SourceControlProviderKind.make("github"), ...CLONE_URLS }, | |
| 558 | 565 | remoteName: "origin", |
| 559 | 566 | remoteUrl: CLONE_URLS.sshUrl, |
| 560 | 567 | branch: "feature/remote-v1", |
| ... | ... | @@ -601,7 +608,7 @@ it.effect("publishes to the remote name returned by ensureRemote", () => { |
| 601 | 608 | const service = yield* SourceControlRepositoryService.SourceControlRepositoryService; |
| 602 | 609 | const result = yield* service.publishRepository({ |
| 603 | 610 | cwd: "/workspace", |
| 604 | provider: "github", | |
| 611 | provider: SourceControlProviderKind.make("github"), | |
| 605 | 612 | repository: "octocat/t3code", |
| 606 | 613 | visibility: "private", |
| 607 | 614 | remoteName: "origin", |
| ... | ... | @@ -637,7 +644,7 @@ it.effect("publish succeeds with status remote_added when the local repo has no |
| 637 | 644 | const service = yield* SourceControlRepositoryService.SourceControlRepositoryService; |
| 638 | 645 | const result = yield* service.publishRepository({ |
| 639 | 646 | cwd: "/workspace", |
| 640 | provider: "github", | |
| 647 | provider: SourceControlProviderKind.make("github"), | |
| 641 | 648 | repository: "octocat/t3code", |
| 642 | 649 | visibility: "private", |
| 643 | 650 | remoteName: "origin", |
| ... | ... | @@ -645,7 +652,7 @@ it.effect("publish succeeds with status remote_added when the local repo has no |
| 645 | 652 | }); |
| 646 | 653 | |
| 647 | 654 | assert.deepStrictEqual(result, { |
| 648 | repository: { provider: "github", ...CLONE_URLS }, | |
| 655 | repository: { provider: SourceControlProviderKind.make("github"), ...CLONE_URLS }, | |
| 649 | 656 | remoteName: "origin", |
| 650 | 657 | remoteUrl: CLONE_URLS.sshUrl, |
| 651 | 658 | branch: "main", |
apps/server/src/sourceControl/SourceControlRepositoryService.ts+23-12| ... | ... | @@ -12,7 +12,7 @@ import { |
| 12 | 12 | type SourceControlCloneRepositoryInput, |
| 13 | 13 | type SourceControlCloneRepositoryResult, |
| 14 | 14 | type SourceControlCloneProtocol, |
| 15 | type SourceControlProviderKind, | |
| 15 | SourceControlProviderKind, | |
| 16 | 16 | type SourceControlPublishRepositoryInput, |
| 17 | 17 | type SourceControlPublishRepositoryResult, |
| 18 | 18 | type SourceControlRepositoryCloneUrls, |
| ... | ... | @@ -209,7 +209,7 @@ export const make = Effect.gen(function* () { |
| 209 | 209 | if (trimmed.length === 0) { |
| 210 | 210 | return yield* new SourceControlRepositoryError({ |
| 211 | 211 | operation: "cloneRepository", |
| 212 | provider: "unknown", | |
| 212 | provider: SourceControlProviderKind.make("unknown"), | |
| 213 | 213 | detail: "Choose a destination path before cloning.", |
| 214 | 214 | }); |
| 215 | 215 | } |
| ... | ... | @@ -229,7 +229,7 @@ export const make = Effect.gen(function* () { |
| 229 | 229 | (cause) => |
| 230 | 230 | new SourceControlRepositoryError({ |
| 231 | 231 | operation: "cloneRepository", |
| 232 | provider: "unknown", | |
| 232 | provider: SourceControlProviderKind.make("unknown"), | |
| 233 | 233 | detail: "Destination path already exists and is not a directory.", |
| 234 | 234 | cause, |
| 235 | 235 | }), |
| ... | ... | @@ -238,7 +238,7 @@ export const make = Effect.gen(function* () { |
| 238 | 238 | if (entries.length > 0) { |
| 239 | 239 | return yield* new SourceControlRepositoryError({ |
| 240 | 240 | operation: "cloneRepository", |
| 241 | provider: "unknown", | |
| 241 | provider: SourceControlProviderKind.make("unknown"), | |
| 242 | 242 | detail: "Destination path already exists and is not empty.", |
| 243 | 243 | }); |
| 244 | 244 | } |
| ... | ... | @@ -260,7 +260,8 @@ export const make = Effect.gen(function* () { |
| 260 | 260 | const preparedDestination = yield* prepareDestination(input.destinationPath); |
| 261 | 261 | let repository: SourceControlRepositoryInfo | null = null; |
| 262 | 262 | let remoteUrl = input.remoteUrl?.trim() ?? null; |
| 263 | let provider: SourceControlProviderKind = input.provider ?? "unknown"; | |
| 263 | let provider: SourceControlProviderKind = | |
| 264 | input.provider ?? SourceControlProviderKind.make("unknown"); | |
| 264 | 265 | |
| 265 | 266 | if (input.provider && input.repository) { |
| 266 | 267 | repository = yield* lookupRepository({ |
| ... | ... | @@ -335,7 +336,7 @@ export const make = Effect.gen(function* () { |
| 335 | 336 | (cause) => |
| 336 | 337 | new SourceControlRepositoryError({ |
| 337 | 338 | operation: "cloneRepository", |
| 338 | provider: input.provider ?? "unknown", | |
| 339 | provider: input.provider ?? SourceControlProviderKind.make("unknown"), | |
| 339 | 340 | detail: |
| 340 | 341 | stderrTail.length > 0 |
| 341 | 342 | ? stderrTail.join(" ") |
| ... | ... | @@ -370,7 +371,7 @@ export const make = Effect.gen(function* () { |
| 370 | 371 | (cause) => |
| 371 | 372 | new SourceControlRepositoryError({ |
| 372 | 373 | operation: "discardClone", |
| 373 | provider: "unknown", | |
| 374 | provider: SourceControlProviderKind.make("unknown"), | |
| 374 | 375 | detail: "The clone destination could not be inspected.", |
| 375 | 376 | cause, |
| 376 | 377 | }), |
| ... | ... | @@ -379,7 +380,7 @@ export const make = Effect.gen(function* () { |
| 379 | 380 | if (entries.length > 0 && !entries.includes(".git")) { |
| 380 | 381 | return yield* new SourceControlRepositoryError({ |
| 381 | 382 | operation: "discardClone", |
| 382 | provider: "unknown", | |
| 383 | provider: SourceControlProviderKind.make("unknown"), | |
| 383 | 384 | detail: "Destination path contains files that are not from the clone.", |
| 384 | 385 | }); |
| 385 | 386 | } |
| ... | ... | @@ -393,7 +394,7 @@ export const make = Effect.gen(function* () { |
| 393 | 394 | (cause) => |
| 394 | 395 | new SourceControlRepositoryError({ |
| 395 | 396 | operation: "discardClone", |
| 396 | provider: "unknown", | |
| 397 | provider: SourceControlProviderKind.make("unknown"), | |
| 397 | 398 | detail: "The partial clone could not be removed.", |
| 398 | 399 | cause, |
| 399 | 400 | }), |
| ... | ... | @@ -462,13 +463,23 @@ export const make = Effect.gen(function* () { |
| 462 | 463 | lookupRepository: (input) => |
| 463 | 464 | lookupRepository(input).pipe(mapRepositoryError("lookupRepository", input.provider)), |
| 464 | 465 | prepareClone: (input) => |
| 465 | prepareClone(input).pipe(mapRepositoryError("cloneRepository", input.provider ?? "unknown")), | |
| 466 | prepareClone(input).pipe( | |
| 467 | mapRepositoryError( | |
| 468 | "cloneRepository", | |
| 469 | input.provider ?? SourceControlProviderKind.make("unknown"), | |
| 470 | ), | |
| 471 | ), | |
| 466 | 472 | cloneRepository: (input, options) => |
| 467 | 473 | cloneRepository(input, options).pipe( |
| 468 | mapRepositoryError("cloneRepository", input.provider ?? "unknown"), | |
| 474 | mapRepositoryError( | |
| 475 | "cloneRepository", | |
| 476 | input.provider ?? SourceControlProviderKind.make("unknown"), | |
| 477 | ), | |
| 469 | 478 | ), |
| 470 | 479 | discardClone: (destinationPath) => |
| 471 | discardClone(destinationPath).pipe(mapRepositoryError("discardClone", "unknown")), | |
| 480 | discardClone(destinationPath).pipe( | |
| 481 | mapRepositoryError("discardClone", SourceControlProviderKind.make("unknown")), | |
| 482 | ), | |
| 472 | 483 | publishRepository: (input) => |
| 473 | 484 | publishRepository(input).pipe(mapRepositoryError("publishRepository", input.provider)), |
| 474 | 485 | }); |
apps/server/src/textGeneration/ThreadTitleLinks.test.ts+2-1| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | |
| 1 | 2 | import { expect, it } from "@effect/vitest"; |
| 2 | 3 | import * as Effect from "effect/Effect"; |
| 3 | 4 | import * as Schema from "effect/Schema"; |
| ... | ... | @@ -87,7 +88,7 @@ it.effect("keeps lookup failure out of generation and skips unlinked messages", |
| 87 | 88 | resolveLink: () => |
| 88 | 89 | Effect.fail( |
| 89 | 90 | new SourceControlProviderError({ |
| 90 | provider: "unknown", | |
| 91 | provider: SourceControlProviderKind.make("unknown"), | |
| 91 | 92 | operation: "resolveLink", |
| 92 | 93 | cwd: "/tmp", |
| 93 | 94 | detail: "Unavailable", |
apps/server/src/vcs/VcsStatusBroadcaster.test.ts+2-1| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | |
| 1 | 2 | import { assert, it, describe } from "@effect/vitest"; |
| 2 | 3 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 3 | 4 | import * as Cause from "effect/Cause"; |
| ... | ... | @@ -34,7 +35,7 @@ const TEST_EPOCH = DateTime.makeUnsafe("1970-01-01T00:00:00.000Z"); |
| 34 | 35 | const baseLocalStatus: VcsStatusLocalResult = { |
| 35 | 36 | isRepo: true, |
| 36 | 37 | sourceControlProvider: { |
| 37 | kind: "github", | |
| 38 | kind: SourceControlProviderKind.make("github"), | |
| 38 | 39 | name: "GitHub", |
| 39 | 40 | baseUrl: "https://github.com", |
| 40 | 41 | }, |
apps/web/src/components/CommandPalette.tsx+10-7| ... | ... | @@ -39,7 +39,7 @@ import { |
| 39 | 39 | type FilesystemBrowseResult, |
| 40 | 40 | type ProjectId, |
| 41 | 41 | type SourceControlDiscoveryResult, |
| 42 | type SourceControlProviderKind, | |
| 42 | SourceControlProviderKind, | |
| 43 | 43 | type SourceControlRepositoryInfo, |
| 44 | 44 | PRIMARY_LOCAL_ENVIRONMENT_ID, |
| 45 | 45 | resolveEnvironmentMachineKind, |
| ... | ... | @@ -257,10 +257,13 @@ interface AddProjectEnvironmentOption { |
| 257 | 257 | readonly status: string; |
| 258 | 258 | } |
| 259 | 259 | |
| 260 | type AddProjectRemoteProviderKind = Extract< | |
| 261 | SourceControlProviderKind, | |
| 262 | "github" | "gitlab" | "forgejo" | "bitbucket" | "azure-devops" | "gitcafe" | |
| 263 | >; | |
| 260 | type AddProjectRemoteProviderKind = | |
| 261 | | "github" | |
| 262 | | "gitlab" | |
| 263 | | "forgejo" | |
| 264 | | "bitbucket" | |
| 265 | | "azure-devops" | |
| 266 | | "gitcafe"; | |
| 264 | 267 | type AddProjectRemoteSource = AddProjectRemoteProviderKind | "url"; |
| 265 | 268 | |
| 266 | 269 | type AddProjectCloneFlow = |
| ... | ... | @@ -421,7 +424,7 @@ function buildAddProjectRemoteSourceReadiness( |
| 421 | 424 | for (const source of REMOTE_PROJECT_SOURCES) { |
| 422 | 425 | const kind = sourceProviderKind(source); |
| 423 | 426 | if (!kind) continue; |
| 424 | const provider = providerByKind.get(kind); | |
| 427 | const provider = providerByKind.get(SourceControlProviderKind.make(kind)); | |
| 425 | 428 | if (!provider) { |
| 426 | 429 | readiness[source] = unavailable; |
| 427 | 430 | continue; |
| ... | ... | @@ -2628,7 +2631,7 @@ function OpenCommandPaletteDialog(props: { |
| 2628 | 2631 | const lookupResult = await lookupRepository({ |
| 2629 | 2632 | environmentId: addProjectCloneFlow.environmentId, |
| 2630 | 2633 | input: { |
| 2631 | provider, | |
| 2634 | provider: SourceControlProviderKind.make(provider), | |
| 2632 | 2635 | repository: rawRepository, |
| 2633 | 2636 | }, |
| 2634 | 2637 | }); |
apps/web/src/components/GitActionsControl.logic.test.ts+2-1| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | |
| 1 | 2 | import type { VcsStatusResult } from "@t3tools/contracts"; |
| 2 | 3 | import { assert, describe, it } from "vite-plus/test"; |
| 3 | 4 | import { |
| ... | ... | @@ -343,7 +344,7 @@ describe("when: source control provider uses merge requests", () => { |
| 343 | 344 | const gitlabStatus = status({ |
| 344 | 345 | aheadCount: 2, |
| 345 | 346 | sourceControlProvider: { |
| 346 | kind: "gitlab", | |
| 347 | kind: SourceControlProviderKind.make("gitlab"), | |
| 347 | 348 | name: "GitLab", |
| 348 | 349 | baseUrl: "https://gitlab.com", |
| 349 | 350 | }, |
apps/web/src/components/GitActionsControl.tsx+13-11| ... | ... | @@ -3,6 +3,7 @@ import { useAtomValue } from "@effect/atom-react"; |
| 3 | 3 | import { |
| 4 | 4 | AuthOrchestrationOperateScope, |
| 5 | 5 | AuthSourceControlWriteScope, |
| 6 | SourceControlProviderKind, | |
| 6 | 7 | type ScopedThreadRef, |
| 7 | 8 | } from "@t3tools/contracts"; |
| 8 | 9 | import { |
| ... | ... | @@ -14,7 +15,6 @@ import type { |
| 14 | 15 | GitStackedAction, |
| 15 | 16 | SourceControlCloneProtocol, |
| 16 | 17 | SourceControlProviderDiscoveryItem, |
| 17 | SourceControlProviderKind, | |
| 18 | 18 | SourceControlPublishRepositoryResult, |
| 19 | 19 | SourceControlRepositoryVisibility, |
| 20 | 20 | VcsStatusResult, |
| ... | ... | @@ -158,10 +158,13 @@ interface PendingDefaultBranchAction { |
| 158 | 158 | filePaths?: string[]; |
| 159 | 159 | } |
| 160 | 160 | |
| 161 | type PublishProviderKind = Extract< | |
| 162 | SourceControlProviderKind, | |
| 163 | "github" | "gitlab" | "forgejo" | "bitbucket" | "azure-devops" | "gitcafe" | |
| 164 | >; | |
| 161 | type PublishProviderKind = | |
| 162 | | "github" | |
| 163 | | "gitlab" | |
| 164 | | "forgejo" | |
| 165 | | "bitbucket" | |
| 166 | | "azure-devops" | |
| 167 | | "gitcafe"; | |
| 165 | 168 | |
| 166 | 169 | type GitActionToastId = ReturnType<typeof toastManager.add>; |
| 167 | 170 | |
| ... | ... | @@ -265,9 +268,7 @@ function publishProviderOption(provider: PublishProviderKind) { |
| 265 | 268 | ); |
| 266 | 269 | } |
| 267 | 270 | |
| 268 | function isPublishProviderKind( | |
| 269 | provider: SourceControlProviderKind, | |
| 270 | ): provider is PublishProviderKind { | |
| 271 | function isPublishProviderKind(provider: string): provider is PublishProviderKind { | |
| 271 | 272 | return PUBLISH_PROVIDER_OPTIONS.some((option) => option.value === provider); |
| 272 | 273 | } |
| 273 | 274 | |
| ... | ... | @@ -582,8 +583,9 @@ function PublishRepositoryDialog(props: PublishRepositoryDialogProps) { |
| 582 | 583 | gitcafe: null, |
| 583 | 584 | }; |
| 584 | 585 | for (const provider of sourceControlDiscovery.data?.sourceControlProviders ?? []) { |
| 585 | if (isPublishProviderKind(provider.kind)) { | |
| 586 | accounts[provider.kind] = Option.getOrNull(provider.auth.account); | |
| 586 | const kind: string = provider.kind; | |
| 587 | if (isPublishProviderKind(kind)) { | |
| 588 | accounts[kind] = Option.getOrNull(provider.auth.account); | |
| 587 | 589 | } |
| 588 | 590 | } |
| 589 | 591 | return accounts; |
| ... | ... | @@ -670,7 +672,7 @@ function PublishRepositoryDialog(props: PublishRepositoryDialogProps) { |
| 670 | 672 | |
| 671 | 673 | void (async () => { |
| 672 | 674 | const result = await publishRepositoryAction.run({ |
| 673 | provider: publishProvider, | |
| 675 | provider: SourceControlProviderKind.make(publishProvider), | |
| 674 | 676 | repository: publishRepository.trim(), |
| 675 | 677 | visibility: publishVisibility, |
| 676 | 678 | remoteName: publishRemoteName.trim() || "origin", |
apps/web/src/components/ThreadStatusIndicators.test.ts+3-2| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | |
| 1 | 2 | import { ProjectId, type PullRequestSummary, type VcsStatusResult } from "@t3tools/contracts"; |
| 2 | 3 | import { describe, expect, it } from "@effect/vitest"; |
| 3 | 4 | import * as Effect from "effect/Effect"; |
| ... | ... | @@ -93,7 +94,7 @@ function pullRequestSummary( |
| 93 | 94 | updatedAt: string, |
| 94 | 95 | ): PullRequestSummary { |
| 95 | 96 | return { |
| 96 | provider: "github", | |
| 97 | provider: SourceControlProviderKind.make("github"), | |
| 97 | 98 | projectId: ProjectId.make("project-1"), |
| 98 | 99 | repository: "pingdotgg/t3code", |
| 99 | 100 | number: 42, |
| ... | ... | @@ -179,7 +180,7 @@ describe("resolveDisplayedThreadPr + nextThreadChangeRequestSnapshot", () => { |
| 179 | 180 | url: "https://github.com/pingdotgg/t3code/pull/42", |
| 180 | 181 | }; |
| 181 | 182 | const provider = { |
| 182 | kind: "github" as const, | |
| 183 | kind: SourceControlProviderKind.make("github"), | |
| 183 | 184 | name: "GitHub", |
| 184 | 185 | baseUrl: "https://github.com", |
| 185 | 186 | }; |
apps/web/src/components/ThreadStatusIndicators.tsx+2-1| ... | ... | @@ -13,6 +13,7 @@ import { |
| 13 | 13 | type ThreadLinkedPullRequest, |
| 14 | 14 | type ThreadPullRequestLink, |
| 15 | 15 | type VcsStatusResult, |
| 16 | SourceControlProviderKind, | |
| 16 | 17 | } from "@t3tools/contracts"; |
| 17 | 18 | import { Atom } from "effect/reactivity"; |
| 18 | 19 | import { FolderGit2Icon, TerminalIcon } from "lucide-react"; |
| ... | ... | @@ -154,7 +155,7 @@ export function linkedPullRequestSnapshotStatus( |
| 154 | 155 | baseRef: snapshot.baseBranch, |
| 155 | 156 | ...(snapshot.updatedAt === null ? {} : { updatedAt: snapshot.updatedAt }), |
| 156 | 157 | }, |
| 157 | sourceControlProvider: { kind, name: kind, baseUrl: "" }, | |
| 158 | sourceControlProvider: { kind: SourceControlProviderKind.make(kind), name: kind, baseUrl: "" }, | |
| 158 | 159 | }; |
| 159 | 160 | } |
| 160 | 161 |
apps/web/src/components/pullRequest/PullRequestDetailPanel.test.tsx+2-1| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | |
| 1 | 2 | import { |
| 2 | 3 | EnvironmentId, |
| 3 | 4 | ProjectId, |
| ... | ... | @@ -151,7 +152,7 @@ import { PullRequestDetailPanel } from "./PullRequestDetailPanel"; |
| 151 | 152 | import { pullRequestPanelContext } from "./pullRequestDetail.logic"; |
| 152 | 153 | |
| 153 | 154 | const detail: PullRequestDetailView = { |
| 154 | provider: "github", | |
| 155 | provider: SourceControlProviderKind.make("github"), | |
| 155 | 156 | projectId: ProjectId.make("project"), |
| 156 | 157 | projectTitle: "Project", |
| 157 | 158 | workspaceRoot: "/workspace", |
apps/web/src/components/pullRequest/PullRequestSummaryTab.test.tsx+2-1| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | |
| 1 | 2 | import { EnvironmentId, ProjectId, type PullRequestDetailView } from "@t3tools/contracts"; |
| 2 | 3 | import { act, type ReactNode } from "react"; |
| 3 | 4 | import { create, type ReactTestRenderer } from "react-test-renderer"; |
| ... | ... | @@ -26,7 +27,7 @@ vi.mock("../ui/tooltip", () => ({ |
| 26 | 27 | import { PullRequestSummaryTab } from "./PullRequestSummaryTab"; |
| 27 | 28 | |
| 28 | 29 | const detail: PullRequestDetailView = { |
| 29 | provider: "github", | |
| 30 | provider: SourceControlProviderKind.make("github"), | |
| 30 | 31 | projectId: ProjectId.make("project"), |
| 31 | 32 | projectTitle: "Project", |
| 32 | 33 | workspaceRoot: "/workspace", |
apps/web/src/components/pullRequest/pullRequestDetail.logic.test.ts+12-8| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | |
| 1 | 2 | import { resolvePlanFollowUpSubmission } from "../../proposedPlan"; |
| 2 | 3 | import { serializeLegacyContextMessage } from "@t3tools/shared/composerContextLegacySend"; |
| 3 | 4 | import { |
| ... | ... | @@ -95,12 +96,14 @@ describe("pull request checkout commands", () => { |
| 95 | 96 | ], |
| 96 | 97 | ["unknown", "feature", null, null], |
| 97 | 98 | ] as const)("builds the %s command", (provider, branch, repository, expected) => { |
| 98 | expect(pullRequestCheckoutCommand(provider, 42, branch, repository)).toBe(expected); | |
| 99 | expect( | |
| 100 | pullRequestCheckoutCommand(SourceControlProviderKind.make(provider), 42, branch, repository), | |
| 101 | ).toBe(expected); | |
| 99 | 102 | }); |
| 100 | 103 | it("fetches Forgejo pull refs from the actual repository, including a mounted host and port", () => { |
| 101 | 104 | expect( |
| 102 | 105 | pullRequestCheckoutCommand( |
| 103 | "forgejo", | |
| 106 | SourceControlProviderKind.make("forgejo"), | |
| 104 | 107 | 42, |
| 105 | 108 | "feature", |
| 106 | 109 | null, |
| ... | ... | @@ -113,7 +116,7 @@ describe("pull request checkout commands", () => { |
| 113 | 116 | it("quotes shell metacharacters in Forgejo repository URLs", () => { |
| 114 | 117 | expect( |
| 115 | 118 | pullRequestCheckoutCommand( |
| 116 | "forgejo", | |
| 119 | SourceControlProviderKind.make("forgejo"), | |
| 117 | 120 | 42, |
| 118 | 121 | "feature", |
| 119 | 122 | null, |
| ... | ... | @@ -1714,7 +1717,7 @@ describe("cached pull request detail", () => { |
| 1714 | 1717 | const reference = { projectId: ProjectId.make("project-1"), repository: "acme/web", number: 7 }; |
| 1715 | 1718 | const detail = (overrides: Partial<PullRequestDetail> = {}): PullRequestDetail => |
| 1716 | 1719 | ({ |
| 1717 | provider: "github", | |
| 1720 | provider: SourceControlProviderKind.make("github"), | |
| 1718 | 1721 | capabilities: { |
| 1719 | 1722 | diff: true, |
| 1720 | 1723 | comment: true, |
| ... | ... | @@ -1797,7 +1800,7 @@ describe("cached pull request detail", () => { |
| 1797 | 1800 | remoteName: "origin", |
| 1798 | 1801 | remoteUrl: "https://github.com/acme/web.git", |
| 1799 | 1802 | }, |
| 1800 | provider: "github", | |
| 1803 | provider: SourceControlProviderKind.make("github"), | |
| 1801 | 1804 | }); |
| 1802 | 1805 | expect(readPullRequestDetailSnapshot(storage, "env-1", resolved)?.title).toBe( |
| 1803 | 1806 | "Cache the title", |
| ... | ... | @@ -1824,7 +1827,7 @@ describe("cached pull request detail", () => { |
| 1824 | 1827 | remoteName: "origin", |
| 1825 | 1828 | remoteUrl: "git@ssh.dev.azure.com:v3/org/project/web", |
| 1826 | 1829 | }, |
| 1827 | provider: "azure-devops", | |
| 1830 | provider: SourceControlProviderKind.make("azure-devops"), | |
| 1828 | 1831 | }), |
| 1829 | 1832 | ).toBe(reference); |
| 1830 | 1833 | expect(resolvePullRequestReferenceHost(reference, undefined)).toBe(reference); |
| ... | ... | @@ -1847,7 +1850,7 @@ describe("cached pull request detail", () => { |
| 1847 | 1850 | it("keeps Forgejo ports isolated when recovering legacy snapshots", () => { |
| 1848 | 1851 | const storage = makeStorage(); |
| 1849 | 1852 | const cached = detail({ |
| 1850 | provider: "forgejo", | |
| 1853 | provider: SourceControlProviderKind.make("forgejo"), | |
| 1851 | 1854 | url: "https://forge.example:8443/acme/web/pulls/7", |
| 1852 | 1855 | }); |
| 1853 | 1856 | writePullRequestDetailSnapshot(storage, "env-1", reference, cached); |
| ... | ... | @@ -1869,7 +1872,8 @@ describe("cached pull request detail", () => { |
| 1869 | 1872 | |
| 1870 | 1873 | it.each(["github", "gitlab"] as const)( |
| 1871 | 1874 | "retains portless %s snapshot identities for custom web ports", |
| 1872 | (provider) => { | |
| 1875 | (providerName) => { | |
| 1876 | const provider = SourceControlProviderKind.make(providerName); | |
| 1873 | 1877 | const storage = makeStorage(); |
| 1874 | 1878 | const host = `${provider}.example.com`; |
| 1875 | 1879 | const hosted = { ...reference, host }; |
apps/web/src/components/pullRequest/pullRequestDetail.logic.ts+9-5| ... | ... | @@ -21,7 +21,7 @@ import { |
| 21 | 21 | type PullRequestReviewThread, |
| 22 | 22 | type PullRequestState, |
| 23 | 23 | type PullRequestUpdateMethod, |
| 24 | type SourceControlProviderKind, | |
| 24 | SourceControlProviderKind, | |
| 25 | 25 | type ThreadLinkedPullRequest, |
| 26 | 26 | type ThreadPullRequestLink, |
| 27 | 27 | type VcsRef, |
| ... | ... | @@ -140,7 +140,7 @@ export function pullRequestCheckoutCommand( |
| 140 | 140 | } |
| 141 | 141 | return `git clone --single-branch --branch ${headBranch} https://bitbucket.org/${headRepositoryNameWithOwner}.git t3code-pr-${number}`; |
| 142 | 142 | } |
| 143 | case "unknown": | |
| 143 | default: | |
| 144 | 144 | return null; |
| 145 | 145 | } |
| 146 | 146 | } |
| ... | ... | @@ -155,10 +155,11 @@ export function loadingPullRequestCheckoutCommand( |
| 155 | 155 | identity?.provider ?? |
| 156 | 156 | (host === "github.com" ? "github" : host === "gitlab.com" ? "gitlab" : null); |
| 157 | 157 | if (provider !== "github" && provider !== "gitlab" && provider !== "azure-devops") return null; |
| 158 | if (identity?.provider !== undefined && host && pullRequestHostOf(identity, provider) !== host) { | |
| 158 | const kind = SourceControlProviderKind.make(provider); | |
| 159 | if (identity?.provider !== undefined && host && pullRequestHostOf(identity, kind) !== host) { | |
| 159 | 160 | return null; |
| 160 | 161 | } |
| 161 | return pullRequestCheckoutCommand(provider, reference.number, ""); | |
| 162 | return pullRequestCheckoutCommand(kind, reference.number, ""); | |
| 162 | 163 | } |
| 163 | 164 | |
| 164 | 165 | /** Activity changes only when the same host resource reports a newer revision. */ |
| ... | ... | @@ -1304,7 +1305,10 @@ export function resolvePullRequestReferenceHost( |
| 1304 | 1305 | ): PullRequestRef { |
| 1305 | 1306 | // Other providers may resolve an SSH remote to a different web authority on the server. |
| 1306 | 1307 | if (reference.host !== undefined || identity?.provider !== "github") return reference; |
| 1307 | return { ...reference, host: pullRequestHostOf(identity, "github") }; | |
| 1308 | return { | |
| 1309 | ...reference, | |
| 1310 | host: pullRequestHostOf(identity, SourceControlProviderKind.make("github")), | |
| 1311 | }; | |
| 1308 | 1312 | } |
| 1309 | 1313 | |
| 1310 | 1314 | export interface PullRequestDetailSnapshotRef { |
apps/web/src/components/pullRequest/pullRequestList.logic.test.ts+3-2| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | |
| 1 | 2 | import type { EnvironmentId, ProjectId, PullRequestListEntry } from "@t3tools/contracts"; |
| 2 | 3 | import { describe, expect, it } from "vite-plus/test"; |
| 3 | 4 | |
| ... | ... | @@ -330,7 +331,7 @@ describe("pull request involvement filtering", () => { |
| 330 | 331 | entry({ number: 1, author: { login: "Bilal", name: null, avatarUrl: null } }), |
| 331 | 332 | entry({ |
| 332 | 333 | number: 2, |
| 333 | provider: "gitlab", | |
| 334 | provider: SourceControlProviderKind.make("gitlab"), | |
| 334 | 335 | host: "gitlab.com", |
| 335 | 336 | author: { login: "Bilal", name: null, avatarUrl: null }, |
| 336 | 337 | }), |
| ... | ... | @@ -1322,7 +1323,7 @@ describe("merging the environments' own listings", () => { |
| 1322 | 1323 | providers: [ |
| 1323 | 1324 | { |
| 1324 | 1325 | host: "github.com", |
| 1325 | kind: "github", | |
| 1326 | kind: SourceControlProviderKind.make("github"), | |
| 1326 | 1327 | searchesOnHost: false, |
| 1327 | 1328 | projectCount: 2, |
| 1328 | 1329 | configured: false, |
apps/web/src/components/settings/SourceControlSettings.tsx+1-1| ... | ... | @@ -78,7 +78,7 @@ const EMPTY_DISCOVERY_RESULT: SourceControlDiscoveryResult = { |
| 78 | 78 | sourceControlProviders: [], |
| 79 | 79 | }; |
| 80 | 80 | |
| 81 | const SOURCE_CONTROL_PROVIDER_ICONS: Partial<Record<SourceControlProviderKind, Icon>> = { | |
| 81 | const SOURCE_CONTROL_PROVIDER_ICONS: Partial<Record<string, Icon>> = { | |
| 82 | 82 | github: GitHubIcon, |
| 83 | 83 | gitlab: GitLabIcon, |
| 84 | 84 | forgejo: ForgejoIcon, |
apps/web/src/hooks/useNewProject.ts+2-1| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | |
| 1 | 2 | import { scopeProjectRef } from "@t3tools/client-runtime/environment"; |
| 2 | 3 | import { getNewProjectGitHubRepository } from "@t3tools/client-runtime/operations/projects"; |
| 3 | 4 | import { |
| ... | ... | @@ -45,7 +46,7 @@ export function useNewProject() { |
| 45 | 46 | environmentId: input.environmentId, |
| 46 | 47 | input: { |
| 47 | 48 | cwd: input.workspaceRoot, |
| 48 | provider: "github", | |
| 49 | provider: SourceControlProviderKind.make("github"), | |
| 49 | 50 | repository: getNewProjectGitHubRepository(input, input.workspaceRoot), |
| 50 | 51 | visibility: "private", |
| 51 | 52 | }, |
apps/web/src/state/pullRequests.test.ts+2-1| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | |
| 1 | 2 | import { ProjectId, type PullRequestSummary } from "@t3tools/contracts"; |
| 2 | 3 | import { describe, expect, it } from "vite-plus/test"; |
| 3 | 4 | |
| ... | ... | @@ -5,7 +6,7 @@ import { newestPullRequestObservation, newestPullRequestSummary } from "./pullRe |
| 5 | 6 | |
| 6 | 7 | function summary(overrides: Partial<PullRequestSummary> = {}): PullRequestSummary { |
| 7 | 8 | return { |
| 8 | provider: "github", | |
| 9 | provider: SourceControlProviderKind.make("github"), | |
| 9 | 10 | projectId: ProjectId.make("pull-request-cache-test"), |
| 10 | 11 | repository: "acme/widget", |
| 11 | 12 | number: 7, |
apps/web/src/state/sourceControlActions.test.ts+2-1| ... | ... | @@ -3,6 +3,7 @@ import { |
| 3 | 3 | AuthSourceControlWriteScope, |
| 4 | 4 | EnvironmentId, |
| 5 | 5 | ThreadId, |
| 6 | SourceControlProviderKind, | |
| 6 | 7 | } from "@t3tools/contracts"; |
| 7 | 8 | import * as Cause from "effect/Cause"; |
| 8 | 9 | import { AsyncResult } from "effect/reactivity"; |
| ... | ... | @@ -98,7 +99,7 @@ const cases = [ |
| 98 | 99 | isAllowed: action.isAllowed, |
| 99 | 100 | run: () => |
| 100 | 101 | action.run({ |
| 101 | provider: "github", | |
| 102 | provider: SourceControlProviderKind.make("github"), | |
| 102 | 103 | repository: "owner/repo", |
| 103 | 104 | visibility: "private", |
| 104 | 105 | remoteName: "origin", |
apps/web/src/state/sourceControlActions.ts+2-1| ... | ... | @@ -16,6 +16,7 @@ import { |
| 16 | 16 | type EnvironmentId, |
| 17 | 17 | type GitResolvePullRequestResult, |
| 18 | 18 | type SourceControlCloneProtocol, |
| 19 | type SourceControlProviderKind, | |
| 19 | 20 | type SourceControlRepositoryVisibility, |
| 20 | 21 | type ThreadId, |
| 21 | 22 | } from "@t3tools/contracts"; |
| ... | ... | @@ -275,7 +276,7 @@ export function useSourceControlPublishRepositoryAction(scope: SourceControlActi |
| 275 | 276 | ); |
| 276 | 277 | const action = useCallback( |
| 277 | 278 | async (input: { |
| 278 | provider: "github" | "gitlab" | "forgejo" | "bitbucket" | "azure-devops" | "gitcafe"; | |
| 279 | provider: SourceControlProviderKind; | |
| 279 | 280 | repository: string; |
| 280 | 281 | visibility: SourceControlRepositoryVisibility; |
| 281 | 282 | remoteName: string; |
packages/client-runtime/src/operations/projects.test.ts+6-5| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | |
| 1 | 2 | import { describe, expect, it } from "vite-plus/test"; |
| 2 | 3 | import { |
| 3 | 4 | EnvironmentId, |
| ... | ... | @@ -71,14 +72,14 @@ describe("add project shared logic", () => { |
| 71 | 72 | it("uses HTTPS for repositories selected through a provider", () => { |
| 72 | 73 | expect( |
| 73 | 74 | getDefaultCloneUrl({ |
| 74 | provider: "github", | |
| 75 | provider: SourceControlProviderKind.make("github"), | |
| 75 | 76 | url: "https://github.com/imputnet/helium", |
| 76 | 77 | sshUrl: "git@github.com:imputnet/helium.git", |
| 77 | 78 | }), |
| 78 | 79 | ).toBe("https://github.com/imputnet/helium"); |
| 79 | 80 | expect( |
| 80 | 81 | getDefaultCloneUrl({ |
| 81 | provider: "forgejo", | |
| 82 | provider: SourceControlProviderKind.make("forgejo"), | |
| 82 | 83 | url: "https://forgejo.example.test:8443/owner/repo.git", |
| 83 | 84 | sshUrl: "ssh://git@forgejo.example.test:2222/owner/repo.git", |
| 84 | 85 | }), |
| ... | ... | @@ -88,7 +89,7 @@ describe("add project shared logic", () => { |
| 88 | 89 | it("preserves existing clone transport behavior for other providers", () => { |
| 89 | 90 | expect( |
| 90 | 91 | getDefaultCloneUrl({ |
| 91 | provider: "gitlab", | |
| 92 | provider: SourceControlProviderKind.make("gitlab"), | |
| 92 | 93 | url: "https://gitlab.com/group/project.git", |
| 93 | 94 | sshUrl: "git@gitlab.com:group/project.git", |
| 94 | 95 | }), |
| ... | ... | @@ -187,7 +188,7 @@ describe("add project shared logic", () => { |
| 187 | 188 | versionControlSystems: [], |
| 188 | 189 | sourceControlProviders: [ |
| 189 | 190 | { |
| 190 | kind: "github", | |
| 191 | kind: SourceControlProviderKind.make("github"), | |
| 191 | 192 | label: "GitHub", |
| 192 | 193 | status: "available", |
| 193 | 194 | installHint: "Install gh", |
| ... | ... | @@ -201,7 +202,7 @@ describe("add project shared logic", () => { |
| 201 | 202 | }, |
| 202 | 203 | }, |
| 203 | 204 | { |
| 204 | kind: "gitlab", | |
| 205 | kind: SourceControlProviderKind.make("gitlab"), | |
| 205 | 206 | label: "GitLab", |
| 206 | 207 | status: "available", |
| 207 | 208 | installHint: "Install glab", |
packages/client-runtime/src/operations/projects.ts+8-6| ... | ... | @@ -6,9 +6,9 @@ import type { |
| 6 | 6 | ProjectId, |
| 7 | 7 | ServerConfig, |
| 8 | 8 | SourceControlDiscoveryResult, |
| 9 | SourceControlProviderKind, | |
| 10 | 9 | SourceControlRepositoryInfo, |
| 11 | 10 | } from "@t3tools/contracts"; |
| 11 | import { SourceControlProviderKind } from "@t3tools/contracts"; | |
| 12 | 12 | import { newProjectFolderName } from "@t3tools/shared/path"; |
| 13 | 13 | import * as Arr from "effect/Array"; |
| 14 | 14 | import * as Option from "effect/Option"; |
| ... | ... | @@ -25,10 +25,12 @@ import { |
| 25 | 25 | } from "../state/projects.ts"; |
| 26 | 26 | import type { EnvironmentProject } from "../state/models.ts"; |
| 27 | 27 | |
| 28 | export type AddProjectRemoteProviderKind = Extract< | |
| 29 | SourceControlProviderKind, | |
| 30 | "github" | "gitlab" | "forgejo" | "bitbucket" | "azure-devops" | |
| 31 | >; | |
| 28 | export type AddProjectRemoteProviderKind = | |
| 29 | | "github" | |
| 30 | | "gitlab" | |
| 31 | | "forgejo" | |
| 32 | | "bitbucket" | |
| 33 | | "azure-devops"; | |
| 32 | 34 | export type AddProjectRemoteSource = AddProjectRemoteProviderKind | "url"; |
| 33 | 35 | |
| 34 | 36 | export function canCreateProjectInEnvironment( |
| ... | ... | @@ -190,7 +192,7 @@ export function buildAddProjectRemoteSourceReadiness( |
| 190 | 192 | for (const source of ADD_PROJECT_REMOTE_SOURCES) { |
| 191 | 193 | const kind = addProjectRemoteSourceProvider(source); |
| 192 | 194 | if (!kind) continue; |
| 193 | const provider = providerByKind.get(kind); | |
| 195 | const provider = providerByKind.get(SourceControlProviderKind.make(kind)); | |
| 194 | 196 | if (!provider) { |
| 195 | 197 | readiness[source] = unavailable; |
| 196 | 198 | continue; |
packages/client-runtime/src/state/sourceControl.test.ts+4-3| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | |
| 1 | 2 | import { vi } from "vite-plus/test"; |
| 2 | 3 | import { |
| 3 | 4 | EnvironmentId, |
| ... | ... | @@ -36,7 +37,7 @@ const TARGET = new PrimaryConnectionTarget({ |
| 36 | 37 | |
| 37 | 38 | const PUBLISH_RESULT: SourceControlPublishRepositoryResult = { |
| 38 | 39 | repository: { |
| 39 | provider: "github", | |
| 40 | provider: SourceControlProviderKind.make("github"), | |
| 40 | 41 | nameWithOwner: "t3tools/t3code", |
| 41 | 42 | url: "https://github.com/t3tools/t3code", |
| 42 | 43 | sshUrl: "git@github.com:t3tools/t3code.git", |
| ... | ... | @@ -140,7 +141,7 @@ describe("source control environment atoms", () => { |
| 140 | 141 | environmentId: TARGET.environmentId, |
| 141 | 142 | input: { |
| 142 | 143 | cwd: "/repo", |
| 143 | provider: "github", | |
| 144 | provider: SourceControlProviderKind.make("github"), | |
| 144 | 145 | repository: "t3tools/t3code", |
| 145 | 146 | visibility: "private", |
| 146 | 147 | }, |
| ... | ... | @@ -156,7 +157,7 @@ describe("source control environment atoms", () => { |
| 156 | 157 | environmentId: TARGET.environmentId, |
| 157 | 158 | input: { |
| 158 | 159 | cwd: "/repo", |
| 159 | provider: "github", | |
| 160 | provider: SourceControlProviderKind.make("github"), | |
| 160 | 161 | repository: "t3tools/t3code", |
| 161 | 162 | visibility: "private", |
| 162 | 163 | }, |
packages/contracts/src/pullRequest.test.ts+11-6| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "./sourceControl.ts"; | |
| 1 | 2 | import * as Schema from "effect/Schema"; |
| 2 | 3 | import { describe, expect, it } from "vite-plus/test"; |
| 3 | 4 | |
| ... | ... | @@ -25,7 +26,7 @@ const LIST_RESULT: PullRequestListResult = { |
| 25 | 26 | providers: [ |
| 26 | 27 | { |
| 27 | 28 | host: "github.com", |
| 28 | kind: "github", | |
| 29 | kind: SourceControlProviderKind.make("github"), | |
| 29 | 30 | searchesOnHost: true, |
| 30 | 31 | projectCount: 1, |
| 31 | 32 | configured: true, |
| ... | ... | @@ -33,7 +34,7 @@ const LIST_RESULT: PullRequestListResult = { |
| 33 | 34 | }, |
| 34 | 35 | { |
| 35 | 36 | host: "gitlab.com", |
| 36 | kind: "gitlab", | |
| 37 | kind: SourceControlProviderKind.make("gitlab"), | |
| 37 | 38 | searchesOnHost: true, |
| 38 | 39 | projectCount: 1, |
| 39 | 40 | configured: false, |
| ... | ... | @@ -42,7 +43,7 @@ const LIST_RESULT: PullRequestListResult = { |
| 42 | 43 | ], |
| 43 | 44 | entries: [ |
| 44 | 45 | { |
| 45 | provider: "github", | |
| 46 | provider: SourceControlProviderKind.make("github"), | |
| 46 | 47 | host: "github.com", |
| 47 | 48 | projectId: "project-1" as PullRequestListResult["entries"][number]["projectId"], |
| 48 | 49 | projectTitle: "t3code", |
| ... | ... | @@ -75,12 +76,16 @@ describe("PullRequestListResult", () => { |
| 75 | 76 | canonicalKey: "forge.example/team/repo", |
| 76 | 77 | locator: { remoteUrl: "http://forge.example:3000/team/repo.git" }, |
| 77 | 78 | }; |
| 78 | expect(pullRequestHostOf(identity, "forgejo")).toBe("forge.example:3000"); | |
| 79 | expect(pullRequestHostOf(identity, "gitlab")).toBe("forge.example"); | |
| 79 | expect(pullRequestHostOf(identity, SourceControlProviderKind.make("forgejo"))).toBe( | |
| 80 | "forge.example:3000", | |
| 81 | ); | |
| 82 | expect(pullRequestHostOf(identity, SourceControlProviderKind.make("gitlab"))).toBe( | |
| 83 | "forge.example", | |
| 84 | ); | |
| 80 | 85 | expect( |
| 81 | 86 | pullRequestHostOf( |
| 82 | 87 | { ...identity, locator: { remoteUrl: "ssh://git@forge.example:2222/team/repo.git" } }, |
| 83 | "forgejo", | |
| 88 | SourceControlProviderKind.make("forgejo"), | |
| 84 | 89 | ), |
| 85 | 90 | ).toBe("forge.example"); |
| 86 | 91 | }); |
packages/contracts/src/pullRequest.ts+50-34| ... | ... | @@ -1301,38 +1301,54 @@ export type PullRequestUnavailableReason = typeof PullRequestUnavailableReason.T |
| 1301 | 1301 | * symptom. The reason names keep their `cli-` prefix for wire compatibility; for GitHub and |
| 1302 | 1302 | * Bitbucket they mean "no credential" and "a refused credential", not a missing tool. |
| 1303 | 1303 | */ |
| 1304 | const PROVIDER_REQUIREMENT: Partial< | |
| 1305 | Record<SourceControlProviderKind, { readonly missing: string; readonly unauthenticated: string }> | |
| 1306 | > = { | |
| 1307 | github: { | |
| 1308 | missing: | |
| 1309 | "No GitHub credential on the server. Set GH_TOKEN, or install the GitHub CLI (https://cli.github.com/) and run `gh auth login`.", | |
| 1310 | unauthenticated: | |
| 1311 | "GitHub has no working credential for this host. Run `gh auth login`, or check the account and hosts in Settings → Source Control.", | |
| 1312 | }, | |
| 1313 | forgejo: { | |
| 1314 | missing: | |
| 1315 | "Install Forgejo CLI (`fj` 0.6 or later) from https://codeberg.org/forgejo-contrib/forgejo-cli or Gitea CLI (`tea` 0.16 or later) from https://gitea.com/gitea/tea to browse Forgejo pull requests.", | |
| 1316 | unauthenticated: | |
| 1317 | "Authenticate your Forgejo or Gitea server with `fj --host <server-url> auth add-token` on the T3 Code server. If fj is missing or unconfigured for that server, use `tea login add`. A configured fj account must be repaired with fj.", | |
| 1318 | }, | |
| 1319 | gitlab: { | |
| 1320 | missing: | |
| 1321 | "GitLab CLI (`glab`) is required to browse change requests on this host. Install it from https://gitlab.com/gitlab-org/cli and reload.", | |
| 1322 | unauthenticated: "GitLab CLI is not authenticated. Run `glab auth login` and retry.", | |
| 1323 | }, | |
| 1324 | "azure-devops": { | |
| 1325 | missing: | |
| 1326 | "Azure CLI (`az`) with the Azure DevOps extension is required. Install `az`, then run `az extension add --name azure-devops`.", | |
| 1327 | unauthenticated: "Azure CLI is not signed in. Run `az login` and retry.", | |
| 1328 | }, | |
| 1329 | bitbucket: { | |
| 1330 | missing: | |
| 1331 | "Bitbucket needs API credentials on the server. Add them in Settings → Source Control.", | |
| 1332 | unauthenticated: | |
| 1333 | "Bitbucket rejected the configured credentials. Check them in Settings → Source Control.", | |
| 1334 | }, | |
| 1335 | }; | |
| 1304 | const PROVIDER_REQUIREMENT = new Map< | |
| 1305 | string, | |
| 1306 | { readonly missing: string; readonly unauthenticated: string } | |
| 1307 | >([ | |
| 1308 | [ | |
| 1309 | "github", | |
| 1310 | { | |
| 1311 | missing: | |
| 1312 | "No GitHub credential on the server. Set GH_TOKEN, or install the GitHub CLI (https://cli.github.com/) and run `gh auth login`.", | |
| 1313 | unauthenticated: | |
| 1314 | "GitHub has no working credential for this host. Run `gh auth login`, or check the account and hosts in Settings → Source Control.", | |
| 1315 | }, | |
| 1316 | ], | |
| 1317 | [ | |
| 1318 | "forgejo", | |
| 1319 | { | |
| 1320 | missing: | |
| 1321 | "Install Forgejo CLI (`fj` 0.6 or later) from https://codeberg.org/forgejo-contrib/forgejo-cli or Gitea CLI (`tea` 0.16 or later) from https://gitea.com/gitea/tea to browse Forgejo pull requests.", | |
| 1322 | unauthenticated: | |
| 1323 | "Authenticate your Forgejo or Gitea server with `fj --host <server-url> auth add-token` on the T3 Code server. If fj is missing or unconfigured for that server, use `tea login add`. A configured fj account must be repaired with fj.", | |
| 1324 | }, | |
| 1325 | ], | |
| 1326 | [ | |
| 1327 | "gitlab", | |
| 1328 | { | |
| 1329 | missing: | |
| 1330 | "GitLab CLI (`glab`) is required to browse change requests on this host. Install it from https://gitlab.com/gitlab-org/cli and reload.", | |
| 1331 | unauthenticated: "GitLab CLI is not authenticated. Run `glab auth login` and retry.", | |
| 1332 | }, | |
| 1333 | ], | |
| 1334 | [ | |
| 1335 | "azure-devops", | |
| 1336 | { | |
| 1337 | missing: | |
| 1338 | "Azure CLI (`az`) with the Azure DevOps extension is required. Install `az`, then run `az extension add --name azure-devops`.", | |
| 1339 | unauthenticated: "Azure CLI is not signed in. Run `az login` and retry.", | |
| 1340 | }, | |
| 1341 | ], | |
| 1342 | [ | |
| 1343 | "bitbucket", | |
| 1344 | { | |
| 1345 | missing: | |
| 1346 | "Bitbucket needs API credentials on the server. Add them in Settings → Source Control.", | |
| 1347 | unauthenticated: | |
| 1348 | "Bitbucket rejected the configured credentials. Check them in Settings → Source Control.", | |
| 1349 | }, | |
| 1350 | ], | |
| 1351 | ]); | |
| 1336 | 1352 | |
| 1337 | 1353 | /** |
| 1338 | 1354 | * The host a project's repository is addressed below. `canonicalKey` is the normalized remote, |
| ... | ... | @@ -1394,7 +1410,7 @@ export function pullRequestProviderRequirement( |
| 1394 | 1410 | provider: SourceControlProviderKind, |
| 1395 | 1411 | reason: PullRequestUnavailableReason, |
| 1396 | 1412 | ): string | null { |
| 1397 | const requirement = PROVIDER_REQUIREMENT[provider]; | |
| 1413 | const requirement = PROVIDER_REQUIREMENT.get(provider); | |
| 1398 | 1414 | if (requirement === undefined) return null; |
| 1399 | 1415 | switch (reason) { |
| 1400 | 1416 | case "cli-missing": |
| ... | ... | @@ -1427,7 +1443,7 @@ export class PullRequestUnavailableError extends Schema.TaggedError<PullRequestU |
| 1427 | 1443 | |
| 1428 | 1444 | override get message(): string { |
| 1429 | 1445 | const requirement = |
| 1430 | this.provider === undefined ? undefined : PROVIDER_REQUIREMENT[this.provider]; | |
| 1446 | this.provider === undefined ? undefined : PROVIDER_REQUIREMENT.get(this.provider); | |
| 1431 | 1447 | switch (this.reason) { |
| 1432 | 1448 | case "cli-missing": |
| 1433 | 1449 | return ( |
packages/contracts/src/sourceControl.ts+11-9| ... | ... | @@ -2,15 +2,17 @@ import * as Schema from "effect/Schema"; |
| 2 | 2 | import { PositiveInt, TrimmedNonEmptyString } from "./baseSchemas.ts"; |
| 3 | 3 | import { VcsDriverKind } from "./vcs.ts"; |
| 4 | 4 | |
| 5 | export const SourceControlProviderKind = Schema.Literals([ | |
| 6 | "github", | |
| 7 | "gitlab", | |
| 8 | "forgejo", | |
| 9 | "azure-devops", | |
| 10 | "bitbucket", | |
| 11 | "gitcafe", | |
| 12 | "unknown", | |
| 13 | ]); | |
| 5 | /** | |
| 6 | * `SourceControlProviderKind` — open branded slug naming a source control host, such as `github`. | |
| 7 | * | |
| 8 | * Open on purpose, like `ProviderDriverKind`: a server can report a host this client does not | |
| 9 | * ship a definition for, and clients fall back to a generic presentation instead of failing to | |
| 10 | * decode. `unknown` is what the server reports for a remote it cannot attribute to any host. | |
| 11 | */ | |
| 12 | export const SourceControlProviderKind = TrimmedNonEmptyString.check( | |
| 13 | Schema.isMaxLength(64), | |
| 14 | Schema.isPattern(/^[a-z][a-z0-9-]*$/), | |
| 15 | ).pipe(Schema.brand("SourceControlProviderKind")); | |
| 14 | 16 | export type SourceControlProviderKind = typeof SourceControlProviderKind.Type; |
| 15 | 17 | |
| 16 | 18 | export const SourceControlProviderInfo = Schema.Struct({ |
packages/shared/src/git.test.ts+2-1| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | |
| 1 | 2 | import type { VcsStatusRemoteResult, VcsStatusResult } from "@t3tools/contracts"; |
| 2 | 3 | import { describe, expect, it } from "vite-plus/test"; |
| 3 | 4 | |
| ... | ... | @@ -257,7 +258,7 @@ describe("applyGitStatusStreamEvent", () => { |
| 257 | 258 | const current: VcsStatusResult = { |
| 258 | 259 | isRepo: true, |
| 259 | 260 | sourceControlProvider: { |
| 260 | kind: "github", | |
| 261 | kind: SourceControlProviderKind.make("github"), | |
| 261 | 262 | name: "GitHub", |
| 262 | 263 | baseUrl: "https://github.com", |
| 263 | 264 | }, |
packages/shared/src/sourceControl.test.ts+18-9| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | |
| 1 | 2 | import { describe, expect, it } from "vite-plus/test"; |
| 2 | 3 | |
| 3 | 4 | import { |
| ... | ... | @@ -10,30 +11,38 @@ import { |
| 10 | 11 | |
| 11 | 12 | describe("source control presentation", () => { |
| 12 | 13 | it("uses merge request terminology for GitLab", () => { |
| 13 | expect(getChangeRequestTerminologyForKind("gitlab")).toEqual({ | |
| 14 | expect(getChangeRequestTerminologyForKind(SourceControlProviderKind.make("gitlab"))).toEqual({ | |
| 14 | 15 | shortLabel: "MR", |
| 15 | 16 | singular: "merge request", |
| 16 | 17 | }); |
| 17 | 18 | }); |
| 18 | 19 | |
| 19 | 20 | it("uses pull request terminology for GitHub-compatible providers", () => { |
| 20 | expect(getChangeRequestTerminologyForKind("github")).toEqual({ | |
| 21 | expect(getChangeRequestTerminologyForKind(SourceControlProviderKind.make("github"))).toEqual({ | |
| 21 | 22 | shortLabel: "PR", |
| 22 | 23 | singular: "pull request", |
| 23 | 24 | }); |
| 24 | expect(getChangeRequestTerminologyForKind("azure-devops")).toEqual({ | |
| 25 | shortLabel: "PR", | |
| 26 | singular: "pull request", | |
| 27 | }); | |
| 28 | expect(getChangeRequestTerminologyForKind("bitbucket")).toEqual({ | |
| 25 | expect( | |
| 26 | getChangeRequestTerminologyForKind(SourceControlProviderKind.make("azure-devops")), | |
| 27 | ).toEqual({ | |
| 29 | 28 | shortLabel: "PR", |
| 30 | 29 | singular: "pull request", |
| 31 | 30 | }); |
| 31 | expect(getChangeRequestTerminologyForKind(SourceControlProviderKind.make("bitbucket"))).toEqual( | |
| 32 | { | |
| 33 | shortLabel: "PR", | |
| 34 | singular: "pull request", | |
| 35 | }, | |
| 36 | ); | |
| 32 | 37 | }); |
| 33 | 38 | |
| 34 | 39 | it("falls back to generic change request copy for unknown providers", () => { |
| 35 | 40 | expect( |
| 36 | resolveChangeRequestPresentation({ kind: "unknown", name: "forge", baseUrl: "" }), | |
| 41 | resolveChangeRequestPresentation({ | |
| 42 | kind: SourceControlProviderKind.make("unknown"), | |
| 43 | name: "forge", | |
| 44 | baseUrl: "", | |
| 45 | }), | |
| 37 | 46 | ).toEqual( |
| 38 | 47 | expect.objectContaining({ |
| 39 | 48 | shortName: "change request", |
| ... | ... | @@ -69,7 +78,7 @@ describe("detectSourceControlProviderFromRemoteUrl", () => { |
| 69 | 78 | }, |
| 70 | 79 | ); |
| 71 | 80 | } |
| 72 | expect(getChangeRequestTerminologyForKind("forgejo")).toEqual({ | |
| 81 | expect(getChangeRequestTerminologyForKind(SourceControlProviderKind.make("forgejo"))).toEqual({ | |
| 73 | 82 | shortLabel: "PR", |
| 74 | 83 | singular: "pull request", |
| 75 | 84 | }); |
packages/shared/src/sourceControl.ts+14-13| ... | ... | @@ -1,8 +1,5 @@ |
| 1 | import type { | |
| 2 | RepositoryIdentity, | |
| 3 | SourceControlProviderInfo, | |
| 4 | SourceControlProviderKind, | |
| 5 | } from "@t3tools/contracts"; | |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | |
| 2 | import type { RepositoryIdentity, SourceControlProviderInfo } from "@t3tools/contracts"; | |
| 6 | 3 | |
| 7 | 4 | export interface ChangeRequestPresentation { |
| 8 | 5 | readonly icon: |
| ... | ... | @@ -124,7 +121,7 @@ export function resolveChangeRequestPresentation( |
| 124 | 121 | return BITBUCKET_CHANGE_REQUEST_PRESENTATION; |
| 125 | 122 | case "gitcafe": |
| 126 | 123 | return GITCAFE_CHANGE_REQUEST_PRESENTATION; |
| 127 | case "unknown": | |
| 124 | default: | |
| 128 | 125 | return GENERIC_CHANGE_REQUEST_PRESENTATION; |
| 129 | 126 | } |
| 130 | 127 | } |
| ... | ... | @@ -244,7 +241,11 @@ export function detectSourceControlProviderFromRemoteUrl( |
| 244 | 241 | const hostname = parseHostName(host); |
| 245 | 242 | |
| 246 | 243 | if (isGitCafeHost(hostname)) { |
| 247 | return { kind: "gitcafe", name: "GitCafe", baseUrl: toBaseUrl(hostname) }; | |
| 244 | return { | |
| 245 | kind: SourceControlProviderKind.make("gitcafe"), | |
| 246 | name: "GitCafe", | |
| 247 | baseUrl: toBaseUrl(hostname), | |
| 248 | }; | |
| 248 | 249 | } |
| 249 | 250 | |
| 250 | 251 | if ( |
| ... | ... | @@ -253,7 +254,7 @@ export function detectSourceControlProviderFromRemoteUrl( |
| 253 | 254 | hasDnsLabel(hostname, "gitea") |
| 254 | 255 | ) { |
| 255 | 256 | return { |
| 256 | kind: "forgejo", | |
| 257 | kind: SourceControlProviderKind.make("forgejo"), | |
| 257 | 258 | name: "Forgejo", |
| 258 | 259 | baseUrl: /^https?:/iu.test(remoteUrl.trim()) |
| 259 | 260 | ? new URL(remoteUrl.trim()).origin |
| ... | ... | @@ -263,7 +264,7 @@ export function detectSourceControlProviderFromRemoteUrl( |
| 263 | 264 | |
| 264 | 265 | if (isGitHubHost(hostname)) { |
| 265 | 266 | return { |
| 266 | kind: "github", | |
| 267 | kind: SourceControlProviderKind.make("github"), | |
| 267 | 268 | name: hostname === "github.com" ? "GitHub" : "GitHub Self-Hosted", |
| 268 | 269 | baseUrl: toBaseUrl(host), |
| 269 | 270 | }; |
| ... | ... | @@ -271,7 +272,7 @@ export function detectSourceControlProviderFromRemoteUrl( |
| 271 | 272 | |
| 272 | 273 | if (isGitLabHost(hostname)) { |
| 273 | 274 | return { |
| 274 | kind: "gitlab", | |
| 275 | kind: SourceControlProviderKind.make("gitlab"), | |
| 275 | 276 | name: hostname === "gitlab.com" ? "GitLab" : "GitLab Self-Hosted", |
| 276 | 277 | baseUrl: toBaseUrl(host), |
| 277 | 278 | }; |
| ... | ... | @@ -279,7 +280,7 @@ export function detectSourceControlProviderFromRemoteUrl( |
| 279 | 280 | |
| 280 | 281 | if (isAzureDevOpsHost(hostname)) { |
| 281 | 282 | return { |
| 282 | kind: "azure-devops", | |
| 283 | kind: SourceControlProviderKind.make("azure-devops"), | |
| 283 | 284 | name: "Azure DevOps", |
| 284 | 285 | baseUrl: toBaseUrl(host), |
| 285 | 286 | }; |
| ... | ... | @@ -287,14 +288,14 @@ export function detectSourceControlProviderFromRemoteUrl( |
| 287 | 288 | |
| 288 | 289 | if (isBitbucketHost(hostname)) { |
| 289 | 290 | return { |
| 290 | kind: "bitbucket", | |
| 291 | kind: SourceControlProviderKind.make("bitbucket"), | |
| 291 | 292 | name: hostname === "bitbucket.org" ? "Bitbucket" : "Bitbucket Self-Hosted", |
| 292 | 293 | baseUrl: toBaseUrl(host), |
| 293 | 294 | }; |
| 294 | 295 | } |
| 295 | 296 | |
| 296 | 297 | return { |
| 297 | kind: "unknown", | |
| 298 | kind: SourceControlProviderKind.make("unknown"), | |
| 298 | 299 | name: host, |
| 299 | 300 | baseUrl: toBaseUrl(host), |
| 300 | 301 | }; |
packages/source-control-azure-devops/src/server/AzureDevOpsPullRequestProvider.ts+5-4| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | |
| 1 | 2 | import * as Effect from "effect/Effect"; |
| 2 | 3 | import * as Semaphore from "effect/Semaphore"; |
| 3 | 4 | import type { PullRequestCapabilities, PullRequestViewerPermissions } from "@t3tools/contracts"; |
| ... | ... | @@ -157,7 +158,7 @@ export const make = Effect.gen(function* () { |
| 157 | 158 | const fail = |
| 158 | 159 | (operation: string) => (error: AzureDevOpsPullRequestCli.AzureDevOpsPullRequestCliError) => |
| 159 | 160 | new PullRequestProviderError({ |
| 160 | provider: "azure-devops", | |
| 161 | provider: SourceControlProviderKind.make("azure-devops"), | |
| 161 | 162 | operation, |
| 162 | 163 | ...azureDevOpsProviderFailure(error), |
| 163 | 164 | detail: error.detail, |
| ... | ... | @@ -168,7 +169,7 @@ export const make = Effect.gen(function* () { |
| 168 | 169 | const unsupported = (operation: string) => |
| 169 | 170 | Effect.fail( |
| 170 | 171 | new PullRequestProviderError({ |
| 171 | provider: "azure-devops", | |
| 172 | provider: SourceControlProviderKind.make("azure-devops"), | |
| 172 | 173 | operation, |
| 173 | 174 | reason: "failed", |
| 174 | 175 | detail: "Azure DevOps reviews cannot be written from here yet.", |
| ... | ... | @@ -296,7 +297,7 @@ export const make = Effect.gen(function* () { |
| 296 | 297 | }; |
| 297 | 298 | |
| 298 | 299 | const provider: PullRequestProviderApi = { |
| 299 | kind: "azure-devops", | |
| 300 | kind: SourceControlProviderKind.make("azure-devops"), | |
| 300 | 301 | capabilities: CAPABILITIES, |
| 301 | 302 | // The bare repository name repeats across an organization; the project path does not. |
| 302 | 303 | repositoryKey: ({ canonicalKey }) => canonicalKey, |
| ... | ... | @@ -612,7 +613,7 @@ export const make = Effect.gen(function* () { |
| 612 | 613 | listReviewerCandidates: () => |
| 613 | 614 | Effect.fail( |
| 614 | 615 | new PullRequestProviderError({ |
| 615 | provider: "azure-devops", | |
| 616 | provider: SourceControlProviderKind.make("azure-devops"), | |
| 616 | 617 | operation: "listReviewerCandidates", |
| 617 | 618 | reason: "failed", |
| 618 | 619 | detail: "Azure DevOps cannot say who may review a pull request.", |
packages/source-control-azure-devops/src/server/AzureDevOpsSourceControlProvider.test.ts+2-1| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | |
| 1 | 2 | import { assert, it } from "@effect/vitest"; |
| 2 | 3 | import * as Effect from "effect/Effect"; |
| 3 | 4 | import * as Layer from "effect/Layer"; |
| ... | ... | @@ -35,7 +36,7 @@ it.effect("maps Azure DevOps PR summaries into provider-neutral change requests" |
| 35 | 36 | }); |
| 36 | 37 | |
| 37 | 38 | assert.deepStrictEqual(changeRequest, { |
| 38 | provider: "azure-devops", | |
| 39 | provider: SourceControlProviderKind.make("azure-devops"), | |
| 39 | 40 | number: 42, |
| 40 | 41 | title: "Add Azure provider", |
| 41 | 42 | url: "https://dev.azure.com/acme/project/_git/repo/pullrequest/42", |
packages/source-control-azure-devops/src/server/AzureDevOpsSourceControlProvider.ts+11-10| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | |
| 1 | 2 | import * as Effect from "effect/Effect"; |
| 2 | 3 | import { SourceControlProviderError, type ChangeRequest } from "@t3tools/contracts"; |
| 3 | 4 | |
| ... | ... | @@ -39,7 +40,7 @@ function parseAzureAuth(input: SourceControlAuthProbeInput) { |
| 39 | 40 | |
| 40 | 41 | export const discovery = { |
| 41 | 42 | type: "cli", |
| 42 | kind: "azure-devops", | |
| 43 | kind: SourceControlProviderKind.make("azure-devops"), | |
| 43 | 44 | label: "Azure DevOps", |
| 44 | 45 | executable: "az", |
| 45 | 46 | versionArgs: ["--version"], |
| ... | ... | @@ -67,7 +68,7 @@ function toChangeRequest(summary: { |
| 67 | 68 | readonly updatedAt: ChangeRequest["updatedAt"]; |
| 68 | 69 | }): ChangeRequest { |
| 69 | 70 | return { |
| 70 | provider: "azure-devops", | |
| 71 | provider: SourceControlProviderKind.make("azure-devops"), | |
| 71 | 72 | number: summary.number, |
| 72 | 73 | title: summary.title, |
| 73 | 74 | url: summary.url, |
| ... | ... | @@ -86,7 +87,7 @@ export const make = Effect.gen(function* () { |
| 86 | 87 | const azure = yield* AzureDevOpsCli.AzureDevOpsCli; |
| 87 | 88 | |
| 88 | 89 | return SourceControlProvider.SourceControlProvider.of({ |
| 89 | kind: "azure-devops", | |
| 90 | kind: SourceControlProviderKind.make("azure-devops"), | |
| 90 | 91 | listChangeRequests: (input) => { |
| 91 | 92 | const source = SourceControlProvider.sourceControlRefFromInput(input); |
| 92 | 93 | return azure |
| ... | ... | @@ -102,7 +103,7 @@ export const make = Effect.gen(function* () { |
| 102 | 103 | Effect.mapError( |
| 103 | 104 | (error) => |
| 104 | 105 | new SourceControlProviderError({ |
| 105 | provider: "azure-devops", | |
| 106 | provider: SourceControlProviderKind.make("azure-devops"), | |
| 106 | 107 | operation: "listChangeRequests", |
| 107 | 108 | command: error.command, |
| 108 | 109 | cwd: input.cwd, |
| ... | ... | @@ -121,7 +122,7 @@ export const make = Effect.gen(function* () { |
| 121 | 122 | Effect.mapError( |
| 122 | 123 | (error) => |
| 123 | 124 | new SourceControlProviderError({ |
| 124 | provider: "azure-devops", | |
| 125 | provider: SourceControlProviderKind.make("azure-devops"), | |
| 125 | 126 | operation: "getChangeRequest", |
| 126 | 127 | command: error.command, |
| 127 | 128 | cwd: input.cwd, |
| ... | ... | @@ -149,7 +150,7 @@ export const make = Effect.gen(function* () { |
| 149 | 150 | Effect.mapError( |
| 150 | 151 | (error) => |
| 151 | 152 | new SourceControlProviderError({ |
| 152 | provider: "azure-devops", | |
| 153 | provider: SourceControlProviderKind.make("azure-devops"), | |
| 153 | 154 | operation: "createChangeRequest", |
| 154 | 155 | command: error.command, |
| 155 | 156 | cwd: input.cwd, |
| ... | ... | @@ -167,7 +168,7 @@ export const make = Effect.gen(function* () { |
| 167 | 168 | Effect.mapError( |
| 168 | 169 | (error) => |
| 169 | 170 | new SourceControlProviderError({ |
| 170 | provider: "azure-devops", | |
| 171 | provider: SourceControlProviderKind.make("azure-devops"), | |
| 171 | 172 | operation: "getRepositoryCloneUrls", |
| 172 | 173 | command: error.command, |
| 173 | 174 | cwd: input.cwd, |
| ... | ... | @@ -184,7 +185,7 @@ export const make = Effect.gen(function* () { |
| 184 | 185 | Effect.mapError( |
| 185 | 186 | (error) => |
| 186 | 187 | new SourceControlProviderError({ |
| 187 | provider: "azure-devops", | |
| 188 | provider: SourceControlProviderKind.make("azure-devops"), | |
| 188 | 189 | operation: "createRepository", |
| 189 | 190 | command: error.command, |
| 190 | 191 | cwd: input.cwd, |
| ... | ... | @@ -201,7 +202,7 @@ export const make = Effect.gen(function* () { |
| 201 | 202 | Effect.mapError( |
| 202 | 203 | (error) => |
| 203 | 204 | new SourceControlProviderError({ |
| 204 | provider: "azure-devops", | |
| 205 | provider: SourceControlProviderKind.make("azure-devops"), | |
| 205 | 206 | operation: "getDefaultBranch", |
| 206 | 207 | command: error.command, |
| 207 | 208 | cwd: input.cwd, |
| ... | ... | @@ -221,7 +222,7 @@ export const make = Effect.gen(function* () { |
| 221 | 222 | Effect.mapError( |
| 222 | 223 | (error) => |
| 223 | 224 | new SourceControlProviderError({ |
| 224 | provider: "azure-devops", | |
| 225 | provider: SourceControlProviderKind.make("azure-devops"), | |
| 225 | 226 | operation: "checkoutChangeRequest", |
| 226 | 227 | command: error.command, |
| 227 | 228 | cwd: input.cwd, |
packages/source-control-azure-devops/src/server/driver.ts+2-1| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | |
| 1 | 2 | /** |
| 2 | 3 | * The Azure DevOps driver: repository operations, discovery, and pull requests through `az`. |
| 3 | 4 | * |
| ... | ... | @@ -10,7 +11,7 @@ import * as AzureDevOpsPullRequestProvider from "./AzureDevOpsPullRequestProvide |
| 10 | 11 | import * as AzureDevOpsSourceControlProvider from "./AzureDevOpsSourceControlProvider.ts"; |
| 11 | 12 | |
| 12 | 13 | export const driver = defineSourceControlDriver({ |
| 13 | kind: "azure-devops", | |
| 14 | kind: SourceControlProviderKind.make("azure-devops"), | |
| 14 | 15 | make: Effect.all({ |
| 15 | 16 | sourceControl: AzureDevOpsSourceControlProvider.make, |
| 16 | 17 | discovery: Effect.succeed(AzureDevOpsSourceControlProvider.discovery), |
packages/source-control-bitbucket/src/server/BitbucketPullRequestProvider.ts+4-3| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | |
| 1 | 2 | import * as Effect from "effect/Effect"; |
| 2 | 3 | import type { PullRequestCapabilities, PullRequestViewerPermissions } from "@t3tools/contracts"; |
| 3 | 4 | |
| ... | ... | @@ -122,7 +123,7 @@ export const make = Effect.gen(function* () { |
| 122 | 123 | const fail = |
| 123 | 124 | (operation: string) => (error: BitbucketPullRequestApi.BitbucketPullRequestApiError) => |
| 124 | 125 | new PullRequestProviderError({ |
| 125 | provider: "bitbucket", | |
| 126 | provider: SourceControlProviderKind.make("bitbucket"), | |
| 126 | 127 | operation, |
| 127 | 128 | ...bitbucketProviderFailure(error), |
| 128 | 129 | // Every Bitbucket failure states its own fact; this names the operation around it, so |
| ... | ... | @@ -157,7 +158,7 @@ export const make = Effect.gen(function* () { |
| 157 | 158 | }; |
| 158 | 159 | |
| 159 | 160 | const provider: PullRequestProviderApi = { |
| 160 | kind: "bitbucket", | |
| 161 | kind: SourceControlProviderKind.make("bitbucket"), | |
| 161 | 162 | capabilities: CAPABILITIES, |
| 162 | 163 | |
| 163 | 164 | // Bitbucket credentials come from the server's environment rather than a checkout, so the |
| ... | ... | @@ -363,7 +364,7 @@ export const make = Effect.gen(function* () { |
| 363 | 364 | setReaction: () => |
| 364 | 365 | Effect.fail( |
| 365 | 366 | new PullRequestProviderError({ |
| 366 | provider: "bitbucket", | |
| 367 | provider: SourceControlProviderKind.make("bitbucket"), | |
| 367 | 368 | operation: "setReaction", |
| 368 | 369 | reason: "failed", |
| 369 | 370 | detail: "Bitbucket does not support reactions.", |
packages/source-control-bitbucket/src/server/BitbucketSourceControlProvider.test.ts+2-1| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | |
| 1 | 2 | import { assert, it } from "@effect/vitest"; |
| 2 | 3 | import * as Effect from "effect/Effect"; |
| 3 | 4 | import * as Layer from "effect/Layer"; |
| ... | ... | @@ -36,7 +37,7 @@ it.effect("maps Bitbucket PR summaries into provider-neutral change requests", ( |
| 36 | 37 | }); |
| 37 | 38 | |
| 38 | 39 | assert.deepStrictEqual(changeRequest, { |
| 39 | provider: "bitbucket", | |
| 40 | provider: SourceControlProviderKind.make("bitbucket"), | |
| 40 | 41 | number: 42, |
| 41 | 42 | title: "Add Bitbucket provider", |
| 42 | 43 | url: "https://bitbucket.org/pingdotgg/t3code/pull-requests/42", |
packages/source-control-bitbucket/src/server/BitbucketSourceControlProvider.ts+11-10| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | |
| 1 | 2 | import * as Effect from "effect/Effect"; |
| 2 | 3 | import * as Option from "effect/Option"; |
| 3 | 4 | import { SourceControlProviderError, type ChangeRequest } from "@t3tools/contracts"; |
| ... | ... | @@ -9,7 +10,7 @@ import type { SourceControlApiDiscoverySpec } from "@t3tools/source-control-core |
| 9 | 10 | |
| 10 | 11 | function toChangeRequest(summary: NormalizedBitbucketPullRequestRecord): ChangeRequest { |
| 11 | 12 | return { |
| 12 | provider: "bitbucket", | |
| 13 | provider: SourceControlProviderKind.make("bitbucket"), | |
| 13 | 14 | number: summary.number, |
| 14 | 15 | title: summary.title, |
| 15 | 16 | url: summary.url, |
| ... | ... | @@ -34,7 +35,7 @@ export const make = Effect.gen(function* () { |
| 34 | 35 | const bitbucket = yield* BitbucketApi.BitbucketApi; |
| 35 | 36 | |
| 36 | 37 | return SourceControlProvider.SourceControlProvider.of({ |
| 37 | kind: "bitbucket", | |
| 38 | kind: SourceControlProviderKind.make("bitbucket"), | |
| 38 | 39 | listChangeRequests: (input) => { |
| 39 | 40 | const source = SourceControlProvider.sourceControlRefFromInput(input); |
| 40 | 41 | return bitbucket |
| ... | ... | @@ -51,7 +52,7 @@ export const make = Effect.gen(function* () { |
| 51 | 52 | Effect.mapError( |
| 52 | 53 | (error) => |
| 53 | 54 | new SourceControlProviderError({ |
| 54 | provider: "bitbucket", | |
| 55 | provider: SourceControlProviderKind.make("bitbucket"), | |
| 55 | 56 | operation: "listChangeRequests", |
| 56 | 57 | cwd: input.cwd, |
| 57 | 58 | reference: SourceControlProvider.transportSafeSourceControlErrorValue( |
| ... | ... | @@ -69,7 +70,7 @@ export const make = Effect.gen(function* () { |
| 69 | 70 | Effect.mapError( |
| 70 | 71 | (error) => |
| 71 | 72 | new SourceControlProviderError({ |
| 72 | provider: "bitbucket", | |
| 73 | provider: SourceControlProviderKind.make("bitbucket"), | |
| 73 | 74 | operation: "getChangeRequest", |
| 74 | 75 | cwd: input.cwd, |
| 75 | 76 | reference: SourceControlProvider.transportSafeSourceControlErrorValue( |
| ... | ... | @@ -97,7 +98,7 @@ export const make = Effect.gen(function* () { |
| 97 | 98 | Effect.mapError( |
| 98 | 99 | (error) => |
| 99 | 100 | new SourceControlProviderError({ |
| 100 | provider: "bitbucket", | |
| 101 | provider: SourceControlProviderKind.make("bitbucket"), | |
| 101 | 102 | operation: "createChangeRequest", |
| 102 | 103 | cwd: input.cwd, |
| 103 | 104 | reference: SourceControlProvider.transportSafeSourceControlErrorValue( |
| ... | ... | @@ -114,7 +115,7 @@ export const make = Effect.gen(function* () { |
| 114 | 115 | Effect.mapError( |
| 115 | 116 | (error) => |
| 116 | 117 | new SourceControlProviderError({ |
| 117 | provider: "bitbucket", | |
| 118 | provider: SourceControlProviderKind.make("bitbucket"), | |
| 118 | 119 | operation: "getRepositoryCloneUrls", |
| 119 | 120 | cwd: input.cwd, |
| 120 | 121 | repository: SourceControlProvider.transportSafeSourceControlErrorValue( |
| ... | ... | @@ -130,7 +131,7 @@ export const make = Effect.gen(function* () { |
| 130 | 131 | Effect.mapError( |
| 131 | 132 | (error) => |
| 132 | 133 | new SourceControlProviderError({ |
| 133 | provider: "bitbucket", | |
| 134 | provider: SourceControlProviderKind.make("bitbucket"), | |
| 134 | 135 | operation: "createRepository", |
| 135 | 136 | cwd: input.cwd, |
| 136 | 137 | repository: SourceControlProvider.transportSafeSourceControlErrorValue( |
| ... | ... | @@ -151,7 +152,7 @@ export const make = Effect.gen(function* () { |
| 151 | 152 | Effect.mapError( |
| 152 | 153 | (error) => |
| 153 | 154 | new SourceControlProviderError({ |
| 154 | provider: "bitbucket", | |
| 155 | provider: SourceControlProviderKind.make("bitbucket"), | |
| 155 | 156 | operation: "getDefaultBranch", |
| 156 | 157 | cwd: input.cwd, |
| 157 | 158 | detail: "Failed to get default branch.", |
| ... | ... | @@ -171,7 +172,7 @@ export const make = Effect.gen(function* () { |
| 171 | 172 | Effect.mapError( |
| 172 | 173 | (error) => |
| 173 | 174 | new SourceControlProviderError({ |
| 174 | provider: "bitbucket", | |
| 175 | provider: SourceControlProviderKind.make("bitbucket"), | |
| 175 | 176 | operation: "checkoutChangeRequest", |
| 176 | 177 | cwd: input.cwd, |
| 177 | 178 | reference: SourceControlProvider.transportSafeSourceControlErrorValue( |
| ... | ... | @@ -190,7 +191,7 @@ export const makeDiscovery = Effect.gen(function* () { |
| 190 | 191 | |
| 191 | 192 | return { |
| 192 | 193 | type: "api", |
| 193 | kind: "bitbucket", | |
| 194 | kind: SourceControlProviderKind.make("bitbucket"), | |
| 194 | 195 | label: "Bitbucket", |
| 195 | 196 | installHint: "Add a Bitbucket token in Settings → Source Control.", |
| 196 | 197 | probeAuth: bitbucket.probeAuth, |
packages/source-control-bitbucket/src/server/driver.ts+2-1| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | |
| 1 | 2 | /** |
| 2 | 3 | * The Bitbucket Cloud driver: repository operations, discovery, and pull requests through the |
| 3 | 4 | * Bitbucket REST API. |
| ... | ... | @@ -11,7 +12,7 @@ import * as BitbucketPullRequestProvider from "./BitbucketPullRequestProvider.ts |
| 11 | 12 | import * as BitbucketSourceControlProvider from "./BitbucketSourceControlProvider.ts"; |
| 12 | 13 | |
| 13 | 14 | export const driver = defineSourceControlDriver({ |
| 14 | kind: "bitbucket", | |
| 15 | kind: SourceControlProviderKind.make("bitbucket"), | |
| 15 | 16 | make: Effect.all({ |
| 16 | 17 | sourceControl: BitbucketSourceControlProvider.make, |
| 17 | 18 | discovery: BitbucketSourceControlProvider.makeDiscovery, |
packages/source-control-core/src/server/SourceControlRateLimit.test.ts+17-4| ... | ... | @@ -1,10 +1,11 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | |
| 1 | 2 | import { assert, it } from "@effect/vitest"; |
| 2 | 3 | import * as Effect from "effect/Effect"; |
| 3 | 4 | import * as TestClock from "effect/testing/TestClock"; |
| 4 | 5 | |
| 5 | 6 | import * as SourceControlRateLimit from "./SourceControlRateLimit.ts"; |
| 6 | 7 | |
| 7 | const github = { provider: "github" as const, host: "github.com" }; | |
| 8 | const github = { provider: SourceControlProviderKind.make("github"), host: "github.com" }; | |
| 8 | 9 | |
| 9 | 10 | it.effect("isolates cooldowns for verified credentials on the same host", () => |
| 10 | 11 | Effect.gen(function* () { |
| ... | ... | @@ -41,7 +42,7 @@ it.effect("backs off repeated rate limits until a successful request", () => |
| 41 | 42 | const firstPause = yield* Effect.flip(limits.check(github)); |
| 42 | 43 | assert.deepInclude(firstPause, { |
| 43 | 44 | _tag: "SourceControlRateLimitPausedError", |
| 44 | provider: "github", | |
| 45 | provider: SourceControlProviderKind.make("github"), | |
| 45 | 46 | host: "github.com", |
| 46 | 47 | retryAt: 30_000, |
| 47 | 48 | }); |
| ... | ... | @@ -105,8 +106,20 @@ it.effect("keeps providers and hosts isolated", () => |
| 105 | 106 | const lease = yield* limits.check(github); |
| 106 | 107 | yield* limits.recordRateLimit({ ...github, lease }); |
| 107 | 108 | |
| 108 | assert.equal(yield* limits.check({ provider: "gitlab", host: "github.com" }), 0); | |
| 109 | assert.equal(yield* limits.check({ provider: "github", host: "github.example.com" }), 0); | |
| 109 | assert.equal( | |
| 110 | yield* limits.check({ | |
| 111 | provider: SourceControlProviderKind.make("gitlab"), | |
| 112 | host: "github.com", | |
| 113 | }), | |
| 114 | 0, | |
| 115 | ); | |
| 116 | assert.equal( | |
| 117 | yield* limits.check({ | |
| 118 | provider: SourceControlProviderKind.make("github"), | |
| 119 | host: "github.example.com", | |
| 120 | }), | |
| 121 | 0, | |
| 122 | ); | |
| 110 | 123 | }).pipe(Effect.provide(SourceControlRateLimit.layer)), |
| 111 | 124 | ); |
| 112 | 125 |
packages/source-control-forgejo/src/server/ForgejoPullRequestProvider.ts+4-3| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | |
| 1 | 2 | import * as Effect from "effect/Effect"; |
| 2 | 3 | import * as Schema from "effect/Schema"; |
| 3 | 4 | import * as Result from "effect/Result"; |
| ... | ... | @@ -67,7 +68,7 @@ export const make = Effect.gen(function* () { |
| 67 | 68 | const cli = yield* ForgejoCli.ForgejoCli; |
| 68 | 69 | const failure = (operation: string, detail: string, cause?: unknown) => |
| 69 | 70 | new PullRequestProviderError({ |
| 70 | provider: "forgejo", | |
| 71 | provider: SourceControlProviderKind.make("forgejo"), | |
| 71 | 72 | operation, |
| 72 | 73 | reason: "failed", |
| 73 | 74 | detail, |
| ... | ... | @@ -78,7 +79,7 @@ export const make = Effect.gen(function* () { |
| 78 | 79 | Effect.mapError( |
| 79 | 80 | (error) => |
| 80 | 81 | new PullRequestProviderError({ |
| 81 | provider: "forgejo", | |
| 82 | provider: SourceControlProviderKind.make("forgejo"), | |
| 82 | 83 | operation: input.path, |
| 83 | 84 | detail: error.detail, |
| 84 | 85 | cause: error, |
| ... | ... | @@ -203,7 +204,7 @@ export const make = Effect.gen(function* () { |
| 203 | 204 | const unsupported = (operation: string) => |
| 204 | 205 | Effect.fail(failure(operation, `Forgejo does not expose ${operation} through its API.`)); |
| 205 | 206 | const provider: PullRequestProviderApi = { |
| 206 | kind: "forgejo", | |
| 207 | kind: SourceControlProviderKind.make("forgejo"), | |
| 207 | 208 | capabilities: CAPABILITIES, |
| 208 | 209 | getViewer, |
| 209 | 210 | listChangeRequests: Effect.fn("ForgejoPullRequestProvider.listChangeRequests")( |
packages/source-control-forgejo/src/server/ForgejoSourceControlProvider.test.ts+25-8| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | |
| 1 | 2 | import { assert, it } from "@effect/vitest"; |
| 2 | 3 | import * as Effect from "effect/Effect"; |
| 3 | 4 | import * as FileSystem from "effect/FileSystem"; |
| ... | ... | @@ -258,7 +259,11 @@ it.effect("gives Forgejo identities the browser URL of the login that serves the |
| 258 | 259 | }); |
| 259 | 260 | const served = (baseUrl: string) => () => |
| 260 | 261 | Effect.succeed({ |
| 261 | provider: { kind: "forgejo" as const, name: "Forgejo / Gitea", baseUrl }, | |
| 262 | provider: { | |
| 263 | kind: SourceControlProviderKind.make("forgejo"), | |
| 264 | name: "Forgejo / Gitea", | |
| 265 | baseUrl, | |
| 266 | }, | |
| 262 | 267 | remoteName: "origin", |
| 263 | 268 | remoteUrl: "", |
| 264 | 269 | }); |
| ... | ... | @@ -271,7 +276,7 @@ it.effect("gives Forgejo identities the browser URL of the login that serves the |
| 271 | 276 | }), |
| 272 | 277 | { |
| 273 | 278 | ...identity("http://forge.test:3000/git/team/repo.git"), |
| 274 | provider: "forgejo", | |
| 279 | provider: SourceControlProviderKind.make("forgejo"), | |
| 275 | 280 | webUrl: "http://forge.test:3000/git/team/repo", |
| 276 | 281 | }, |
| 277 | 282 | ); |
| ... | ... | @@ -557,7 +562,7 @@ it.effect("discovers Forgejo accounts and retains the server port", () => |
| 557 | 562 | cwd: "/repo", |
| 558 | 563 | context: { |
| 559 | 564 | provider: { |
| 560 | kind: "unknown", | |
| 565 | kind: SourceControlProviderKind.make("unknown"), | |
| 561 | 566 | name: "git.forgejo.local", |
| 562 | 567 | baseUrl: "https://git.forgejo.local", |
| 563 | 568 | }, |
| ... | ... | @@ -577,7 +582,7 @@ it.effect("discovers Forgejo accounts and retains the server port", () => |
| 577 | 582 | ), |
| 578 | 583 | }); |
| 579 | 584 | assert.deepStrictEqual(refined, { |
| 580 | kind: "forgejo", | |
| 585 | kind: SourceControlProviderKind.make("forgejo"), | |
| 581 | 586 | name: "Forgejo / Gitea", |
| 582 | 587 | baseUrl: "http://forgejo.local:3000", |
| 583 | 588 | }); |
| ... | ... | @@ -622,7 +627,11 @@ it.effect("does not choose a default Forgejo login across ambiguous SSH server p |
| 622 | 627 | const refined = ForgejoSourceControlProvider.discovery.refineUnknownRemote({ |
| 623 | 628 | cwd: "/repo", |
| 624 | 629 | context: { |
| 625 | provider: { kind: "unknown", name: "Forgejo", baseUrl: "https://forgejo.local" }, | |
| 630 | provider: { | |
| 631 | kind: SourceControlProviderKind.make("unknown"), | |
| 632 | name: "Forgejo", | |
| 633 | baseUrl: "https://forgejo.local", | |
| 634 | }, | |
| 626 | 635 | remoteName: "origin", |
| 627 | 636 | remoteUrl: "git@forgejo.local:maria/project.git", |
| 628 | 637 | requestedHost: "forgejo.local:4000", |
| ... | ... | @@ -732,7 +741,11 @@ it.effect("routes mounted Forgejo repositories without repeating the mount in AP |
| 732 | 741 | cwd: "/repo", |
| 733 | 742 | repository: "forgejo/maria/project", |
| 734 | 743 | context: { |
| 735 | provider: { kind: "forgejo", name: "Forgejo", baseUrl: "https://code.test/forgejo" }, | |
| 744 | provider: { | |
| 745 | kind: SourceControlProviderKind.make("forgejo"), | |
| 746 | name: "Forgejo", | |
| 747 | baseUrl: "https://code.test/forgejo", | |
| 748 | }, | |
| 736 | 749 | remoteName: "origin", |
| 737 | 750 | remoteUrl: "https://code.test/forgejo/maria/project.git", |
| 738 | 751 | }, |
| ... | ... | @@ -744,7 +757,11 @@ it.effect("routes mounted Forgejo repositories without repeating the mount in AP |
| 744 | 757 | cwd: "/repo", |
| 745 | 758 | repository: "forgejo/project", |
| 746 | 759 | context: { |
| 747 | provider: { kind: "forgejo", name: "Forgejo", baseUrl: "https://code.test/forgejo" }, | |
| 760 | provider: { | |
| 761 | kind: SourceControlProviderKind.make("forgejo"), | |
| 762 | name: "Forgejo", | |
| 763 | baseUrl: "https://code.test/forgejo", | |
| 764 | }, | |
| 748 | 765 | remoteName: "origin", |
| 749 | 766 | remoteUrl: "ssh://git@code.test/forgejo/project.git", |
| 750 | 767 | }, |
| ... | ... | @@ -827,7 +844,7 @@ it.effect("prefers fj for HTTP and ported SSH aliases on root servers", () => { |
| 827 | 844 | repository: "maria/project", |
| 828 | 845 | context: { |
| 829 | 846 | provider: { |
| 830 | kind: "forgejo", | |
| 847 | kind: SourceControlProviderKind.make("forgejo"), | |
| 831 | 848 | name: "Forgejo", |
| 832 | 849 | baseUrl: "http://forgejo.local:3000", |
| 833 | 850 | }, |
packages/source-control-forgejo/src/server/ForgejoSourceControlProvider.ts+24-8| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | |
| 1 | 2 | import * as Effect from "effect/Effect"; |
| 2 | 3 | import * as FileSystem from "effect/FileSystem"; |
| 3 | 4 | import * as Schema from "effect/Schema"; |
| ... | ... | @@ -18,7 +19,7 @@ const isForgejoCliError = Schema.is(ForgejoCli.ForgejoCliError); |
| 18 | 19 | |
| 19 | 20 | export const discovery = { |
| 20 | 21 | type: "cli", |
| 21 | kind: "forgejo", | |
| 22 | kind: SourceControlProviderKind.make("forgejo"), | |
| 22 | 23 | label: "Forgejo / Gitea", |
| 23 | 24 | executable: "tea", |
| 24 | 25 | versionArgs: ["--version"], |
| ... | ... | @@ -47,7 +48,13 @@ export const discovery = { |
| 47 | 48 | remote, |
| 48 | 49 | input.context.requestedHost, |
| 49 | 50 | ); |
| 50 | return login ? { kind: "forgejo", name: "Forgejo / Gitea", baseUrl: login.url } : null; | |
| 51 | return login | |
| 52 | ? { | |
| 53 | kind: SourceControlProviderKind.make("forgejo"), | |
| 54 | name: "Forgejo / Gitea", | |
| 55 | baseUrl: login.url, | |
| 56 | } | |
| 57 | : null; | |
| 51 | 58 | }, |
| 52 | 59 | installHint: |
| 53 | 60 | "Install `fj` 0.6 or later from https://codeberg.org/forgejo-contrib/forgejo-cli and run `fj --host <server-url> auth add-token`, or install `tea` 0.16 or later from https://gitea.com/gitea/tea and run `tea login add` for each Forgejo or Gitea server.", |
| ... | ... | @@ -60,7 +67,7 @@ export const makeDiscovery = Effect.gen(function* () { |
| 60 | 67 | if (!listLogins) return discovery; |
| 61 | 68 | return { |
| 62 | 69 | type: "managed-cli", |
| 63 | kind: "forgejo", | |
| 70 | kind: SourceControlProviderKind.make("forgejo"), | |
| 64 | 71 | label: discovery.label, |
| 65 | 72 | installHint: discovery.installHint, |
| 66 | 73 | probe: Effect.fn("ForgejoSourceControlProvider.discovery")(function* (cwd: string) { |
| ... | ... | @@ -150,7 +157,12 @@ export const makeDiscovery = Effect.gen(function* () { |
| 150 | 157 | remoteUrl: input.context.remoteUrl, |
| 151 | 158 | }).pipe(Effect.orElseSucceed(() => [])); |
| 152 | 159 | const login = ForgejoCli.matchForgejoLogin(logins, remote, input.context.requestedHost); |
| 153 | if (login) return { kind: "forgejo" as const, name: discovery.label, baseUrl: login.url }; | |
| 160 | if (login) | |
| 161 | return { | |
| 162 | kind: SourceControlProviderKind.make("forgejo"), | |
| 163 | name: discovery.label, | |
| 164 | baseUrl: login.url, | |
| 165 | }; | |
| 154 | 166 | } |
| 155 | 167 | return null; |
| 156 | 168 | }, |
| ... | ... | @@ -188,7 +200,7 @@ const refineRepositoryIdentity: NonNullable< |
| 188 | 200 | const context = yield* resolveContext({ |
| 189 | 201 | cwd: identity.rootPath, |
| 190 | 202 | context: { |
| 191 | provider: { kind: "unknown", name: "Unknown", baseUrl: "" }, | |
| 203 | provider: { kind: SourceControlProviderKind.make("unknown"), name: "Unknown", baseUrl: "" }, | |
| 192 | 204 | remoteName: identity.locator.remoteName, |
| 193 | 205 | remoteUrl: identity.locator.remoteUrl, |
| 194 | 206 | }, |
| ... | ... | @@ -200,7 +212,11 @@ const refineRepositoryIdentity: NonNullable< |
| 200 | 212 | !remote.ssh && basePath && remote.path.startsWith(`${basePath}/`) |
| 201 | 213 | ? remote.path.slice(basePath.length + 1) |
| 202 | 214 | : remote.path; |
| 203 | return { ...identity, provider: "forgejo", webUrl: `${baseUrl}/${path}` }; | |
| 215 | return { | |
| 216 | ...identity, | |
| 217 | provider: SourceControlProviderKind.make("forgejo"), | |
| 218 | webUrl: `${baseUrl}/${path}`, | |
| 219 | }; | |
| 204 | 220 | }); |
| 205 | 221 | |
| 206 | 222 | const RepositorySchema = Schema.Struct({ |
| ... | ... | @@ -245,7 +261,7 @@ export const make = Effect.gen(function* () { |
| 245 | 261 | Effect.mapError( |
| 246 | 262 | (cause: unknown) => |
| 247 | 263 | new SourceControlProviderError({ |
| 248 | provider: "forgejo", | |
| 264 | provider: SourceControlProviderKind.make("forgejo"), | |
| 249 | 265 | operation, |
| 250 | 266 | cwd, |
| 251 | 267 | ...(isForgejoCliError(cause) ? { command: cause.command } : {}), |
| ... | ... | @@ -272,7 +288,7 @@ export const make = Effect.gen(function* () { |
| 272 | 288 | ); |
| 273 | 289 | }); |
| 274 | 290 | return SourceControlProvider.SourceControlProvider.of({ |
| 275 | kind: "forgejo", | |
| 291 | kind: SourceControlProviderKind.make("forgejo"), | |
| 276 | 292 | repositoryNameFromRemoteUrl, |
| 277 | 293 | refineRepositoryIdentity, |
| 278 | 294 | listChangeRequests: (input) => |
packages/source-control-forgejo/src/server/driver.ts+2-1| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | |
| 1 | 2 | /** |
| 2 | 3 | * The Forgejo / Gitea driver: repository operations, discovery, and pull requests through |
| 3 | 4 | * `fj` or `tea`. |
| ... | ... | @@ -11,7 +12,7 @@ import * as ForgejoPullRequestProvider from "./ForgejoPullRequestProvider.ts"; |
| 11 | 12 | import * as ForgejoSourceControlProvider from "./ForgejoSourceControlProvider.ts"; |
| 12 | 13 | |
| 13 | 14 | export const driver = defineSourceControlDriver({ |
| 14 | kind: "forgejo", | |
| 15 | kind: SourceControlProviderKind.make("forgejo"), | |
| 15 | 16 | make: Effect.all({ |
| 16 | 17 | sourceControl: ForgejoSourceControlProvider.make, |
| 17 | 18 | discovery: ForgejoSourceControlProvider.makeDiscovery, |
packages/source-control-forgejo/src/server/forgejoPullRequests.ts+2-1| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | |
| 1 | 2 | import * as Schema from "effect/Schema"; |
| 2 | 3 | import * as Option from "effect/Option"; |
| 3 | 4 | import type { ChangeRequest } from "@t3tools/contracts"; |
| ... | ... | @@ -26,7 +27,7 @@ export const ForgejoPullRequestSchema = Schema.Struct({ |
| 26 | 27 | }); |
| 27 | 28 | export function toForgejoChangeRequest(raw: typeof ForgejoPullRequestSchema.Type): ChangeRequest { |
| 28 | 29 | return { |
| 29 | provider: "forgejo", | |
| 30 | provider: SourceControlProviderKind.make("forgejo"), | |
| 30 | 31 | number: raw.number, |
| 31 | 32 | title: raw.title, |
| 32 | 33 | url: raw.html_url, |
packages/source-control-gitcafe/src/server/GitCafePullRequestProvider.ts+4-3| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | |
| 1 | 2 | import * as Effect from "effect/Effect"; |
| 2 | 3 | import * as Option from "effect/Option"; |
| 3 | 4 | import * as Schema from "effect/Schema"; |
| ... | ... | @@ -176,7 +177,7 @@ export const make = Effect.gen(function* () { |
| 176 | 177 | |
| 177 | 178 | const failure = (operation: string, detail: string, cause?: unknown) => |
| 178 | 179 | new PullRequestProviderError({ |
| 179 | provider: "gitcafe", | |
| 180 | provider: SourceControlProviderKind.make("gitcafe"), | |
| 180 | 181 | operation, |
| 181 | 182 | reason: "failed", |
| 182 | 183 | detail, |
| ... | ... | @@ -207,7 +208,7 @@ export const make = Effect.gen(function* () { |
| 207 | 208 | Effect.mapError( |
| 208 | 209 | (error) => |
| 209 | 210 | new PullRequestProviderError({ |
| 210 | provider: "gitcafe", | |
| 211 | provider: SourceControlProviderKind.make("gitcafe"), | |
| 211 | 212 | operation, |
| 212 | 213 | reason: error.reason, |
| 213 | 214 | detail: error.detail, |
| ... | ... | @@ -475,7 +476,7 @@ export const make = Effect.gen(function* () { |
| 475 | 476 | }); |
| 476 | 477 | |
| 477 | 478 | const provider: PullRequestProviderApi = { |
| 478 | kind: "gitcafe", | |
| 479 | kind: SourceControlProviderKind.make("gitcafe"), | |
| 479 | 480 | capabilities: CAPABILITIES, |
| 480 | 481 | getViewer: (input) => |
| 481 | 482 | read( |
packages/source-control-gitcafe/src/server/GitCafeSourceControlProvider.test.ts+11-2| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | |
| 1 | 2 | /** |
| 2 | 3 | * Pins `./GitCafeSourceControlProvider.ts`, which pass 2 creates: |
| 3 | 4 | * |
| ... | ... | @@ -87,7 +88,11 @@ describe("GitCafeSourceControlProvider", () => { |
| 87 | 88 | ); |
| 88 | 89 | }); |
| 89 | 90 | const context = { |
| 90 | provider: { kind: "gitcafe" as const, name: "GitCafe", baseUrl: `https://${host}` }, | |
| 91 | provider: { | |
| 92 | kind: SourceControlProviderKind.make("gitcafe"), | |
| 93 | name: "GitCafe", | |
| 94 | baseUrl: `https://${host}`, | |
| 95 | }, | |
| 91 | 96 | remoteName: "origin", |
| 92 | 97 | remoteUrl, |
| 93 | 98 | }; |
| ... | ... | @@ -220,7 +225,11 @@ describe("GitCafeSourceControlProvider", () => { |
| 220 | 225 | remoteUrl === undefined |
| 221 | 226 | ? undefined |
| 222 | 227 | : { |
| 223 | provider: { kind: "gitcafe" as const, name: "GitCafe", baseUrl: "https://git.cafe" }, | |
| 228 | provider: { | |
| 229 | kind: SourceControlProviderKind.make("gitcafe"), | |
| 230 | name: "GitCafe", | |
| 231 | baseUrl: "https://git.cafe", | |
| 232 | }, | |
| 224 | 233 | remoteName: "origin", |
| 225 | 234 | remoteUrl, |
| 226 | 235 | }; |
packages/source-control-gitcafe/src/server/GitCafeSourceControlProvider.ts+5-4| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | |
| 1 | 2 | import * as Effect from "effect/Effect"; |
| 2 | 3 | import * as Option from "effect/Option"; |
| 3 | 4 | import * as Result from "effect/Result"; |
| ... | ... | @@ -59,7 +60,7 @@ const cliFailure = (output: string) => |
| 59 | 60 | |
| 60 | 61 | export const discovery = { |
| 61 | 62 | type: "cli", |
| 62 | kind: "gitcafe", | |
| 63 | kind: SourceControlProviderKind.make("gitcafe"), | |
| 63 | 64 | label: "GitCafe", |
| 64 | 65 | executable: "cafe", |
| 65 | 66 | versionArgs: ["--version"], |
| ... | ... | @@ -150,7 +151,7 @@ export const make = Effect.gen(function* () { |
| 150 | 151 | |
| 151 | 152 | const error = (operation: string, cwd: string, detail: string, cause?: unknown) => |
| 152 | 153 | new SourceControlProviderError({ |
| 153 | provider: "gitcafe", | |
| 154 | provider: SourceControlProviderKind.make("gitcafe"), | |
| 154 | 155 | operation, |
| 155 | 156 | cwd, |
| 156 | 157 | detail, |
| ... | ... | @@ -269,7 +270,7 @@ export const make = Effect.gen(function* () { |
| 269 | 270 | ); |
| 270 | 271 | const owner = target.repository.split("/")[0] ?? null; |
| 271 | 272 | return { |
| 272 | provider: "gitcafe", | |
| 273 | provider: SourceControlProviderKind.make("gitcafe"), | |
| 273 | 274 | number: pull.number, |
| 274 | 275 | title: pull.title, |
| 275 | 276 | url: `https://${target.host}/${target.repository}/pulls/${pull.number}`, |
| ... | ... | @@ -290,7 +291,7 @@ export const make = Effect.gen(function* () { |
| 290 | 291 | }); |
| 291 | 292 | |
| 292 | 293 | return SourceControlProvider.SourceControlProvider.of({ |
| 293 | kind: "gitcafe", | |
| 294 | kind: SourceControlProviderKind.make("gitcafe"), | |
| 294 | 295 | repositoryNameFromRemoteUrl: (url) => GitCafeHosts.parseGitCafeRemote(url)?.repository ?? null, |
| 295 | 296 | getChangeRequest: getPull, |
| 296 | 297 | listChangeRequests: Effect.fn("GitCafeSourceControlProvider.listChangeRequests")( |
packages/source-control-gitcafe/src/server/driver.ts+2-1| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | |
| 1 | 2 | /** |
| 2 | 3 | * The GitCafe driver: repository operations and discovery through `cafe` and git, and pull |
| 3 | 4 | * requests through GitCafe's REST API on git.cafe and staging.git.cafe. |
| ... | ... | @@ -11,7 +12,7 @@ import * as GitCafePullRequestProvider from "./GitCafePullRequestProvider.ts"; |
| 11 | 12 | import * as GitCafeSourceControlProvider from "./GitCafeSourceControlProvider.ts"; |
| 12 | 13 | |
| 13 | 14 | export const driver = defineSourceControlDriver({ |
| 14 | kind: "gitcafe", | |
| 15 | kind: SourceControlProviderKind.make("gitcafe"), | |
| 15 | 16 | make: Effect.all({ |
| 16 | 17 | sourceControl: GitCafeSourceControlProvider.make, |
| 17 | 18 | discovery: Effect.succeed(GitCafeSourceControlProvider.discovery), |
packages/source-control-github/src/server/GitHubApi.ts+2-1| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | |
| 1 | 2 | import * as Clock from "effect/Clock"; |
| 2 | 3 | import * as Context from "effect/Context"; |
| 3 | 4 | import * as Data from "effect/Data"; |
| ... | ... | @@ -389,7 +390,7 @@ export const make = Effect.gen(function* () { |
| 389 | 390 | // this reads today is `core`; a `search/` read would need its own resource here. A refusal |
| 390 | 391 | // still pauses the whole host, the key PullRequestService records its own backoff under. |
| 391 | 392 | const resource = input.graphql === true ? "graphql" : "core"; |
| 392 | const key = { provider: "github" as const, host }; | |
| 393 | const key = { provider: SourceControlProviderKind.make("github"), host }; | |
| 393 | 394 | const run = Effect.gen(function* () { |
| 394 | 395 | const lease = yield* quota |
| 395 | 396 | .admit(host, resource, { allowReserve: input.allowReserve }) |
packages/source-control-github/src/server/GitHubPullRequestProvider.ts+3-2| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | |
| 1 | 2 | import { removeAgentCredits } from "@t3tools/source-control-core/server/mergeMessage"; |
| 2 | 3 | import * as Effect from "effect/Effect"; |
| 3 | 4 | import type { |
| ... | ... | @@ -202,7 +203,7 @@ export const make = Effect.gen(function* () { |
| 202 | 203 | |
| 203 | 204 | const fail = (operation: string) => (error: GitHubPullRequestApi.GitHubPullRequestApiError) => |
| 204 | 205 | new PullRequestProviderError({ |
| 205 | provider: "github", | |
| 206 | provider: SourceControlProviderKind.make("github"), | |
| 206 | 207 | operation, |
| 207 | 208 | ...gitHubProviderFailure(error), |
| 208 | 209 | detail: error.message, |
| ... | ... | @@ -261,7 +262,7 @@ export const make = Effect.gen(function* () { |
| 261 | 262 | ); |
| 262 | 263 | |
| 263 | 264 | const provider: PullRequestProviderApi = { |
| 264 | kind: "github", | |
| 265 | kind: SourceControlProviderKind.make("github"), | |
| 265 | 266 | capabilities: CAPABILITIES, |
| 266 | 267 | mergeMessageRewrite: removeAgentCredits, |
| 267 | 268 | getRoutingIdentity: (input) => |
packages/source-control-github/src/server/GitHubQuota.ts+2-1| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | |
| 1 | 2 | import * as Clock from "effect/Clock"; |
| 2 | 3 | import * as Context from "effect/Context"; |
| 3 | 4 | import * as Effect from "effect/Effect"; |
| ... | ... | @@ -86,7 +87,7 @@ const make = Effect.gen(function* () { |
| 86 | 87 | const floor = options?.allowReserve === true ? 1 : snapshot.limit * RESERVE_RATIO; |
| 87 | 88 | if (snapshot.remaining >= floor) return; |
| 88 | 89 | return yield* new SourceControlRateLimit.SourceControlRateLimitPausedError({ |
| 89 | provider: "github", | |
| 90 | provider: SourceControlProviderKind.make("github"), | |
| 90 | 91 | host: host.trim().toLowerCase(), |
| 91 | 92 | retryAt: snapshot.resetAtMs, |
| 92 | 93 | }); |
packages/source-control-github/src/server/GitHubSourceControlProvider.test.ts+12-3| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | |
| 1 | 2 | import * as HostProcess from "@t3tools/shared/HostProcess"; |
| 2 | 3 | import { assert, describe, it } from "@effect/vitest"; |
| 3 | 4 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| ... | ... | @@ -137,7 +138,11 @@ const makeProvider = (api: Partial<GitHubApi.GitHubApi["Service"]>) => |
| 137 | 138 | GitHubSourceControlProvider.make.pipe(Effect.provide(harness({ remotes: "", api }).layer)); |
| 138 | 139 | |
| 139 | 140 | const githubContext = (host: string) => ({ |
| 140 | provider: { kind: "github" as const, name: "GitHub", baseUrl: `https://${host}` }, | |
| 141 | provider: { | |
| 142 | kind: SourceControlProviderKind.make("github"), | |
| 143 | name: "GitHub", | |
| 144 | baseUrl: `https://${host}`, | |
| 145 | }, | |
| 141 | 146 | remoteName: "origin", |
| 142 | 147 | remoteUrl: `git@${host}:acme/web.git`, |
| 143 | 148 | }); |
| ... | ... | @@ -1067,7 +1072,7 @@ it.live.each([ |
| 1067 | 1072 | cwd: "/repo", |
| 1068 | 1073 | context: { |
| 1069 | 1074 | provider: { |
| 1070 | kind: "github" as const, | |
| 1075 | kind: SourceControlProviderKind.make("github"), | |
| 1071 | 1076 | name: "GitHub Self-Hosted", |
| 1072 | 1077 | baseUrl: "https://code.example.test", |
| 1073 | 1078 | }, |
| ... | ... | @@ -1184,7 +1189,11 @@ it.effect.each([ |
| 1184 | 1189 | const input = { |
| 1185 | 1190 | cwd: "/repo", |
| 1186 | 1191 | context: { |
| 1187 | provider: { kind: "github" as const, name: "GitHub", baseUrl: `https://${host}` }, | |
| 1192 | provider: { | |
| 1193 | kind: SourceControlProviderKind.make("github"), | |
| 1194 | name: "GitHub", | |
| 1195 | baseUrl: `https://${host}`, | |
| 1196 | }, | |
| 1188 | 1197 | remoteName: "origin", |
| 1189 | 1198 | remoteUrl: `git@${host}:me/web.git`, |
| 1190 | 1199 | }, |
packages/source-control-github/src/server/GitHubSourceControlProvider.ts+8-7| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | |
| 1 | 2 | import * as Context from "effect/Context"; |
| 2 | 3 | import * as Data from "effect/Data"; |
| 3 | 4 | import * as Effect from "effect/Effect"; |
| ... | ... | @@ -62,7 +63,7 @@ const decodeLinkSubject = Schema.decodeUnknownEffect( |
| 62 | 63 | ); |
| 63 | 64 | |
| 64 | 65 | function toChangeRequest(record: NormalizedGitHubPullRequestRecord): ChangeRequest { |
| 65 | return { provider: "github", ...record }; | |
| 66 | return { provider: SourceControlProviderKind.make("github"), ...record }; | |
| 66 | 67 | } |
| 67 | 68 | |
| 68 | 69 | function authAccounts(accounts: ReadonlyArray<GitHubAuthStatusAccount>) { |
| ... | ... | @@ -164,7 +165,7 @@ export function parseGitHubAuth( |
| 164 | 165 | |
| 165 | 166 | export const discovery = { |
| 166 | 167 | type: "cli", |
| 167 | kind: "github", | |
| 168 | kind: SourceControlProviderKind.make("github"), | |
| 168 | 169 | label: "GitHub", |
| 169 | 170 | executable: "gh", |
| 170 | 171 | versionArgs: ["--version"], |
| ... | ... | @@ -271,7 +272,7 @@ export const makeDiscovery = Effect.gen(function* () { |
| 271 | 272 | Effect.map((known) => |
| 272 | 273 | known |
| 273 | 274 | ? ({ |
| 274 | kind: "github", | |
| 275 | kind: SourceControlProviderKind.make("github"), | |
| 275 | 276 | name: "GitHub Self-Hosted", |
| 276 | 277 | baseUrl: context.provider.baseUrl, |
| 277 | 278 | } as const) |
| ... | ... | @@ -878,7 +879,7 @@ export const make = Effect.gen(function* () { |
| 878 | 879 | ) => |
| 879 | 880 | (error: GitHubFailure) => |
| 880 | 881 | new SourceControlProviderError({ |
| 881 | provider: "github", | |
| 882 | provider: SourceControlProviderKind.make("github"), | |
| 882 | 883 | operation, |
| 883 | 884 | cwd, |
| 884 | 885 | ...(context?.reference === undefined |
| ... | ... | @@ -915,7 +916,7 @@ export const make = Effect.gen(function* () { |
| 915 | 916 | Effect.mapError( |
| 916 | 917 | (cause) => |
| 917 | 918 | new SourceControlProviderError({ |
| 918 | provider: "github", | |
| 919 | provider: SourceControlProviderKind.make("github"), | |
| 919 | 920 | operation: "resolveLink", |
| 920 | 921 | cwd: input.cwd, |
| 921 | 922 | detail: "The linked subject could not be read.", |
| ... | ... | @@ -927,7 +928,7 @@ export const make = Effect.gen(function* () { |
| 927 | 928 | Effect.mapError( |
| 928 | 929 | (cause) => |
| 929 | 930 | new SourceControlProviderError({ |
| 930 | provider: "github", | |
| 931 | provider: SourceControlProviderKind.make("github"), | |
| 931 | 932 | operation: "resolveLink.decode", |
| 932 | 933 | cwd: input.cwd, |
| 933 | 934 | detail: "The linked subject could not be read.", |
| ... | ... | @@ -939,7 +940,7 @@ export const make = Effect.gen(function* () { |
| 939 | 940 | }); |
| 940 | 941 | |
| 941 | 942 | return SourceControlProvider.SourceControlProvider.of({ |
| 942 | kind: "github", | |
| 943 | kind: SourceControlProviderKind.make("github"), | |
| 943 | 944 | // `gh pr list --head` filters on the head ref name alone and accepts anything, so an |
| 944 | 945 | // `owner:branch` or `remote:branch` selector silently lists zero pull requests while |
| 945 | 946 | // spending a GraphQL call; the bare branch is always among the selectors. Without the owner, |
packages/source-control-github/src/server/driver.ts+2-1| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | |
| 1 | 2 | /** |
| 2 | 3 | * The GitHub driver: repository operations and discovery through `gh` and git, and pull requests |
| 3 | 4 | * through the GitHub REST and GraphQL APIs. |
| ... | ... | @@ -11,7 +12,7 @@ import * as GitHubPullRequestProvider from "./GitHubPullRequestProvider.ts"; |
| 11 | 12 | import * as GitHubSourceControlProvider from "./GitHubSourceControlProvider.ts"; |
| 12 | 13 | |
| 13 | 14 | export const driver = defineSourceControlDriver({ |
| 14 | kind: "github", | |
| 15 | kind: SourceControlProviderKind.make("github"), | |
| 15 | 16 | make: Effect.all({ |
| 16 | 17 | sourceControl: GitHubSourceControlProvider.make, |
| 17 | 18 | discovery: GitHubSourceControlProvider.makeDiscovery, |
packages/source-control-gitlab/src/server/GitLabPullRequestProvider.ts+3-2| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | |
| 1 | 2 | import * as Effect from "effect/Effect"; |
| 2 | 3 | import type { |
| 3 | 4 | PullRequestCapabilities, |
| ... | ... | @@ -111,7 +112,7 @@ export const make = Effect.gen(function* () { |
| 111 | 112 | |
| 112 | 113 | const fail = (operation: string) => (error: GitLabPullRequestCli.GitLabPullRequestCliError) => |
| 113 | 114 | new PullRequestProviderError({ |
| 114 | provider: "gitlab", | |
| 115 | provider: SourceControlProviderKind.make("gitlab"), | |
| 115 | 116 | operation, |
| 116 | 117 | ...gitLabProviderFailure(error), |
| 117 | 118 | detail: error.detail, |
| ... | ... | @@ -119,7 +120,7 @@ export const make = Effect.gen(function* () { |
| 119 | 120 | }); |
| 120 | 121 | |
| 121 | 122 | const provider: PullRequestProviderApi = { |
| 122 | kind: "gitlab", | |
| 123 | kind: SourceControlProviderKind.make("gitlab"), | |
| 123 | 124 | capabilities: CAPABILITIES, |
| 124 | 125 | |
| 125 | 126 | getViewer: (input) => |
packages/source-control-gitlab/src/server/GitLabSourceControlProvider.test.ts+3-2| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | |
| 1 | 2 | import { assert, it } from "@effect/vitest"; |
| 2 | 3 | import * as Effect from "effect/Effect"; |
| 3 | 4 | import * as Layer from "effect/Layer"; |
| ... | ... | @@ -38,7 +39,7 @@ it.effect("maps GitLab MR summaries into provider-neutral change requests", () = |
| 38 | 39 | }); |
| 39 | 40 | |
| 40 | 41 | assert.deepStrictEqual(changeRequest, { |
| 41 | provider: "gitlab", | |
| 42 | provider: SourceControlProviderKind.make("gitlab"), | |
| 42 | 43 | number: 42, |
| 43 | 44 | title: "Add GitLab provider", |
| 44 | 45 | url: "https://gitlab.com/pingdotgg/t3code/-/merge_requests/42", |
| ... | ... | @@ -189,7 +190,7 @@ it("refines unknown GitLab remotes with mixed-case provider hosts", () => { |
| 189 | 190 | cwd: "/repo", |
| 190 | 191 | context: { |
| 191 | 192 | provider: { |
| 192 | kind: "unknown", | |
| 193 | kind: SourceControlProviderKind.make("unknown"), | |
| 193 | 194 | name: "Self-Hosted.Example.Test", |
| 194 | 195 | baseUrl: "https://Self-Hosted.Example.Test", |
| 195 | 196 | }, |
packages/source-control-gitlab/src/server/GitLabSourceControlProvider.ts+14-13| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | |
| 1 | 2 | import * as Schema from "effect/Schema"; |
| 2 | 3 | import * as Effect from "effect/Effect"; |
| 3 | 4 | import * as Option from "effect/Option"; |
| ... | ... | @@ -25,7 +26,7 @@ const decodeLinkSubject = Schema.decodeUnknownEffect( |
| 25 | 26 | |
| 26 | 27 | function toChangeRequest(summary: GitLabCli.GitLabMergeRequestSummary): ChangeRequest { |
| 27 | 28 | return { |
| 28 | provider: "gitlab", | |
| 29 | provider: SourceControlProviderKind.make("gitlab"), | |
| 29 | 30 | number: summary.number, |
| 30 | 31 | title: summary.title, |
| 31 | 32 | url: summary.url, |
| ... | ... | @@ -90,7 +91,7 @@ function refineUnknownGitLabRemote(input: SourceControlUnknownRemoteRefinementIn |
| 90 | 91 | } |
| 91 | 92 | |
| 92 | 93 | return { |
| 93 | kind: "gitlab", | |
| 94 | kind: SourceControlProviderKind.make("gitlab"), | |
| 94 | 95 | name: "GitLab Self-Hosted", |
| 95 | 96 | baseUrl: input.context.provider.baseUrl, |
| 96 | 97 | } as const; |
| ... | ... | @@ -98,7 +99,7 @@ function refineUnknownGitLabRemote(input: SourceControlUnknownRemoteRefinementIn |
| 98 | 99 | |
| 99 | 100 | export const discovery = { |
| 100 | 101 | type: "cli", |
| 101 | kind: "gitlab", | |
| 102 | kind: SourceControlProviderKind.make("gitlab"), | |
| 102 | 103 | label: "GitLab", |
| 103 | 104 | executable: "glab", |
| 104 | 105 | versionArgs: ["--version"], |
| ... | ... | @@ -127,7 +128,7 @@ export const make = Effect.gen(function* () { |
| 127 | 128 | Effect.mapError( |
| 128 | 129 | (cause) => |
| 129 | 130 | new SourceControlProviderError({ |
| 130 | provider: "gitlab", | |
| 131 | provider: SourceControlProviderKind.make("gitlab"), | |
| 131 | 132 | operation: "resolveLink", |
| 132 | 133 | cwd: input.cwd, |
| 133 | 134 | detail: "The linked subject could not be read.", |
| ... | ... | @@ -139,7 +140,7 @@ export const make = Effect.gen(function* () { |
| 139 | 140 | Effect.mapError( |
| 140 | 141 | (cause) => |
| 141 | 142 | new SourceControlProviderError({ |
| 142 | provider: "gitlab", | |
| 143 | provider: SourceControlProviderKind.make("gitlab"), | |
| 143 | 144 | operation: "resolveLink.decode", |
| 144 | 145 | cwd: input.cwd, |
| 145 | 146 | detail: "The linked subject could not be read.", |
| ... | ... | @@ -151,7 +152,7 @@ export const make = Effect.gen(function* () { |
| 151 | 152 | }); |
| 152 | 153 | |
| 153 | 154 | return SourceControlProvider.SourceControlProvider.of({ |
| 154 | kind: "gitlab", | |
| 155 | kind: SourceControlProviderKind.make("gitlab"), | |
| 155 | 156 | resolveLink: (input) => { |
| 156 | 157 | // Automatic enrichment must not send ambient CLI credentials to a host from message text. |
| 157 | 158 | if (input.url.host !== "gitlab.com") return undefined; |
| ... | ... | @@ -179,7 +180,7 @@ export const make = Effect.gen(function* () { |
| 179 | 180 | Effect.mapError( |
| 180 | 181 | (error) => |
| 181 | 182 | new SourceControlProviderError({ |
| 182 | provider: "gitlab", | |
| 183 | provider: SourceControlProviderKind.make("gitlab"), | |
| 183 | 184 | operation: "listChangeRequests", |
| 184 | 185 | command: error.command, |
| 185 | 186 | cwd: input.cwd, |
| ... | ... | @@ -198,7 +199,7 @@ export const make = Effect.gen(function* () { |
| 198 | 199 | Effect.mapError( |
| 199 | 200 | (error) => |
| 200 | 201 | new SourceControlProviderError({ |
| 201 | provider: "gitlab", | |
| 202 | provider: SourceControlProviderKind.make("gitlab"), | |
| 202 | 203 | operation: "getChangeRequest", |
| 203 | 204 | command: error.command, |
| 204 | 205 | cwd: input.cwd, |
| ... | ... | @@ -226,7 +227,7 @@ export const make = Effect.gen(function* () { |
| 226 | 227 | Effect.mapError( |
| 227 | 228 | (error) => |
| 228 | 229 | new SourceControlProviderError({ |
| 229 | provider: "gitlab", | |
| 230 | provider: SourceControlProviderKind.make("gitlab"), | |
| 230 | 231 | operation: "createChangeRequest", |
| 231 | 232 | command: error.command, |
| 232 | 233 | cwd: input.cwd, |
| ... | ... | @@ -244,7 +245,7 @@ export const make = Effect.gen(function* () { |
| 244 | 245 | Effect.mapError( |
| 245 | 246 | (error) => |
| 246 | 247 | new SourceControlProviderError({ |
| 247 | provider: "gitlab", | |
| 248 | provider: SourceControlProviderKind.make("gitlab"), | |
| 248 | 249 | operation: "getRepositoryCloneUrls", |
| 249 | 250 | command: error.command, |
| 250 | 251 | cwd: input.cwd, |
| ... | ... | @@ -261,7 +262,7 @@ export const make = Effect.gen(function* () { |
| 261 | 262 | Effect.mapError( |
| 262 | 263 | (error) => |
| 263 | 264 | new SourceControlProviderError({ |
| 264 | provider: "gitlab", | |
| 265 | provider: SourceControlProviderKind.make("gitlab"), | |
| 265 | 266 | operation: "createRepository", |
| 266 | 267 | command: error.command, |
| 267 | 268 | cwd: input.cwd, |
| ... | ... | @@ -278,7 +279,7 @@ export const make = Effect.gen(function* () { |
| 278 | 279 | Effect.mapError( |
| 279 | 280 | (error) => |
| 280 | 281 | new SourceControlProviderError({ |
| 281 | provider: "gitlab", | |
| 282 | provider: SourceControlProviderKind.make("gitlab"), | |
| 282 | 283 | operation: "getDefaultBranch", |
| 283 | 284 | command: error.command, |
| 284 | 285 | cwd: input.cwd, |
| ... | ... | @@ -292,7 +293,7 @@ export const make = Effect.gen(function* () { |
| 292 | 293 | Effect.mapError( |
| 293 | 294 | (error) => |
| 294 | 295 | new SourceControlProviderError({ |
| 295 | provider: "gitlab", | |
| 296 | provider: SourceControlProviderKind.make("gitlab"), | |
| 296 | 297 | operation: "checkoutChangeRequest", |
| 297 | 298 | command: error.command, |
| 298 | 299 | cwd: input.cwd, |
packages/source-control-gitlab/src/server/driver.ts+2-1| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | |
| 1 | 2 | /** |
| 2 | 3 | * The GitLab driver: repository operations, discovery, and merge requests through `glab`. |
| 3 | 4 | * |
| ... | ... | @@ -10,7 +11,7 @@ import * as GitLabPullRequestProvider from "./GitLabPullRequestProvider.ts"; |
| 10 | 11 | import * as GitLabSourceControlProvider from "./GitLabSourceControlProvider.ts"; |
| 11 | 12 | |
| 12 | 13 | export const driver = defineSourceControlDriver({ |
| 13 | kind: "gitlab", | |
| 14 | kind: SourceControlProviderKind.make("gitlab"), | |
| 14 | 15 | make: Effect.all({ |
| 15 | 16 | sourceControl: GitLabSourceControlProvider.make, |
| 16 | 17 | discovery: Effect.succeed(GitLabSourceControlProvider.discovery), |