| 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,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | ||
| 1 | import { MaterialListRow } from "../../components/MaterialListRow"; | 2 | import { MaterialListRow } from "../../components/MaterialListRow"; |
| 2 | import { SettingsScreen } from "../settings/components/SettingsScreen"; | 3 | import { SettingsScreen } from "../settings/components/SettingsScreen"; |
| 3 | import { ScreenScrollView as ScrollView } from "../../components/ScreenScrollView"; | 4 | import { ScreenScrollView as ScrollView } from "../../components/ScreenScrollView"; |
| ... | @@ -812,7 +813,7 @@ export function AddProjectRepositoryScreen(props: { | ... | @@ -812,7 +813,7 @@ export function AddProjectRepositoryScreen(props: { |
| 812 | const result = await lookupRepositoryQuery({ | 813 | const result = await lookupRepositoryQuery({ |
| 813 | environmentId: environment.environmentId, | 814 | environmentId: environment.environmentId, |
| 814 | input: { | 815 | input: { |
| 815 | provider, | 816 | provider: SourceControlProviderKind.make(provider), |
| 816 | repository: repositoryInput.trim(), | 817 | repository: repositoryInput.trim(), |
| 817 | }, | 818 | }, |
| 818 | }); | 819 | }); |
| ... | @@ -1079,7 +1080,7 @@ export function AddProjectNewScreen(props: { readonly environmentId?: string | s | ... | @@ -1079,7 +1080,7 @@ export function AddProjectNewScreen(props: { readonly environmentId?: string | s |
| 1079 | environmentId: environment.environmentId, | 1080 | environmentId: environment.environmentId, |
| 1080 | input: { | 1081 | input: { |
| 1081 | cwd: workspaceRoot, | 1082 | cwd: workspaceRoot, |
| 1082 | provider: "github", | 1083 | provider: SourceControlProviderKind.make("github"), |
| 1083 | repository: getNewProjectGitHubRepository(githubTarget, workspaceRoot), | 1084 | repository: getNewProjectGitHubRepository(githubTarget, workspaceRoot), |
| 1084 | visibility: "private", | 1085 | visibility: "private", |
| 1085 | }, | 1086 | }, |
apps/mobile/src/state/use-thread-pr.test.ts+2-1| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | ||
| 1 | import { ProjectId, type ThreadPullRequestLink, type VcsStatusResult } from "@t3tools/contracts"; | 2 | import { ProjectId, type ThreadPullRequestLink, type VcsStatusResult } from "@t3tools/contracts"; |
| 2 | import { describe, expect, it } from "vite-plus/test"; | 3 | import { describe, expect, it } from "vite-plus/test"; |
| 3 | 4 | ||
| ... | @@ -28,7 +29,7 @@ describe("presentThreadPr", () => { | ... | @@ -28,7 +29,7 @@ describe("presentThreadPr", () => { |
| 28 | it("uses merge-request terminology for GitLab", () => { | 29 | it("uses merge-request terminology for GitLab", () => { |
| 29 | expect( | 30 | expect( |
| 30 | presentThreadPr(pullRequest, { | 31 | presentThreadPr(pullRequest, { |
| 31 | kind: "gitlab", | 32 | kind: SourceControlProviderKind.make("gitlab"), |
| 32 | name: "GitLab", | 33 | name: "GitLab", |
| 33 | baseUrl: "https://gitlab.com", | 34 | baseUrl: "https://gitlab.com", |
| 34 | }), | 35 | }), |
apps/server/src/git/GitManager.test.ts+15-5| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | ||
| 1 | // @effect-diagnostics nodeBuiltinImport:off | 2 | // @effect-diagnostics nodeBuiltinImport:off |
| 2 | import * as NodeFS from "node:fs"; | 3 | import * as NodeFS from "node:fs"; |
| 3 | import * as NodePath from "node:path"; | 4 | import * as NodePath from "node:path"; |
| ... | @@ -410,7 +411,13 @@ function createGitHubProviderWithFakeGh(scenario: FakeGhScenario = {}): { | ... | @@ -410,7 +411,13 @@ function createGitHubProviderWithFakeGh(scenario: FakeGhScenario = {}): { |
| 410 | const ghCalls: string[] = []; | 411 | const ghCalls: string[] = []; |
| 411 | 412 | ||
| 412 | const fail = (cwd: string, detail: string, cause?: unknown, operation = "fakeGh") => | 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 | /** `operation` is the provider method the call answers, which a failure reports. */ | 422 | /** `operation` is the provider method the call answers, which a failure reports. */ |
| 416 | const execute = (input: { | 423 | const execute = (input: { |
| ... | @@ -534,7 +541,7 @@ function createGitHubProviderWithFakeGh(scenario: FakeGhScenario = {}): { | ... | @@ -534,7 +541,7 @@ function createGitHubProviderWithFakeGh(scenario: FakeGhScenario = {}): { |
| 534 | /** What GitHubSourceControlProvider makes of a pull request it read. */ | 541 | /** What GitHubSourceControlProvider makes of a pull request it read. */ |
| 535 | const toChangeRequest = (summary: FakePullRequestSummary): ChangeRequest => ({ | 542 | const toChangeRequest = (summary: FakePullRequestSummary): ChangeRequest => ({ |
| 536 | ...summary, | 543 | ...summary, |
| 537 | provider: "github", | 544 | provider: SourceControlProviderKind.make("github"), |
| 538 | state: summary.state ?? "open", | 545 | state: summary.state ?? "open", |
| 539 | closedAt: summary.closedAt ?? null, | 546 | closedAt: summary.closedAt ?? null, |
| 540 | mergedAt: summary.mergedAt ?? null, | 547 | mergedAt: summary.mergedAt ?? null, |
| ... | @@ -546,7 +553,7 @@ function createGitHubProviderWithFakeGh(scenario: FakeGhScenario = {}): { | ... | @@ -546,7 +553,7 @@ function createGitHubProviderWithFakeGh(scenario: FakeGhScenario = {}): { |
| 546 | 553 | ||
| 547 | return { | 554 | return { |
| 548 | service: { | 555 | service: { |
| 549 | kind: "github", | 556 | kind: SourceControlProviderKind.make("github"), |
| 550 | // The GitHub provider's own lookup rule and template convention, which GitManager reads | 557 | // The GitHub provider's own lookup rule and template convention, which GitManager reads |
| 551 | // instead of the kind. | 558 | // instead of the kind. |
| 552 | headBranchProbe: ({ headSelectors }) => ({ | 559 | headBranchProbe: ({ headSelectors }) => ({ |
| ... | @@ -602,7 +609,10 @@ function createGitHubProviderWithFakeGh(scenario: FakeGhScenario = {}): { | ... | @@ -602,7 +609,10 @@ function createGitHubProviderWithFakeGh(scenario: FakeGhScenario = {}): { |
| 602 | if (raw.length === 0) return []; | 609 | if (raw.length === 0) return []; |
| 603 | const decoded = decodeGitHubPullRequestListJson(raw); | 610 | const decoded = decodeGitHubPullRequestListJson(raw); |
| 604 | return Result.isSuccess(decoded) | 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,7 +2851,7 @@ it.layer(layerGitManagerTest)("GitManager", (it) => { |
| 2841 | operation: "lookupStatusPr", | 2851 | operation: "lookupStatusPr", |
| 2842 | branch: "feature/status-rate-limited", | 2852 | branch: "feature/status-rate-limited", |
| 2843 | errorTag: "SourceControlProviderError", | 2853 | errorTag: "SourceControlProviderError", |
| 2844 | provider: "github", | 2854 | provider: SourceControlProviderKind.make("github"), |
| 2845 | providerOperation: "listChangeRequests", | 2855 | providerOperation: "listChangeRequests", |
| 2846 | errorDetail: "GitHub API rate limit exceeded. Requests resume when the limit resets.", | 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,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | ||
| 1 | import * as Arr from "effect/Array"; | 2 | import * as Arr from "effect/Array"; |
| 2 | import * as Cache from "effect/Cache"; | 3 | import * as Cache from "effect/Cache"; |
| 3 | import * as Context from "effect/Context"; | 4 | import * as Context from "effect/Context"; |
| ... | @@ -1787,7 +1788,9 @@ export const make = Effect.gen(function* () { | ... | @@ -1787,7 +1788,9 @@ export const make = Effect.gen(function* () { |
| 1787 | ) { | 1788 | ) { |
| 1788 | const terms = yield* sourceControlProvider(cwd).pipe( | 1789 | const terms = yield* sourceControlProvider(cwd).pipe( |
| 1789 | Effect.map((provider) => getChangeRequestTerminologyForKind(provider.kind)), | 1790 | Effect.map((provider) => getChangeRequestTerminologyForKind(provider.kind)), |
| 1790 | Effect.orElseSucceed(() => getChangeRequestTerminologyForKind("unknown")), | 1791 | Effect.orElseSucceed(() => |
| 1792 | getChangeRequestTerminologyForKind(SourceControlProviderKind.make("unknown")), | ||
| 1793 | ), | ||
| 1791 | ); | 1794 | ); |
| 1792 | const summary = summarizeGitActionResult(result, terms); | 1795 | const summary = summarizeGitActionResult(result, terms); |
| 1793 | let latestOpenPr: PullRequestInfo | null = null; | 1796 | let latestOpenPr: PullRequestInfo | null = null; |
| ... | @@ -2853,7 +2856,9 @@ export const make = Effect.gen(function* () { | ... | @@ -2853,7 +2856,9 @@ export const make = Effect.gen(function* () { |
| 2853 | const changeRequestTerms = wantsPr | 2856 | const changeRequestTerms = wantsPr |
| 2854 | ? yield* sourceControlProvider(input.cwd).pipe( | 2857 | ? yield* sourceControlProvider(input.cwd).pipe( |
| 2855 | Effect.map((provider) => getChangeRequestTerminologyForKind(provider.kind)), | 2858 | Effect.map((provider) => getChangeRequestTerminologyForKind(provider.kind)), |
| 2856 | Effect.orElseSucceed(() => getChangeRequestTerminologyForKind("unknown")), | 2859 | Effect.orElseSucceed(() => |
| 2860 | getChangeRequestTerminologyForKind(SourceControlProviderKind.make("unknown")), | ||
| 2861 | ), | ||
| 2857 | ) | 2862 | ) |
| 2858 | : null; | 2863 | : null; |
| 2859 | 2864 |
apps/server/src/orchestration-v2/PullRequestSyncReactor.test.ts+4-3| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | ||
| 1 | import * as Stream from "effect/Stream"; | 2 | import * as Stream from "effect/Stream"; |
| 2 | import { | 3 | import { |
| 3 | EventId, | 4 | EventId, |
| ... | @@ -148,7 +149,7 @@ function makeSummary( | ... | @@ -148,7 +149,7 @@ function makeSummary( |
| 148 | overrides: Partial<PullRequestSummary> = {}, | 149 | overrides: Partial<PullRequestSummary> = {}, |
| 149 | ): PullRequestSummary { | 150 | ): PullRequestSummary { |
| 150 | return { | 151 | return { |
| 151 | provider: "github", | 152 | provider: SourceControlProviderKind.make("github"), |
| 152 | projectId: input.projectId, | 153 | projectId: input.projectId, |
| 153 | repository: input.repository, | 154 | repository: input.repository, |
| 154 | number: input.number, | 155 | number: input.number, |
| ... | @@ -1009,7 +1010,7 @@ describe("PullRequestSyncReactor", () => { | ... | @@ -1009,7 +1010,7 @@ describe("PullRequestSyncReactor", () => { |
| 1009 | Effect.gen(function* () { | 1010 | Effect.gen(function* () { |
| 1010 | if (input.host === "github.com" && (yield* Clock.currentTimeMillis) < retryAt) { | 1011 | if (input.host === "github.com" && (yield* Clock.currentTimeMillis) < retryAt) { |
| 1011 | const paused = new PullRequestProviderError({ | 1012 | const paused = new PullRequestProviderError({ |
| 1012 | provider: "github", | 1013 | provider: SourceControlProviderKind.make("github"), |
| 1013 | operation: "getChangeRequestSummary", | 1014 | operation: "getChangeRequestSummary", |
| 1014 | reason: "rate-limited", | 1015 | reason: "rate-limited", |
| 1015 | detail: "paused", | 1016 | detail: "paused", |
| ... | @@ -1079,7 +1080,7 @@ describe("PullRequestSyncReactor", () => { | ... | @@ -1079,7 +1080,7 @@ describe("PullRequestSyncReactor", () => { |
| 1079 | operation: "stack", | 1080 | operation: "stack", |
| 1080 | detail: "paused", | 1081 | detail: "paused", |
| 1081 | cause: new PullRequestProviderError({ | 1082 | cause: new PullRequestProviderError({ |
| 1082 | provider: "github", | 1083 | provider: SourceControlProviderKind.make("github"), |
| 1083 | operation: "getChangeRequestStack", | 1084 | operation: "getChangeRequestStack", |
| 1084 | reason: "rate-limited", | 1085 | reason: "rate-limited", |
| 1085 | detail: "paused", | 1086 | detail: "paused", |
apps/server/src/orchestration-v2/ThreadSettlementService.test.ts+2-1| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | ||
| 1 | import { assert, describe, expect, it } from "@effect/vitest"; | 2 | import { assert, describe, expect, it } from "@effect/vitest"; |
| 2 | import * as DateTime from "effect/DateTime"; | 3 | import * as DateTime from "effect/DateTime"; |
| 3 | import { | 4 | import { |
| ... | @@ -472,7 +473,7 @@ function makePullRequestSummary(input: { | ... | @@ -472,7 +473,7 @@ function makePullRequestSummary(input: { |
| 472 | readonly updatedAt?: string; | 473 | readonly updatedAt?: string; |
| 473 | }): PullRequestSummary { | 474 | }): PullRequestSummary { |
| 474 | return { | 475 | return { |
| 475 | provider: "github", | 476 | provider: SourceControlProviderKind.make("github"), |
| 476 | projectId: input.projectId, | 477 | projectId: input.projectId, |
| 477 | repository: input.repository, | 478 | repository: input.repository, |
| 478 | number: input.number, | 479 | number: input.number, |
apps/server/src/orchestration-v2/runtimeLayer.test.ts+4-3| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | ||
| 1 | import { limitRecoveryCommand } from "./UsageLimitRecoveryWorker.ts"; | 2 | import { limitRecoveryCommand } from "./UsageLimitRecoveryWorker.ts"; |
| 2 | import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions"; | 3 | import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions"; |
| 3 | import * as SourceControlProviderRegistry from "../sourceControl/SourceControlProviderRegistry.ts"; | 4 | import * as SourceControlProviderRegistry from "../sourceControl/SourceControlProviderRegistry.ts"; |
| ... | @@ -169,7 +170,7 @@ const watchedPullRequestDetail = (input: { | ... | @@ -169,7 +170,7 @@ const watchedPullRequestDetail = (input: { |
| 169 | readonly number: number; | 170 | readonly number: number; |
| 170 | readonly at: string; | 171 | readonly at: string; |
| 171 | }): PullRequestDetail => ({ | 172 | }): PullRequestDetail => ({ |
| 172 | provider: "github", | 173 | provider: SourceControlProviderKind.make("github"), |
| 173 | capabilities: { | 174 | capabilities: { |
| 174 | diff: true, | 175 | diff: true, |
| 175 | comment: true, | 176 | comment: true, |
| ... | @@ -2572,7 +2573,7 @@ it.layer(layerTest)("OrchestrationV2LayerLive lifecycle", (it) => { | ... | @@ -2572,7 +2573,7 @@ it.layer(layerTest)("OrchestrationV2LayerLive lifecycle", (it) => { |
| 2572 | operation: "getChangeRequest", | 2573 | operation: "getChangeRequest", |
| 2573 | detail: "github requests are paused until the rate limit resets", | 2574 | detail: "github requests are paused until the rate limit resets", |
| 2574 | cause: new PullRequestProviderError({ | 2575 | cause: new PullRequestProviderError({ |
| 2575 | provider: "github", | 2576 | provider: SourceControlProviderKind.make("github"), |
| 2576 | operation: "getChangeRequest", | 2577 | operation: "getChangeRequest", |
| 2577 | reason: "rate-limited", | 2578 | reason: "rate-limited", |
| 2578 | detail: "paused", | 2579 | detail: "paused", |
| ... | @@ -2831,7 +2832,7 @@ it.layer(layerTest)("OrchestrationV2LayerLive lifecycle", (it) => { | ... | @@ -2831,7 +2832,7 @@ it.layer(layerTest)("OrchestrationV2LayerLive lifecycle", (it) => { |
| 2831 | operation: "watchFingerprint", | 2832 | operation: "watchFingerprint", |
| 2832 | detail: "paused", | 2833 | detail: "paused", |
| 2833 | cause: new PullRequestProviderError({ | 2834 | cause: new PullRequestProviderError({ |
| 2834 | provider: "github", | 2835 | provider: SourceControlProviderKind.make("github"), |
| 2835 | operation: "getChangeRequestWatchFingerprint", | 2836 | operation: "getChangeRequestWatchFingerprint", |
| 2836 | reason: "rate-limited", | 2837 | reason: "rate-limited", |
| 2837 | detail: "paused", | 2838 | detail: "paused", |
apps/server/src/project/ProjectCloneTracker.test.ts+3-2| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | ||
| 1 | import { describe, expect, it } from "@effect/vitest"; | 2 | import { describe, expect, it } from "@effect/vitest"; |
| 2 | import { | 3 | import { |
| 3 | OrchestrationDispatchCommandError, | 4 | OrchestrationDispatchCommandError, |
| ... | @@ -116,7 +117,7 @@ describe("ProjectCloneTracker", () => { | ... | @@ -116,7 +117,7 @@ describe("ProjectCloneTracker", () => { |
| 116 | ? Effect.fail( | 117 | ? Effect.fail( |
| 117 | new SourceControlRepositoryError({ | 118 | new SourceControlRepositoryError({ |
| 118 | operation: "cloneRepository", | 119 | operation: "cloneRepository", |
| 119 | provider: "unknown", | 120 | provider: SourceControlProviderKind.make("unknown"), |
| 120 | detail: "fatal: repository not found", | 121 | detail: "fatal: repository not found", |
| 121 | }), | 122 | }), |
| 122 | ) | 123 | ) |
| ... | @@ -255,7 +256,7 @@ describe("ProjectCloneTracker", () => { | ... | @@ -255,7 +256,7 @@ describe("ProjectCloneTracker", () => { |
| 255 | Effect.fail( | 256 | Effect.fail( |
| 256 | new SourceControlRepositoryError({ | 257 | new SourceControlRepositoryError({ |
| 257 | operation: "cloneRepository", | 258 | operation: "cloneRepository", |
| 258 | provider: "unknown", | 259 | provider: SourceControlProviderKind.make("unknown"), |
| 259 | detail: "Destination path already exists and is not empty.", | 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,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | ||
| 1 | import type { | 2 | import type { |
| 2 | ProjectCloneSnapshot, | 3 | ProjectCloneSnapshot, |
| 3 | ProjectCloneStage, | 4 | ProjectCloneStage, |
| ... | @@ -301,7 +302,7 @@ export const make = Effect.gen(function* () { | ... | @@ -301,7 +302,7 @@ export const make = Effect.gen(function* () { |
| 301 | if (!claimed) { | 302 | if (!claimed) { |
| 302 | return yield* new SourceControlRepositoryError({ | 303 | return yield* new SourceControlRepositoryError({ |
| 303 | operation: "cloneRepository", | 304 | operation: "cloneRepository", |
| 304 | provider: input.provider ?? "unknown", | 305 | provider: input.provider ?? SourceControlProviderKind.make("unknown"), |
| 305 | detail: "A clone into this destination is already in progress.", | 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,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | ||
| 1 | // @effect-diagnostics nodeBuiltinImport:off - realpathSync.native resolves Windows 8.3 short names, which the Effect realPath does not. | 2 | // @effect-diagnostics nodeBuiltinImport:off - realpathSync.native resolves Windows 8.3 short names, which the Effect realPath does not. |
| 2 | import * as NodeFS from "node:fs"; | 3 | import * as NodeFS from "node:fs"; |
| 3 | import * as NodeServices from "@effect/platform-node/NodeServices"; | 4 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| ... | @@ -71,7 +72,7 @@ it.layer(NodeServices.layer)("RepositoryIdentityResolverLive", (it) => { | ... | @@ -71,7 +72,7 @@ it.layer(NodeServices.layer)("RepositoryIdentityResolverLive", (it) => { |
| 71 | if (refinementFails) | 72 | if (refinementFails) |
| 72 | return Effect.fail( | 73 | return Effect.fail( |
| 73 | new SourceControlProviderError({ | 74 | new SourceControlProviderError({ |
| 74 | provider: "forgejo", | 75 | provider: SourceControlProviderKind.make("forgejo"), |
| 75 | operation: "detectProvider", | 76 | operation: "detectProvider", |
| 76 | cwd: rootPath, | 77 | cwd: rootPath, |
| 77 | detail: "account unavailable", | 78 | detail: "account unavailable", |
apps/server/src/pullRequest/PullRequestProviderRateLimit.test.ts+2-1| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | ||
| 1 | import { assert, it } from "@effect/vitest"; | 2 | import { assert, it } from "@effect/vitest"; |
| 2 | 3 | ||
| 3 | import * as AzureDevOpsCli from "@t3tools/source-control-azure-devops/server/AzureDevOpsCli"; | 4 | import * as AzureDevOpsCli from "@t3tools/source-control-azure-devops/server/AzureDevOpsCli"; |
| ... | @@ -59,7 +60,7 @@ it("keeps GitHub's exact retry time", () => { | ... | @@ -59,7 +60,7 @@ it("keeps GitHub's exact retry time", () => { |
| 59 | assert.deepStrictEqual( | 60 | assert.deepStrictEqual( |
| 60 | gitHubProviderFailure( | 61 | gitHubProviderFailure( |
| 61 | new SourceControlRateLimit.SourceControlRateLimitPausedError({ | 62 | new SourceControlRateLimit.SourceControlRateLimitPausedError({ |
| 62 | provider: "github", | 63 | provider: SourceControlProviderKind.make("github"), |
| 63 | host: "github.com", | 64 | host: "github.com", |
| 64 | retryAt: 1_786_802_400_000, | 65 | retryAt: 1_786_802_400_000, |
| 65 | }), | 66 | }), |
apps/server/src/pullRequest/PullRequestService.test.ts+295-238| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | ||
| 1 | import * as NodeServices from "@effect/platform-node/NodeServices"; | 2 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 2 | import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"; | 3 | import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"; |
| 3 | import * as KeyValueStore from "effect/persistence/KeyValueStore"; | 4 | import * as KeyValueStore from "effect/persistence/KeyValueStore"; |
| ... | @@ -17,7 +18,6 @@ import type { | ... | @@ -17,7 +18,6 @@ import type { |
| 17 | ProjectId, | 18 | ProjectId, |
| 18 | PullRequestReviewCapabilities, | 19 | PullRequestReviewCapabilities, |
| 19 | PullRequestReviewerCapabilities, | 20 | PullRequestReviewerCapabilities, |
| 20 | SourceControlProviderKind, | ||
| 21 | } from "@t3tools/contracts"; | 21 | } from "@t3tools/contracts"; |
| 22 | import { DEFAULT_SERVER_SETTINGS } from "@t3tools/contracts/settings"; | 22 | import { DEFAULT_SERVER_SETTINGS } from "@t3tools/contracts/settings"; |
| 23 | import type { ServerSettings } from "@t3tools/contracts"; | 23 | import type { ServerSettings } from "@t3tools/contracts"; |
| ... | @@ -129,7 +129,7 @@ it.effect("caches narrow previews and invalidates them after refresh or mutation | ... | @@ -129,7 +129,7 @@ it.effect("caches narrow previews and invalidates them after refresh or mutation |
| 129 | const service = yield* makeService({ | 129 | const service = yield* makeService({ |
| 130 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/w", repository: "acme/web" })], | 130 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/w", repository: "acme/web" })], |
| 131 | providers: [ | 131 | providers: [ |
| 132 | fakeProvider("github", { | 132 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 133 | getViewer: () => Effect.die("preview must not read the viewer"), | 133 | getViewer: () => Effect.die("preview must not read the viewer"), |
| 134 | getChangeRequestPreview: () => | 134 | getChangeRequestPreview: () => |
| 135 | Effect.sync(() => { | 135 | Effect.sync(() => { |
| ... | @@ -175,13 +175,13 @@ it.effect("keeps cached previews available and pauses uncached previews until qu | ... | @@ -175,13 +175,13 @@ it.effect("keeps cached previews available and pauses uncached previews until qu |
| 175 | const service = yield* makeService({ | 175 | const service = yield* makeService({ |
| 176 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/w", repository: "acme/web" })], | 176 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/w", repository: "acme/web" })], |
| 177 | providers: [ | 177 | providers: [ |
| 178 | fakeProvider("github", { | 178 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 179 | getChangeRequestPreview: ({ number }) => | 179 | getChangeRequestPreview: ({ number }) => |
| 180 | Effect.gen(function* () { | 180 | Effect.gen(function* () { |
| 181 | reads++; | 181 | reads++; |
| 182 | if (reads === 2) | 182 | if (reads === 2) |
| 183 | return yield* new PullRequestProviderError({ | 183 | return yield* new PullRequestProviderError({ |
| 184 | provider: "github", | 184 | provider: SourceControlProviderKind.make("github"), |
| 185 | operation: "getChangeRequestPreview", | 185 | operation: "getChangeRequestPreview", |
| 186 | reason: "rate-limited", | 186 | reason: "rate-limited", |
| 187 | detail: "GitHub requests are paused until the rate limit resets.", | 187 | detail: "GitHub requests are paused until the rate limit resets.", |
| ... | @@ -218,7 +218,7 @@ it.effect("reuses only unexpired detail for previews", () => | ... | @@ -218,7 +218,7 @@ it.effect("reuses only unexpired detail for previews", () => |
| 218 | const service = yield* makeService({ | 218 | const service = yield* makeService({ |
| 219 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/w", repository: "acme/web" })], | 219 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/w", repository: "acme/web" })], |
| 220 | providers: [ | 220 | providers: [ |
| 221 | fakeProvider("github", { | 221 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 222 | getChangeRequest: () => Effect.succeed(hostedChangeRequest("Description")), | 222 | getChangeRequest: () => Effect.succeed(hostedChangeRequest("Description")), |
| 223 | getChangeRequestPreview: () => | 223 | getChangeRequestPreview: () => |
| 224 | Effect.sync(() => { | 224 | Effect.sync(() => { |
| ... | @@ -250,7 +250,7 @@ it.effect("shares detail and activity for a minute, and for ten once merged", () | ... | @@ -250,7 +250,7 @@ it.effect("shares detail and activity for a minute, and for ten once merged", () |
| 250 | const service = yield* makeService({ | 250 | const service = yield* makeService({ |
| 251 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/w", repository: "acme/web" })], | 251 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/w", repository: "acme/web" })], |
| 252 | providers: [ | 252 | providers: [ |
| 253 | fakeProvider("github", { | 253 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 254 | getChangeRequest: ({ number }) => | 254 | getChangeRequest: ({ number }) => |
| 255 | Effect.sync(() => { | 255 | Effect.sync(() => { |
| 256 | reads.push(`detail #${number}`); | 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,7 +314,7 @@ it.effect("does not wait for an in-flight detail read to display a preview", () |
| 314 | const service = yield* makeService({ | 314 | const service = yield* makeService({ |
| 315 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/w", repository: "acme/web" })], | 315 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/w", repository: "acme/web" })], |
| 316 | providers: [ | 316 | providers: [ |
| 317 | fakeProvider("github", { | 317 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 318 | getChangeRequest: () => | 318 | getChangeRequest: () => |
| 319 | Deferred.succeed(detailStarted, undefined).pipe( | 319 | Deferred.succeed(detailStarted, undefined).pipe( |
| 320 | Effect.andThen(Deferred.await(releaseDetail)), | 320 | Effect.andThen(Deferred.await(releaseDetail)), |
| ... | @@ -342,7 +342,7 @@ it.effect("keeps previews warm when another project finishes a turn", () => | ... | @@ -342,7 +342,7 @@ it.effect("keeps previews warm when another project finishes a turn", () => |
| 342 | project({ id: "p2", title: "docs", workspaceRoot: "/d", repository: "acme/docs" }), | 342 | project({ id: "p2", title: "docs", workspaceRoot: "/d", repository: "acme/docs" }), |
| 343 | ], | 343 | ], |
| 344 | providers: [ | 344 | providers: [ |
| 345 | fakeProvider("github", { | 345 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 346 | getChangeRequestPreview: (input) => | 346 | getChangeRequestPreview: (input) => |
| 347 | Effect.sync(() => { | 347 | Effect.sync(() => { |
| 348 | reads.push(input.repository); | 348 | reads.push(input.repository); |
| ... | @@ -374,11 +374,11 @@ it.effect("uses full detail for hosts without a narrow preview", () => | ... | @@ -374,11 +374,11 @@ it.effect("uses full detail for hosts without a narrow preview", () => |
| 374 | title: "web", | 374 | title: "web", |
| 375 | workspaceRoot: "/w", | 375 | workspaceRoot: "/w", |
| 376 | repository: "acme/web", | 376 | repository: "acme/web", |
| 377 | provider: "gitlab", | 377 | provider: SourceControlProviderKind.make("gitlab"), |
| 378 | }), | 378 | }), |
| 379 | ], | 379 | ], |
| 380 | providers: [ | 380 | providers: [ |
| 381 | fakeProvider("gitlab", { | 381 | fakeProvider(SourceControlProviderKind.make("gitlab"), { |
| 382 | getChangeRequest: () => | 382 | getChangeRequest: () => |
| 383 | Effect.sync(() => { | 383 | Effect.sync(() => { |
| 384 | reads += 1; | 384 | reads += 1; |
| ... | @@ -405,7 +405,7 @@ function unusable(provider: SourceControlProviderKind, reason: "missing-tool" | | ... | @@ -405,7 +405,7 @@ function unusable(provider: SourceControlProviderKind, reason: "missing-tool" | |
| 405 | } | 405 | } |
| 406 | 406 | ||
| 407 | const requestFailed = new PullRequestProviderError({ | 407 | const requestFailed = new PullRequestProviderError({ |
| 408 | provider: "github", | 408 | provider: SourceControlProviderKind.make("github"), |
| 409 | operation: "listChangeRequests", | 409 | operation: "listChangeRequests", |
| 410 | reason: "failed", | 410 | reason: "failed", |
| 411 | detail: "HTTP 404", | 411 | detail: "HTTP 404", |
| ... | @@ -530,12 +530,12 @@ it.effect("lists GitHub Enterprise PRs for a stored unknown repository after hos | ... | @@ -530,12 +530,12 @@ it.effect("lists GitHub Enterprise PRs for a stored unknown repository after hos |
| 530 | title: "enterprise", | 530 | title: "enterprise", |
| 531 | workspaceRoot: "/repo", | 531 | workspaceRoot: "/repo", |
| 532 | repository: "team/project", | 532 | repository: "team/project", |
| 533 | provider: "unknown", | 533 | provider: SourceControlProviderKind.make("unknown"), |
| 534 | host: "code.example.test", | 534 | host: "code.example.test", |
| 535 | }), | 535 | }), |
| 536 | ], | 536 | ], |
| 537 | providers: [ | 537 | providers: [ |
| 538 | fakeProvider("github", { | 538 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 539 | /** Supplies a PR only after verifying discovery retained the Enterprise repository target. */ | 539 | /** Supplies a PR only after verifying discovery retained the Enterprise repository target. */ |
| 540 | listChangeRequests: ({ host, repository }) => { | 540 | listChangeRequests: ({ host, repository }) => { |
| 541 | assert.strictEqual(host, "code.example.test"); | 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,7 +554,11 @@ it.effect("lists GitHub Enterprise PRs for a stored unknown repository after hos |
| 554 | return Effect.succeed({ | 554 | return Effect.succeed({ |
| 555 | context: { | 555 | context: { |
| 556 | ...context, | 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 | provider: undefined as never, | 563 | provider: undefined as never, |
| 560 | }); | 564 | }); |
| ... | @@ -576,7 +580,7 @@ it.effect("refines unknown self-hosted GitLab projects before listing merge requ | ... | @@ -576,7 +580,7 @@ it.effect("refines unknown self-hosted GitLab projects before listing merge requ |
| 576 | title: "self-hosted", | 580 | title: "self-hosted", |
| 577 | workspaceRoot: "/gitlab", | 581 | workspaceRoot: "/gitlab", |
| 578 | repository: "group/project", | 582 | repository: "group/project", |
| 579 | provider: "unknown", | 583 | provider: SourceControlProviderKind.make("unknown"), |
| 580 | host: "code.example.test", | 584 | host: "code.example.test", |
| 581 | }); | 585 | }); |
| 582 | const service = yield* makeService({ | 586 | const service = yield* makeService({ |
| ... | @@ -584,12 +588,15 @@ it.effect("refines unknown self-hosted GitLab projects before listing merge requ | ... | @@ -584,12 +588,15 @@ it.effect("refines unknown self-hosted GitLab projects before listing merge requ |
| 584 | selfHosted, | 588 | selfHosted, |
| 585 | { ...selfHosted, id: "p2" as ProjectId, workspaceRoot: "/gitlab-worktree" }, | 589 | { ...selfHosted, id: "p2" as ProjectId, workspaceRoot: "/gitlab-worktree" }, |
| 586 | ], | 590 | ], |
| 587 | providers: [fakeProvider("gitlab")], | 591 | providers: [fakeProvider(SourceControlProviderKind.make("gitlab"))], |
| 588 | resolveHandle: ({ context }) => { | 592 | resolveHandle: ({ context }) => { |
| 589 | refinementCalls += 1; | 593 | refinementCalls += 1; |
| 590 | assert.strictEqual(context?.remoteUrl, "https://code.example.test/group/project.git"); | 594 | assert.strictEqual(context?.remoteUrl, "https://code.example.test/group/project.git"); |
| 591 | return Effect.succeed({ | 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 | provider: undefined as never, | 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,7 +617,7 @@ it.effect("derives a legacy repository host after refining its provider", () => |
| 610 | title: "legacy self-hosted", | 617 | title: "legacy self-hosted", |
| 611 | workspaceRoot: "/gitlab", | 618 | workspaceRoot: "/gitlab", |
| 612 | repository: "group/project", | 619 | repository: "group/project", |
| 613 | provider: "unknown", | 620 | provider: SourceControlProviderKind.make("unknown"), |
| 614 | host: "code.example.test", | 621 | host: "code.example.test", |
| 615 | }); | 622 | }); |
| 616 | const identity = current.repositoryIdentity!; | 623 | const identity = current.repositoryIdentity!; |
| ... | @@ -625,10 +632,13 @@ it.effect("derives a legacy repository host after refining its provider", () => | ... | @@ -625,10 +632,13 @@ it.effect("derives a legacy repository host after refining its provider", () => |
| 625 | } as unknown as OrchestrationProjectShell; | 632 | } as unknown as OrchestrationProjectShell; |
| 626 | const service = yield* makeService({ | 633 | const service = yield* makeService({ |
| 627 | projects: [legacy], | 634 | projects: [legacy], |
| 628 | providers: [fakeProvider("gitlab")], | 635 | providers: [fakeProvider(SourceControlProviderKind.make("gitlab"))], |
| 629 | resolveHandle: ({ context }) => | 636 | resolveHandle: ({ context }) => |
| 630 | Effect.succeed({ | 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 | provider: undefined as never, | 642 | provider: undefined as never, |
| 633 | }), | 643 | }), |
| 634 | }); | 644 | }); |
| ... | @@ -648,18 +658,21 @@ it.effect("tries another checkout when provider refinement remains unknown", () | ... | @@ -648,18 +658,21 @@ it.effect("tries another checkout when provider refinement remains unknown", () |
| 648 | title: "self-hosted", | 658 | title: "self-hosted", |
| 649 | workspaceRoot: "/gone", | 659 | workspaceRoot: "/gone", |
| 650 | repository: "group/project", | 660 | repository: "group/project", |
| 651 | provider: "unknown", | 661 | provider: SourceControlProviderKind.make("unknown"), |
| 652 | host: "code.example.test", | 662 | host: "code.example.test", |
| 653 | }); | 663 | }); |
| 654 | const service = yield* makeService({ | 664 | const service = yield* makeService({ |
| 655 | projects: [selfHosted, { ...selfHosted, id: "p2" as ProjectId, workspaceRoot: "/healthy" }], | 665 | projects: [selfHosted, { ...selfHosted, id: "p2" as ProjectId, workspaceRoot: "/healthy" }], |
| 656 | providers: [fakeProvider("gitlab")], | 666 | providers: [fakeProvider(SourceControlProviderKind.make("gitlab"))], |
| 657 | resolveHandle: ({ cwd, context }) => { | 667 | resolveHandle: ({ cwd, context }) => { |
| 658 | asked.push(cwd); | 668 | asked.push(cwd); |
| 659 | return cwd === "/gone" | 669 | return cwd === "/gone" |
| 660 | ? Effect.succeed({ context: context!, provider: undefined as never }) | 670 | ? Effect.succeed({ context: context!, provider: undefined as never }) |
| 661 | : Effect.succeed({ | 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 | provider: undefined as never, | 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,11 +702,11 @@ it.effect("reads nothing from a host with no implementation, but reports it", () |
| 689 | title: "on gitlab", | 702 | title: "on gitlab", |
| 690 | workspaceRoot: "/c", | 703 | workspaceRoot: "/c", |
| 691 | repository: "group/project", | 704 | repository: "group/project", |
| 692 | provider: "gitlab", | 705 | provider: SourceControlProviderKind.make("gitlab"), |
| 693 | }), | 706 | }), |
| 694 | ], | 707 | ], |
| 695 | providers: [ | 708 | providers: [ |
| 696 | fakeProvider("github", { | 709 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 697 | listChangeRequests: (input) => { | 710 | listChangeRequests: (input) => { |
| 698 | listed.push(input.repository); | 711 | listed.push(input.repository); |
| 699 | return Effect.succeed({ | 712 | return Effect.succeed({ |
| ... | @@ -718,8 +731,8 @@ it.effect("reads nothing from a host with no implementation, but reports it", () | ... | @@ -718,8 +731,8 @@ it.effect("reads nothing from a host with no implementation, but reports it", () |
| 718 | projectCount: summary.projectCount, | 731 | projectCount: summary.projectCount, |
| 719 | })), | 732 | })), |
| 720 | [ | 733 | [ |
| 721 | { kind: "github", configured: true, projectCount: 1 }, | 734 | { kind: SourceControlProviderKind.make("github"), configured: true, projectCount: 1 }, |
| 722 | { kind: "gitlab", configured: false, 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,7 +746,7 @@ it.effect("asks for a whole page of a host, and for the reader's own size when g |
| 733 | project({ id: "p1", title: "t3code", workspaceRoot: "/a", repository: "pingdotgg/t3code" }), | 746 | project({ id: "p1", title: "t3code", workspaceRoot: "/a", repository: "pingdotgg/t3code" }), |
| 734 | ], | 747 | ], |
| 735 | providers: [ | 748 | providers: [ |
| 736 | fakeProvider("github", { | 749 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 737 | listChangeRequests: (input) => { | 750 | listChangeRequests: (input) => { |
| 738 | limits.push(input.limit); | 751 | limits.push(input.limit); |
| 739 | return Effect.succeed({ items: [], truncated: false, continues: true }); | 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,7 +772,7 @@ it.effect("says where each repository carries on, and from nothing it has run ou |
| 759 | project({ id: "p2", title: "web", workspaceRoot: "/b", repository: "acme/web" }), | 772 | project({ id: "p2", title: "web", workspaceRoot: "/b", repository: "acme/web" }), |
| 760 | ], | 773 | ], |
| 761 | providers: [ | 774 | providers: [ |
| 762 | fakeProvider("github", { | 775 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 763 | listChangeRequests: ({ repository }) => | 776 | listChangeRequests: ({ repository }) => |
| 764 | Effect.succeed({ | 777 | Effect.succeed({ |
| 765 | items: [changeRequest(1, "2026-07-02T00:00:00Z")], | 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,7 +800,7 @@ it.effect("offers no continuation for a host that cannot be carried on from", () |
| 787 | project({ id: "p1", title: "t3code", workspaceRoot: "/a", repository: "pingdotgg/t3code" }), | 800 | project({ id: "p1", title: "t3code", workspaceRoot: "/a", repository: "pingdotgg/t3code" }), |
| 788 | ], | 801 | ], |
| 789 | providers: [ | 802 | providers: [ |
| 790 | fakeProvider("github", { | 803 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 791 | listChangeRequests: () => | 804 | listChangeRequests: () => |
| 792 | Effect.succeed({ | 805 | Effect.succeed({ |
| 793 | items: [changeRequest(1, "2026-07-02T00:00:00Z")], | 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,12 +828,12 @@ it.effect("uses a provider's raw cursor advance when it consumed malformed rows" |
| 815 | title: "web", | 828 | title: "web", |
| 816 | workspaceRoot: "/a", | 829 | workspaceRoot: "/a", |
| 817 | repository: "acme/web", | 830 | repository: "acme/web", |
| 818 | provider: "azure-devops", | 831 | provider: SourceControlProviderKind.make("azure-devops"), |
| 819 | host: "dev.azure.com", | 832 | host: "dev.azure.com", |
| 820 | }), | 833 | }), |
| 821 | ], | 834 | ], |
| 822 | providers: [ | 835 | providers: [ |
| 823 | fakeProvider("azure-devops", { | 836 | fakeProvider(SourceControlProviderKind.make("azure-devops"), { |
| 824 | listChangeRequests: () => | 837 | listChangeRequests: () => |
| 825 | Effect.succeed({ | 838 | Effect.succeed({ |
| 826 | items: [changeRequest(7, "2026-07-02T00:00:00Z")], | 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,7 +864,7 @@ it.effect("reads only the repositories it was asked to carry on with", () => |
| 851 | project({ id: "p2", title: "web", workspaceRoot: "/b", repository: "acme/web" }), | 864 | project({ id: "p2", title: "web", workspaceRoot: "/b", repository: "acme/web" }), |
| 852 | ], | 865 | ], |
| 853 | providers: [ | 866 | providers: [ |
| 854 | fakeProvider("github", { | 867 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 855 | listChangeRequests: (input) => { | 868 | listChangeRequests: (input) => { |
| 856 | listed.push(input.repository); | 869 | listed.push(input.repository); |
| 857 | cursors.push(input.cursor); | 870 | cursors.push(input.cursor); |
| ... | @@ -882,7 +895,7 @@ it.effect("keeps a row already sent at the boundary instant from arriving twice" | ... | @@ -882,7 +895,7 @@ it.effect("keeps a row already sent at the boundary instant from arriving twice" |
| 882 | project({ id: "p1", title: "t3code", workspaceRoot: "/a", repository: "pingdotgg/t3code" }), | 895 | project({ id: "p1", title: "t3code", workspaceRoot: "/a", repository: "pingdotgg/t3code" }), |
| 883 | ], | 896 | ], |
| 884 | providers: [ | 897 | providers: [ |
| 885 | fakeProvider("github", { | 898 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 886 | // The boundary instant is asked for inclusively, so the host hands back the rows | 899 | // The boundary instant is asked for inclusively, so the host hands back the rows |
| 887 | // already sent at it alongside the ones beside them — which a strictly-older read | 900 | // already sent at it alongside the ones beside them — which a strictly-older read |
| 888 | // would have lost instead. | 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,7 +935,7 @@ it.effect("keeps the earlier exclusions when a slice ends on the instant it bega |
| 922 | project({ id: "p1", title: "t3code", workspaceRoot: "/a", repository: "pingdotgg/t3code" }), | 935 | project({ id: "p1", title: "t3code", workspaceRoot: "/a", repository: "pingdotgg/t3code" }), |
| 923 | ], | 936 | ], |
| 924 | providers: [ | 937 | providers: [ |
| 925 | fakeProvider("github", { | 938 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 926 | listChangeRequests: () => | 939 | listChangeRequests: () => |
| 927 | Effect.succeed({ | 940 | Effect.succeed({ |
| 928 | items: [ | 941 | items: [ |
| ... | @@ -960,7 +973,9 @@ it.effect("refuses a continuation it did not issue, before asking any host anyth | ... | @@ -960,7 +973,9 @@ it.effect("refuses a continuation it did not issue, before asking any host anyth |
| 960 | project({ id: "p1", title: "t3code", workspaceRoot: "/a", repository: "pingdotgg/t3code" }), | 973 | project({ id: "p1", title: "t3code", workspaceRoot: "/a", repository: "pingdotgg/t3code" }), |
| 961 | ], | 974 | ], |
| 962 | providers: [ | 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,11 +998,11 @@ it.effect("calls a transient viewer failure a failed operation, not a signed-out |
| 983 | project({ id: "p1", title: "t3code", workspaceRoot: "/a", repository: "pingdotgg/t3code" }), | 998 | project({ id: "p1", title: "t3code", workspaceRoot: "/a", repository: "pingdotgg/t3code" }), |
| 984 | ], | 999 | ], |
| 985 | providers: [ | 1000 | providers: [ |
| 986 | fakeProvider("github", { | 1001 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 987 | getViewer: () => | 1002 | getViewer: () => |
| 988 | Effect.fail( | 1003 | Effect.fail( |
| 989 | new PullRequestProviderError({ | 1004 | new PullRequestProviderError({ |
| 990 | provider: "github", | 1005 | provider: SourceControlProviderKind.make("github"), |
| 991 | operation: "getViewer", | 1006 | operation: "getViewer", |
| 992 | reason: "failed", | 1007 | reason: "failed", |
| 993 | detail: "HTTP 500", | 1008 | detail: "HTTP 500", |
| ... | @@ -1014,23 +1029,24 @@ it.effect("reports an unusable host over a merely failing one", () => | ... | @@ -1014,23 +1029,24 @@ it.effect("reports an unusable host over a merely failing one", () => |
| 1014 | title: "on gitlab", | 1029 | title: "on gitlab", |
| 1015 | workspaceRoot: "/c", | 1030 | workspaceRoot: "/c", |
| 1016 | repository: "group/project", | 1031 | repository: "group/project", |
| 1017 | provider: "gitlab", | 1032 | provider: SourceControlProviderKind.make("gitlab"), |
| 1018 | }), | 1033 | }), |
| 1019 | ], | 1034 | ], |
| 1020 | providers: [ | 1035 | providers: [ |
| 1021 | fakeProvider("github", { | 1036 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 1022 | getViewer: () => | 1037 | getViewer: () => |
| 1023 | Effect.fail( | 1038 | Effect.fail( |
| 1024 | new PullRequestProviderError({ | 1039 | new PullRequestProviderError({ |
| 1025 | provider: "github", | 1040 | provider: SourceControlProviderKind.make("github"), |
| 1026 | operation: "getViewer", | 1041 | operation: "getViewer", |
| 1027 | reason: "failed", | 1042 | reason: "failed", |
| 1028 | detail: "HTTP 500", | 1043 | detail: "HTTP 500", |
| 1029 | }), | 1044 | }), |
| 1030 | ), | 1045 | ), |
| 1031 | }), | 1046 | }), |
| 1032 | fakeProvider("gitlab", { | 1047 | fakeProvider(SourceControlProviderKind.make("gitlab"), { |
| 1033 | getViewer: () => Effect.fail(unusable("gitlab", "missing-tool")), | 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,11 +1068,11 @@ it.effect("lists every host that has an implementation", () => |
| 1052 | title: "on gitlab", | 1068 | title: "on gitlab", |
| 1053 | workspaceRoot: "/b", | 1069 | workspaceRoot: "/b", |
| 1054 | repository: "group/sub/project", | 1070 | repository: "group/sub/project", |
| 1055 | provider: "gitlab", | 1071 | provider: SourceControlProviderKind.make("gitlab"), |
| 1056 | }), | 1072 | }), |
| 1057 | ], | 1073 | ], |
| 1058 | providers: [ | 1074 | providers: [ |
| 1059 | fakeProvider("github", { | 1075 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 1060 | listChangeRequests: () => | 1076 | listChangeRequests: () => |
| 1061 | Effect.succeed({ | 1077 | Effect.succeed({ |
| 1062 | items: [changeRequest(1, "2026-07-01T00:00:00Z")], | 1078 | items: [changeRequest(1, "2026-07-01T00:00:00Z")], |
| ... | @@ -1064,7 +1080,7 @@ it.effect("lists every host that has an implementation", () => | ... | @@ -1064,7 +1080,7 @@ it.effect("lists every host that has an implementation", () => |
| 1064 | continues: true, | 1080 | continues: true, |
| 1065 | }), | 1081 | }), |
| 1066 | }), | 1082 | }), |
| 1067 | fakeProvider("gitlab", { | 1083 | fakeProvider(SourceControlProviderKind.make("gitlab"), { |
| 1068 | listChangeRequests: (input) => | 1084 | listChangeRequests: (input) => |
| 1069 | // Nested groups need the full path, not the last two segments. | 1085 | // Nested groups need the full path, not the last two segments. |
| 1070 | input.repository === "group/sub/project" | 1086 | input.repository === "group/sub/project" |
| ... | @@ -1100,12 +1116,14 @@ it.effect("narrows the listing to one host when asked", () => | ... | @@ -1100,12 +1116,14 @@ it.effect("narrows the listing to one host when asked", () => |
| 1100 | title: "on gitlab", | 1116 | title: "on gitlab", |
| 1101 | workspaceRoot: "/b", | 1117 | workspaceRoot: "/b", |
| 1102 | repository: "group/project", | 1118 | repository: "group/project", |
| 1103 | provider: "gitlab", | 1119 | provider: SourceControlProviderKind.make("gitlab"), |
| 1104 | }), | 1120 | }), |
| 1105 | ], | 1121 | ], |
| 1106 | providers: [ | 1122 | providers: [ |
| 1107 | fakeProvider("github", { listChangeRequests: () => Effect.die("should not be read") }), | 1123 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 1108 | fakeProvider("gitlab", { | 1124 | listChangeRequests: () => Effect.die("should not be read"), |
| 1125 | }), | ||
| 1126 | fakeProvider(SourceControlProviderKind.make("gitlab"), { | ||
| 1109 | listChangeRequests: () => | 1127 | listChangeRequests: () => |
| 1110 | Effect.succeed({ | 1128 | Effect.succeed({ |
| 1111 | items: [changeRequest(2, "2026-07-05T00:00:00Z")], | 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,7 +1157,7 @@ it.effect("tells two hosts of one kind apart in the switcher and the filter", () |
| 1139 | }), | 1157 | }), |
| 1140 | ], | 1158 | ], |
| 1141 | providers: [ | 1159 | providers: [ |
| 1142 | fakeProvider("github", { | 1160 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 1143 | listChangeRequests: ({ host }) => | 1161 | listChangeRequests: ({ host }) => |
| 1144 | Effect.succeed({ | 1162 | Effect.succeed({ |
| 1145 | items: host === "ghe.example.com" ? [changeRequest(2, "2026-07-05T00:00:00Z")] : [], | 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,11 +1197,11 @@ it.effect("keeps one host listed when another is not set up", () => |
| 1179 | title: "on gitlab", | 1197 | title: "on gitlab", |
| 1180 | workspaceRoot: "/b", | 1198 | workspaceRoot: "/b", |
| 1181 | repository: "group/project", | 1199 | repository: "group/project", |
| 1182 | provider: "gitlab", | 1200 | provider: SourceControlProviderKind.make("gitlab"), |
| 1183 | }), | 1201 | }), |
| 1184 | ], | 1202 | ], |
| 1185 | providers: [ | 1203 | providers: [ |
| 1186 | fakeProvider("github", { | 1204 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 1187 | listChangeRequests: () => | 1205 | listChangeRequests: () => |
| 1188 | Effect.succeed({ | 1206 | Effect.succeed({ |
| 1189 | items: [changeRequest(1, "2026-07-01T00:00:00Z")], | 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,8 +1209,9 @@ it.effect("keeps one host listed when another is not set up", () => |
| 1191 | continues: true, | 1209 | continues: true, |
| 1192 | }), | 1210 | }), |
| 1193 | }), | 1211 | }), |
| 1194 | fakeProvider("gitlab", { | 1212 | fakeProvider(SourceControlProviderKind.make("gitlab"), { |
| 1195 | getViewer: () => Effect.fail(unusable("gitlab", "missing-tool")), | 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,8 +1239,9 @@ it.effect("fails as unavailable only when no host can be read", () => |
| 1220 | project({ id: "p1", title: "t3code", workspaceRoot: "/a", repository: "pingdotgg/t3code" }), | 1239 | project({ id: "p1", title: "t3code", workspaceRoot: "/a", repository: "pingdotgg/t3code" }), |
| 1221 | ], | 1240 | ], |
| 1222 | providers: [ | 1241 | providers: [ |
| 1223 | fakeProvider("github", { | 1242 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 1224 | getViewer: () => Effect.fail(unusable("github", "missing-tool")), | 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,7 +1270,7 @@ it.effect("reads a repository once when several worktrees share it", () => |
| 1250 | }), | 1270 | }), |
| 1251 | ], | 1271 | ], |
| 1252 | providers: [ | 1272 | providers: [ |
| 1253 | fakeProvider("github", { | 1273 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 1254 | listChangeRequests: () => { | 1274 | listChangeRequests: () => { |
| 1255 | calls += 1; | 1275 | calls += 1; |
| 1256 | return Effect.succeed({ | 1276 | return Effect.succeed({ |
| ... | @@ -1278,7 +1298,7 @@ it.effect("keeps healthy repositories when one of them cannot be read", () => | ... | @@ -1278,7 +1298,7 @@ it.effect("keeps healthy repositories when one of them cannot be read", () => |
| 1278 | project({ id: "p2", title: "broken", workspaceRoot: "/b", repository: "pingdotgg/broken" }), | 1298 | project({ id: "p2", title: "broken", workspaceRoot: "/b", repository: "pingdotgg/broken" }), |
| 1279 | ], | 1299 | ], |
| 1280 | providers: [ | 1300 | providers: [ |
| 1281 | fakeProvider("github", { | 1301 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 1282 | listChangeRequests: (input) => | 1302 | listChangeRequests: (input) => |
| 1283 | input.repository === "pingdotgg/broken" | 1303 | input.repository === "pingdotgg/broken" |
| 1284 | ? Effect.fail(requestFailed) | 1304 | ? Effect.fail(requestFailed) |
| ... | @@ -1309,11 +1329,11 @@ it.effect("tries another workspace on the same host for the viewer", () => | ... | @@ -1309,11 +1329,11 @@ it.effect("tries another workspace on the same host for the viewer", () => |
| 1309 | project({ id: "p2", title: "healthy", workspaceRoot: "/healthy", repository: "acme/two" }), | 1329 | project({ id: "p2", title: "healthy", workspaceRoot: "/healthy", repository: "acme/two" }), |
| 1310 | ], | 1330 | ], |
| 1311 | providers: [ | 1331 | providers: [ |
| 1312 | fakeProvider("github", { | 1332 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 1313 | getViewer: (input) => | 1333 | getViewer: (input) => |
| 1314 | input.cwd === "/healthy" | 1334 | input.cwd === "/healthy" |
| 1315 | ? Effect.succeed("bilal") | 1335 | ? Effect.succeed("bilal") |
| 1316 | : Effect.fail(unusable("github", "missing-tool")), | 1336 | : Effect.fail(unusable(SourceControlProviderKind.make("github"), "missing-tool")), |
| 1317 | listChangeRequests: () => | 1337 | listChangeRequests: () => |
| 1318 | Effect.succeed({ | 1338 | Effect.succeed({ |
| 1319 | items: [changeRequest(1, "2026-07-02T00:00:00Z")], | 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,7 +1365,7 @@ it.effect("routing verifies the current account on the requested host without ca |
| 1345 | }), | 1365 | }), |
| 1346 | ], | 1366 | ], |
| 1347 | providers: [ | 1367 | providers: [ |
| 1348 | fakeProvider("github", { | 1368 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 1349 | getRoutingIdentity: (input) => { | 1369 | getRoutingIdentity: (input) => { |
| 1350 | assert.deepStrictEqual(input, { cwd: "/a", host: "github.example.test" }); | 1370 | assert.deepStrictEqual(input, { cwd: "/a", host: "github.example.test" }); |
| 1351 | return Effect.succeed({ | 1371 | return Effect.succeed({ |
| ... | @@ -1359,7 +1379,7 @@ it.effect("routing verifies the current account on the requested host without ca | ... | @@ -1359,7 +1379,7 @@ it.effect("routing verifies the current account on the requested host without ca |
| 1359 | const ref = { projectId: "p1" as ProjectId, repository: "acme/web", number: 1 }; | 1379 | const ref = { projectId: "p1" as ProjectId, repository: "acme/web", number: 1 }; |
| 1360 | assert.deepStrictEqual(yield* service.routing(ref), { | 1380 | assert.deepStrictEqual(yield* service.routing(ref), { |
| 1361 | host: "github.example.test", | 1381 | host: "github.example.test", |
| 1362 | provider: "github", | 1382 | provider: SourceControlProviderKind.make("github"), |
| 1363 | viewer: "first-account", | 1383 | viewer: "first-account", |
| 1364 | accountId: "123", | 1384 | accountId: "123", |
| 1365 | projectTitle: "web", | 1385 | projectTitle: "web", |
| ... | @@ -1382,7 +1402,7 @@ it.effect("refuses an action the host never claimed it could run", () => | ... | @@ -1382,7 +1402,7 @@ it.effect("refuses an action the host never claimed it could run", () => |
| 1382 | const service = yield* makeService({ | 1402 | const service = yield* makeService({ |
| 1383 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], | 1403 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 1384 | providers: [ | 1404 | providers: [ |
| 1385 | fakeProvider("github", { | 1405 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 1386 | capabilities: { | 1406 | capabilities: { |
| 1387 | diff: true, | 1407 | diff: true, |
| 1388 | comment: true, | 1408 | comment: true, |
| ... | @@ -1428,12 +1448,12 @@ it.effect("publishes a merge for immediate settlement only after host confirmati | ... | @@ -1428,12 +1448,12 @@ it.effect("publishes a merge for immediate settlement only after host confirmati |
| 1428 | project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" }), | 1448 | project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" }), |
| 1429 | ], | 1449 | ], |
| 1430 | providers: [ | 1450 | providers: [ |
| 1431 | fakeProvider("github", { | 1451 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 1432 | getChangeRequestSummary: () => | 1452 | getChangeRequestSummary: () => |
| 1433 | confirmationFails | 1453 | confirmationFails |
| 1434 | ? Effect.fail( | 1454 | ? Effect.fail( |
| 1435 | new PullRequestProviderError({ | 1455 | new PullRequestProviderError({ |
| 1436 | provider: "github", | 1456 | provider: SourceControlProviderKind.make("github"), |
| 1437 | operation: "getChangeRequestSummary", | 1457 | operation: "getChangeRequestSummary", |
| 1438 | reason: "failed", | 1458 | reason: "failed", |
| 1439 | detail: "HTTP 504", | 1459 | detail: "HTTP 504", |
| ... | @@ -1486,7 +1506,7 @@ it.effect("refreshes every reader before a queued merge confirmation finishes", | ... | @@ -1486,7 +1506,7 @@ it.effect("refreshes every reader before a queued merge confirmation finishes", |
| 1486 | project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" }), | 1506 | project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" }), |
| 1487 | ], | 1507 | ], |
| 1488 | providers: [ | 1508 | providers: [ |
| 1489 | fakeProvider("github", { | 1509 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 1490 | getChangeRequestSummary: () => | 1510 | getChangeRequestSummary: () => |
| 1491 | Effect.gen(function* () { | 1511 | Effect.gen(function* () { |
| 1492 | yield* Deferred.succeed(confirmationStarted, undefined); | 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,7 +1548,7 @@ it.effect("refuses an action this viewer may not take, and says what access it t |
| 1528 | const service = yield* makeService({ | 1548 | const service = yield* makeService({ |
| 1529 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], | 1549 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 1530 | providers: [ | 1550 | providers: [ |
| 1531 | fakeProvider("github", { | 1551 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 1532 | // The host merges; this account only reads it, and opened the change request — which | 1552 | // The host merges; this account only reads it, and opened the change request — which |
| 1533 | // is every contributor to a repository they do not own. | 1553 | // is every contributor to a repository they do not own. |
| 1534 | getViewerPermissions: () => | 1554 | getViewerPermissions: () => |
| ... | @@ -1565,7 +1585,7 @@ it.effect("gates arming a merge for later exactly as it gates merging now", () = | ... | @@ -1565,7 +1585,7 @@ it.effect("gates arming a merge for later exactly as it gates merging now", () = |
| 1565 | const service = yield* makeService({ | 1585 | const service = yield* makeService({ |
| 1566 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], | 1586 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 1567 | providers: [ | 1587 | providers: [ |
| 1568 | fakeProvider("github", { | 1588 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 1569 | capabilities: { | 1589 | capabilities: { |
| 1570 | diff: true, | 1590 | diff: true, |
| 1571 | comment: true, | 1591 | comment: true, |
| ... | @@ -1620,7 +1640,7 @@ it.effect("hands the host the strategy an armed merge was asked for", () => | ... | @@ -1620,7 +1640,7 @@ it.effect("hands the host the strategy an armed merge was asked for", () => |
| 1620 | const service = yield* makeService({ | 1640 | const service = yield* makeService({ |
| 1621 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], | 1641 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 1622 | providers: [ | 1642 | providers: [ |
| 1623 | fakeProvider("github", { | 1643 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 1624 | capabilities: { | 1644 | capabilities: { |
| 1625 | diff: true, | 1645 | diff: true, |
| 1626 | comment: true, | 1646 | comment: true, |
| ... | @@ -1666,7 +1686,7 @@ it.effect("refuses an auto-merge the host never claimed, without asking it", () | ... | @@ -1666,7 +1686,7 @@ it.effect("refuses an auto-merge the host never claimed, without asking it", () |
| 1666 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], | 1686 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 1667 | providers: [ | 1687 | providers: [ |
| 1668 | // Bitbucket's shape: it merges, and has nothing that merges later on its own. | 1688 | // Bitbucket's shape: it merges, and has nothing that merges later on its own. |
| 1669 | fakeProvider("github", { | 1689 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 1670 | runAction: () => { | 1690 | runAction: () => { |
| 1671 | ran = true; | 1691 | ran = true; |
| 1672 | return Effect.void; | 1692 | return Effect.void; |
| ... | @@ -1694,7 +1714,7 @@ it.effect("refuses to resolve a conversation this viewer may not, without asking | ... | @@ -1694,7 +1714,7 @@ it.effect("refuses to resolve a conversation this viewer may not, without asking |
| 1694 | const service = yield* makeService({ | 1714 | const service = yield* makeService({ |
| 1695 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], | 1715 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 1696 | providers: [ | 1716 | providers: [ |
| 1697 | fakeProvider("github", { | 1717 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 1698 | getViewerPermissions: () => | 1718 | getViewerPermissions: () => |
| 1699 | Effect.succeed({ | 1719 | Effect.succeed({ |
| 1700 | actions: ["merge", "ready", "draft", "close", "reopen"], | 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,7 +1749,7 @@ it.effect("asks nobody what the viewer may do when the host cannot do it at all" |
| 1729 | const service = yield* makeService({ | 1749 | const service = yield* makeService({ |
| 1730 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], | 1750 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 1731 | providers: [ | 1751 | providers: [ |
| 1732 | fakeProvider("github", { | 1752 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 1733 | capabilities: { | 1753 | capabilities: { |
| 1734 | diff: true, | 1754 | diff: true, |
| 1735 | comment: true, | 1755 | comment: true, |
| ... | @@ -1768,7 +1788,7 @@ it.effect("refuses a comment on a host that cannot post one", () => | ... | @@ -1768,7 +1788,7 @@ it.effect("refuses a comment on a host that cannot post one", () => |
| 1768 | const service = yield* makeService({ | 1788 | const service = yield* makeService({ |
| 1769 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], | 1789 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 1770 | providers: [ | 1790 | providers: [ |
| 1771 | fakeProvider("github", { | 1791 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 1772 | capabilities: { | 1792 | capabilities: { |
| 1773 | diff: false, | 1793 | diff: false, |
| 1774 | comment: false, | 1794 | comment: false, |
| ... | @@ -1817,7 +1837,7 @@ it.effect("keeps two hosts of one provider kind as two accounts", () => | ... | @@ -1817,7 +1837,7 @@ it.effect("keeps two hosts of one provider kind as two accounts", () => |
| 1817 | }), | 1837 | }), |
| 1818 | ], | 1838 | ], |
| 1819 | providers: [ | 1839 | providers: [ |
| 1820 | fakeProvider("github", { | 1840 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 1821 | getViewer: (input) => Effect.succeed(viewerFor[input.cwd] ?? "unknown"), | 1841 | getViewer: (input) => Effect.succeed(viewerFor[input.cwd] ?? "unknown"), |
| 1822 | listChangeRequests: () => | 1842 | listChangeRequests: () => |
| 1823 | Effect.succeed({ | 1843 | Effect.succeed({ |
| ... | @@ -1858,11 +1878,11 @@ it.effect("reports repositories on a host that could not be read", () => | ... | @@ -1858,11 +1878,11 @@ it.effect("reports repositories on a host that could not be read", () => |
| 1858 | }), | 1878 | }), |
| 1859 | ], | 1879 | ], |
| 1860 | providers: [ | 1880 | providers: [ |
| 1861 | fakeProvider("github", { | 1881 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 1862 | getViewer: (input) => | 1882 | getViewer: (input) => |
| 1863 | input.cwd === "/cloud" | 1883 | input.cwd === "/cloud" |
| 1864 | ? Effect.succeed("bilal") | 1884 | ? Effect.succeed("bilal") |
| 1865 | : Effect.fail(unusable("github", "unauthenticated")), | 1885 | : Effect.fail(unusable(SourceControlProviderKind.make("github"), "unauthenticated")), |
| 1866 | listChangeRequests: () => | 1886 | listChangeRequests: () => |
| 1867 | Effect.succeed({ | 1887 | Effect.succeed({ |
| 1868 | items: [changeRequest(1, "2026-07-02T00:00:00Z")], | 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,7 +1913,7 @@ it.effect("stops new reads after a rate limit while leaving manual actions avail |
| 1893 | project({ id: "p1", title: "cloud", workspaceRoot: "/cloud", repository: "acme/web" }), | 1913 | project({ id: "p1", title: "cloud", workspaceRoot: "/cloud", repository: "acme/web" }), |
| 1894 | ], | 1914 | ], |
| 1895 | providers: [ | 1915 | providers: [ |
| 1896 | fakeProvider("github", { | 1916 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 1897 | listChangeRequests: () => | 1917 | listChangeRequests: () => |
| 1898 | Effect.sync(() => { | 1918 | Effect.sync(() => { |
| 1899 | listCalls += 1; | 1919 | listCalls += 1; |
| ... | @@ -1901,7 +1921,7 @@ it.effect("stops new reads after a rate limit while leaving manual actions avail | ... | @@ -1901,7 +1921,7 @@ it.effect("stops new reads after a rate limit while leaving manual actions avail |
| 1901 | Effect.andThen( | 1921 | Effect.andThen( |
| 1902 | Effect.fail( | 1922 | Effect.fail( |
| 1903 | new PullRequestProviderError({ | 1923 | new PullRequestProviderError({ |
| 1904 | provider: "github", | 1924 | provider: SourceControlProviderKind.make("github"), |
| 1905 | operation: "listChangeRequests", | 1925 | operation: "listChangeRequests", |
| 1906 | reason: "rate-limited", | 1926 | reason: "rate-limited", |
| 1907 | detail: "GitHub API rate limit exceeded.", | 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,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 | ["github", "github.com"], | 1957 | ["github", "github.com"], |
| 1938 | ["gitlab", "gitlab.com"], | 1958 | ["gitlab", "gitlab.com"], |
| 1939 | ["forgejo", "code.example.test"], | 1959 | ["forgejo", "code.example.test"], |
| 1940 | ["bitbucket", "bitbucket.org"], | 1960 | ["bitbucket", "bitbucket.org"], |
| 1941 | ["azure-devops", "dev.azure.com"], | 1961 | ["azure-devops", "dev.azure.com"], |
| 1942 | ] as const) { | 1962 | ] as const) { |
| 1963 | const provider = SourceControlProviderKind.make(providerName); | ||
| 1943 | it.effect.each(["detail", "checks"] as const)( | 1964 | it.effect.each(["detail", "checks"] as const)( |
| 1944 | `shares fresh ${provider} %s reads and respects rate-limit resets`, | 1965 | `shares fresh ${provider} %s reads and respects rate-limit resets`, |
| 1945 | (read) => | 1966 | (read) => |
| ... | @@ -2006,7 +2027,7 @@ it.effect("does not fall back to full detail when checks are unsupported", () => | ... | @@ -2006,7 +2027,7 @@ it.effect("does not fall back to full detail when checks are unsupported", () => |
| 2006 | project({ id: "p1", title: "web", workspaceRoot: "/repo", repository: "acme/web" }), | 2027 | project({ id: "p1", title: "web", workspaceRoot: "/repo", repository: "acme/web" }), |
| 2007 | ], | 2028 | ], |
| 2008 | providers: [ | 2029 | providers: [ |
| 2009 | fakeProvider("github", { | 2030 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 2010 | getChangeRequest: () => Effect.die("Unexpected full detail read"), | 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,7 +2046,7 @@ it.effect("uses a manual rate limit to pause later reads", () => |
| 2025 | project({ id: "p1", title: "cloud", workspaceRoot: "/cloud", repository: "acme/web" }), | 2046 | project({ id: "p1", title: "cloud", workspaceRoot: "/cloud", repository: "acme/web" }), |
| 2026 | ], | 2047 | ], |
| 2027 | providers: [ | 2048 | providers: [ |
| 2028 | fakeProvider("github", { | 2049 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 2029 | listChangeRequests: () => | 2050 | listChangeRequests: () => |
| 2030 | Effect.sync(() => { | 2051 | Effect.sync(() => { |
| 2031 | listCalls += 1; | 2052 | listCalls += 1; |
| ... | @@ -2034,7 +2055,7 @@ it.effect("uses a manual rate limit to pause later reads", () => | ... | @@ -2034,7 +2055,7 @@ it.effect("uses a manual rate limit to pause later reads", () => |
| 2034 | runAction: () => | 2055 | runAction: () => |
| 2035 | Effect.fail( | 2056 | Effect.fail( |
| 2036 | new PullRequestProviderError({ | 2057 | new PullRequestProviderError({ |
| 2037 | provider: "github", | 2058 | provider: SourceControlProviderKind.make("github"), |
| 2038 | operation: "runAction", | 2059 | operation: "runAction", |
| 2039 | reason: "rate-limited", | 2060 | reason: "rate-limited", |
| 2040 | detail: "GitHub API rate limit exceeded.", | 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,7 +2087,7 @@ it.effect("flags a review request for the viewer but not on their own change req |
| 2066 | project({ id: "p1", title: "t3code", workspaceRoot: "/a", repository: "pingdotgg/t3code" }), | 2087 | project({ id: "p1", title: "t3code", workspaceRoot: "/a", repository: "pingdotgg/t3code" }), |
| 2067 | ], | 2088 | ], |
| 2068 | providers: [ | 2089 | providers: [ |
| 2069 | fakeProvider("github", { | 2090 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 2070 | listChangeRequests: () => | 2091 | listChangeRequests: () => |
| 2071 | Effect.succeed({ | 2092 | Effect.succeed({ |
| 2072 | items: [ | 2093 | items: [ |
| ... | @@ -2099,7 +2120,7 @@ it.effect("refuses a repository that does not belong to the requested project", | ... | @@ -2099,7 +2120,7 @@ it.effect("refuses a repository that does not belong to the requested project", |
| 2099 | projects: [ | 2120 | projects: [ |
| 2100 | project({ id: "p1", title: "t3code", workspaceRoot: "/a", repository: "pingdotgg/t3code" }), | 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 | const error = yield* service | 2126 | const error = yield* service |
| ... | @@ -2116,7 +2137,7 @@ it.effect("caches stack membership separately from action details", () => | ... | @@ -2116,7 +2137,7 @@ it.effect("caches stack membership separately from action details", () => |
| 2116 | const service = yield* makeService({ | 2137 | const service = yield* makeService({ |
| 2117 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], | 2138 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 2118 | providers: [ | 2139 | providers: [ |
| 2119 | fakeProvider("github", { | 2140 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 2120 | getChangeRequestStack: (input) => | 2141 | getChangeRequestStack: (input) => |
| 2121 | Effect.sync(() => { | 2142 | Effect.sync(() => { |
| 2122 | reads.push(input.includeDetails === true); | 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,7 +2178,7 @@ it.effect("reads a host-native stack through the provider and null where it has |
| 2157 | const service = yield* makeService({ | 2178 | const service = yield* makeService({ |
| 2158 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], | 2179 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 2159 | providers: [ | 2180 | providers: [ |
| 2160 | fakeProvider("github", { | 2181 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 2161 | getChangeRequestStack: () => | 2182 | getChangeRequestStack: () => |
| 2162 | Effect.succeed({ | 2183 | Effect.succeed({ |
| 2163 | id: "9", | 2184 | id: "9", |
| ... | @@ -2185,7 +2206,7 @@ it.effect("reads a host-native stack through the provider and null where it has | ... | @@ -2185,7 +2206,7 @@ it.effect("reads a host-native stack through the provider and null where it has |
| 2185 | 2206 | ||
| 2186 | const withoutStacks = yield* makeService({ | 2207 | const withoutStacks = yield* makeService({ |
| 2187 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], | 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 | assert.isNull( | 2211 | assert.isNull( |
| 2191 | yield* withoutStacks.stack({ | 2212 | yield* withoutStacks.stack({ |
| ... | @@ -2215,7 +2236,7 @@ it.effect("routes explicit Forgejo HTTP authorities through SSH checkouts after | ... | @@ -2215,7 +2236,7 @@ it.effect("routes explicit Forgejo HTTP authorities through SSH checkouts after |
| 2215 | }), | 2236 | }), |
| 2216 | ], | 2237 | ], |
| 2217 | providers: [ | 2238 | providers: [ |
| 2218 | fakeProvider("forgejo", { | 2239 | fakeProvider(SourceControlProviderKind.make("forgejo"), { |
| 2219 | getViewer: (input) => { | 2240 | getViewer: (input) => { |
| 2220 | viewers.push(input.host); | 2241 | viewers.push(input.host); |
| 2221 | assert.strictEqual(input.host, "code.example:3000"); | 2242 | assert.strictEqual(input.host, "code.example:3000"); |
| ... | @@ -2244,7 +2265,11 @@ it.effect("routes explicit Forgejo HTTP authorities through SSH checkouts after | ... | @@ -2244,7 +2265,11 @@ it.effect("routes explicit Forgejo HTTP authorities through SSH checkouts after |
| 2244 | return Effect.succeed({ | 2265 | return Effect.succeed({ |
| 2245 | context: { | 2266 | context: { |
| 2246 | ...context, | 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 | provider: undefined as never, | 2274 | provider: undefined as never, |
| 2250 | }); | 2275 | }); |
| ... | @@ -2294,12 +2319,12 @@ it.effect("rejects a different Forgejo HTTP port for an HTTP checkout", () => | ... | @@ -2294,12 +2319,12 @@ it.effect("rejects a different Forgejo HTTP port for an HTTP checkout", () => |
| 2294 | title: "http", | 2319 | title: "http", |
| 2295 | workspaceRoot: "/http", | 2320 | workspaceRoot: "/http", |
| 2296 | repository: "team/repo", | 2321 | repository: "team/repo", |
| 2297 | provider: "forgejo", | 2322 | provider: SourceControlProviderKind.make("forgejo"), |
| 2298 | host: "code.example", | 2323 | host: "code.example", |
| 2299 | remoteUrl: "http://code.example:4000/team/repo.git", | 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 | const failure = yield* service | 2329 | const failure = yield* service |
| 2305 | .summary( | 2330 | .summary( |
| ... | @@ -2327,7 +2352,7 @@ it.effect("resolves a project's repository identity when its shell has none cach | ... | @@ -2327,7 +2352,7 @@ it.effect("resolves a project's repository identity when its shell has none cach |
| 2327 | const service = yield* makeService({ | 2352 | const service = yield* makeService({ |
| 2328 | projects: [{ ...resolved, repositoryIdentity: null }], | 2353 | projects: [{ ...resolved, repositoryIdentity: null }], |
| 2329 | providers: [ | 2354 | providers: [ |
| 2330 | fakeProvider("github", { | 2355 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 2331 | getChangeRequestSummary: () => Effect.succeed(changeRequest(7, "2026-07-02T00:00:00Z")), | 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,7 +2376,7 @@ it.effect("routes a hosted reference to another repository through a project on |
| 2351 | project({ id: "frontend", title: "web", workspaceRoot: "/web", repository: "acme/web" }), | 2376 | project({ id: "frontend", title: "web", workspaceRoot: "/web", repository: "acme/web" }), |
| 2352 | ], | 2377 | ], |
| 2353 | providers: [ | 2378 | providers: [ |
| 2354 | fakeProvider("github", { | 2379 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 2355 | getChangeRequestSummary: (input) => | 2380 | getChangeRequestSummary: (input) => |
| 2356 | Effect.sync(() => { | 2381 | Effect.sync(() => { |
| 2357 | seen.push({ cwd: input.cwd, repository: input.repository, host: input.host }); | 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,12 +2406,12 @@ it.effect("routes Azure reads and writes through the requested organization's ch |
| 2381 | title: organization, | 2406 | title: organization, |
| 2382 | workspaceRoot: `/${organization}`, | 2407 | workspaceRoot: `/${organization}`, |
| 2383 | repository: `${organization}/project/_git/web`, | 2408 | repository: `${organization}/project/_git/web`, |
| 2384 | provider: "azure-devops", | 2409 | provider: SourceControlProviderKind.make("azure-devops"), |
| 2385 | host: "dev.azure.com", | 2410 | host: "dev.azure.com", |
| 2386 | }), | 2411 | }), |
| 2387 | ), | 2412 | ), |
| 2388 | providers: [ | 2413 | providers: [ |
| 2389 | fakeProvider("azure-devops", { | 2414 | fakeProvider(SourceControlProviderKind.make("azure-devops"), { |
| 2390 | getChangeRequestSummary: (input) => | 2415 | getChangeRequestSummary: (input) => |
| 2391 | Effect.sync(() => { | 2416 | Effect.sync(() => { |
| 2392 | seen.push(`read ${input.cwd} ${input.repository}`); | 2417 | seen.push(`read ${input.cwd} ${input.repository}`); |
| ... | @@ -2434,7 +2459,7 @@ it.effect.each([ | ... | @@ -2434,7 +2459,7 @@ it.effect.each([ |
| 2434 | id: "target", | 2459 | id: "target", |
| 2435 | title: "target", | 2460 | title: "target", |
| 2436 | workspaceRoot: "/target", | 2461 | workspaceRoot: "/target", |
| 2437 | provider: "azure-devops", | 2462 | provider: SourceControlProviderKind.make("azure-devops"), |
| 2438 | ...checkout, | 2463 | ...checkout, |
| 2439 | }); | 2464 | }); |
| 2440 | const service = yield* makeService({ | 2465 | const service = yield* makeService({ |
| ... | @@ -2444,7 +2469,7 @@ it.effect.each([ | ... | @@ -2444,7 +2469,7 @@ it.effect.each([ |
| 2444 | id: repository, | 2469 | id: repository, |
| 2445 | title: repository, | 2470 | title: repository, |
| 2446 | workspaceRoot: `/${repository}`, | 2471 | workspaceRoot: `/${repository}`, |
| 2447 | provider: "azure-devops", | 2472 | provider: SourceControlProviderKind.make("azure-devops"), |
| 2448 | host: "dev.azure.com", | 2473 | host: "dev.azure.com", |
| 2449 | repository, | 2474 | repository, |
| 2450 | }), | 2475 | }), |
| ... | @@ -2452,7 +2477,7 @@ it.effect.each([ | ... | @@ -2452,7 +2477,7 @@ it.effect.each([ |
| 2452 | target, | 2477 | target, |
| 2453 | ], | 2478 | ], |
| 2454 | providers: [ | 2479 | providers: [ |
| 2455 | fakeProvider("azure-devops", { | 2480 | fakeProvider(SourceControlProviderKind.make("azure-devops"), { |
| 2456 | getChangeRequestSummary: (input) => | 2481 | getChangeRequestSummary: (input) => |
| 2457 | Effect.sync(() => { | 2482 | Effect.sync(() => { |
| 2458 | seen.push(`read ${input.cwd} ${input.repository}`); | 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,12 +2511,12 @@ it.effect("refuses Azure cross-organization reads and writes without its checkou |
| 2486 | title: "org-a", | 2511 | title: "org-a", |
| 2487 | workspaceRoot: "/org-a", | 2512 | workspaceRoot: "/org-a", |
| 2488 | repository: "org-a/project/_git/web", | 2513 | repository: "org-a/project/_git/web", |
| 2489 | provider: "azure-devops", | 2514 | provider: SourceControlProviderKind.make("azure-devops"), |
| 2490 | host: "dev.azure.com", | 2515 | host: "dev.azure.com", |
| 2491 | }), | 2516 | }), |
| 2492 | ], | 2517 | ], |
| 2493 | providers: [ | 2518 | providers: [ |
| 2494 | fakeProvider("azure-devops", { | 2519 | fakeProvider(SourceControlProviderKind.make("azure-devops"), { |
| 2495 | getChangeRequestSummary: () => Effect.die("must not read the wrong organization"), | 2520 | getChangeRequestSummary: () => Effect.die("must not read the wrong organization"), |
| 2496 | runAction: () => Effect.die("must not modify the wrong organization"), | 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,7 +2543,7 @@ it.effect("refuses a hosted reference when nothing is checked out from that host |
| 2518 | projects: [ | 2543 | projects: [ |
| 2519 | project({ id: "frontend", title: "web", workspaceRoot: "/web", repository: "acme/web" }), | 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 | const error = yield* service | 2549 | const error = yield* service |
| ... | @@ -2546,11 +2571,11 @@ it.effect("refuses a diff on a host that cannot produce one", () => | ... | @@ -2546,11 +2571,11 @@ it.effect("refuses a diff on a host that cannot produce one", () => |
| 2546 | title: "on azure", | 2571 | title: "on azure", |
| 2547 | workspaceRoot: "/a", | 2572 | workspaceRoot: "/a", |
| 2548 | repository: "org/project", | 2573 | repository: "org/project", |
| 2549 | provider: "azure-devops", | 2574 | provider: SourceControlProviderKind.make("azure-devops"), |
| 2550 | }), | 2575 | }), |
| 2551 | ], | 2576 | ], |
| 2552 | providers: [ | 2577 | providers: [ |
| 2553 | fakeProvider("azure-devops", { | 2578 | fakeProvider(SourceControlProviderKind.make("azure-devops"), { |
| 2554 | capabilities: { | 2579 | capabilities: { |
| 2555 | diff: false, | 2580 | diff: false, |
| 2556 | comment: true, | 2581 | comment: true, |
| ... | @@ -2580,7 +2605,11 @@ it.effect("rejects an empty comment before reaching the host", () => | ... | @@ -2580,7 +2605,11 @@ it.effect("rejects an empty comment before reaching the host", () => |
| 2580 | projects: [ | 2605 | projects: [ |
| 2581 | project({ id: "p1", title: "t3code", workspaceRoot: "/a", repository: "pingdotgg/t3code" }), | 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 | const error = yield* service | 2615 | const error = yield* service |
| ... | @@ -2606,11 +2635,11 @@ it.effect("refuses a verdict the host never claimed, without asking the provider | ... | @@ -2606,11 +2635,11 @@ it.effect("refuses a verdict the host never claimed, without asking the provider |
| 2606 | title: "on gitlab", | 2635 | title: "on gitlab", |
| 2607 | workspaceRoot: "/a", | 2636 | workspaceRoot: "/a", |
| 2608 | repository: "group/project", | 2637 | repository: "group/project", |
| 2609 | provider: "gitlab", | 2638 | provider: SourceControlProviderKind.make("gitlab"), |
| 2610 | }), | 2639 | }), |
| 2611 | ], | 2640 | ], |
| 2612 | providers: [ | 2641 | providers: [ |
| 2613 | fakeProvider("gitlab", { | 2642 | fakeProvider(SourceControlProviderKind.make("gitlab"), { |
| 2614 | capabilities: { | 2643 | capabilities: { |
| 2615 | diff: true, | 2644 | diff: true, |
| 2616 | comment: true, | 2645 | comment: true, |
| ... | @@ -2658,7 +2687,7 @@ it.effect("refuses line comments on a host that takes only a summary", () => | ... | @@ -2658,7 +2687,7 @@ it.effect("refuses line comments on a host that takes only a summary", () => |
| 2658 | project({ id: "p1", title: "t3code", workspaceRoot: "/a", repository: "pingdotgg/t3code" }), | 2687 | project({ id: "p1", title: "t3code", workspaceRoot: "/a", repository: "pingdotgg/t3code" }), |
| 2659 | ], | 2688 | ], |
| 2660 | providers: [ | 2689 | providers: [ |
| 2661 | fakeProvider("github", { | 2690 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 2662 | capabilities: { | 2691 | capabilities: { |
| 2663 | diff: true, | 2692 | diff: true, |
| 2664 | comment: true, | 2693 | comment: true, |
| ... | @@ -2704,7 +2733,7 @@ it.effect( | ... | @@ -2704,7 +2733,7 @@ it.effect( |
| 2704 | }), | 2733 | }), |
| 2705 | ], | 2734 | ], |
| 2706 | providers: [ | 2735 | providers: [ |
| 2707 | fakeProvider("github", { | 2736 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 2708 | submitReview: () => { | 2737 | submitReview: () => { |
| 2709 | approved = true; | 2738 | approved = true; |
| 2710 | return Effect.void; | 2739 | return Effect.void; |
| ... | @@ -2736,7 +2765,7 @@ it.effect("refuses to resolve a conversation on a host that cannot", () => | ... | @@ -2736,7 +2765,7 @@ it.effect("refuses to resolve a conversation on a host that cannot", () => |
| 2736 | project({ id: "p1", title: "t3code", workspaceRoot: "/a", repository: "pingdotgg/t3code" }), | 2765 | project({ id: "p1", title: "t3code", workspaceRoot: "/a", repository: "pingdotgg/t3code" }), |
| 2737 | ], | 2766 | ], |
| 2738 | providers: [ | 2767 | providers: [ |
| 2739 | fakeProvider("github", { | 2768 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 2740 | capabilities: { | 2769 | capabilities: { |
| 2741 | diff: true, | 2770 | diff: true, |
| 2742 | comment: true, | 2771 | comment: true, |
| ... | @@ -2777,7 +2806,7 @@ it.effect("refuses to react on a host with no reactions", () => | ... | @@ -2777,7 +2806,7 @@ it.effect("refuses to react on a host with no reactions", () => |
| 2777 | project({ id: "p1", title: "t3code", workspaceRoot: "/a", repository: "pingdotgg/t3code" }), | 2806 | project({ id: "p1", title: "t3code", workspaceRoot: "/a", repository: "pingdotgg/t3code" }), |
| 2778 | ], | 2807 | ], |
| 2779 | providers: [ | 2808 | providers: [ |
| 2780 | fakeProvider("github", { | 2809 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 2781 | capabilities: { | 2810 | capabilities: { |
| 2782 | diff: true, | 2811 | diff: true, |
| 2783 | comment: true, | 2812 | comment: true, |
| ... | @@ -2814,7 +2843,7 @@ it.effect("refuses to react on a host whose capabilities omit reactions entirely | ... | @@ -2814,7 +2843,7 @@ it.effect("refuses to react on a host whose capabilities omit reactions entirely |
| 2814 | project({ id: "p1", title: "t3code", workspaceRoot: "/a", repository: "pingdotgg/t3code" }), | 2843 | project({ id: "p1", title: "t3code", workspaceRoot: "/a", repository: "pingdotgg/t3code" }), |
| 2815 | ], | 2844 | ], |
| 2816 | providers: [ | 2845 | providers: [ |
| 2817 | fakeProvider("github", { | 2846 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 2818 | capabilities: { | 2847 | capabilities: { |
| 2819 | diff: true, | 2848 | diff: true, |
| 2820 | comment: true, | 2849 | comment: true, |
| ... | @@ -2855,7 +2884,7 @@ it.effect("passes a reaction through with its subject id on a host that has them | ... | @@ -2855,7 +2884,7 @@ it.effect("passes a reaction through with its subject id on a host that has them |
| 2855 | project({ id: "p1", title: "t3code", workspaceRoot: "/a", repository: "pingdotgg/t3code" }), | 2884 | project({ id: "p1", title: "t3code", workspaceRoot: "/a", repository: "pingdotgg/t3code" }), |
| 2856 | ], | 2885 | ], |
| 2857 | providers: [ | 2886 | providers: [ |
| 2858 | fakeProvider("github", { | 2887 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 2859 | setReaction: (input) => { | 2888 | setReaction: (input) => { |
| 2860 | received = { | 2889 | received = { |
| 2861 | subjectId: input.subjectId, | 2890 | subjectId: input.subjectId, |
| ... | @@ -2888,7 +2917,7 @@ it.effect("invalidates the cached activity after reacting, like the other mutati | ... | @@ -2888,7 +2917,7 @@ it.effect("invalidates the cached activity after reacting, like the other mutati |
| 2888 | const service = yield* makeService({ | 2917 | const service = yield* makeService({ |
| 2889 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], | 2918 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 2890 | providers: [ | 2919 | providers: [ |
| 2891 | fakeProvider("github", { | 2920 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 2892 | getChangeRequestActivity: () => { | 2921 | getChangeRequestActivity: () => { |
| 2893 | activityCalls += 1; | 2922 | activityCalls += 1; |
| 2894 | return Effect.succeed({ | 2923 | return Effect.succeed({ |
| ... | @@ -2926,7 +2955,9 @@ it.effect("refuses an empty reply before it reaches the host", () => | ... | @@ -2926,7 +2955,9 @@ it.effect("refuses an empty reply before it reaches the host", () => |
| 2926 | project({ id: "p1", title: "t3code", workspaceRoot: "/a", repository: "pingdotgg/t3code" }), | 2955 | project({ id: "p1", title: "t3code", workspaceRoot: "/a", repository: "pingdotgg/t3code" }), |
| 2927 | ], | 2956 | ], |
| 2928 | providers: [ | 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,7 +2983,7 @@ it.effect("refuses a merge strategy the host does not offer", () => |
| 2952 | project({ id: "p1", title: "t3code", workspaceRoot: "/a", repository: "pingdotgg/t3code" }), | 2983 | project({ id: "p1", title: "t3code", workspaceRoot: "/a", repository: "pingdotgg/t3code" }), |
| 2953 | ], | 2984 | ], |
| 2954 | providers: [ | 2985 | providers: [ |
| 2955 | fakeProvider("github", { | 2986 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 2956 | capabilities: { | 2987 | capabilities: { |
| 2957 | diff: true, | 2988 | diff: true, |
| 2958 | comment: true, | 2989 | comment: true, |
| ... | @@ -3005,7 +3036,7 @@ it.effect("hands the provider the host its repository lives on", () => | ... | @@ -3005,7 +3036,7 @@ it.effect("hands the provider the host its repository lives on", () => |
| 3005 | }), | 3036 | }), |
| 3006 | ], | 3037 | ], |
| 3007 | providers: [ | 3038 | providers: [ |
| 3008 | fakeProvider("github", { | 3039 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 3009 | listChangeRequests: (input) => { | 3040 | listChangeRequests: (input) => { |
| 3010 | hosts.push(input.host); | 3041 | hosts.push(input.host); |
| 3011 | return Effect.succeed({ items: [], truncated: false, continues: true }); | 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,12 +3068,12 @@ it.effect("asks every host the reader's search, rather than filtering what came |
| 3037 | title: "on gitlab", | 3068 | title: "on gitlab", |
| 3038 | workspaceRoot: "/b", | 3069 | workspaceRoot: "/b", |
| 3039 | repository: "group/project", | 3070 | repository: "group/project", |
| 3040 | provider: "gitlab", | 3071 | provider: SourceControlProviderKind.make("gitlab"), |
| 3041 | }), | 3072 | }), |
| 3042 | ], | 3073 | ], |
| 3043 | providers: [ | 3074 | providers: [ |
| 3044 | fakeProvider("github", { listChangeRequests: listing }), | 3075 | fakeProvider(SourceControlProviderKind.make("github"), { listChangeRequests: listing }), |
| 3045 | fakeProvider("gitlab", { 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,7 +3093,7 @@ it.effect("asks for no search when the reader has typed nothing", () => |
| 3062 | project({ id: "p1", title: "t3code", workspaceRoot: "/a", repository: "pingdotgg/t3code" }), | 3093 | project({ id: "p1", title: "t3code", workspaceRoot: "/a", repository: "pingdotgg/t3code" }), |
| 3063 | ], | 3094 | ], |
| 3064 | providers: [ | 3095 | providers: [ |
| 3065 | fakeProvider("github", { | 3096 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 3066 | listChangeRequests: (input) => { | 3097 | listChangeRequests: (input) => { |
| 3067 | asked.push(input.query); | 3098 | asked.push(input.query); |
| 3068 | return Effect.succeed({ items: [], truncated: false, continues: true }); | 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,13 +3129,13 @@ it.effect("asks another checkout who is signed in when the first one cannot answ |
| 3098 | }), | 3129 | }), |
| 3099 | ], | 3130 | ], |
| 3100 | providers: [ | 3131 | providers: [ |
| 3101 | fakeProvider("github", { | 3132 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 3102 | getViewer: (input) => { | 3133 | getViewer: (input) => { |
| 3103 | asked.push(input.cwd); | 3134 | asked.push(input.cwd); |
| 3104 | return input.cwd === "/gone" | 3135 | return input.cwd === "/gone" |
| 3105 | ? Effect.fail( | 3136 | ? Effect.fail( |
| 3106 | new PullRequestProviderError({ | 3137 | new PullRequestProviderError({ |
| 3107 | provider: "github", | 3138 | provider: SourceControlProviderKind.make("github"), |
| 3108 | operation: "getViewer", | 3139 | operation: "getViewer", |
| 3109 | reason: "failed", | 3140 | reason: "failed", |
| 3110 | detail: "not a git repository", | 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,7 +3169,7 @@ it.effect("refuses to ask for a review on a host that cannot, before any call is |
| 3138 | const service = yield* makeService({ | 3169 | const service = yield* makeService({ |
| 3139 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], | 3170 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 3140 | providers: [ | 3171 | providers: [ |
| 3141 | fakeProvider("github", { | 3172 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 3142 | capabilities: { | 3173 | capabilities: { |
| 3143 | diff: true, | 3174 | diff: true, |
| 3144 | comment: true, | 3175 | comment: true, |
| ... | @@ -3179,7 +3210,7 @@ it.effect("refuses the candidate list on a host that has no such list to give", | ... | @@ -3179,7 +3210,7 @@ it.effect("refuses the candidate list on a host that has no such list to give", |
| 3179 | const service = yield* makeService({ | 3210 | const service = yield* makeService({ |
| 3180 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], | 3211 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 3181 | providers: [ | 3212 | providers: [ |
| 3182 | fakeProvider("github", { | 3213 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 3183 | capabilities: { | 3214 | capabilities: { |
| 3184 | diff: false, | 3215 | diff: false, |
| 3185 | comment: false, | 3216 | comment: false, |
| ... | @@ -3215,7 +3246,7 @@ it.effect("refuses a review request this viewer may not make, and says what acce | ... | @@ -3215,7 +3246,7 @@ it.effect("refuses a review request this viewer may not make, and says what acce |
| 3215 | const service = yield* makeService({ | 3246 | const service = yield* makeService({ |
| 3216 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], | 3247 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 3217 | providers: [ | 3248 | providers: [ |
| 3218 | fakeProvider("github", { | 3249 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 3219 | // The host asks for reviews; this account only reads the repository. | 3250 | // The host asks for reviews; this account only reads the repository. |
| 3220 | getViewerPermissions: () => | 3251 | getViewerPermissions: () => |
| 3221 | Effect.succeed({ | 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,7 +3285,7 @@ it.effect("keeps the menu from a viewer who may not ask, which is all it is for" |
| 3254 | const service = yield* makeService({ | 3285 | const service = yield* makeService({ |
| 3255 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], | 3286 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 3256 | providers: [ | 3287 | providers: [ |
| 3257 | fakeProvider("github", { | 3288 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 3258 | getViewerPermissions: () => | 3289 | getViewerPermissions: () => |
| 3259 | Effect.succeed({ | 3290 | Effect.succeed({ |
| 3260 | actions: [], | 3291 | actions: [], |
| ... | @@ -3286,7 +3317,7 @@ it.effect("hands the host's own candidate list back, and asks for it with the ch | ... | @@ -3286,7 +3317,7 @@ it.effect("hands the host's own candidate list back, and asks for it with the ch |
| 3286 | const service = yield* makeService({ | 3317 | const service = yield* makeService({ |
| 3287 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], | 3318 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 3288 | providers: [ | 3319 | providers: [ |
| 3289 | fakeProvider("github", { | 3320 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 3290 | listReviewerCandidates: (input) => { | 3321 | listReviewerCandidates: (input) => { |
| 3291 | askedFor = input.number; | 3322 | askedFor = input.number; |
| 3292 | return Effect.succeed({ | 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,7 +3359,7 @@ it.effect("refuses a label change on a host that has not said it takes one", () |
| 3328 | const service = yield* makeService({ | 3359 | const service = yield* makeService({ |
| 3329 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], | 3360 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 3330 | providers: [ | 3361 | providers: [ |
| 3331 | fakeProvider("github", { | 3362 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 3332 | // The method is there; the capability that would let it be called is not. | 3363 | // The method is there; the capability that would let it be called is not. |
| 3333 | setLabels: () => { | 3364 | setLabels: () => { |
| 3334 | changed = true; | 3365 | changed = true; |
| ... | @@ -3360,8 +3391,11 @@ it.effect("refuses a label change this viewer may not make, and says what access | ... | @@ -3360,8 +3391,11 @@ it.effect("refuses a label change this viewer may not make, and says what access |
| 3360 | const service = yield* makeService({ | 3391 | const service = yield* makeService({ |
| 3361 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], | 3392 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 3362 | providers: [ | 3393 | providers: [ |
| 3363 | fakeProvider("github", { | 3394 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 3364 | capabilities: { ...fakeProvider("github").capabilities, labels: true }, | 3395 | capabilities: { |
| 3396 | ...fakeProvider(SourceControlProviderKind.make("github")).capabilities, | ||
| 3397 | labels: true, | ||
| 3398 | }, | ||
| 3365 | getViewerPermissions: () => | 3399 | getViewerPermissions: () => |
| 3366 | Effect.succeed({ | 3400 | Effect.succeed({ |
| 3367 | actions: [], | 3401 | actions: [], |
| ... | @@ -3405,8 +3439,11 @@ it.effect("hands a label change to the host, and reads the labels back for the m | ... | @@ -3405,8 +3439,11 @@ it.effect("hands a label change to the host, and reads the labels back for the m |
| 3405 | const service = yield* makeService({ | 3439 | const service = yield* makeService({ |
| 3406 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], | 3440 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 3407 | providers: [ | 3441 | providers: [ |
| 3408 | fakeProvider("github", { | 3442 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 3409 | capabilities: { ...fakeProvider("github").capabilities, labels: true }, | 3443 | capabilities: { |
| 3444 | ...fakeProvider(SourceControlProviderKind.make("github")).capabilities, | ||
| 3445 | labels: true, | ||
| 3446 | }, | ||
| 3410 | listLabelCandidates: () => | 3447 | listLabelCandidates: () => |
| 3411 | Effect.succeed({ | 3448 | Effect.succeed({ |
| 3412 | candidates: [{ name: "bug", color: null, description: null, isApplied: false }], | 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,7 +3484,7 @@ it.effect("answers a repeated listing from cache, and concurrent readers share o |
| 3447 | const service = yield* makeService({ | 3484 | const service = yield* makeService({ |
| 3448 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], | 3485 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 3449 | providers: [ | 3486 | providers: [ |
| 3450 | fakeProvider("github", { | 3487 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 3451 | listChangeRequests: () => { | 3488 | listChangeRequests: () => { |
| 3452 | hostCalls += 1; | 3489 | hostCalls += 1; |
| 3453 | return Effect.succeed({ | 3490 | return Effect.succeed({ |
| ... | @@ -3496,7 +3533,7 @@ it.effect.each(["list", "listStats"] as const)( | ... | @@ -3496,7 +3533,7 @@ it.effect.each(["list", "listStats"] as const)( |
| 3496 | project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" }), | 3533 | project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" }), |
| 3497 | ], | 3534 | ], |
| 3498 | providers: [ | 3535 | providers: [ |
| 3499 | fakeProvider("github", { | 3536 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 3500 | listChangeRequests: () => | 3537 | listChangeRequests: () => |
| 3501 | lookup.pipe( | 3538 | lookup.pipe( |
| 3502 | Effect.as({ | 3539 | Effect.as({ |
| ... | @@ -3545,7 +3582,7 @@ it.effect("shares one cold viewer lookup across distinct concurrent lists", () = | ... | @@ -3545,7 +3582,7 @@ it.effect("shares one cold viewer lookup across distinct concurrent lists", () = |
| 3545 | const service = yield* makeService({ | 3582 | const service = yield* makeService({ |
| 3546 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], | 3583 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 3547 | providers: [ | 3584 | providers: [ |
| 3548 | fakeProvider("github", { | 3585 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 3549 | getViewer: () => | 3586 | getViewer: () => |
| 3550 | Effect.sync(() => { | 3587 | Effect.sync(() => { |
| 3551 | viewerCalls += 1; | 3588 | viewerCalls += 1; |
| ... | @@ -3583,7 +3620,7 @@ it.effect("uses five host reads for the normal indexed-repository page workflow" | ... | @@ -3583,7 +3620,7 @@ it.effect("uses five host reads for the normal indexed-repository page workflow" |
| 3583 | const service = yield* makeService({ | 3620 | const service = yield* makeService({ |
| 3584 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], | 3621 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 3585 | providers: [ | 3622 | providers: [ |
| 3586 | fakeProvider("github", { | 3623 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 3587 | getViewer: () => | 3624 | getViewer: () => |
| 3588 | Effect.sync(() => { | 3625 | Effect.sync(() => { |
| 3589 | viewerCalls += 1; | 3626 | viewerCalls += 1; |
| ... | @@ -3643,7 +3680,7 @@ it.effect("returns the refreshed listing on the first read after its cache expir | ... | @@ -3643,7 +3680,7 @@ it.effect("returns the refreshed listing on the first read after its cache expir |
| 3643 | const service = yield* makeService({ | 3680 | const service = yield* makeService({ |
| 3644 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], | 3681 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 3645 | providers: [ | 3682 | providers: [ |
| 3646 | fakeProvider("github", { | 3683 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 3647 | listChangeRequests: () => { | 3684 | listChangeRequests: () => { |
| 3648 | hostCalls += 1; | 3685 | hostCalls += 1; |
| 3649 | return Effect.succeed({ | 3686 | return Effect.succeed({ |
| ... | @@ -3682,7 +3719,7 @@ it.effect("a listing narrowed to some projects is its own cache entry", () => | ... | @@ -3682,7 +3719,7 @@ it.effect("a listing narrowed to some projects is its own cache entry", () => |
| 3682 | project({ id: "p2", title: "docs", workspaceRoot: "/b", repository: "acme/docs" }), | 3719 | project({ id: "p2", title: "docs", workspaceRoot: "/b", repository: "acme/docs" }), |
| 3683 | ], | 3720 | ], |
| 3684 | providers: [ | 3721 | providers: [ |
| 3685 | fakeProvider("github", { | 3722 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 3686 | listChangeRequestsAcross: (input) => { | 3723 | listChangeRequestsAcross: (input) => { |
| 3687 | asked.push(input.repositories); | 3724 | asked.push(input.repositories); |
| 3688 | return Effect.succeed({ | 3725 | return Effect.succeed({ |
| ... | @@ -3725,7 +3762,7 @@ it.effect( | ... | @@ -3725,7 +3762,7 @@ it.effect( |
| 3725 | project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" }), | 3762 | project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" }), |
| 3726 | ], | 3763 | ], |
| 3727 | providers: [ | 3764 | providers: [ |
| 3728 | fakeProvider("github", { | 3765 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 3729 | listChangeRequests: ({ filters }) => | 3766 | listChangeRequests: ({ filters }) => |
| 3730 | Effect.gen(function* () { | 3767 | Effect.gen(function* () { |
| 3731 | reads += 1; | 3768 | reads += 1; |
| ... | @@ -3782,7 +3819,7 @@ it.effect("keeps unrelated PRs warm after a mutation, explicit refresh, and proj | ... | @@ -3782,7 +3819,7 @@ it.effect("keeps unrelated PRs warm after a mutation, explicit refresh, and proj |
| 3782 | project({ id: "p2", title: "docs", workspaceRoot: "/b", repository: "acme/docs" }), | 3819 | project({ id: "p2", title: "docs", workspaceRoot: "/b", repository: "acme/docs" }), |
| 3783 | ], | 3820 | ], |
| 3784 | providers: [ | 3821 | providers: [ |
| 3785 | fakeProvider("github", { | 3822 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 3786 | getChangeRequest: (input) => | 3823 | getChangeRequest: (input) => |
| 3787 | Effect.sync(() => { | 3824 | Effect.sync(() => { |
| 3788 | calls.push(`${input.repository}/${input.number}`); | 3825 | calls.push(`${input.repository}/${input.number}`); |
| ... | @@ -3827,7 +3864,7 @@ it.effect( | ... | @@ -3827,7 +3864,7 @@ it.effect( |
| 3827 | }), | 3864 | }), |
| 3828 | ], | 3865 | ], |
| 3829 | providers: [ | 3866 | providers: [ |
| 3830 | fakeProvider("github", { | 3867 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 3831 | getChangeRequest: (input) => | 3868 | getChangeRequest: (input) => |
| 3832 | Effect.sync(() => { | 3869 | Effect.sync(() => { |
| 3833 | hosts.push(input.host); | 3870 | hosts.push(input.host); |
| ... | @@ -3858,7 +3895,7 @@ it.effect("does not revive old summaries when project epochs are evicted", () => | ... | @@ -3858,7 +3895,7 @@ it.effect("does not revive old summaries when project epochs are evicted", () => |
| 3858 | const service = yield* makeService({ | 3895 | const service = yield* makeService({ |
| 3859 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], | 3896 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 3860 | providers: [ | 3897 | providers: [ |
| 3861 | fakeProvider("github", { | 3898 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 3862 | getChangeRequest: () => Effect.succeed({ ...hostedChangeRequest("body"), title }), | 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,7 +3919,7 @@ it.effect("explicit and turn invalidations make the next listing ask the host ag |
| 3882 | const service = yield* makeService({ | 3919 | const service = yield* makeService({ |
| 3883 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], | 3920 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 3884 | providers: [ | 3921 | providers: [ |
| 3885 | fakeProvider("github", { | 3922 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 3886 | getViewer: () => { | 3923 | getViewer: () => { |
| 3887 | viewerCalls += 1; | 3924 | viewerCalls += 1; |
| 3888 | return Effect.succeed("bilal"); | 3925 | return Effect.succeed("bilal"); |
| ... | @@ -3924,7 +3961,7 @@ it.effect("close and reopen notify subscribed readers after invalidating their c | ... | @@ -3924,7 +3961,7 @@ it.effect("close and reopen notify subscribed readers after invalidating their c |
| 3924 | project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" }), | 3961 | project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" }), |
| 3925 | ], | 3962 | ], |
| 3926 | providers: [ | 3963 | providers: [ |
| 3927 | fakeProvider("github", { | 3964 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 3928 | getChangeRequestSummary: () => | 3965 | getChangeRequestSummary: () => |
| 3929 | Effect.succeed({ ...changeRequest(1, "2026-09-16T00:00:00.000Z"), state }), | 3966 | Effect.succeed({ ...changeRequest(1, "2026-09-16T00:00:00.000Z"), state }), |
| 3930 | runAction: (input) => | 3967 | runAction: (input) => |
| ... | @@ -3976,7 +4013,7 @@ it.effect("explicit invalidation refreshes origin readers after a routed host mu | ... | @@ -3976,7 +4013,7 @@ it.effect("explicit invalidation refreshes origin readers after a routed host mu |
| 3976 | project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" }), | 4013 | project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" }), |
| 3977 | ], | 4014 | ], |
| 3978 | providers: [ | 4015 | providers: [ |
| 3979 | fakeProvider("github", { | 4016 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 3980 | getChangeRequestSummary: () => | 4017 | getChangeRequestSummary: () => |
| 3981 | Effect.succeed({ ...changeRequest(1, "2026-09-16T00:00:00.000Z"), state }), | 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,7 +4058,7 @@ it.effect("does not cache a failed listing", () => |
| 4021 | const service = yield* makeService({ | 4058 | const service = yield* makeService({ |
| 4022 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], | 4059 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 4023 | providers: [ | 4060 | providers: [ |
| 4024 | fakeProvider("github", { | 4061 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 4025 | // The viewer lookup is what fails the whole listing rather than one repository. | 4062 | // The viewer lookup is what fails the whole listing rather than one repository. |
| 4026 | getViewer: () => { | 4063 | getViewer: () => { |
| 4027 | hostCalls += 1; | 4064 | hostCalls += 1; |
| ... | @@ -4052,11 +4089,11 @@ it.effect("reads a host's repositories in one search, and files the rows back un | ... | @@ -4052,11 +4089,11 @@ it.effect("reads a host's repositories in one search, and files the rows back un |
| 4052 | title: "on gitlab", | 4089 | title: "on gitlab", |
| 4053 | workspaceRoot: "/c", | 4090 | workspaceRoot: "/c", |
| 4054 | repository: "group/project", | 4091 | repository: "group/project", |
| 4055 | provider: "gitlab", | 4092 | provider: SourceControlProviderKind.make("gitlab"), |
| 4056 | }), | 4093 | }), |
| 4057 | ], | 4094 | ], |
| 4058 | providers: [ | 4095 | providers: [ |
| 4059 | fakeProvider("github", { | 4096 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 4060 | listChangeRequests: ({ repository }) => { | 4097 | listChangeRequests: ({ repository }) => { |
| 4061 | separately.push(repository); | 4098 | separately.push(repository); |
| 4062 | return Effect.succeed({ items: [], truncated: false, continues: true }); | 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,7 +4110,7 @@ it.effect("reads a host's repositories in one search, and files the rows back un |
| 4073 | }, | 4110 | }, |
| 4074 | }), | 4111 | }), |
| 4075 | // A host with no search across repositories keeps being asked one at a time. | 4112 | // A host with no search across repositories keeps being asked one at a time. |
| 4076 | fakeProvider("gitlab", { | 4113 | fakeProvider(SourceControlProviderKind.make("gitlab"), { |
| 4077 | listChangeRequests: ({ repository }) => { | 4114 | listChangeRequests: ({ repository }) => { |
| 4078 | separately.push(repository); | 4115 | separately.push(repository); |
| 4079 | return Effect.succeed({ | 4116 | return Effect.succeed({ |
| ... | @@ -4111,7 +4148,7 @@ it.effect("carries every repository of a slice on from the oldest row in it", () | ... | @@ -4111,7 +4148,7 @@ it.effect("carries every repository of a slice on from the oldest row in it", () |
| 4111 | project({ id: "p3", title: "docs", workspaceRoot: "/c", repository: "acme/docs" }), | 4148 | project({ id: "p3", title: "docs", workspaceRoot: "/c", repository: "acme/docs" }), |
| 4112 | ], | 4149 | ], |
| 4113 | providers: [ | 4150 | providers: [ |
| 4114 | fakeProvider("github", { | 4151 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 4115 | listChangeRequestsAcross: () => | 4152 | listChangeRequestsAcross: () => |
| 4116 | Effect.succeed({ | 4153 | Effect.succeed({ |
| 4117 | items: [ | 4154 | items: [ |
| ... | @@ -4144,7 +4181,7 @@ it.effect("carries a slice on without sending the rows it already sent", () => | ... | @@ -4144,7 +4181,7 @@ it.effect("carries a slice on without sending the rows it already sent", () => |
| 4144 | const service = yield* makeService({ | 4181 | const service = yield* makeService({ |
| 4145 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], | 4182 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 4146 | providers: [ | 4183 | providers: [ |
| 4147 | fakeProvider("github", { | 4184 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 4148 | listChangeRequestsAcross: (input) => { | 4185 | listChangeRequestsAcross: (input) => { |
| 4149 | cursors.push(input.cursor); | 4186 | cursors.push(input.cursor); |
| 4150 | return Effect.succeed({ | 4187 | return Effect.succeed({ |
| ... | @@ -4189,7 +4226,7 @@ it.effect("reads a workspace larger than one search in chunks, and merges them", | ... | @@ -4189,7 +4226,7 @@ it.effect("reads a workspace larger than one search in chunks, and merges them", |
| 4189 | }), | 4226 | }), |
| 4190 | ), | 4227 | ), |
| 4191 | providers: [ | 4228 | providers: [ |
| 4192 | fakeProvider("github", { | 4229 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 4193 | listChangeRequestsAcross: (input) => { | 4230 | listChangeRequestsAcross: (input) => { |
| 4194 | asked.push(input.repositories.length); | 4231 | asked.push(input.repositories.length); |
| 4195 | return Effect.succeed({ | 4232 | return Effect.succeed({ |
| ... | @@ -4218,7 +4255,7 @@ it.effect("asks on its own for a repository a search answered nothing for", () = | ... | @@ -4218,7 +4255,7 @@ it.effect("asks on its own for a repository a search answered nothing for", () = |
| 4218 | project({ id: "p2", title: "docs", workspaceRoot: "/b", repository: "acme/docs" }), | 4255 | project({ id: "p2", title: "docs", workspaceRoot: "/b", repository: "acme/docs" }), |
| 4219 | ], | 4256 | ], |
| 4220 | providers: [ | 4257 | providers: [ |
| 4221 | fakeProvider("github", { | 4258 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 4222 | listChangeRequests: ({ repository }) => { | 4259 | listChangeRequests: ({ repository }) => { |
| 4223 | separately.push(repository); | 4260 | separately.push(repository); |
| 4224 | return repository === "acme/docs" | 4261 | return repository === "acme/docs" |
| ... | @@ -4262,7 +4299,7 @@ it.effect("reads the repositories one at a time when the search itself fails", ( | ... | @@ -4262,7 +4299,7 @@ it.effect("reads the repositories one at a time when the search itself fails", ( |
| 4262 | project({ id: "p2", title: "docs", workspaceRoot: "/b", repository: "acme/docs" }), | 4299 | project({ id: "p2", title: "docs", workspaceRoot: "/b", repository: "acme/docs" }), |
| 4263 | ], | 4300 | ], |
| 4264 | providers: [ | 4301 | providers: [ |
| 4265 | fakeProvider("github", { | 4302 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 4266 | listChangeRequests: ({ repository }) => { | 4303 | listChangeRequests: ({ repository }) => { |
| 4267 | separately.push(repository); | 4304 | separately.push(repository); |
| 4268 | return Effect.succeed({ | 4305 | return Effect.succeed({ |
| ... | @@ -4295,11 +4332,11 @@ it.effect("fills in the line counts for the rows it is given", () => | ... | @@ -4295,11 +4332,11 @@ it.effect("fills in the line counts for the rows it is given", () => |
| 4295 | title: "on gitlab", | 4332 | title: "on gitlab", |
| 4296 | workspaceRoot: "/b", | 4333 | workspaceRoot: "/b", |
| 4297 | repository: "group/project", | 4334 | repository: "group/project", |
| 4298 | provider: "gitlab", | 4335 | provider: SourceControlProviderKind.make("gitlab"), |
| 4299 | }), | 4336 | }), |
| 4300 | ], | 4337 | ], |
| 4301 | providers: [ | 4338 | providers: [ |
| 4302 | fakeProvider("github", { | 4339 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 4303 | listChangeRequestStats: (input) => { | 4340 | listChangeRequestStats: (input) => { |
| 4304 | asked.push(input.changeRequests); | 4341 | asked.push(input.changeRequests); |
| 4305 | return Effect.succeed([ | 4342 | return Effect.succeed([ |
| ... | @@ -4308,7 +4345,7 @@ it.effect("fills in the line counts for the rows it is given", () => | ... | @@ -4308,7 +4345,7 @@ it.effect("fills in the line counts for the rows it is given", () => |
| 4308 | }, | 4345 | }, |
| 4309 | }), | 4346 | }), |
| 4310 | // Its listing carries the counts already, so it has nothing to be asked. | 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,7 +4392,7 @@ it.effect( |
| 4355 | project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" }), | 4392 | project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" }), |
| 4356 | ], | 4393 | ], |
| 4357 | providers: [ | 4394 | providers: [ |
| 4358 | fakeProvider("github", { | 4395 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 4359 | listChangeRequestStats: (input) => { | 4396 | listChangeRequestStats: (input) => { |
| 4360 | asked.push(input.changeRequests.map((ref) => ref.number)); | 4397 | asked.push(input.changeRequests.map((ref) => ref.number)); |
| 4361 | return Effect.succeed( | 4398 | return Effect.succeed( |
| ... | @@ -4404,7 +4441,7 @@ it.effect("reads the fresh diff when detail or summary discovers a changed revis | ... | @@ -4404,7 +4441,7 @@ it.effect("reads the fresh diff when detail or summary discovers a changed revis |
| 4404 | const service = yield* makeService({ | 4441 | const service = yield* makeService({ |
| 4405 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], | 4442 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 4406 | providers: [ | 4443 | providers: [ |
| 4407 | fakeProvider("github", { | 4444 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 4408 | getChangeRequest: () => | 4445 | getChangeRequest: () => |
| 4409 | Effect.sync(() => ({ ...hostedChangeRequest("body"), updatedAt: revision })), | 4446 | Effect.sync(() => ({ ...hostedChangeRequest("body"), updatedAt: revision })), |
| 4410 | getChangeRequestSummary: () => | 4447 | getChangeRequestSummary: () => |
| ... | @@ -4453,7 +4490,9 @@ it.effect("keeps the rows when the line counts cannot be read", () => | ... | @@ -4453,7 +4490,9 @@ it.effect("keeps the rows when the line counts cannot be read", () => |
| 4453 | const service = yield* makeService({ | 4490 | const service = yield* makeService({ |
| 4454 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], | 4491 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 4455 | providers: [ | 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,7 +4517,7 @@ it.effect( |
| 4478 | project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" }), | 4517 | project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" }), |
| 4479 | ], | 4518 | ], |
| 4480 | providers: [ | 4519 | providers: [ |
| 4481 | fakeProvider("github", { | 4520 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 4482 | listChangeRequestStats: () => { | 4521 | listChangeRequestStats: () => { |
| 4483 | statsCalls += 1; | 4522 | statsCalls += 1; |
| 4484 | return Effect.succeed([]); | 4523 | return Effect.succeed([]); |
| ... | @@ -4556,7 +4595,7 @@ it.effect("shares linked summaries and reuses them for display without asking th | ... | @@ -4556,7 +4595,7 @@ it.effect("shares linked summaries and reuses them for display without asking th |
| 4556 | const service = yield* makeService({ | 4595 | const service = yield* makeService({ |
| 4557 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], | 4596 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 4558 | providers: [ | 4597 | providers: [ |
| 4559 | fakeProvider("github", { | 4598 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 4560 | getChangeRequestSummary: () => | 4599 | getChangeRequestSummary: () => |
| 4561 | Effect.sync(() => { | 4600 | Effect.sync(() => { |
| 4562 | calls += 1; | 4601 | calls += 1; |
| ... | @@ -4567,7 +4606,7 @@ it.effect("shares linked summaries and reuses them for display without asking th | ... | @@ -4567,7 +4606,7 @@ it.effect("shares linked summaries and reuses them for display without asking th |
| 4567 | shouldFail | 4606 | shouldFail |
| 4568 | ? Effect.fail( | 4607 | ? Effect.fail( |
| 4569 | new PullRequestProviderError({ | 4608 | new PullRequestProviderError({ |
| 4570 | provider: "github", | 4609 | provider: SourceControlProviderKind.make("github"), |
| 4571 | operation: "getChangeRequestSummary", | 4610 | operation: "getChangeRequestSummary", |
| 4572 | reason: "failed", | 4611 | reason: "failed", |
| 4573 | detail: "HTTP 504", | 4612 | detail: "HTTP 504", |
| ... | @@ -4622,8 +4661,11 @@ it.effect("keeps routed reads separate when the GitHub account changes", () => | ... | @@ -4622,8 +4661,11 @@ it.effect("keeps routed reads separate when the GitHub account changes", () => |
| 4622 | project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" }), | 4661 | project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" }), |
| 4623 | ], | 4662 | ], |
| 4624 | providers: [ | 4663 | providers: [ |
| 4625 | fakeProvider("github", { | 4664 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 4626 | capabilities: { ...fakeProvider("github").capabilities, viewedFiles: "host" }, | 4665 | capabilities: { |
| 4666 | ...fakeProvider(SourceControlProviderKind.make("github")).capabilities, | ||
| 4667 | viewedFiles: "host", | ||
| 4668 | }, | ||
| 4627 | getFilesViewed: () => | 4669 | getFilesViewed: () => |
| 4628 | read().pipe( | 4670 | read().pipe( |
| 4629 | Effect.as({ | 4671 | Effect.as({ |
| ... | @@ -4685,8 +4727,11 @@ it.effect("isolates routed caches for two credentials belonging to the same acco | ... | @@ -4685,8 +4727,11 @@ it.effect("isolates routed caches for two credentials belonging to the same acco |
| 4685 | project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" }), | 4727 | project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" }), |
| 4686 | ], | 4728 | ], |
| 4687 | providers: [ | 4729 | providers: [ |
| 4688 | fakeProvider("github", { | 4730 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 4689 | capabilities: { ...fakeProvider("github").capabilities, viewedFiles: "host" }, | 4731 | capabilities: { |
| 4732 | ...fakeProvider(SourceControlProviderKind.make("github")).capabilities, | ||
| 4733 | viewedFiles: "host", | ||
| 4734 | }, | ||
| 4690 | getFilesViewed: () => | 4735 | getFilesViewed: () => |
| 4691 | read().pipe( | 4736 | read().pipe( |
| 4692 | Effect.as({ | 4737 | Effect.as({ |
| ... | @@ -4744,7 +4789,7 @@ it.effect("rejects mismatched routing credentials before use and preserves actio | ... | @@ -4744,7 +4789,7 @@ it.effect("rejects mismatched routing credentials before use and preserves actio |
| 4744 | const service = yield* makeService({ | 4789 | const service = yield* makeService({ |
| 4745 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], | 4790 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 4746 | providers: [ | 4791 | providers: [ |
| 4747 | fakeProvider("github", { | 4792 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 4748 | getRoutingIdentity: () => Effect.succeed({ accountId: "101", viewer: "octocat" }), | 4793 | getRoutingIdentity: () => Effect.succeed({ accountId: "101", viewer: "octocat" }), |
| 4749 | withVerifiedCredential: (_, use) => | 4794 | withVerifiedCredential: (_, use) => |
| 4750 | use({ | 4795 | use({ |
| ... | @@ -4798,7 +4843,7 @@ it.effect("answers a known pull request immediately while the host refreshes", ( | ... | @@ -4798,7 +4843,7 @@ it.effect("answers a known pull request immediately while the host refreshes", ( |
| 4798 | const service = yield* makeService({ | 4843 | const service = yield* makeService({ |
| 4799 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], | 4844 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 4800 | providers: [ | 4845 | providers: [ |
| 4801 | fakeProvider("github", { | 4846 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 4802 | getChangeRequest: () => | 4847 | getChangeRequest: () => |
| 4803 | Effect.gen(function* () { | 4848 | Effect.gen(function* () { |
| 4804 | calls += 1; | 4849 | calls += 1; |
| ... | @@ -4829,7 +4874,7 @@ it.effect("does not ask the host again for a linked summary it already holds", ( | ... | @@ -4829,7 +4874,7 @@ it.effect("does not ask the host again for a linked summary it already holds", ( |
| 4829 | const service = yield* makeService({ | 4874 | const service = yield* makeService({ |
| 4830 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], | 4875 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 4831 | providers: [ | 4876 | providers: [ |
| 4832 | fakeProvider("github", { | 4877 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 4833 | getChangeRequestSummary: () => | 4878 | getChangeRequestSummary: () => |
| 4834 | Effect.sync(() => { | 4879 | Effect.sync(() => { |
| 4835 | calls += 1; | 4880 | calls += 1; |
| ... | @@ -4858,7 +4903,7 @@ it.effect( | ... | @@ -4858,7 +4903,7 @@ it.effect( |
| 4858 | project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" }), | 4903 | project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" }), |
| 4859 | ], | 4904 | ], |
| 4860 | providers: [ | 4905 | providers: [ |
| 4861 | fakeProvider("github", { | 4906 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 4862 | getChangeRequestSummary: () => | 4907 | getChangeRequestSummary: () => |
| 4863 | Effect.succeed({ | 4908 | Effect.succeed({ |
| 4864 | ...changeRequest(1, "2026-07-02T00:00:00Z"), | 4909 | ...changeRequest(1, "2026-07-02T00:00:00Z"), |
| ... | @@ -4896,7 +4941,7 @@ it.effect("reuses an observed merged state for strict settlement reads", () => | ... | @@ -4896,7 +4941,7 @@ it.effect("reuses an observed merged state for strict settlement reads", () => |
| 4896 | const service = yield* makeService({ | 4941 | const service = yield* makeService({ |
| 4897 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], | 4942 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 4898 | providers: [ | 4943 | providers: [ |
| 4899 | fakeProvider("github", { | 4944 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 4900 | getChangeRequest: () => | 4945 | getChangeRequest: () => |
| 4901 | Effect.succeed({ | 4946 | Effect.succeed({ |
| 4902 | ...hostedChangeRequest("merged body", 4), | 4947 | ...hostedChangeRequest("merged body", 4), |
| ... | @@ -4927,7 +4972,7 @@ it.effect("announces state a detail read sees first or newly", () => | ... | @@ -4927,7 +4972,7 @@ it.effect("announces state a detail read sees first or newly", () => |
| 4927 | const service = yield* makeService({ | 4972 | const service = yield* makeService({ |
| 4928 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], | 4973 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 4929 | providers: [ | 4974 | providers: [ |
| 4930 | fakeProvider("github", { | 4975 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 4931 | getChangeRequest: () => Effect.succeed({ ...hostedChangeRequest("body"), ...detail }), | 4976 | getChangeRequest: () => Effect.succeed({ ...hostedChangeRequest("body"), ...detail }), |
| 4932 | getChangeRequestSummary: () => | 4977 | getChangeRequestSummary: () => |
| 4933 | Effect.succeed({ | 4978 | Effect.succeed({ |
| ... | @@ -4979,7 +5024,7 @@ it.effect("announces a reported state without trusting it", () => | ... | @@ -4979,7 +5024,7 @@ it.effect("announces a reported state without trusting it", () => |
| 4979 | const service = yield* makeService({ | 5024 | const service = yield* makeService({ |
| 4980 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], | 5025 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 4981 | providers: [ | 5026 | providers: [ |
| 4982 | fakeProvider("github", { | 5027 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 4983 | getChangeRequest: () => Effect.succeed({ ...hostedChangeRequest("body"), ...detail }), | 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,7 +5069,7 @@ it.effect("does not let a stale detail reopen overwrite a fresher linked summary |
| 5024 | const service = yield* makeService({ | 5069 | const service = yield* makeService({ |
| 5025 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], | 5070 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 5026 | providers: [ | 5071 | providers: [ |
| 5027 | fakeProvider("github", { | 5072 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 5028 | getChangeRequest: () => | 5073 | getChangeRequest: () => |
| 5029 | Effect.gen(function* () { | 5074 | Effect.gen(function* () { |
| 5030 | detailCalls += 1; | 5075 | detailCalls += 1; |
| ... | @@ -5078,7 +5123,7 @@ it.effect("does not let a still-cached detail overwrite a fresher linked summary | ... | @@ -5078,7 +5123,7 @@ it.effect("does not let a still-cached detail overwrite a fresher linked summary |
| 5078 | const service = yield* makeService({ | 5123 | const service = yield* makeService({ |
| 5079 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], | 5124 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 5080 | providers: [ | 5125 | providers: [ |
| 5081 | fakeProvider("github", { | 5126 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 5082 | getChangeRequest: () => Effect.succeed(hostedChangeRequest("old body", 4)), | 5127 | getChangeRequest: () => Effect.succeed(hostedChangeRequest("old body", 4)), |
| 5083 | getChangeRequestSummary: () => | 5128 | getChangeRequestSummary: () => |
| 5084 | Effect.succeed({ | 5129 | Effect.succeed({ |
| ... | @@ -5114,11 +5159,11 @@ it.effect("tells the client when the host has no pull request under that number" | ... | @@ -5114,11 +5159,11 @@ it.effect("tells the client when the host has no pull request under that number" |
| 5114 | const service = yield* makeService({ | 5159 | const service = yield* makeService({ |
| 5115 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], | 5160 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 5116 | providers: [ | 5161 | providers: [ |
| 5117 | fakeProvider("github", { | 5162 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 5118 | getChangeRequest: () => | 5163 | getChangeRequest: () => |
| 5119 | Effect.fail( | 5164 | Effect.fail( |
| 5120 | new PullRequestProviderError({ | 5165 | new PullRequestProviderError({ |
| 5121 | provider: "github", | 5166 | provider: SourceControlProviderKind.make("github"), |
| 5122 | operation: "getChangeRequest", | 5167 | operation: "getChangeRequest", |
| 5123 | reason: "not-found", | 5168 | reason: "not-found", |
| 5124 | detail: "Pull request not found. Check the PR number or URL and try again.", | 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,12 +5187,12 @@ it.effect("keeps recent detail on a transient refresh failure but not after inva |
| 5142 | const service = yield* makeService({ | 5187 | const service = yield* makeService({ |
| 5143 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], | 5188 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 5144 | providers: [ | 5189 | providers: [ |
| 5145 | fakeProvider("github", { | 5190 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 5146 | getChangeRequest: () => | 5191 | getChangeRequest: () => |
| 5147 | failing | 5192 | failing |
| 5148 | ? Effect.fail( | 5193 | ? Effect.fail( |
| 5149 | new PullRequestProviderError({ | 5194 | new PullRequestProviderError({ |
| 5150 | provider: "github", | 5195 | provider: SourceControlProviderKind.make("github"), |
| 5151 | operation: "getChangeRequest", | 5196 | operation: "getChangeRequest", |
| 5152 | reason: "failed", | 5197 | reason: "failed", |
| 5153 | detail: "spawn gh EAGAIN", | 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,7 +5242,7 @@ it.effect("carries an armed auto-merge through to the detail, and silence as sil |
| 5197 | project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" }), | 5242 | project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" }), |
| 5198 | ], | 5243 | ], |
| 5199 | providers: [ | 5244 | providers: [ |
| 5200 | fakeProvider("github", { | 5245 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 5201 | getChangeRequest: () => | 5246 | getChangeRequest: () => |
| 5202 | Effect.succeed({ | 5247 | Effect.succeed({ |
| 5203 | ...changeRequest(1, "2026-07-02T00:00:00Z"), | 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,13 +5288,13 @@ it.effect("narrows the rows of a host that ignored the filters it was handed", ( |
| 5243 | title: "web", | 5288 | title: "web", |
| 5244 | workspaceRoot: "/a", | 5289 | workspaceRoot: "/a", |
| 5245 | repository: "acme/web", | 5290 | repository: "acme/web", |
| 5246 | provider: "gitlab", | 5291 | provider: SourceControlProviderKind.make("gitlab"), |
| 5247 | }), | 5292 | }), |
| 5248 | ], | 5293 | ], |
| 5249 | providers: [ | 5294 | providers: [ |
| 5250 | // Only GitHub narrows a listing for itself; every other host answers unnarrowed, and | 5295 | // Only GitHub narrows a listing for itself; every other host answers unnarrowed, and |
| 5251 | // sending it a draft filter it quietly ignores used to put drafts on a filtered page. | 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 | listChangeRequests: () => | 5298 | listChangeRequests: () => |
| 5254 | Effect.succeed({ | 5299 | Effect.succeed({ |
| 5255 | items: [ | 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,11 +5330,11 @@ it.effect("keeps a row of a host that ignored the filters if any name of a label |
| 5285 | title: "web", | 5330 | title: "web", |
| 5286 | workspaceRoot: "/a", | 5331 | workspaceRoot: "/a", |
| 5287 | repository: "acme/web", | 5332 | repository: "acme/web", |
| 5288 | provider: "gitlab", | 5333 | provider: SourceControlProviderKind.make("gitlab"), |
| 5289 | }), | 5334 | }), |
| 5290 | ], | 5335 | ], |
| 5291 | providers: [ | 5336 | providers: [ |
| 5292 | fakeProvider("gitlab", { | 5337 | fakeProvider(SourceControlProviderKind.make("gitlab"), { |
| 5293 | listChangeRequests: () => | 5338 | listChangeRequests: () => |
| 5294 | Effect.succeed({ | 5339 | Effect.succeed({ |
| 5295 | items: [ | 5340 | items: [ |
| ... | @@ -5328,13 +5373,13 @@ it.effect('resolves an author filter of "me" to the viewer before narrowing a ho | ... | @@ -5328,13 +5373,13 @@ it.effect('resolves an author filter of "me" to the viewer before narrowing a ho |
| 5328 | title: "web", | 5373 | title: "web", |
| 5329 | workspaceRoot: "/a", | 5374 | workspaceRoot: "/a", |
| 5330 | repository: "acme/web", | 5375 | repository: "acme/web", |
| 5331 | provider: "gitlab", | 5376 | provider: SourceControlProviderKind.make("gitlab"), |
| 5332 | }), | 5377 | }), |
| 5333 | ], | 5378 | ], |
| 5334 | providers: [ | 5379 | providers: [ |
| 5335 | // Only GitHub narrows a listing for itself, so this fixture's "me" has to be resolved | 5380 | // Only GitHub narrows a listing for itself, so this fixture's "me" has to be resolved |
| 5336 | // locally too — the same helper both call sites lean on. | 5381 | // locally too — the same helper both call sites lean on. |
| 5337 | fakeProvider("gitlab", { | 5382 | fakeProvider(SourceControlProviderKind.make("gitlab"), { |
| 5338 | listChangeRequests: () => | 5383 | listChangeRequests: () => |
| 5339 | Effect.succeed({ | 5384 | Effect.succeed({ |
| 5340 | items: [ | 5385 | items: [ |
| ... | @@ -5395,7 +5440,7 @@ it.effect.each([false, true])( | ... | @@ -5395,7 +5440,7 @@ it.effect.each([false, true])( |
| 5395 | }), | 5440 | }), |
| 5396 | ], | 5441 | ], |
| 5397 | providers: [ | 5442 | providers: [ |
| 5398 | fakeProvider("github", { | 5443 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 5399 | capabilities, | 5444 | capabilities, |
| 5400 | getViewerPermissions: () => | 5445 | getViewerPermissions: () => |
| 5401 | Effect.succeed({ | 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,7 +5508,7 @@ it.effect("refuses a way of updating a branch that the host or the viewer does n |
| 5463 | const service = yield* makeService({ | 5508 | const service = yield* makeService({ |
| 5464 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], | 5509 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 5465 | providers: [ | 5510 | providers: [ |
| 5466 | fakeProvider("github", { | 5511 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 5467 | capabilities: { | 5512 | capabilities: { |
| 5468 | diff: true, | 5513 | diff: true, |
| 5469 | comment: true, | 5514 | comment: true, |
| ... | @@ -5516,11 +5561,11 @@ it.effect("refuses to merge a target branch into a source branch on a host that | ... | @@ -5516,11 +5561,11 @@ it.effect("refuses to merge a target branch into a source branch on a host that |
| 5516 | title: "on gitlab", | 5561 | title: "on gitlab", |
| 5517 | workspaceRoot: "/a", | 5562 | workspaceRoot: "/a", |
| 5518 | repository: "group/project", | 5563 | repository: "group/project", |
| 5519 | provider: "gitlab", | 5564 | provider: SourceControlProviderKind.make("gitlab"), |
| 5520 | }), | 5565 | }), |
| 5521 | ], | 5566 | ], |
| 5522 | providers: [ | 5567 | providers: [ |
| 5523 | fakeProvider("gitlab", { | 5568 | fakeProvider(SourceControlProviderKind.make("gitlab"), { |
| 5524 | capabilities: { | 5569 | capabilities: { |
| 5525 | diff: true, | 5570 | diff: true, |
| 5526 | comment: true, | 5571 | comment: true, |
| ... | @@ -5575,12 +5620,12 @@ it.effect("judges the review filter only on a host that summarises its reviews", | ... | @@ -5575,12 +5620,12 @@ it.effect("judges the review filter only on a host that summarises its reviews", |
| 5575 | title: "on gitlab", | 5620 | title: "on gitlab", |
| 5576 | workspaceRoot: "/b", | 5621 | workspaceRoot: "/b", |
| 5577 | repository: "group/project", | 5622 | repository: "group/project", |
| 5578 | provider: "gitlab", | 5623 | provider: SourceControlProviderKind.make("gitlab"), |
| 5579 | }), | 5624 | }), |
| 5580 | ], | 5625 | ], |
| 5581 | providers: [ | 5626 | providers: [ |
| 5582 | // GitHub answers with the field on every row: null is "nobody has decided yet". | 5627 | // GitHub answers with the field on every row: null is "nobody has decided yet". |
| 5583 | fakeProvider("github", { | 5628 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 5584 | listChangeRequests: () => | 5629 | listChangeRequests: () => |
| 5585 | Effect.succeed({ | 5630 | Effect.succeed({ |
| 5586 | items: [ | 5631 | items: [ |
| ... | @@ -5595,7 +5640,7 @@ it.effect("judges the review filter only on a host that summarises its reviews", | ... | @@ -5595,7 +5640,7 @@ it.effect("judges the review filter only on a host that summarises its reviews", |
| 5595 | }), | 5640 | }), |
| 5596 | }), | 5641 | }), |
| 5597 | // GitLab never supplies the field, so its rows are not the filter's to judge. | 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 | listChangeRequests: () => | 5644 | listChangeRequests: () => |
| 5600 | Effect.succeed({ | 5645 | Effect.succeed({ |
| 5601 | items: [changeRequest(3, "2026-07-02T00:00:00Z")], | 5646 | items: [changeRequest(3, "2026-07-02T00:00:00Z")], |
| ... | @@ -5621,7 +5666,7 @@ it.effect("sends only the words a rewrite carries", () => | ... | @@ -5621,7 +5666,7 @@ it.effect("sends only the words a rewrite carries", () => |
| 5621 | const service = yield* makeService({ | 5666 | const service = yield* makeService({ |
| 5622 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], | 5667 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 5623 | providers: [ | 5668 | providers: [ |
| 5624 | fakeProvider("github", { | 5669 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 5625 | updateChangeRequest: (input) => { | 5670 | updateChangeRequest: (input) => { |
| 5626 | received.push({ title: input.title, body: input.body }); | 5671 | received.push({ title: input.title, body: input.body }); |
| 5627 | return Effect.void; | 5672 | return Effect.void; |
| ... | @@ -5647,7 +5692,9 @@ it.effect("refuses a rewrite that changes nothing, before any call is made", () | ... | @@ -5647,7 +5692,9 @@ it.effect("refuses a rewrite that changes nothing, before any call is made", () |
| 5647 | const service = yield* makeService({ | 5692 | const service = yield* makeService({ |
| 5648 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], | 5693 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 5649 | providers: [ | 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,7 +5712,7 @@ it.effect("refuses to rewrite anything on a host that never claimed it", () => |
| 5665 | const service = yield* makeService({ | 5712 | const service = yield* makeService({ |
| 5666 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], | 5713 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 5667 | providers: [ | 5714 | providers: [ |
| 5668 | fakeProvider("github", { | 5715 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 5669 | capabilities: { | 5716 | capabilities: { |
| 5670 | diff: true, | 5717 | diff: true, |
| 5671 | comment: true, | 5718 | comment: true, |
| ... | @@ -5704,7 +5751,7 @@ it.effect("passes a rewritten remark through with the id and kind it arrived und | ... | @@ -5704,7 +5751,7 @@ it.effect("passes a rewritten remark through with the id and kind it arrived und |
| 5704 | const service = yield* makeService({ | 5751 | const service = yield* makeService({ |
| 5705 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], | 5752 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 5706 | providers: [ | 5753 | providers: [ |
| 5707 | fakeProvider("github", { | 5754 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 5708 | updateComment: (input) => { | 5755 | updateComment: (input) => { |
| 5709 | received = { id: input.commentId, kind: input.kind, body: input.body }; | 5756 | received = { id: input.commentId, kind: input.kind, body: input.body }; |
| 5710 | return Effect.void; | 5757 | return Effect.void; |
| ... | @@ -5735,7 +5782,9 @@ it.effect("refuses a remark rewritten into nothing but whitespace", () => | ... | @@ -5735,7 +5782,9 @@ it.effect("refuses a remark rewritten into nothing but whitespace", () => |
| 5735 | const service = yield* makeService({ | 5782 | const service = yield* makeService({ |
| 5736 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], | 5783 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 5737 | providers: [ | 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,7 +5810,7 @@ it.effect("forgets the cached detail after a rewrite or terminal turn", () => |
| 5761 | const service = yield* makeService({ | 5810 | const service = yield* makeService({ |
| 5762 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], | 5811 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 5763 | providers: [ | 5812 | providers: [ |
| 5764 | fakeProvider("github", { | 5813 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 5765 | getChangeRequest: () => { | 5814 | getChangeRequest: () => { |
| 5766 | coreCalls += 1; | 5815 | coreCalls += 1; |
| 5767 | return Effect.succeed({ | 5816 | return Effect.succeed({ |
| ... | @@ -5813,7 +5862,7 @@ it.effect("names the signed-in account in the detail, and says nothing where the | ... | @@ -5813,7 +5862,7 @@ it.effect("names the signed-in account in the detail, and says nothing where the |
| 5813 | number: 1, | 5862 | number: 1, |
| 5814 | }); | 5863 | }); |
| 5815 | }); | 5864 | }); |
| 5816 | const readable = fakeProvider("github", { | 5865 | const readable = fakeProvider(SourceControlProviderKind.make("github"), { |
| 5817 | getChangeRequest: () => | 5866 | getChangeRequest: () => |
| 5818 | Effect.succeed({ | 5867 | Effect.succeed({ |
| 5819 | ...changeRequest(1, "2026-07-02T00:00:00Z"), | 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,7 +5886,8 @@ it.effect("names the signed-in account in the detail, and says nothing where the |
| 5837 | const named = yield* detailFrom(readable); | 5886 | const named = yield* detailFrom(readable); |
| 5838 | const unnamed = yield* detailFrom({ | 5887 | const unnamed = yield* detailFrom({ |
| 5839 | ...readable, | 5888 | ...readable, |
| 5840 | getViewer: () => Effect.fail(unusable("github", "unauthenticated")), | 5889 | getViewer: () => |
| 5890 | Effect.fail(unusable(SourceControlProviderKind.make("github"), "unauthenticated")), | ||
| 5841 | }); | 5891 | }); |
| 5842 | 5892 | ||
| 5843 | assert.strictEqual(named.viewer, "bilal"); | 5893 | assert.strictEqual(named.viewer, "bilal"); |
| ... | @@ -5855,7 +5905,7 @@ it.effect("keeps the diff cached across a file being ticked off", () => | ... | @@ -5855,7 +5905,7 @@ it.effect("keeps the diff cached across a file being ticked off", () => |
| 5855 | project({ id: "p1", title: "t3code", workspaceRoot: "/a", repository: "pingdotgg/t3code" }), | 5905 | project({ id: "p1", title: "t3code", workspaceRoot: "/a", repository: "pingdotgg/t3code" }), |
| 5856 | ], | 5906 | ], |
| 5857 | providers: [ | 5907 | providers: [ |
| 5858 | fakeProvider("github", { | 5908 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 5859 | capabilities: { | 5909 | capabilities: { |
| 5860 | diff: true, | 5910 | diff: true, |
| 5861 | comment: true, | 5911 | comment: true, |
| ... | @@ -5912,7 +5962,7 @@ it.effect("returns large diff slices intact without retaining them in either cac | ... | @@ -5912,7 +5962,7 @@ it.effect("returns large diff slices intact without retaining them in either cac |
| 5912 | const service = yield* makeService({ | 5962 | const service = yield* makeService({ |
| 5913 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], | 5963 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 5914 | providers: [ | 5964 | providers: [ |
| 5915 | fakeProvider("github", { | 5965 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 5916 | getDiff: () => | 5966 | getDiff: () => |
| 5917 | Effect.sync(() => { | 5967 | Effect.sync(() => { |
| 5918 | reads += 1; | 5968 | reads += 1; |
| ... | @@ -5950,7 +6000,7 @@ it.effect("caches a small replacement after releasing a large diff", () => | ... | @@ -5950,7 +6000,7 @@ it.effect("caches a small replacement after releasing a large diff", () => |
| 5950 | const service = yield* makeService({ | 6000 | const service = yield* makeService({ |
| 5951 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], | 6001 | projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], |
| 5952 | providers: [ | 6002 | providers: [ |
| 5953 | fakeProvider("github", { | 6003 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 5954 | getDiff: () => | 6004 | getDiff: () => |
| 5955 | Effect.sync(() => { | 6005 | Effect.sync(() => { |
| 5956 | reads += 1; | 6006 | reads += 1; |
| ... | @@ -5976,7 +6026,7 @@ const environmentViewedProvider = ( | ... | @@ -5976,7 +6026,7 @@ const environmentViewedProvider = ( |
| 5976 | asked: Array<ReadonlyArray<string>>, | 6026 | asked: Array<ReadonlyArray<string>>, |
| 5977 | unreadable: ReadonlySet<string> = new Set(), | 6027 | unreadable: ReadonlySet<string> = new Set(), |
| 5978 | ) => | 6028 | ) => |
| 5979 | fakeProvider("gitlab", { | 6029 | fakeProvider(SourceControlProviderKind.make("gitlab"), { |
| 5980 | capabilities: { | 6030 | capabilities: { |
| 5981 | diff: true, | 6031 | diff: true, |
| 5982 | comment: true, | 6032 | comment: true, |
| ... | @@ -6018,7 +6068,7 @@ const environmentViewedService = ( | ... | @@ -6018,7 +6068,7 @@ const environmentViewedService = ( |
| 6018 | title: "on gitlab", | 6068 | title: "on gitlab", |
| 6019 | workspaceRoot: "/a", | 6069 | workspaceRoot: "/a", |
| 6020 | repository: "group/project", | 6070 | repository: "group/project", |
| 6021 | provider: "gitlab", | 6071 | provider: SourceControlProviderKind.make("gitlab"), |
| 6022 | }), | 6072 | }), |
| 6023 | ], | 6073 | ], |
| 6024 | providers: [environmentViewedProvider(revisions, asked, unreadable)], | 6074 | providers: [environmentViewedProvider(revisions, asked, unreadable)], |
| ... | @@ -6083,7 +6133,7 @@ it.effect("tracks Forgejo viewed files through its diff and refuses truncated ba | ... | @@ -6083,7 +6133,7 @@ it.effect("tracks Forgejo viewed files through its diff and refuses truncated ba |
| 6083 | title: "on forgejo", | 6133 | title: "on forgejo", |
| 6084 | workspaceRoot: "/forgejo", | 6134 | workspaceRoot: "/forgejo", |
| 6085 | repository: "reviewer/project", | 6135 | repository: "reviewer/project", |
| 6086 | provider: "forgejo", | 6136 | provider: SourceControlProviderKind.make("forgejo"), |
| 6087 | host: "forge.example:3000", | 6137 | host: "forge.example:3000", |
| 6088 | remoteUrl: "http://forge.example:3000/reviewer/project.git", | 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,7 +6195,7 @@ it.effect("keeps hosted Forgejo marks with their repository instead of the servi |
| 6145 | title: "first repository", | 6195 | title: "first repository", |
| 6146 | workspaceRoot: "/first", | 6196 | workspaceRoot: "/first", |
| 6147 | repository: "reviewer/first", | 6197 | repository: "reviewer/first", |
| 6148 | provider: "forgejo", | 6198 | provider: SourceControlProviderKind.make("forgejo"), |
| 6149 | host: "forge.example", | 6199 | host: "forge.example", |
| 6150 | remoteUrl: "https://forge.example:3000/reviewer/first.git", | 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,13 +6203,20 @@ it.effect("keeps hosted Forgejo marks with their repository instead of the servi |
| 6153 | const service = yield* makeService({ | 6203 | const service = yield* makeService({ |
| 6154 | projects, | 6204 | projects, |
| 6155 | providers: [ | 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 | resolveHandle: ({ context }) => | 6211 | resolveHandle: ({ context }) => |
| 6159 | Effect.succeed({ | 6212 | Effect.succeed({ |
| 6160 | context: { | 6213 | context: { |
| 6161 | ...context!, | 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 | provider: undefined as never, | 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,7 +6243,7 @@ it.effect("keeps hosted Forgejo marks with their repository instead of the servi |
| 6186 | title: "second repository", | 6243 | title: "second repository", |
| 6187 | workspaceRoot: "/second", | 6244 | workspaceRoot: "/second", |
| 6188 | repository: "reviewer/second", | 6245 | repository: "reviewer/second", |
| 6189 | provider: "forgejo", | 6246 | provider: SourceControlProviderKind.make("forgejo"), |
| 6190 | host: "forge.example", | 6247 | host: "forge.example", |
| 6191 | remoteUrl: "https://forge.example:3000/reviewer/second.git", | 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,7 +6258,7 @@ it.effect("keeps hosted Forgejo marks with their repository instead of the servi |
| 6201 | title: "second repository over SSH", | 6258 | title: "second repository over SSH", |
| 6202 | workspaceRoot: "/ssh", | 6259 | workspaceRoot: "/ssh", |
| 6203 | repository: "reviewer/second", | 6260 | repository: "reviewer/second", |
| 6204 | provider: "forgejo", | 6261 | provider: SourceControlProviderKind.make("forgejo"), |
| 6205 | host: "ssh.forge.example", | 6262 | host: "ssh.forge.example", |
| 6206 | remoteUrl: "git@ssh.forge.example:reviewer/second.git", | 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,7 +6326,7 @@ it.effect("holds what a whole-change answer carried, so the next tick reads noth |
| 6269 | title: "on gitlab", | 6326 | title: "on gitlab", |
| 6270 | workspaceRoot: "/a", | 6327 | workspaceRoot: "/a", |
| 6271 | repository: "group/project", | 6328 | repository: "group/project", |
| 6272 | provider: "gitlab", | 6329 | provider: SourceControlProviderKind.make("gitlab"), |
| 6273 | }), | 6330 | }), |
| 6274 | ], | 6331 | ], |
| 6275 | providers: [ | 6332 | providers: [ |
| ... | @@ -6643,11 +6700,11 @@ it.effect("still reports its own marks when the host will not say what the head | ... | @@ -6643,11 +6700,11 @@ it.effect("still reports its own marks when the host will not say what the head |
| 6643 | title: "on gitlab", | 6700 | title: "on gitlab", |
| 6644 | workspaceRoot: "/a", | 6701 | workspaceRoot: "/a", |
| 6645 | repository: "group/project", | 6702 | repository: "group/project", |
| 6646 | provider: "gitlab", | 6703 | provider: SourceControlProviderKind.make("gitlab"), |
| 6647 | }), | 6704 | }), |
| 6648 | ], | 6705 | ], |
| 6649 | providers: [ | 6706 | providers: [ |
| 6650 | fakeProvider("gitlab", { | 6707 | fakeProvider(SourceControlProviderKind.make("gitlab"), { |
| 6651 | capabilities: { | 6708 | capabilities: { |
| 6652 | diff: true, | 6709 | diff: true, |
| 6653 | comment: true, | 6710 | comment: true, |
| ... | @@ -6699,11 +6756,11 @@ it.effect("finishes two presses on one file in the order they were made", () => | ... | @@ -6699,11 +6756,11 @@ it.effect("finishes two presses on one file in the order they were made", () => |
| 6699 | title: "on gitlab", | 6756 | title: "on gitlab", |
| 6700 | workspaceRoot: "/a", | 6757 | workspaceRoot: "/a", |
| 6701 | repository: "group/project", | 6758 | repository: "group/project", |
| 6702 | provider: "gitlab", | 6759 | provider: SourceControlProviderKind.make("gitlab"), |
| 6703 | }), | 6760 | }), |
| 6704 | ], | 6761 | ], |
| 6705 | providers: [ | 6762 | providers: [ |
| 6706 | fakeProvider("gitlab", { | 6763 | fakeProvider(SourceControlProviderKind.make("gitlab"), { |
| 6707 | capabilities: { | 6764 | capabilities: { |
| 6708 | diff: true, | 6765 | diff: true, |
| 6709 | comment: true, | 6766 | comment: true, |
| ... | @@ -6755,7 +6812,7 @@ const azureViewedService = (projects: ReadonlyArray<OrchestrationProjectShell>) | ... | @@ -6755,7 +6812,7 @@ const azureViewedService = (projects: ReadonlyArray<OrchestrationProjectShell>) |
| 6755 | makeService({ | 6812 | makeService({ |
| 6756 | projects, | 6813 | projects, |
| 6757 | providers: [ | 6814 | providers: [ |
| 6758 | fakeProvider("azure-devops", { | 6815 | fakeProvider(SourceControlProviderKind.make("azure-devops"), { |
| 6759 | capabilities: { | 6816 | capabilities: { |
| 6760 | diff: true, | 6817 | diff: true, |
| 6761 | comment: true, | 6818 | comment: true, |
| ... | @@ -6781,7 +6838,7 @@ const AZURE_PAIR = [ | ... | @@ -6781,7 +6838,7 @@ const AZURE_PAIR = [ |
| 6781 | title: "platform web", | 6838 | title: "platform web", |
| 6782 | workspaceRoot: "/a", | 6839 | workspaceRoot: "/a", |
| 6783 | repository: "acme/platform/_git/web", | 6840 | repository: "acme/platform/_git/web", |
| 6784 | provider: "azure-devops", | 6841 | provider: SourceControlProviderKind.make("azure-devops"), |
| 6785 | host: "dev.azure.com", | 6842 | host: "dev.azure.com", |
| 6786 | }), | 6843 | }), |
| 6787 | project({ | 6844 | project({ |
| ... | @@ -6789,7 +6846,7 @@ const AZURE_PAIR = [ | ... | @@ -6789,7 +6846,7 @@ const AZURE_PAIR = [ |
| 6789 | title: "other web", | 6846 | title: "other web", |
| 6790 | workspaceRoot: "/b", | 6847 | workspaceRoot: "/b", |
| 6791 | repository: "acme/other/_git/web", | 6848 | repository: "acme/other/_git/web", |
| 6792 | provider: "azure-devops", | 6849 | provider: SourceControlProviderKind.make("azure-devops"), |
| 6793 | host: "dev.azure.com", | 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,7 +6937,7 @@ it.effect("keeps the marked paths when a whole-change answer is wider than the c |
| 6880 | title: "on gitlab", | 6937 | title: "on gitlab", |
| 6881 | workspaceRoot: "/a", | 6938 | workspaceRoot: "/a", |
| 6882 | repository: "group/project", | 6939 | repository: "group/project", |
| 6883 | provider: "gitlab", | 6940 | provider: SourceControlProviderKind.make("gitlab"), |
| 6884 | }), | 6941 | }), |
| 6885 | ], | 6942 | ], |
| 6886 | providers: [ | 6943 | providers: [ |
| ... | @@ -6949,7 +7006,7 @@ const environmentViewedServiceWithViewer = ( | ... | @@ -6949,7 +7006,7 @@ const environmentViewedServiceWithViewer = ( |
| 6949 | title: "on gitlab", | 7006 | title: "on gitlab", |
| 6950 | workspaceRoot: "/a", | 7007 | workspaceRoot: "/a", |
| 6951 | repository: "group/project", | 7008 | repository: "group/project", |
| 6952 | provider: "gitlab", | 7009 | provider: SourceControlProviderKind.make("gitlab"), |
| 6953 | }), | 7010 | }), |
| 6954 | ], | 7011 | ], |
| 6955 | providers: [{ ...environmentViewedProvider(revisions, []), getViewer }], | 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,7 +7040,7 @@ it.effect("puts a listing and a press for one host on a single viewer lookup", ( |
| 6983 | title: "on gitlab", | 7040 | title: "on gitlab", |
| 6984 | workspaceRoot: "/a", | 7041 | workspaceRoot: "/a", |
| 6985 | repository: "group/project", | 7042 | repository: "group/project", |
| 6986 | provider: "gitlab", | 7043 | provider: SourceControlProviderKind.make("gitlab"), |
| 6987 | }), | 7044 | }), |
| 6988 | ], | 7045 | ], |
| 6989 | providers: [ | 7046 | providers: [ |
| ... | @@ -7048,7 +7105,7 @@ it.effect("records a press while the host is backing off", () => | ... | @@ -7048,7 +7105,7 @@ it.effect("records a press while the host is backing off", () => |
| 7048 | title: "on gitlab", | 7105 | title: "on gitlab", |
| 7049 | workspaceRoot: "/a", | 7106 | workspaceRoot: "/a", |
| 7050 | repository: "group/project", | 7107 | repository: "group/project", |
| 7051 | provider: "gitlab", | 7108 | provider: SourceControlProviderKind.make("gitlab"), |
| 7052 | }), | 7109 | }), |
| 7053 | ], | 7110 | ], |
| 7054 | providers: [ | 7111 | providers: [ |
| ... | @@ -7064,7 +7121,7 @@ it.effect("records a press while the host is backing off", () => | ... | @@ -7064,7 +7121,7 @@ it.effect("records a press while the host is backing off", () => |
| 7064 | getFileRevisions: () => | 7121 | getFileRevisions: () => |
| 7065 | Effect.fail( | 7122 | Effect.fail( |
| 7066 | new PullRequestProviderError({ | 7123 | new PullRequestProviderError({ |
| 7067 | provider: "gitlab", | 7124 | provider: SourceControlProviderKind.make("gitlab"), |
| 7068 | operation: "getFileRevisions", | 7125 | operation: "getFileRevisions", |
| 7069 | reason: "rate-limited", | 7126 | reason: "rate-limited", |
| 7070 | detail: "API rate limit exceeded.", | 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,7 +7168,7 @@ it.effect("asks who is reading through a pause only for the press that is waitin |
| 7111 | title: "on gitlab", | 7168 | title: "on gitlab", |
| 7112 | workspaceRoot: "/a", | 7169 | workspaceRoot: "/a", |
| 7113 | repository: "group/project", | 7170 | repository: "group/project", |
| 7114 | provider: "gitlab", | 7171 | provider: SourceControlProviderKind.make("gitlab"), |
| 7115 | }), | 7172 | }), |
| 7116 | ], | 7173 | ], |
| 7117 | providers: [ | 7174 | providers: [ |
| ... | @@ -7129,7 +7186,7 @@ it.effect("asks who is reading through a pause only for the press that is waitin | ... | @@ -7129,7 +7186,7 @@ it.effect("asks who is reading through a pause only for the press that is waitin |
| 7129 | getFileRevisions: () => | 7186 | getFileRevisions: () => |
| 7130 | Effect.fail( | 7187 | Effect.fail( |
| 7131 | new PullRequestProviderError({ | 7188 | new PullRequestProviderError({ |
| 7132 | provider: "gitlab", | 7189 | provider: SourceControlProviderKind.make("gitlab"), |
| 7133 | operation: "getFileRevisions", | 7190 | operation: "getFileRevisions", |
| 7134 | reason: "rate-limited", | 7191 | reason: "rate-limited", |
| 7135 | detail: "API rate limit exceeded.", | 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,7 +7231,7 @@ it.effect("does not ask a paused host who is reading again after the ask failed" |
| 7174 | title: "on gitlab", | 7231 | title: "on gitlab", |
| 7175 | workspaceRoot: "/a", | 7232 | workspaceRoot: "/a", |
| 7176 | repository: "group/project", | 7233 | repository: "group/project", |
| 7177 | provider: "gitlab", | 7234 | provider: SourceControlProviderKind.make("gitlab"), |
| 7178 | }), | 7235 | }), |
| 7179 | ], | 7236 | ], |
| 7180 | providers: [ | 7237 | providers: [ |
| ... | @@ -7185,7 +7242,7 @@ it.effect("does not ask a paused host who is reading again after the ask failed" | ... | @@ -7185,7 +7242,7 @@ it.effect("does not ask a paused host who is reading again after the ask failed" |
| 7185 | viewerLookups += 1; | 7242 | viewerLookups += 1; |
| 7186 | return Effect.fail( | 7243 | return Effect.fail( |
| 7187 | new PullRequestProviderError({ | 7244 | new PullRequestProviderError({ |
| 7188 | provider: "gitlab", | 7245 | provider: SourceControlProviderKind.make("gitlab"), |
| 7189 | operation: "getViewer", | 7246 | operation: "getViewer", |
| 7190 | reason: "failed", | 7247 | reason: "failed", |
| 7191 | detail: "glab exited with status 1", | 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,7 +7254,7 @@ it.effect("does not ask a paused host who is reading again after the ask failed" |
| 7197 | runAction: () => | 7254 | runAction: () => |
| 7198 | Effect.fail( | 7255 | Effect.fail( |
| 7199 | new PullRequestProviderError({ | 7256 | new PullRequestProviderError({ |
| 7200 | provider: "gitlab", | 7257 | provider: SourceControlProviderKind.make("gitlab"), |
| 7201 | operation: "runAction", | 7258 | operation: "runAction", |
| 7202 | reason: "rate-limited", | 7259 | reason: "rate-limited", |
| 7203 | detail: "API rate limit exceeded.", | 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,7 +7288,7 @@ it.effect("refuses the marks when the host could not be asked who is reading", ( |
| 7231 | ? Effect.succeed("bilal") | 7288 | ? Effect.succeed("bilal") |
| 7232 | : Effect.fail( | 7289 | : Effect.fail( |
| 7233 | new PullRequestProviderError({ | 7290 | new PullRequestProviderError({ |
| 7234 | provider: "gitlab", | 7291 | provider: SourceControlProviderKind.make("gitlab"), |
| 7235 | operation: "getViewer", | 7292 | operation: "getViewer", |
| 7236 | reason: "failed", | 7293 | reason: "failed", |
| 7237 | detail: "glab exited with status 1", | 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,11 +7333,11 @@ it.effect("refuses to track viewed files on a host that does not", () => |
| 7276 | title: "on gitlab", | 7333 | title: "on gitlab", |
| 7277 | workspaceRoot: "/a", | 7334 | workspaceRoot: "/a", |
| 7278 | repository: "group/project", | 7335 | repository: "group/project", |
| 7279 | provider: "gitlab", | 7336 | provider: SourceControlProviderKind.make("gitlab"), |
| 7280 | }), | 7337 | }), |
| 7281 | ], | 7338 | ], |
| 7282 | providers: [ | 7339 | providers: [ |
| 7283 | fakeProvider("gitlab", { | 7340 | fakeProvider(SourceControlProviderKind.make("gitlab"), { |
| 7284 | getFilesViewed: () => Effect.die("must not be called"), | 7341 | getFilesViewed: () => Effect.die("must not be called"), |
| 7285 | setFilesViewed: () => Effect.die("must not be called"), | 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,12 +7365,12 @@ it.effect("keeps Azure continuation cursors separate for repositories with the s |
| 7308 | title: organization, | 7365 | title: organization, |
| 7309 | workspaceRoot: `/${organization}`, | 7366 | workspaceRoot: `/${organization}`, |
| 7310 | repository: `${organization}/project/_git/web`, | 7367 | repository: `${organization}/project/_git/web`, |
| 7311 | provider: "azure-devops", | 7368 | provider: SourceControlProviderKind.make("azure-devops"), |
| 7312 | host: "dev.azure.com", | 7369 | host: "dev.azure.com", |
| 7313 | }), | 7370 | }), |
| 7314 | ), | 7371 | ), |
| 7315 | providers: [ | 7372 | providers: [ |
| 7316 | fakeProvider("azure-devops", { | 7373 | fakeProvider(SourceControlProviderKind.make("azure-devops"), { |
| 7317 | listChangeRequests: (input) => | 7374 | listChangeRequests: (input) => |
| 7318 | Effect.sync(() => { | 7375 | Effect.sync(() => { |
| 7319 | seen.push(input.cwd); | 7376 | seen.push(input.cwd); |
| ... | @@ -7363,9 +7420,9 @@ it.effect.each([ | ... | @@ -7363,9 +7420,9 @@ it.effect.each([ |
| 7363 | }, | 7420 | }, |
| 7364 | }, | 7421 | }, |
| 7365 | providers: [ | 7422 | providers: [ |
| 7366 | fakeProvider("github", { | 7423 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 7367 | capabilities: { | 7424 | capabilities: { |
| 7368 | ...fakeProvider("github").capabilities, | 7425 | ...fakeProvider(SourceControlProviderKind.make("github")).capabilities, |
| 7369 | actions: ["merge", "enable-auto-merge"], | 7426 | actions: ["merge", "enable-auto-merge"], |
| 7370 | }, | 7427 | }, |
| 7371 | getChangeRequestSummary: () => Effect.succeed(changeRequest(1, "2026-07-02T00:00:00Z")), | 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,7 +72,7 @@ import { |
| 72 | type PullRequestThreadCommentsResult, | 72 | type PullRequestThreadCommentsResult, |
| 73 | type PullRequestUpdateInput, | 73 | type PullRequestUpdateInput, |
| 74 | type SourceControlProviderInfo, | 74 | type SourceControlProviderInfo, |
| 75 | type SourceControlProviderKind, | 75 | SourceControlProviderKind, |
| 76 | type ThreadPullRequestKey, | 76 | type ThreadPullRequestKey, |
| 77 | } from "@t3tools/contracts"; | 77 | } from "@t3tools/contracts"; |
| 78 | import { resolveProjectSettings } from "@t3tools/shared/projectSettings"; | 78 | import { resolveProjectSettings } from "@t3tools/shared/projectSettings"; |
| ... | @@ -736,14 +736,15 @@ export const make = Effect.gen(function* () { | ... | @@ -736,14 +736,15 @@ export const make = Effect.gen(function* () { |
| 736 | sourceControlRepositorySelector(project.repositoryIdentity) === null | 736 | sourceControlRepositorySelector(project.repositoryIdentity) === null |
| 737 | ) | 737 | ) |
| 738 | continue; | 738 | continue; |
| 739 | const host = pullRequestHostOf(identity, "unknown"); | 739 | const host = pullRequestHostOf(identity, SourceControlProviderKind.make("unknown")); |
| 740 | // A legacy identity has no canonical host until its provider is refined, so it must reach | 740 | // A legacy identity has no canonical host until its provider is refined, so it must reach |
| 741 | // the refinement before a host filter can decide whether it belongs in the result. | 741 | // the refinement before a host filter can decide whether it belongs in the result. |
| 742 | if ( | 742 | if ( |
| 743 | filter.host !== undefined && | 743 | filter.host !== undefined && |
| 744 | host !== "unknown" && | 744 | host !== "unknown" && |
| 745 | host !== filter.host.toLowerCase() && | 745 | host !== filter.host.toLowerCase() && |
| 746 | pullRequestHostOf(identity, "forgejo") !== filter.host.toLowerCase() && | 746 | pullRequestHostOf(identity, SourceControlProviderKind.make("forgejo")) !== |
| 747 | filter.host.toLowerCase() && | ||
| 747 | !isSshRemoteUrl(identity.locator.remoteUrl) | 748 | !isSshRemoteUrl(identity.locator.remoteUrl) |
| 748 | ) { | 749 | ) { |
| 749 | continue; | 750 | continue; |
| ... | @@ -768,7 +769,9 @@ export const make = Effect.gen(function* () { | ... | @@ -768,7 +769,9 @@ export const make = Effect.gen(function* () { |
| 768 | cwd: project.workspaceRoot, | 769 | cwd: project.workspaceRoot, |
| 769 | context: { | 770 | context: { |
| 770 | provider: | 771 | provider: |
| 771 | provider.kind === "forgejo" ? { ...provider, kind: "unknown" } : provider, | 772 | provider.kind === "forgejo" |
| 773 | ? { ...provider, kind: SourceControlProviderKind.make("unknown") } | ||
| 774 | : provider, | ||
| 772 | remoteName, | 775 | remoteName, |
| 773 | remoteUrl, | 776 | remoteUrl, |
| 774 | ...(filter.host !== undefined && isSshRemoteUrl(remoteUrl) | 777 | ...(filter.host !== undefined && isSshRemoteUrl(remoteUrl) |
apps/server/src/pullRequest/pullRequestSyncKey.ts+3-2| ... | @@ -2,7 +2,7 @@ import { | ... | @@ -2,7 +2,7 @@ import { |
| 2 | pullRequestHostOf, | 2 | pullRequestHostOf, |
| 3 | type PullRequestRef, | 3 | type PullRequestRef, |
| 4 | type RepositoryIdentity, | 4 | type RepositoryIdentity, |
| 5 | type SourceControlProviderKind, | 5 | SourceControlProviderKind, |
| 6 | type ThreadPullRequestKey, | 6 | type ThreadPullRequestKey, |
| 7 | } from "@t3tools/contracts"; | 7 | } from "@t3tools/contracts"; |
| 8 | import { sourceControlRepositorySelector } from "@t3tools/shared/sourceControl"; | 8 | import { sourceControlRepositorySelector } from "@t3tools/shared/sourceControl"; |
| ... | @@ -18,7 +18,8 @@ export function pullRequestSyncKey( | ... | @@ -18,7 +18,8 @@ export function pullRequestSyncKey( |
| 18 | reference.repository.toLowerCase() !== | 18 | reference.repository.toLowerCase() !== |
| 19 | sourceControlRepositorySelector(identity)?.toLowerCase() || | 19 | sourceControlRepositorySelector(identity)?.toLowerCase() || |
| 20 | (reference.host !== undefined && | 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 | return null; | 24 | return null; |
| 24 | const [host, ...repository] = identity.canonicalKey.split("/"); | 25 | const [host, ...repository] = identity.canonicalKey.split("/"); |
apps/server/src/sourceControl/BitbucketApi.test.ts+8-3| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | ||
| 1 | import { assert, it, vi } from "@effect/vitest"; | 2 | import { assert, it, vi } from "@effect/vitest"; |
| 2 | import * as NodeServices from "@effect/platform-node/NodeServices"; | 3 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 3 | import * as ConfigProvider from "effect/ConfigProvider"; | 4 | import * as ConfigProvider from "effect/ConfigProvider"; |
| ... | @@ -362,7 +363,7 @@ it.effect.each([false, true])( | ... | @@ -362,7 +363,7 @@ it.effect.each([false, true])( |
| 362 | ? { | 363 | ? { |
| 363 | context: { | 364 | context: { |
| 364 | provider: { | 365 | provider: { |
| 365 | kind: "bitbucket" as const, | 366 | kind: SourceControlProviderKind.make("bitbucket"), |
| 366 | name: "Bitbucket", | 367 | name: "Bitbucket", |
| 367 | baseUrl: "https://bitbucket.org", | 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,7 +397,11 @@ it.effect("prefers an explicit repository and uses context when the repository i |
| 396 | return Effect.gen(function* () { | 397 | return Effect.gen(function* () { |
| 397 | const bitbucket = yield* BitbucketApi.BitbucketApi; | 398 | const bitbucket = yield* BitbucketApi.BitbucketApi; |
| 398 | const context = { | 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 | remoteName: "origin", | 405 | remoteName: "origin", |
| 401 | remoteUrl: "git@bitbucket.org:another/context.git", | 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,7 +843,7 @@ it.effect("checks out same-repository pull requests with the existing Bitbucket |
| 838 | cwd: "/repo", | 843 | cwd: "/repo", |
| 839 | context: { | 844 | context: { |
| 840 | provider: { | 845 | provider: { |
| 841 | kind: "bitbucket", | 846 | kind: SourceControlProviderKind.make("bitbucket"), |
| 842 | name: "Bitbucket", | 847 | name: "Bitbucket", |
| 843 | baseUrl: "https://bitbucket.org", | 848 | baseUrl: "https://bitbucket.org", |
| 844 | }, | 849 | }, |
apps/server/src/sourceControl/SourceControlDiscovery.test.ts+18-13| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | ||
| 1 | import { assert, it } from "@effect/vitest"; | 2 | import { assert, it } from "@effect/vitest"; |
| 2 | import * as NodeServices from "@effect/platform-node/NodeServices"; | 3 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 3 | import * as Effect from "effect/Effect"; | 4 | import * as Effect from "effect/Effect"; |
| ... | @@ -176,37 +177,37 @@ it.effect("reports implemented tools separately from locally available executabl | ... | @@ -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 | status: "available", | 181 | status: "available", |
| 181 | auth: "authenticated", | 182 | auth: "authenticated", |
| 182 | account: Option.some("juliusmarminge"), | 183 | account: Option.some("juliusmarminge"), |
| 183 | }, | 184 | }, |
| 184 | { | 185 | { |
| 185 | kind: "gitlab", | 186 | kind: SourceControlProviderKind.make("gitlab"), |
| 186 | status: "missing", | 187 | status: "missing", |
| 187 | auth: "unknown", | 188 | auth: "unknown", |
| 188 | account: Option.none(), | 189 | account: Option.none(), |
| 189 | }, | 190 | }, |
| 190 | { | 191 | { |
| 191 | kind: "azure-devops", | 192 | kind: SourceControlProviderKind.make("azure-devops"), |
| 192 | status: "missing", | 193 | status: "missing", |
| 193 | auth: "unknown", | 194 | auth: "unknown", |
| 194 | account: Option.none(), | 195 | account: Option.none(), |
| 195 | }, | 196 | }, |
| 196 | { | 197 | { |
| 197 | kind: "bitbucket", | 198 | kind: SourceControlProviderKind.make("bitbucket"), |
| 198 | status: "available", | 199 | status: "available", |
| 199 | auth: "unauthenticated", | 200 | auth: "unauthenticated", |
| 200 | account: Option.none(), | 201 | account: Option.none(), |
| 201 | }, | 202 | }, |
| 202 | { | 203 | { |
| 203 | kind: "forgejo", | 204 | kind: SourceControlProviderKind.make("forgejo"), |
| 204 | status: "missing", | 205 | status: "missing", |
| 205 | auth: "unknown", | 206 | auth: "unknown", |
| 206 | account: Option.none(), | 207 | account: Option.none(), |
| 207 | }, | 208 | }, |
| 208 | { | 209 | { |
| 209 | kind: "gitcafe", | 210 | kind: SourceControlProviderKind.make("gitcafe"), |
| 210 | status: "missing", | 211 | status: "missing", |
| 211 | auth: "unknown", | 212 | auth: "unknown", |
| 212 | account: Option.none(), | 213 | account: Option.none(), |
| ... | @@ -327,37 +328,37 @@ Logged in to gitlab.com as gitlab-user | ... | @@ -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 | auth: "authenticated", | 332 | auth: "authenticated", |
| 332 | account: Option.some("octocat"), | 333 | account: Option.some("octocat"), |
| 333 | detail: Option.none(), | 334 | detail: Option.none(), |
| 334 | }, | 335 | }, |
| 335 | { | 336 | { |
| 336 | kind: "gitlab", | 337 | kind: SourceControlProviderKind.make("gitlab"), |
| 337 | auth: "authenticated", | 338 | auth: "authenticated", |
| 338 | account: Option.some("gitlab-user"), | 339 | account: Option.some("gitlab-user"), |
| 339 | detail: Option.none(), | 340 | detail: Option.none(), |
| 340 | }, | 341 | }, |
| 341 | { | 342 | { |
| 342 | kind: "azure-devops", | 343 | kind: SourceControlProviderKind.make("azure-devops"), |
| 343 | auth: "authenticated", | 344 | auth: "authenticated", |
| 344 | account: Option.some("azure-user@example.com"), | 345 | account: Option.some("azure-user@example.com"), |
| 345 | detail: Option.none(), | 346 | detail: Option.none(), |
| 346 | }, | 347 | }, |
| 347 | { | 348 | { |
| 348 | kind: "bitbucket", | 349 | kind: SourceControlProviderKind.make("bitbucket"), |
| 349 | auth: "authenticated", | 350 | auth: "authenticated", |
| 350 | account: Option.some("bitbucket-user"), | 351 | account: Option.some("bitbucket-user"), |
| 351 | detail: Option.none(), | 352 | detail: Option.none(), |
| 352 | }, | 353 | }, |
| 353 | { | 354 | { |
| 354 | kind: "forgejo", | 355 | kind: SourceControlProviderKind.make("forgejo"), |
| 355 | auth: "authenticated", | 356 | auth: "authenticated", |
| 356 | account: Option.some("forgejo-user"), | 357 | account: Option.some("forgejo-user"), |
| 357 | detail: Option.none(), | 358 | detail: Option.none(), |
| 358 | }, | 359 | }, |
| 359 | { | 360 | { |
| 360 | kind: "gitcafe", | 361 | kind: SourceControlProviderKind.make("gitcafe"), |
| 361 | auth: "authenticated", | 362 | auth: "authenticated", |
| 362 | account: Option.some("cafe-user"), | 363 | account: Option.some("cafe-user"), |
| 363 | detail: Option.none(), | 364 | detail: Option.none(), |
| ... | @@ -528,7 +529,11 @@ it.effect( | ... | @@ -528,7 +529,11 @@ it.effect( |
| 528 | reference: "42", | 529 | reference: "42", |
| 529 | force: true, | 530 | force: true, |
| 530 | context: { | 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 | remoteName: "origin", | 537 | remoteName: "origin", |
| 533 | remoteUrl: "git@forgejo.test:maria/project.git", | 538 | remoteUrl: "git@forgejo.test:maria/project.git", |
| 534 | }, | 539 | }, |
apps/server/src/sourceControl/SourceControlProviderRegistry.test.ts+6-5| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | ||
| 1 | import { assert, it } from "@effect/vitest"; | 2 | import { assert, it } from "@effect/vitest"; |
| 2 | import * as NodeServices from "@effect/platform-node/NodeServices"; | 3 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 3 | import * as DateTime from "effect/DateTime"; | 4 | import * as DateTime from "effect/DateTime"; |
| ... | @@ -154,7 +155,7 @@ it.effect("routes directly by provider kind for remote-first workflows", () => | ... | @@ -154,7 +155,7 @@ it.effect("routes directly by provider kind for remote-first workflows", () => |
| 154 | remotes: [], | 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 | assert.strictEqual(provider.kind, "github"); | 160 | assert.strictEqual(provider.kind, "github"); |
| 160 | }), | 161 | }), |
| ... | @@ -163,7 +164,7 @@ it.effect("routes directly by provider kind for remote-first workflows", () => | ... | @@ -163,7 +164,7 @@ it.effect("routes directly by provider kind for remote-first workflows", () => |
| 163 | it.effect("includes the request cwd when an unregistered provider is used", () => | 164 | it.effect("includes the request cwd when an unregistered provider is used", () => |
| 164 | Effect.gen(function* () { | 165 | Effect.gen(function* () { |
| 165 | const registry = yield* makeRegistry({ remotes: [] }); | 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 | const error = yield* provider | 169 | const error = yield* provider |
| 169 | .getChangeRequest({ cwd: "/repo", reference: "#42" }) | 170 | .getChangeRequest({ cwd: "/repo", reference: "#42" }) |
| ... | @@ -257,7 +258,7 @@ it.effect("refines the caller-selected remote instead of choosing another config | ... | @@ -257,7 +258,7 @@ it.effect("refines the caller-selected remote instead of choosing another config |
| 257 | cwd: "/repo", | 258 | cwd: "/repo", |
| 258 | context: { | 259 | context: { |
| 259 | provider: { | 260 | provider: { |
| 260 | kind: "unknown", | 261 | kind: SourceControlProviderKind.make("unknown"), |
| 261 | name: "self-hosted.example.test", | 262 | name: "self-hosted.example.test", |
| 262 | baseUrl: "https://self-hosted.example.test", | 263 | baseUrl: "https://self-hosted.example.test", |
| 263 | }, | 264 | }, |
| ... | @@ -412,7 +413,7 @@ it.effect.each([ | ... | @@ -412,7 +413,7 @@ it.effect.each([ |
| 412 | }, | 413 | }, |
| 413 | }); | 414 | }); |
| 414 | const handle = yield* registry.resolveHandle({ cwd: "/repo" }); | 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 | assert.strictEqual(handle.context?.provider.baseUrl, "https://code.example.test"); | 417 | assert.strictEqual(handle.context?.provider.baseUrl, "https://code.example.test"); |
| 417 | assert.deepStrictEqual(hosts, ["code.example.test"]); | 418 | assert.deepStrictEqual(hosts, ["code.example.test"]); |
| 418 | // Recognition goes through the credential layer; gh is only one of its sources. | 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,7 +438,7 @@ it.effect("skips GitHub discovery for the identity resolver's empty base URL", ( |
| 437 | }, | 438 | }, |
| 438 | }); | 439 | }); |
| 439 | const context = { | 440 | const context = { |
| 440 | provider: { kind: "unknown" as const, name: "Unknown", baseUrl: "" }, | 441 | provider: { kind: SourceControlProviderKind.make("unknown"), name: "Unknown", baseUrl: "" }, |
| 441 | remoteName: "origin", | 442 | remoteName: "origin", |
| 442 | remoteUrl, | 443 | remoteUrl, |
| 443 | }; | 444 | }; |
apps/server/src/sourceControl/SourceControlProviderRegistry.ts+5-4| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | ||
| 1 | import * as Cache from "effect/Cache"; | 2 | import * as Cache from "effect/Cache"; |
| 2 | import * as Context from "effect/Context"; | 3 | import * as Context from "effect/Context"; |
| 3 | import * as Duration from "effect/Duration"; | 4 | import * as Duration from "effect/Duration"; |
| ... | @@ -8,7 +9,7 @@ import { | ... | @@ -8,7 +9,7 @@ import { |
| 8 | SourceControlProviderError, | 9 | SourceControlProviderError, |
| 9 | type SourceControlProviderDiscoveryItem, | 10 | type SourceControlProviderDiscoveryItem, |
| 10 | } from "@t3tools/contracts"; | 11 | } from "@t3tools/contracts"; |
| 11 | import type { SourceControlProviderKind } from "@t3tools/contracts"; | 12 | import type {} from "@t3tools/contracts"; |
| 12 | import { detectSourceControlProviderFromRemoteUrl } from "@t3tools/shared/sourceControl"; | 13 | import { detectSourceControlProviderFromRemoteUrl } from "@t3tools/shared/sourceControl"; |
| 13 | 14 | ||
| 14 | import * as BuiltInDrivers from "./builtInDrivers.ts"; | 15 | import * as BuiltInDrivers from "./builtInDrivers.ts"; |
| ... | @@ -214,7 +215,7 @@ export const makeWithProviders = Effect.fn("makeSourceControlProviderRegistryWit | ... | @@ -214,7 +215,7 @@ export const makeWithProviders = Effect.fn("makeSourceControlProviderRegistryWit |
| 214 | Effect.mapError( | 215 | Effect.mapError( |
| 215 | (error) => | 216 | (error) => |
| 216 | new SourceControlProviderError({ | 217 | new SourceControlProviderError({ |
| 217 | provider: "unknown", | 218 | provider: SourceControlProviderKind.make("unknown"), |
| 218 | operation: "detectProvider", | 219 | operation: "detectProvider", |
| 219 | cwd, | 220 | cwd, |
| 220 | detail: "Failed to detect source control provider.", | 221 | detail: "Failed to detect source control provider.", |
| ... | @@ -226,7 +227,7 @@ export const makeWithProviders = Effect.fn("makeSourceControlProviderRegistryWit | ... | @@ -226,7 +227,7 @@ export const makeWithProviders = Effect.fn("makeSourceControlProviderRegistryWit |
| 226 | Effect.mapError( | 227 | Effect.mapError( |
| 227 | (error) => | 228 | (error) => |
| 228 | new SourceControlProviderError({ | 229 | new SourceControlProviderError({ |
| 229 | provider: "unknown", | 230 | provider: SourceControlProviderKind.make("unknown"), |
| 230 | operation: "detectProvider", | 231 | operation: "detectProvider", |
| 231 | cwd, | 232 | cwd, |
| 232 | detail: "Failed to detect source control provider.", | 233 | detail: "Failed to detect source control provider.", |
| ... | @@ -265,7 +266,7 @@ export const makeWithProviders = Effect.fn("makeSourceControlProviderRegistryWit | ... | @@ -265,7 +266,7 @@ export const makeWithProviders = Effect.fn("makeSourceControlProviderRegistryWit |
| 265 | }) | 266 | }) |
| 266 | ).pipe( | 267 | ).pipe( |
| 267 | Effect.map((context) => { | 268 | Effect.map((context) => { |
| 268 | const kind = context?.provider.kind ?? "unknown"; | 269 | const kind = context?.provider.kind ?? SourceControlProviderKind.make("unknown"); |
| 269 | const provider = providers.get(kind) ?? unsupportedProvider(kind); | 270 | const provider = providers.get(kind) ?? unsupportedProvider(kind); |
| 270 | return { | 271 | return { |
| 271 | provider: bindProviderContext(provider, context), | 272 | provider: bindProviderContext(provider, context), |
apps/server/src/sourceControl/SourceControlRepositoryService.test.ts+24-17| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | ||
| 1 | import * as NodePath from "@effect/platform-node/NodePath"; | 2 | import * as NodePath from "@effect/platform-node/NodePath"; |
| 2 | import * as NodeServices from "@effect/platform-node/NodeServices"; | 3 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 3 | import { assert, it } from "@effect/vitest"; | 4 | import { assert, it } from "@effect/vitest"; |
| ... | @@ -37,7 +38,7 @@ function makeProvider( | ... | @@ -37,7 +38,7 @@ function makeProvider( |
| 37 | >; | 38 | >; |
| 38 | 39 | ||
| 39 | return { | 40 | return { |
| 40 | kind: "github", | 41 | kind: SourceControlProviderKind.make("github"), |
| 41 | listChangeRequests: () => unsupported("listChangeRequests"), | 42 | listChangeRequests: () => unsupported("listChangeRequests"), |
| 42 | getChangeRequest: () => unsupported("getChangeRequest"), | 43 | getChangeRequest: () => unsupported("getChangeRequest"), |
| 43 | createChangeRequest: () => unsupported("createChangeRequest"), | 44 | createChangeRequest: () => unsupported("createChangeRequest"), |
| ... | @@ -114,19 +115,22 @@ it.effect("looks up repositories through the requested provider without search", | ... | @@ -114,19 +115,22 @@ it.effect("looks up repositories through the requested provider without search", |
| 114 | return Effect.gen(function* () { | 115 | return Effect.gen(function* () { |
| 115 | const service = yield* SourceControlRepositoryService.SourceControlRepositoryService; | 116 | const service = yield* SourceControlRepositoryService.SourceControlRepositoryService; |
| 116 | const result = yield* service.lookupRepository({ | 117 | const result = yield* service.lookupRepository({ |
| 117 | provider: "github", | 118 | provider: SourceControlProviderKind.make("github"), |
| 118 | repository: "octocat/t3code", | 119 | repository: "octocat/t3code", |
| 119 | cwd: "/workspace", | 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 | assert.deepStrictEqual(calls, [{ cwd: "/workspace", repository: "octocat/t3code" }]); | 127 | assert.deepStrictEqual(calls, [{ cwd: "/workspace", repository: "octocat/t3code" }]); |
| 124 | }).pipe(Effect.provide(layer({ provider }))); | 128 | }).pipe(Effect.provide(layer({ provider }))); |
| 125 | }); | 129 | }); |
| 126 | 130 | ||
| 127 | it.effect("preserves provider failures without deriving the repository message from them", () => { | 131 | it.effect("preserves provider failures without deriving the repository message from them", () => { |
| 128 | const providerCause = new SourceControlProviderError({ | 132 | const providerCause = new SourceControlProviderError({ |
| 129 | provider: "github", | 133 | provider: SourceControlProviderKind.make("github"), |
| 130 | operation: "getRepositoryCloneUrls", | 134 | operation: "getRepositoryCloneUrls", |
| 131 | cwd: "/workspace", | 135 | cwd: "/workspace", |
| 132 | repository: "octocat/t3code", | 136 | repository: "octocat/t3code", |
| ... | @@ -140,7 +144,7 @@ it.effect("preserves provider failures without deriving the repository message f | ... | @@ -140,7 +144,7 @@ it.effect("preserves provider failures without deriving the repository message f |
| 140 | const service = yield* SourceControlRepositoryService.SourceControlRepositoryService; | 144 | const service = yield* SourceControlRepositoryService.SourceControlRepositoryService; |
| 141 | const error = yield* Effect.flip( | 145 | const error = yield* Effect.flip( |
| 142 | service.lookupRepository({ | 146 | service.lookupRepository({ |
| 143 | provider: "github", | 147 | provider: SourceControlProviderKind.make("github"), |
| 144 | repository: "octocat/t3code", | 148 | repository: "octocat/t3code", |
| 145 | cwd: "/workspace", | 149 | cwd: "/workspace", |
| 146 | }), | 150 | }), |
| ... | @@ -176,7 +180,7 @@ it.effect( | ... | @@ -176,7 +180,7 @@ it.effect( |
| 176 | yield* Effect.gen(function* () { | 180 | yield* Effect.gen(function* () { |
| 177 | const service = yield* SourceControlRepositoryService.SourceControlRepositoryService; | 181 | const service = yield* SourceControlRepositoryService.SourceControlRepositoryService; |
| 178 | const input = { | 182 | const input = { |
| 179 | provider: "bitbucket" as const, | 183 | provider: SourceControlProviderKind.make("bitbucket"), |
| 180 | repository: locatorCause.repository, | 184 | repository: locatorCause.repository, |
| 181 | cwd: "/workspace", | 185 | cwd: "/workspace", |
| 182 | }; | 186 | }; |
| ... | @@ -204,11 +208,11 @@ it.effect( | ... | @@ -204,11 +208,11 @@ it.effect( |
| 204 | 208 | ||
| 205 | it.effect("keeps a plain Bitbucket locator lookalike out of the repository message", () => { | 209 | it.effect("keeps a plain Bitbucket locator lookalike out of the repository message", () => { |
| 206 | const provider = makeProvider({ | 210 | const provider = makeProvider({ |
| 207 | kind: "bitbucket", | 211 | kind: SourceControlProviderKind.make("bitbucket"), |
| 208 | getRepositoryCloneUrls: () => | 212 | getRepositoryCloneUrls: () => |
| 209 | Effect.fail( | 213 | Effect.fail( |
| 210 | new SourceControlProviderError({ | 214 | new SourceControlProviderError({ |
| 211 | provider: "bitbucket", | 215 | provider: SourceControlProviderKind.make("bitbucket"), |
| 212 | operation: "getRepositoryCloneUrls", | 216 | operation: "getRepositoryCloneUrls", |
| 213 | cwd: "/workspace", | 217 | cwd: "/workspace", |
| 214 | detail: "credential token abc123 was rejected", | 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,7 +228,10 @@ it.effect("keeps a plain Bitbucket locator lookalike out of the repository messa |
| 224 | return Effect.gen(function* () { | 228 | return Effect.gen(function* () { |
| 225 | const service = yield* SourceControlRepositoryService.SourceControlRepositoryService; | 229 | const service = yield* SourceControlRepositoryService.SourceControlRepositoryService; |
| 226 | const error = yield* Effect.flip( | 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 | assert.strictEqual(error.detail, "The source control operation could not be completed."); | 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,7 +257,7 @@ it.effect("keeps arbitrary Bitbucket request failures out of the repository mess |
| 250 | const service = yield* SourceControlRepositoryService.SourceControlRepositoryService; | 257 | const service = yield* SourceControlRepositoryService.SourceControlRepositoryService; |
| 251 | const error = yield* Effect.flip( | 258 | const error = yield* Effect.flip( |
| 252 | service.lookupRepository({ | 259 | service.lookupRepository({ |
| 253 | provider: "bitbucket", | 260 | provider: SourceControlProviderKind.make("bitbucket"), |
| 254 | repository: "pingdotgg/t3code", | 261 | repository: "pingdotgg/t3code", |
| 255 | cwd: "/workspace", | 262 | cwd: "/workspace", |
| 256 | }), | 263 | }), |
| ... | @@ -276,7 +283,7 @@ it.effect("clones a looked-up repository into the requested destination", () => | ... | @@ -276,7 +283,7 @@ it.effect("clones a looked-up repository into the requested destination", () => |
| 276 | yield* Effect.gen(function* () { | 283 | yield* Effect.gen(function* () { |
| 277 | const service = yield* SourceControlRepositoryService.SourceControlRepositoryService; | 284 | const service = yield* SourceControlRepositoryService.SourceControlRepositoryService; |
| 278 | const result = yield* service.cloneRepository({ | 285 | const result = yield* service.cloneRepository({ |
| 279 | provider: "github", | 286 | provider: SourceControlProviderKind.make("github"), |
| 280 | repository: "octocat/t3code", | 287 | repository: "octocat/t3code", |
| 281 | destinationPath, | 288 | destinationPath, |
| 282 | protocol: "https", | 289 | protocol: "https", |
| ... | @@ -285,7 +292,7 @@ it.effect("clones a looked-up repository into the requested destination", () => | ... | @@ -285,7 +292,7 @@ it.effect("clones a looked-up repository into the requested destination", () => |
| 285 | assert.deepStrictEqual(result, { | 292 | assert.deepStrictEqual(result, { |
| 286 | cwd: destinationPath, | 293 | cwd: destinationPath, |
| 287 | remoteUrl: CLONE_URLS.url, | 294 | remoteUrl: CLONE_URLS.url, |
| 288 | repository: { provider: "github", ...CLONE_URLS }, | 295 | repository: { provider: SourceControlProviderKind.make("github"), ...CLONE_URLS }, |
| 289 | }); | 296 | }); |
| 290 | assert.deepStrictEqual(cloneCalls, [ | 297 | assert.deepStrictEqual(cloneCalls, [ |
| 291 | { | 298 | { |
| ... | @@ -546,7 +553,7 @@ it.effect("publishes by creating the repository, adding a remote, and pushing up | ... | @@ -546,7 +553,7 @@ it.effect("publishes by creating the repository, adding a remote, and pushing up |
| 546 | const service = yield* SourceControlRepositoryService.SourceControlRepositoryService; | 553 | const service = yield* SourceControlRepositoryService.SourceControlRepositoryService; |
| 547 | const result = yield* service.publishRepository({ | 554 | const result = yield* service.publishRepository({ |
| 548 | cwd: "/workspace", | 555 | cwd: "/workspace", |
| 549 | provider: "github", | 556 | provider: SourceControlProviderKind.make("github"), |
| 550 | repository: "octocat/t3code", | 557 | repository: "octocat/t3code", |
| 551 | visibility: "private", | 558 | visibility: "private", |
| 552 | remoteName: "origin", | 559 | remoteName: "origin", |
| ... | @@ -554,7 +561,7 @@ it.effect("publishes by creating the repository, adding a remote, and pushing up | ... | @@ -554,7 +561,7 @@ it.effect("publishes by creating the repository, adding a remote, and pushing up |
| 554 | }); | 561 | }); |
| 555 | 562 | ||
| 556 | assert.deepStrictEqual(result, { | 563 | assert.deepStrictEqual(result, { |
| 557 | repository: { provider: "github", ...CLONE_URLS }, | 564 | repository: { provider: SourceControlProviderKind.make("github"), ...CLONE_URLS }, |
| 558 | remoteName: "origin", | 565 | remoteName: "origin", |
| 559 | remoteUrl: CLONE_URLS.sshUrl, | 566 | remoteUrl: CLONE_URLS.sshUrl, |
| 560 | branch: "feature/remote-v1", | 567 | branch: "feature/remote-v1", |
| ... | @@ -601,7 +608,7 @@ it.effect("publishes to the remote name returned by ensureRemote", () => { | ... | @@ -601,7 +608,7 @@ it.effect("publishes to the remote name returned by ensureRemote", () => { |
| 601 | const service = yield* SourceControlRepositoryService.SourceControlRepositoryService; | 608 | const service = yield* SourceControlRepositoryService.SourceControlRepositoryService; |
| 602 | const result = yield* service.publishRepository({ | 609 | const result = yield* service.publishRepository({ |
| 603 | cwd: "/workspace", | 610 | cwd: "/workspace", |
| 604 | provider: "github", | 611 | provider: SourceControlProviderKind.make("github"), |
| 605 | repository: "octocat/t3code", | 612 | repository: "octocat/t3code", |
| 606 | visibility: "private", | 613 | visibility: "private", |
| 607 | remoteName: "origin", | 614 | remoteName: "origin", |
| ... | @@ -637,7 +644,7 @@ it.effect("publish succeeds with status remote_added when the local repo has no | ... | @@ -637,7 +644,7 @@ it.effect("publish succeeds with status remote_added when the local repo has no |
| 637 | const service = yield* SourceControlRepositoryService.SourceControlRepositoryService; | 644 | const service = yield* SourceControlRepositoryService.SourceControlRepositoryService; |
| 638 | const result = yield* service.publishRepository({ | 645 | const result = yield* service.publishRepository({ |
| 639 | cwd: "/workspace", | 646 | cwd: "/workspace", |
| 640 | provider: "github", | 647 | provider: SourceControlProviderKind.make("github"), |
| 641 | repository: "octocat/t3code", | 648 | repository: "octocat/t3code", |
| 642 | visibility: "private", | 649 | visibility: "private", |
| 643 | remoteName: "origin", | 650 | remoteName: "origin", |
| ... | @@ -645,7 +652,7 @@ it.effect("publish succeeds with status remote_added when the local repo has no | ... | @@ -645,7 +652,7 @@ it.effect("publish succeeds with status remote_added when the local repo has no |
| 645 | }); | 652 | }); |
| 646 | 653 | ||
| 647 | assert.deepStrictEqual(result, { | 654 | assert.deepStrictEqual(result, { |
| 648 | repository: { provider: "github", ...CLONE_URLS }, | 655 | repository: { provider: SourceControlProviderKind.make("github"), ...CLONE_URLS }, |
| 649 | remoteName: "origin", | 656 | remoteName: "origin", |
| 650 | remoteUrl: CLONE_URLS.sshUrl, | 657 | remoteUrl: CLONE_URLS.sshUrl, |
| 651 | branch: "main", | 658 | branch: "main", |
apps/server/src/sourceControl/SourceControlRepositoryService.ts+23-12| ... | @@ -12,7 +12,7 @@ import { | ... | @@ -12,7 +12,7 @@ import { |
| 12 | type SourceControlCloneRepositoryInput, | 12 | type SourceControlCloneRepositoryInput, |
| 13 | type SourceControlCloneRepositoryResult, | 13 | type SourceControlCloneRepositoryResult, |
| 14 | type SourceControlCloneProtocol, | 14 | type SourceControlCloneProtocol, |
| 15 | type SourceControlProviderKind, | 15 | SourceControlProviderKind, |
| 16 | type SourceControlPublishRepositoryInput, | 16 | type SourceControlPublishRepositoryInput, |
| 17 | type SourceControlPublishRepositoryResult, | 17 | type SourceControlPublishRepositoryResult, |
| 18 | type SourceControlRepositoryCloneUrls, | 18 | type SourceControlRepositoryCloneUrls, |
| ... | @@ -209,7 +209,7 @@ export const make = Effect.gen(function* () { | ... | @@ -209,7 +209,7 @@ export const make = Effect.gen(function* () { |
| 209 | if (trimmed.length === 0) { | 209 | if (trimmed.length === 0) { |
| 210 | return yield* new SourceControlRepositoryError({ | 210 | return yield* new SourceControlRepositoryError({ |
| 211 | operation: "cloneRepository", | 211 | operation: "cloneRepository", |
| 212 | provider: "unknown", | 212 | provider: SourceControlProviderKind.make("unknown"), |
| 213 | detail: "Choose a destination path before cloning.", | 213 | detail: "Choose a destination path before cloning.", |
| 214 | }); | 214 | }); |
| 215 | } | 215 | } |
| ... | @@ -229,7 +229,7 @@ export const make = Effect.gen(function* () { | ... | @@ -229,7 +229,7 @@ export const make = Effect.gen(function* () { |
| 229 | (cause) => | 229 | (cause) => |
| 230 | new SourceControlRepositoryError({ | 230 | new SourceControlRepositoryError({ |
| 231 | operation: "cloneRepository", | 231 | operation: "cloneRepository", |
| 232 | provider: "unknown", | 232 | provider: SourceControlProviderKind.make("unknown"), |
| 233 | detail: "Destination path already exists and is not a directory.", | 233 | detail: "Destination path already exists and is not a directory.", |
| 234 | cause, | 234 | cause, |
| 235 | }), | 235 | }), |
| ... | @@ -238,7 +238,7 @@ export const make = Effect.gen(function* () { | ... | @@ -238,7 +238,7 @@ export const make = Effect.gen(function* () { |
| 238 | if (entries.length > 0) { | 238 | if (entries.length > 0) { |
| 239 | return yield* new SourceControlRepositoryError({ | 239 | return yield* new SourceControlRepositoryError({ |
| 240 | operation: "cloneRepository", | 240 | operation: "cloneRepository", |
| 241 | provider: "unknown", | 241 | provider: SourceControlProviderKind.make("unknown"), |
| 242 | detail: "Destination path already exists and is not empty.", | 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,7 +260,8 @@ export const make = Effect.gen(function* () { |
| 260 | const preparedDestination = yield* prepareDestination(input.destinationPath); | 260 | const preparedDestination = yield* prepareDestination(input.destinationPath); |
| 261 | let repository: SourceControlRepositoryInfo | null = null; | 261 | let repository: SourceControlRepositoryInfo | null = null; |
| 262 | let remoteUrl = input.remoteUrl?.trim() ?? null; | 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 | if (input.provider && input.repository) { | 266 | if (input.provider && input.repository) { |
| 266 | repository = yield* lookupRepository({ | 267 | repository = yield* lookupRepository({ |
| ... | @@ -335,7 +336,7 @@ export const make = Effect.gen(function* () { | ... | @@ -335,7 +336,7 @@ export const make = Effect.gen(function* () { |
| 335 | (cause) => | 336 | (cause) => |
| 336 | new SourceControlRepositoryError({ | 337 | new SourceControlRepositoryError({ |
| 337 | operation: "cloneRepository", | 338 | operation: "cloneRepository", |
| 338 | provider: input.provider ?? "unknown", | 339 | provider: input.provider ?? SourceControlProviderKind.make("unknown"), |
| 339 | detail: | 340 | detail: |
| 340 | stderrTail.length > 0 | 341 | stderrTail.length > 0 |
| 341 | ? stderrTail.join(" ") | 342 | ? stderrTail.join(" ") |
| ... | @@ -370,7 +371,7 @@ export const make = Effect.gen(function* () { | ... | @@ -370,7 +371,7 @@ export const make = Effect.gen(function* () { |
| 370 | (cause) => | 371 | (cause) => |
| 371 | new SourceControlRepositoryError({ | 372 | new SourceControlRepositoryError({ |
| 372 | operation: "discardClone", | 373 | operation: "discardClone", |
| 373 | provider: "unknown", | 374 | provider: SourceControlProviderKind.make("unknown"), |
| 374 | detail: "The clone destination could not be inspected.", | 375 | detail: "The clone destination could not be inspected.", |
| 375 | cause, | 376 | cause, |
| 376 | }), | 377 | }), |
| ... | @@ -379,7 +380,7 @@ export const make = Effect.gen(function* () { | ... | @@ -379,7 +380,7 @@ export const make = Effect.gen(function* () { |
| 379 | if (entries.length > 0 && !entries.includes(".git")) { | 380 | if (entries.length > 0 && !entries.includes(".git")) { |
| 380 | return yield* new SourceControlRepositoryError({ | 381 | return yield* new SourceControlRepositoryError({ |
| 381 | operation: "discardClone", | 382 | operation: "discardClone", |
| 382 | provider: "unknown", | 383 | provider: SourceControlProviderKind.make("unknown"), |
| 383 | detail: "Destination path contains files that are not from the clone.", | 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,7 +394,7 @@ export const make = Effect.gen(function* () { |
| 393 | (cause) => | 394 | (cause) => |
| 394 | new SourceControlRepositoryError({ | 395 | new SourceControlRepositoryError({ |
| 395 | operation: "discardClone", | 396 | operation: "discardClone", |
| 396 | provider: "unknown", | 397 | provider: SourceControlProviderKind.make("unknown"), |
| 397 | detail: "The partial clone could not be removed.", | 398 | detail: "The partial clone could not be removed.", |
| 398 | cause, | 399 | cause, |
| 399 | }), | 400 | }), |
| ... | @@ -462,13 +463,23 @@ export const make = Effect.gen(function* () { | ... | @@ -462,13 +463,23 @@ export const make = Effect.gen(function* () { |
| 462 | lookupRepository: (input) => | 463 | lookupRepository: (input) => |
| 463 | lookupRepository(input).pipe(mapRepositoryError("lookupRepository", input.provider)), | 464 | lookupRepository(input).pipe(mapRepositoryError("lookupRepository", input.provider)), |
| 464 | prepareClone: (input) => | 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 | cloneRepository: (input, options) => | 472 | cloneRepository: (input, options) => |
| 467 | cloneRepository(input, options).pipe( | 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 | discardClone: (destinationPath) => | 479 | discardClone: (destinationPath) => |
| 471 | discardClone(destinationPath).pipe(mapRepositoryError("discardClone", "unknown")), | 480 | discardClone(destinationPath).pipe( |
| 481 | mapRepositoryError("discardClone", SourceControlProviderKind.make("unknown")), | ||
| 482 | ), | ||
| 472 | publishRepository: (input) => | 483 | publishRepository: (input) => |
| 473 | publishRepository(input).pipe(mapRepositoryError("publishRepository", input.provider)), | 484 | publishRepository(input).pipe(mapRepositoryError("publishRepository", input.provider)), |
| 474 | }); | 485 | }); |
apps/server/src/textGeneration/ThreadTitleLinks.test.ts+2-1| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | ||
| 1 | import { expect, it } from "@effect/vitest"; | 2 | import { expect, it } from "@effect/vitest"; |
| 2 | import * as Effect from "effect/Effect"; | 3 | import * as Effect from "effect/Effect"; |
| 3 | import * as Schema from "effect/Schema"; | 4 | import * as Schema from "effect/Schema"; |
| ... | @@ -87,7 +88,7 @@ it.effect("keeps lookup failure out of generation and skips unlinked messages", | ... | @@ -87,7 +88,7 @@ it.effect("keeps lookup failure out of generation and skips unlinked messages", |
| 87 | resolveLink: () => | 88 | resolveLink: () => |
| 88 | Effect.fail( | 89 | Effect.fail( |
| 89 | new SourceControlProviderError({ | 90 | new SourceControlProviderError({ |
| 90 | provider: "unknown", | 91 | provider: SourceControlProviderKind.make("unknown"), |
| 91 | operation: "resolveLink", | 92 | operation: "resolveLink", |
| 92 | cwd: "/tmp", | 93 | cwd: "/tmp", |
| 93 | detail: "Unavailable", | 94 | detail: "Unavailable", |
apps/server/src/vcs/VcsStatusBroadcaster.test.ts+2-1| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | ||
| 1 | import { assert, it, describe } from "@effect/vitest"; | 2 | import { assert, it, describe } from "@effect/vitest"; |
| 2 | import * as NodeServices from "@effect/platform-node/NodeServices"; | 3 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 3 | import * as Cause from "effect/Cause"; | 4 | import * as Cause from "effect/Cause"; |
| ... | @@ -34,7 +35,7 @@ const TEST_EPOCH = DateTime.makeUnsafe("1970-01-01T00:00:00.000Z"); | ... | @@ -34,7 +35,7 @@ const TEST_EPOCH = DateTime.makeUnsafe("1970-01-01T00:00:00.000Z"); |
| 34 | const baseLocalStatus: VcsStatusLocalResult = { | 35 | const baseLocalStatus: VcsStatusLocalResult = { |
| 35 | isRepo: true, | 36 | isRepo: true, |
| 36 | sourceControlProvider: { | 37 | sourceControlProvider: { |
| 37 | kind: "github", | 38 | kind: SourceControlProviderKind.make("github"), |
| 38 | name: "GitHub", | 39 | name: "GitHub", |
| 39 | baseUrl: "https://github.com", | 40 | baseUrl: "https://github.com", |
| 40 | }, | 41 | }, |
apps/web/src/components/CommandPalette.tsx+10-7| ... | @@ -39,7 +39,7 @@ import { | ... | @@ -39,7 +39,7 @@ import { |
| 39 | type FilesystemBrowseResult, | 39 | type FilesystemBrowseResult, |
| 40 | type ProjectId, | 40 | type ProjectId, |
| 41 | type SourceControlDiscoveryResult, | 41 | type SourceControlDiscoveryResult, |
| 42 | type SourceControlProviderKind, | 42 | SourceControlProviderKind, |
| 43 | type SourceControlRepositoryInfo, | 43 | type SourceControlRepositoryInfo, |
| 44 | PRIMARY_LOCAL_ENVIRONMENT_ID, | 44 | PRIMARY_LOCAL_ENVIRONMENT_ID, |
| 45 | resolveEnvironmentMachineKind, | 45 | resolveEnvironmentMachineKind, |
| ... | @@ -257,10 +257,13 @@ interface AddProjectEnvironmentOption { | ... | @@ -257,10 +257,13 @@ interface AddProjectEnvironmentOption { |
| 257 | readonly status: string; | 257 | readonly status: string; |
| 258 | } | 258 | } |
| 259 | 259 | ||
| 260 | type AddProjectRemoteProviderKind = Extract< | 260 | type AddProjectRemoteProviderKind = |
| 261 | SourceControlProviderKind, | 261 | | "github" |
| 262 | "github" | "gitlab" | "forgejo" | "bitbucket" | "azure-devops" | "gitcafe" | 262 | | "gitlab" |
| 263 | >; | 263 | | "forgejo" |
| 264 | | "bitbucket" | ||
| 265 | | "azure-devops" | ||
| 266 | | "gitcafe"; | ||
| 264 | type AddProjectRemoteSource = AddProjectRemoteProviderKind | "url"; | 267 | type AddProjectRemoteSource = AddProjectRemoteProviderKind | "url"; |
| 265 | 268 | ||
| 266 | type AddProjectCloneFlow = | 269 | type AddProjectCloneFlow = |
| ... | @@ -421,7 +424,7 @@ function buildAddProjectRemoteSourceReadiness( | ... | @@ -421,7 +424,7 @@ function buildAddProjectRemoteSourceReadiness( |
| 421 | for (const source of REMOTE_PROJECT_SOURCES) { | 424 | for (const source of REMOTE_PROJECT_SOURCES) { |
| 422 | const kind = sourceProviderKind(source); | 425 | const kind = sourceProviderKind(source); |
| 423 | if (!kind) continue; | 426 | if (!kind) continue; |
| 424 | const provider = providerByKind.get(kind); | 427 | const provider = providerByKind.get(SourceControlProviderKind.make(kind)); |
| 425 | if (!provider) { | 428 | if (!provider) { |
| 426 | readiness[source] = unavailable; | 429 | readiness[source] = unavailable; |
| 427 | continue; | 430 | continue; |
| ... | @@ -2628,7 +2631,7 @@ function OpenCommandPaletteDialog(props: { | ... | @@ -2628,7 +2631,7 @@ function OpenCommandPaletteDialog(props: { |
| 2628 | const lookupResult = await lookupRepository({ | 2631 | const lookupResult = await lookupRepository({ |
| 2629 | environmentId: addProjectCloneFlow.environmentId, | 2632 | environmentId: addProjectCloneFlow.environmentId, |
| 2630 | input: { | 2633 | input: { |
| 2631 | provider, | 2634 | provider: SourceControlProviderKind.make(provider), |
| 2632 | repository: rawRepository, | 2635 | repository: rawRepository, |
| 2633 | }, | 2636 | }, |
| 2634 | }); | 2637 | }); |
apps/web/src/components/GitActionsControl.logic.test.ts+2-1| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | ||
| 1 | import type { VcsStatusResult } from "@t3tools/contracts"; | 2 | import type { VcsStatusResult } from "@t3tools/contracts"; |
| 2 | import { assert, describe, it } from "vite-plus/test"; | 3 | import { assert, describe, it } from "vite-plus/test"; |
| 3 | import { | 4 | import { |
| ... | @@ -343,7 +344,7 @@ describe("when: source control provider uses merge requests", () => { | ... | @@ -343,7 +344,7 @@ describe("when: source control provider uses merge requests", () => { |
| 343 | const gitlabStatus = status({ | 344 | const gitlabStatus = status({ |
| 344 | aheadCount: 2, | 345 | aheadCount: 2, |
| 345 | sourceControlProvider: { | 346 | sourceControlProvider: { |
| 346 | kind: "gitlab", | 347 | kind: SourceControlProviderKind.make("gitlab"), |
| 347 | name: "GitLab", | 348 | name: "GitLab", |
| 348 | baseUrl: "https://gitlab.com", | 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,6 +3,7 @@ import { useAtomValue } from "@effect/atom-react"; |
| 3 | import { | 3 | import { |
| 4 | AuthOrchestrationOperateScope, | 4 | AuthOrchestrationOperateScope, |
| 5 | AuthSourceControlWriteScope, | 5 | AuthSourceControlWriteScope, |
| 6 | SourceControlProviderKind, | ||
| 6 | type ScopedThreadRef, | 7 | type ScopedThreadRef, |
| 7 | } from "@t3tools/contracts"; | 8 | } from "@t3tools/contracts"; |
| 8 | import { | 9 | import { |
| ... | @@ -14,7 +15,6 @@ import type { | ... | @@ -14,7 +15,6 @@ import type { |
| 14 | GitStackedAction, | 15 | GitStackedAction, |
| 15 | SourceControlCloneProtocol, | 16 | SourceControlCloneProtocol, |
| 16 | SourceControlProviderDiscoveryItem, | 17 | SourceControlProviderDiscoveryItem, |
| 17 | SourceControlProviderKind, | ||
| 18 | SourceControlPublishRepositoryResult, | 18 | SourceControlPublishRepositoryResult, |
| 19 | SourceControlRepositoryVisibility, | 19 | SourceControlRepositoryVisibility, |
| 20 | VcsStatusResult, | 20 | VcsStatusResult, |
| ... | @@ -158,10 +158,13 @@ interface PendingDefaultBranchAction { | ... | @@ -158,10 +158,13 @@ interface PendingDefaultBranchAction { |
| 158 | filePaths?: string[]; | 158 | filePaths?: string[]; |
| 159 | } | 159 | } |
| 160 | 160 | ||
| 161 | type PublishProviderKind = Extract< | 161 | type PublishProviderKind = |
| 162 | SourceControlProviderKind, | 162 | | "github" |
| 163 | "github" | "gitlab" | "forgejo" | "bitbucket" | "azure-devops" | "gitcafe" | 163 | | "gitlab" |
| 164 | >; | 164 | | "forgejo" |
| 165 | | "bitbucket" | ||
| 166 | | "azure-devops" | ||
| 167 | | "gitcafe"; | ||
| 165 | 168 | ||
| 166 | type GitActionToastId = ReturnType<typeof toastManager.add>; | 169 | type GitActionToastId = ReturnType<typeof toastManager.add>; |
| 167 | 170 | ||
| ... | @@ -265,9 +268,7 @@ function publishProviderOption(provider: PublishProviderKind) { | ... | @@ -265,9 +268,7 @@ function publishProviderOption(provider: PublishProviderKind) { |
| 265 | ); | 268 | ); |
| 266 | } | 269 | } |
| 267 | 270 | ||
| 268 | function isPublishProviderKind( | 271 | function isPublishProviderKind(provider: string): provider is PublishProviderKind { |
| 269 | provider: SourceControlProviderKind, | ||
| 270 | ): provider is PublishProviderKind { | ||
| 271 | return PUBLISH_PROVIDER_OPTIONS.some((option) => option.value === provider); | 272 | return PUBLISH_PROVIDER_OPTIONS.some((option) => option.value === provider); |
| 272 | } | 273 | } |
| 273 | 274 | ||
| ... | @@ -582,8 +583,9 @@ function PublishRepositoryDialog(props: PublishRepositoryDialogProps) { | ... | @@ -582,8 +583,9 @@ function PublishRepositoryDialog(props: PublishRepositoryDialogProps) { |
| 582 | gitcafe: null, | 583 | gitcafe: null, |
| 583 | }; | 584 | }; |
| 584 | for (const provider of sourceControlDiscovery.data?.sourceControlProviders ?? []) { | 585 | for (const provider of sourceControlDiscovery.data?.sourceControlProviders ?? []) { |
| 585 | if (isPublishProviderKind(provider.kind)) { | 586 | const kind: string = provider.kind; |
| 586 | accounts[provider.kind] = Option.getOrNull(provider.auth.account); | 587 | if (isPublishProviderKind(kind)) { |
| 588 | accounts[kind] = Option.getOrNull(provider.auth.account); | ||
| 587 | } | 589 | } |
| 588 | } | 590 | } |
| 589 | return accounts; | 591 | return accounts; |
| ... | @@ -670,7 +672,7 @@ function PublishRepositoryDialog(props: PublishRepositoryDialogProps) { | ... | @@ -670,7 +672,7 @@ function PublishRepositoryDialog(props: PublishRepositoryDialogProps) { |
| 670 | 672 | ||
| 671 | void (async () => { | 673 | void (async () => { |
| 672 | const result = await publishRepositoryAction.run({ | 674 | const result = await publishRepositoryAction.run({ |
| 673 | provider: publishProvider, | 675 | provider: SourceControlProviderKind.make(publishProvider), |
| 674 | repository: publishRepository.trim(), | 676 | repository: publishRepository.trim(), |
| 675 | visibility: publishVisibility, | 677 | visibility: publishVisibility, |
| 676 | remoteName: publishRemoteName.trim() || "origin", | 678 | remoteName: publishRemoteName.trim() || "origin", |
apps/web/src/components/ThreadStatusIndicators.test.ts+3-2| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | ||
| 1 | import { ProjectId, type PullRequestSummary, type VcsStatusResult } from "@t3tools/contracts"; | 2 | import { ProjectId, type PullRequestSummary, type VcsStatusResult } from "@t3tools/contracts"; |
| 2 | import { describe, expect, it } from "@effect/vitest"; | 3 | import { describe, expect, it } from "@effect/vitest"; |
| 3 | import * as Effect from "effect/Effect"; | 4 | import * as Effect from "effect/Effect"; |
| ... | @@ -93,7 +94,7 @@ function pullRequestSummary( | ... | @@ -93,7 +94,7 @@ function pullRequestSummary( |
| 93 | updatedAt: string, | 94 | updatedAt: string, |
| 94 | ): PullRequestSummary { | 95 | ): PullRequestSummary { |
| 95 | return { | 96 | return { |
| 96 | provider: "github", | 97 | provider: SourceControlProviderKind.make("github"), |
| 97 | projectId: ProjectId.make("project-1"), | 98 | projectId: ProjectId.make("project-1"), |
| 98 | repository: "pingdotgg/t3code", | 99 | repository: "pingdotgg/t3code", |
| 99 | number: 42, | 100 | number: 42, |
| ... | @@ -179,7 +180,7 @@ describe("resolveDisplayedThreadPr + nextThreadChangeRequestSnapshot", () => { | ... | @@ -179,7 +180,7 @@ describe("resolveDisplayedThreadPr + nextThreadChangeRequestSnapshot", () => { |
| 179 | url: "https://github.com/pingdotgg/t3code/pull/42", | 180 | url: "https://github.com/pingdotgg/t3code/pull/42", |
| 180 | }; | 181 | }; |
| 181 | const provider = { | 182 | const provider = { |
| 182 | kind: "github" as const, | 183 | kind: SourceControlProviderKind.make("github"), |
| 183 | name: "GitHub", | 184 | name: "GitHub", |
| 184 | baseUrl: "https://github.com", | 185 | baseUrl: "https://github.com", |
| 185 | }; | 186 | }; |
apps/web/src/components/ThreadStatusIndicators.tsx+2-1| ... | @@ -13,6 +13,7 @@ import { | ... | @@ -13,6 +13,7 @@ import { |
| 13 | type ThreadLinkedPullRequest, | 13 | type ThreadLinkedPullRequest, |
| 14 | type ThreadPullRequestLink, | 14 | type ThreadPullRequestLink, |
| 15 | type VcsStatusResult, | 15 | type VcsStatusResult, |
| 16 | SourceControlProviderKind, | ||
| 16 | } from "@t3tools/contracts"; | 17 | } from "@t3tools/contracts"; |
| 17 | import { Atom } from "effect/reactivity"; | 18 | import { Atom } from "effect/reactivity"; |
| 18 | import { FolderGit2Icon, TerminalIcon } from "lucide-react"; | 19 | import { FolderGit2Icon, TerminalIcon } from "lucide-react"; |
| ... | @@ -154,7 +155,7 @@ export function linkedPullRequestSnapshotStatus( | ... | @@ -154,7 +155,7 @@ export function linkedPullRequestSnapshotStatus( |
| 154 | baseRef: snapshot.baseBranch, | 155 | baseRef: snapshot.baseBranch, |
| 155 | ...(snapshot.updatedAt === null ? {} : { updatedAt: snapshot.updatedAt }), | 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,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | ||
| 1 | import { | 2 | import { |
| 2 | EnvironmentId, | 3 | EnvironmentId, |
| 3 | ProjectId, | 4 | ProjectId, |
| ... | @@ -151,7 +152,7 @@ import { PullRequestDetailPanel } from "./PullRequestDetailPanel"; | ... | @@ -151,7 +152,7 @@ import { PullRequestDetailPanel } from "./PullRequestDetailPanel"; |
| 151 | import { pullRequestPanelContext } from "./pullRequestDetail.logic"; | 152 | import { pullRequestPanelContext } from "./pullRequestDetail.logic"; |
| 152 | 153 | ||
| 153 | const detail: PullRequestDetailView = { | 154 | const detail: PullRequestDetailView = { |
| 154 | provider: "github", | 155 | provider: SourceControlProviderKind.make("github"), |
| 155 | projectId: ProjectId.make("project"), | 156 | projectId: ProjectId.make("project"), |
| 156 | projectTitle: "Project", | 157 | projectTitle: "Project", |
| 157 | workspaceRoot: "/workspace", | 158 | workspaceRoot: "/workspace", |
apps/web/src/components/pullRequest/PullRequestSummaryTab.test.tsx+2-1| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | ||
| 1 | import { EnvironmentId, ProjectId, type PullRequestDetailView } from "@t3tools/contracts"; | 2 | import { EnvironmentId, ProjectId, type PullRequestDetailView } from "@t3tools/contracts"; |
| 2 | import { act, type ReactNode } from "react"; | 3 | import { act, type ReactNode } from "react"; |
| 3 | import { create, type ReactTestRenderer } from "react-test-renderer"; | 4 | import { create, type ReactTestRenderer } from "react-test-renderer"; |
| ... | @@ -26,7 +27,7 @@ vi.mock("../ui/tooltip", () => ({ | ... | @@ -26,7 +27,7 @@ vi.mock("../ui/tooltip", () => ({ |
| 26 | import { PullRequestSummaryTab } from "./PullRequestSummaryTab"; | 27 | import { PullRequestSummaryTab } from "./PullRequestSummaryTab"; |
| 27 | 28 | ||
| 28 | const detail: PullRequestDetailView = { | 29 | const detail: PullRequestDetailView = { |
| 29 | provider: "github", | 30 | provider: SourceControlProviderKind.make("github"), |
| 30 | projectId: ProjectId.make("project"), | 31 | projectId: ProjectId.make("project"), |
| 31 | projectTitle: "Project", | 32 | projectTitle: "Project", |
| 32 | workspaceRoot: "/workspace", | 33 | workspaceRoot: "/workspace", |
apps/web/src/components/pullRequest/pullRequestDetail.logic.test.ts+12-8| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | ||
| 1 | import { resolvePlanFollowUpSubmission } from "../../proposedPlan"; | 2 | import { resolvePlanFollowUpSubmission } from "../../proposedPlan"; |
| 2 | import { serializeLegacyContextMessage } from "@t3tools/shared/composerContextLegacySend"; | 3 | import { serializeLegacyContextMessage } from "@t3tools/shared/composerContextLegacySend"; |
| 3 | import { | 4 | import { |
| ... | @@ -95,12 +96,14 @@ describe("pull request checkout commands", () => { | ... | @@ -95,12 +96,14 @@ describe("pull request checkout commands", () => { |
| 95 | ], | 96 | ], |
| 96 | ["unknown", "feature", null, null], | 97 | ["unknown", "feature", null, null], |
| 97 | ] as const)("builds the %s command", (provider, branch, repository, expected) => { | 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 | it("fetches Forgejo pull refs from the actual repository, including a mounted host and port", () => { | 103 | it("fetches Forgejo pull refs from the actual repository, including a mounted host and port", () => { |
| 101 | expect( | 104 | expect( |
| 102 | pullRequestCheckoutCommand( | 105 | pullRequestCheckoutCommand( |
| 103 | "forgejo", | 106 | SourceControlProviderKind.make("forgejo"), |
| 104 | 42, | 107 | 42, |
| 105 | "feature", | 108 | "feature", |
| 106 | null, | 109 | null, |
| ... | @@ -113,7 +116,7 @@ describe("pull request checkout commands", () => { | ... | @@ -113,7 +116,7 @@ describe("pull request checkout commands", () => { |
| 113 | it("quotes shell metacharacters in Forgejo repository URLs", () => { | 116 | it("quotes shell metacharacters in Forgejo repository URLs", () => { |
| 114 | expect( | 117 | expect( |
| 115 | pullRequestCheckoutCommand( | 118 | pullRequestCheckoutCommand( |
| 116 | "forgejo", | 119 | SourceControlProviderKind.make("forgejo"), |
| 117 | 42, | 120 | 42, |
| 118 | "feature", | 121 | "feature", |
| 119 | null, | 122 | null, |
| ... | @@ -1714,7 +1717,7 @@ describe("cached pull request detail", () => { | ... | @@ -1714,7 +1717,7 @@ describe("cached pull request detail", () => { |
| 1714 | const reference = { projectId: ProjectId.make("project-1"), repository: "acme/web", number: 7 }; | 1717 | const reference = { projectId: ProjectId.make("project-1"), repository: "acme/web", number: 7 }; |
| 1715 | const detail = (overrides: Partial<PullRequestDetail> = {}): PullRequestDetail => | 1718 | const detail = (overrides: Partial<PullRequestDetail> = {}): PullRequestDetail => |
| 1716 | ({ | 1719 | ({ |
| 1717 | provider: "github", | 1720 | provider: SourceControlProviderKind.make("github"), |
| 1718 | capabilities: { | 1721 | capabilities: { |
| 1719 | diff: true, | 1722 | diff: true, |
| 1720 | comment: true, | 1723 | comment: true, |
| ... | @@ -1797,7 +1800,7 @@ describe("cached pull request detail", () => { | ... | @@ -1797,7 +1800,7 @@ describe("cached pull request detail", () => { |
| 1797 | remoteName: "origin", | 1800 | remoteName: "origin", |
| 1798 | remoteUrl: "https://github.com/acme/web.git", | 1801 | remoteUrl: "https://github.com/acme/web.git", |
| 1799 | }, | 1802 | }, |
| 1800 | provider: "github", | 1803 | provider: SourceControlProviderKind.make("github"), |
| 1801 | }); | 1804 | }); |
| 1802 | expect(readPullRequestDetailSnapshot(storage, "env-1", resolved)?.title).toBe( | 1805 | expect(readPullRequestDetailSnapshot(storage, "env-1", resolved)?.title).toBe( |
| 1803 | "Cache the title", | 1806 | "Cache the title", |
| ... | @@ -1824,7 +1827,7 @@ describe("cached pull request detail", () => { | ... | @@ -1824,7 +1827,7 @@ describe("cached pull request detail", () => { |
| 1824 | remoteName: "origin", | 1827 | remoteName: "origin", |
| 1825 | remoteUrl: "git@ssh.dev.azure.com:v3/org/project/web", | 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 | ).toBe(reference); | 1832 | ).toBe(reference); |
| 1830 | expect(resolvePullRequestReferenceHost(reference, undefined)).toBe(reference); | 1833 | expect(resolvePullRequestReferenceHost(reference, undefined)).toBe(reference); |
| ... | @@ -1847,7 +1850,7 @@ describe("cached pull request detail", () => { | ... | @@ -1847,7 +1850,7 @@ describe("cached pull request detail", () => { |
| 1847 | it("keeps Forgejo ports isolated when recovering legacy snapshots", () => { | 1850 | it("keeps Forgejo ports isolated when recovering legacy snapshots", () => { |
| 1848 | const storage = makeStorage(); | 1851 | const storage = makeStorage(); |
| 1849 | const cached = detail({ | 1852 | const cached = detail({ |
| 1850 | provider: "forgejo", | 1853 | provider: SourceControlProviderKind.make("forgejo"), |
| 1851 | url: "https://forge.example:8443/acme/web/pulls/7", | 1854 | url: "https://forge.example:8443/acme/web/pulls/7", |
| 1852 | }); | 1855 | }); |
| 1853 | writePullRequestDetailSnapshot(storage, "env-1", reference, cached); | 1856 | writePullRequestDetailSnapshot(storage, "env-1", reference, cached); |
| ... | @@ -1869,7 +1872,8 @@ describe("cached pull request detail", () => { | ... | @@ -1869,7 +1872,8 @@ describe("cached pull request detail", () => { |
| 1869 | 1872 | ||
| 1870 | it.each(["github", "gitlab"] as const)( | 1873 | it.each(["github", "gitlab"] as const)( |
| 1871 | "retains portless %s snapshot identities for custom web ports", | 1874 | "retains portless %s snapshot identities for custom web ports", |
| 1872 | (provider) => { | 1875 | (providerName) => { |
| 1876 | const provider = SourceControlProviderKind.make(providerName); | ||
| 1873 | const storage = makeStorage(); | 1877 | const storage = makeStorage(); |
| 1874 | const host = `${provider}.example.com`; | 1878 | const host = `${provider}.example.com`; |
| 1875 | const hosted = { ...reference, host }; | 1879 | const hosted = { ...reference, host }; |
apps/web/src/components/pullRequest/pullRequestDetail.logic.ts+9-5| ... | @@ -21,7 +21,7 @@ import { | ... | @@ -21,7 +21,7 @@ import { |
| 21 | type PullRequestReviewThread, | 21 | type PullRequestReviewThread, |
| 22 | type PullRequestState, | 22 | type PullRequestState, |
| 23 | type PullRequestUpdateMethod, | 23 | type PullRequestUpdateMethod, |
| 24 | type SourceControlProviderKind, | 24 | SourceControlProviderKind, |
| 25 | type ThreadLinkedPullRequest, | 25 | type ThreadLinkedPullRequest, |
| 26 | type ThreadPullRequestLink, | 26 | type ThreadPullRequestLink, |
| 27 | type VcsRef, | 27 | type VcsRef, |
| ... | @@ -140,7 +140,7 @@ export function pullRequestCheckoutCommand( | ... | @@ -140,7 +140,7 @@ export function pullRequestCheckoutCommand( |
| 140 | } | 140 | } |
| 141 | return `git clone --single-branch --branch ${headBranch} https://bitbucket.org/${headRepositoryNameWithOwner}.git t3code-pr-${number}`; | 141 | return `git clone --single-branch --branch ${headBranch} https://bitbucket.org/${headRepositoryNameWithOwner}.git t3code-pr-${number}`; |
| 142 | } | 142 | } |
| 143 | case "unknown": | 143 | default: |
| 144 | return null; | 144 | return null; |
| 145 | } | 145 | } |
| 146 | } | 146 | } |
| ... | @@ -155,10 +155,11 @@ export function loadingPullRequestCheckoutCommand( | ... | @@ -155,10 +155,11 @@ export function loadingPullRequestCheckoutCommand( |
| 155 | identity?.provider ?? | 155 | identity?.provider ?? |
| 156 | (host === "github.com" ? "github" : host === "gitlab.com" ? "gitlab" : null); | 156 | (host === "github.com" ? "github" : host === "gitlab.com" ? "gitlab" : null); |
| 157 | if (provider !== "github" && provider !== "gitlab" && provider !== "azure-devops") return null; | 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 | return null; | 160 | return null; |
| 160 | } | 161 | } |
| 161 | return pullRequestCheckoutCommand(provider, reference.number, ""); | 162 | return pullRequestCheckoutCommand(kind, reference.number, ""); |
| 162 | } | 163 | } |
| 163 | 164 | ||
| 164 | /** Activity changes only when the same host resource reports a newer revision. */ | 165 | /** Activity changes only when the same host resource reports a newer revision. */ |
| ... | @@ -1304,7 +1305,10 @@ export function resolvePullRequestReferenceHost( | ... | @@ -1304,7 +1305,10 @@ export function resolvePullRequestReferenceHost( |
| 1304 | ): PullRequestRef { | 1305 | ): PullRequestRef { |
| 1305 | // Other providers may resolve an SSH remote to a different web authority on the server. | 1306 | // Other providers may resolve an SSH remote to a different web authority on the server. |
| 1306 | if (reference.host !== undefined || identity?.provider !== "github") return reference; | 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 | export interface PullRequestDetailSnapshotRef { | 1314 | export interface PullRequestDetailSnapshotRef { |
apps/web/src/components/pullRequest/pullRequestList.logic.test.ts+3-2| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | ||
| 1 | import type { EnvironmentId, ProjectId, PullRequestListEntry } from "@t3tools/contracts"; | 2 | import type { EnvironmentId, ProjectId, PullRequestListEntry } from "@t3tools/contracts"; |
| 2 | import { describe, expect, it } from "vite-plus/test"; | 3 | import { describe, expect, it } from "vite-plus/test"; |
| 3 | 4 | ||
| ... | @@ -330,7 +331,7 @@ describe("pull request involvement filtering", () => { | ... | @@ -330,7 +331,7 @@ describe("pull request involvement filtering", () => { |
| 330 | entry({ number: 1, author: { login: "Bilal", name: null, avatarUrl: null } }), | 331 | entry({ number: 1, author: { login: "Bilal", name: null, avatarUrl: null } }), |
| 331 | entry({ | 332 | entry({ |
| 332 | number: 2, | 333 | number: 2, |
| 333 | provider: "gitlab", | 334 | provider: SourceControlProviderKind.make("gitlab"), |
| 334 | host: "gitlab.com", | 335 | host: "gitlab.com", |
| 335 | author: { login: "Bilal", name: null, avatarUrl: null }, | 336 | author: { login: "Bilal", name: null, avatarUrl: null }, |
| 336 | }), | 337 | }), |
| ... | @@ -1322,7 +1323,7 @@ describe("merging the environments' own listings", () => { | ... | @@ -1322,7 +1323,7 @@ describe("merging the environments' own listings", () => { |
| 1322 | providers: [ | 1323 | providers: [ |
| 1323 | { | 1324 | { |
| 1324 | host: "github.com", | 1325 | host: "github.com", |
| 1325 | kind: "github", | 1326 | kind: SourceControlProviderKind.make("github"), |
| 1326 | searchesOnHost: false, | 1327 | searchesOnHost: false, |
| 1327 | projectCount: 2, | 1328 | projectCount: 2, |
| 1328 | configured: false, | 1329 | configured: false, |
apps/web/src/components/settings/SourceControlSettings.tsx+1-1| ... | @@ -78,7 +78,7 @@ const EMPTY_DISCOVERY_RESULT: SourceControlDiscoveryResult = { | ... | @@ -78,7 +78,7 @@ const EMPTY_DISCOVERY_RESULT: SourceControlDiscoveryResult = { |
| 78 | sourceControlProviders: [], | 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 | github: GitHubIcon, | 82 | github: GitHubIcon, |
| 83 | gitlab: GitLabIcon, | 83 | gitlab: GitLabIcon, |
| 84 | forgejo: ForgejoIcon, | 84 | forgejo: ForgejoIcon, |
apps/web/src/hooks/useNewProject.ts+2-1| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | ||
| 1 | import { scopeProjectRef } from "@t3tools/client-runtime/environment"; | 2 | import { scopeProjectRef } from "@t3tools/client-runtime/environment"; |
| 2 | import { getNewProjectGitHubRepository } from "@t3tools/client-runtime/operations/projects"; | 3 | import { getNewProjectGitHubRepository } from "@t3tools/client-runtime/operations/projects"; |
| 3 | import { | 4 | import { |
| ... | @@ -45,7 +46,7 @@ export function useNewProject() { | ... | @@ -45,7 +46,7 @@ export function useNewProject() { |
| 45 | environmentId: input.environmentId, | 46 | environmentId: input.environmentId, |
| 46 | input: { | 47 | input: { |
| 47 | cwd: input.workspaceRoot, | 48 | cwd: input.workspaceRoot, |
| 48 | provider: "github", | 49 | provider: SourceControlProviderKind.make("github"), |
| 49 | repository: getNewProjectGitHubRepository(input, input.workspaceRoot), | 50 | repository: getNewProjectGitHubRepository(input, input.workspaceRoot), |
| 50 | visibility: "private", | 51 | visibility: "private", |
| 51 | }, | 52 | }, |
apps/web/src/state/pullRequests.test.ts+2-1| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | ||
| 1 | import { ProjectId, type PullRequestSummary } from "@t3tools/contracts"; | 2 | import { ProjectId, type PullRequestSummary } from "@t3tools/contracts"; |
| 2 | import { describe, expect, it } from "vite-plus/test"; | 3 | import { describe, expect, it } from "vite-plus/test"; |
| 3 | 4 | ||
| ... | @@ -5,7 +6,7 @@ import { newestPullRequestObservation, newestPullRequestSummary } from "./pullRe | ... | @@ -5,7 +6,7 @@ import { newestPullRequestObservation, newestPullRequestSummary } from "./pullRe |
| 5 | 6 | ||
| 6 | function summary(overrides: Partial<PullRequestSummary> = {}): PullRequestSummary { | 7 | function summary(overrides: Partial<PullRequestSummary> = {}): PullRequestSummary { |
| 7 | return { | 8 | return { |
| 8 | provider: "github", | 9 | provider: SourceControlProviderKind.make("github"), |
| 9 | projectId: ProjectId.make("pull-request-cache-test"), | 10 | projectId: ProjectId.make("pull-request-cache-test"), |
| 10 | repository: "acme/widget", | 11 | repository: "acme/widget", |
| 11 | number: 7, | 12 | number: 7, |
apps/web/src/state/sourceControlActions.test.ts+2-1| ... | @@ -3,6 +3,7 @@ import { | ... | @@ -3,6 +3,7 @@ import { |
| 3 | AuthSourceControlWriteScope, | 3 | AuthSourceControlWriteScope, |
| 4 | EnvironmentId, | 4 | EnvironmentId, |
| 5 | ThreadId, | 5 | ThreadId, |
| 6 | SourceControlProviderKind, | ||
| 6 | } from "@t3tools/contracts"; | 7 | } from "@t3tools/contracts"; |
| 7 | import * as Cause from "effect/Cause"; | 8 | import * as Cause from "effect/Cause"; |
| 8 | import { AsyncResult } from "effect/reactivity"; | 9 | import { AsyncResult } from "effect/reactivity"; |
| ... | @@ -98,7 +99,7 @@ const cases = [ | ... | @@ -98,7 +99,7 @@ const cases = [ |
| 98 | isAllowed: action.isAllowed, | 99 | isAllowed: action.isAllowed, |
| 99 | run: () => | 100 | run: () => |
| 100 | action.run({ | 101 | action.run({ |
| 101 | provider: "github", | 102 | provider: SourceControlProviderKind.make("github"), |
| 102 | repository: "owner/repo", | 103 | repository: "owner/repo", |
| 103 | visibility: "private", | 104 | visibility: "private", |
| 104 | remoteName: "origin", | 105 | remoteName: "origin", |
apps/web/src/state/sourceControlActions.ts+2-1| ... | @@ -16,6 +16,7 @@ import { | ... | @@ -16,6 +16,7 @@ import { |
| 16 | type EnvironmentId, | 16 | type EnvironmentId, |
| 17 | type GitResolvePullRequestResult, | 17 | type GitResolvePullRequestResult, |
| 18 | type SourceControlCloneProtocol, | 18 | type SourceControlCloneProtocol, |
| 19 | type SourceControlProviderKind, | ||
| 19 | type SourceControlRepositoryVisibility, | 20 | type SourceControlRepositoryVisibility, |
| 20 | type ThreadId, | 21 | type ThreadId, |
| 21 | } from "@t3tools/contracts"; | 22 | } from "@t3tools/contracts"; |
| ... | @@ -275,7 +276,7 @@ export function useSourceControlPublishRepositoryAction(scope: SourceControlActi | ... | @@ -275,7 +276,7 @@ export function useSourceControlPublishRepositoryAction(scope: SourceControlActi |
| 275 | ); | 276 | ); |
| 276 | const action = useCallback( | 277 | const action = useCallback( |
| 277 | async (input: { | 278 | async (input: { |
| 278 | provider: "github" | "gitlab" | "forgejo" | "bitbucket" | "azure-devops" | "gitcafe"; | 279 | provider: SourceControlProviderKind; |
| 279 | repository: string; | 280 | repository: string; |
| 280 | visibility: SourceControlRepositoryVisibility; | 281 | visibility: SourceControlRepositoryVisibility; |
| 281 | remoteName: string; | 282 | remoteName: string; |
packages/client-runtime/src/operations/projects.test.ts+6-5| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | ||
| 1 | import { describe, expect, it } from "vite-plus/test"; | 2 | import { describe, expect, it } from "vite-plus/test"; |
| 2 | import { | 3 | import { |
| 3 | EnvironmentId, | 4 | EnvironmentId, |
| ... | @@ -71,14 +72,14 @@ describe("add project shared logic", () => { | ... | @@ -71,14 +72,14 @@ describe("add project shared logic", () => { |
| 71 | it("uses HTTPS for repositories selected through a provider", () => { | 72 | it("uses HTTPS for repositories selected through a provider", () => { |
| 72 | expect( | 73 | expect( |
| 73 | getDefaultCloneUrl({ | 74 | getDefaultCloneUrl({ |
| 74 | provider: "github", | 75 | provider: SourceControlProviderKind.make("github"), |
| 75 | url: "https://github.com/imputnet/helium", | 76 | url: "https://github.com/imputnet/helium", |
| 76 | sshUrl: "git@github.com:imputnet/helium.git", | 77 | sshUrl: "git@github.com:imputnet/helium.git", |
| 77 | }), | 78 | }), |
| 78 | ).toBe("https://github.com/imputnet/helium"); | 79 | ).toBe("https://github.com/imputnet/helium"); |
| 79 | expect( | 80 | expect( |
| 80 | getDefaultCloneUrl({ | 81 | getDefaultCloneUrl({ |
| 81 | provider: "forgejo", | 82 | provider: SourceControlProviderKind.make("forgejo"), |
| 82 | url: "https://forgejo.example.test:8443/owner/repo.git", | 83 | url: "https://forgejo.example.test:8443/owner/repo.git", |
| 83 | sshUrl: "ssh://git@forgejo.example.test:2222/owner/repo.git", | 84 | sshUrl: "ssh://git@forgejo.example.test:2222/owner/repo.git", |
| 84 | }), | 85 | }), |
| ... | @@ -88,7 +89,7 @@ describe("add project shared logic", () => { | ... | @@ -88,7 +89,7 @@ describe("add project shared logic", () => { |
| 88 | it("preserves existing clone transport behavior for other providers", () => { | 89 | it("preserves existing clone transport behavior for other providers", () => { |
| 89 | expect( | 90 | expect( |
| 90 | getDefaultCloneUrl({ | 91 | getDefaultCloneUrl({ |
| 91 | provider: "gitlab", | 92 | provider: SourceControlProviderKind.make("gitlab"), |
| 92 | url: "https://gitlab.com/group/project.git", | 93 | url: "https://gitlab.com/group/project.git", |
| 93 | sshUrl: "git@gitlab.com:group/project.git", | 94 | sshUrl: "git@gitlab.com:group/project.git", |
| 94 | }), | 95 | }), |
| ... | @@ -187,7 +188,7 @@ describe("add project shared logic", () => { | ... | @@ -187,7 +188,7 @@ describe("add project shared logic", () => { |
| 187 | versionControlSystems: [], | 188 | versionControlSystems: [], |
| 188 | sourceControlProviders: [ | 189 | sourceControlProviders: [ |
| 189 | { | 190 | { |
| 190 | kind: "github", | 191 | kind: SourceControlProviderKind.make("github"), |
| 191 | label: "GitHub", | 192 | label: "GitHub", |
| 192 | status: "available", | 193 | status: "available", |
| 193 | installHint: "Install gh", | 194 | installHint: "Install gh", |
| ... | @@ -201,7 +202,7 @@ describe("add project shared logic", () => { | ... | @@ -201,7 +202,7 @@ describe("add project shared logic", () => { |
| 201 | }, | 202 | }, |
| 202 | }, | 203 | }, |
| 203 | { | 204 | { |
| 204 | kind: "gitlab", | 205 | kind: SourceControlProviderKind.make("gitlab"), |
| 205 | label: "GitLab", | 206 | label: "GitLab", |
| 206 | status: "available", | 207 | status: "available", |
| 207 | installHint: "Install glab", | 208 | installHint: "Install glab", |
packages/client-runtime/src/operations/projects.ts+8-6| ... | @@ -6,9 +6,9 @@ import type { | ... | @@ -6,9 +6,9 @@ import type { |
| 6 | ProjectId, | 6 | ProjectId, |
| 7 | ServerConfig, | 7 | ServerConfig, |
| 8 | SourceControlDiscoveryResult, | 8 | SourceControlDiscoveryResult, |
| 9 | SourceControlProviderKind, | ||
| 10 | SourceControlRepositoryInfo, | 9 | SourceControlRepositoryInfo, |
| 11 | } from "@t3tools/contracts"; | 10 | } from "@t3tools/contracts"; |
| 11 | import { SourceControlProviderKind } from "@t3tools/contracts"; | ||
| 12 | import { newProjectFolderName } from "@t3tools/shared/path"; | 12 | import { newProjectFolderName } from "@t3tools/shared/path"; |
| 13 | import * as Arr from "effect/Array"; | 13 | import * as Arr from "effect/Array"; |
| 14 | import * as Option from "effect/Option"; | 14 | import * as Option from "effect/Option"; |
| ... | @@ -25,10 +25,12 @@ import { | ... | @@ -25,10 +25,12 @@ import { |
| 25 | } from "../state/projects.ts"; | 25 | } from "../state/projects.ts"; |
| 26 | import type { EnvironmentProject } from "../state/models.ts"; | 26 | import type { EnvironmentProject } from "../state/models.ts"; |
| 27 | 27 | ||
| 28 | export type AddProjectRemoteProviderKind = Extract< | 28 | export type AddProjectRemoteProviderKind = |
| 29 | SourceControlProviderKind, | 29 | | "github" |
| 30 | "github" | "gitlab" | "forgejo" | "bitbucket" | "azure-devops" | 30 | | "gitlab" |
| 31 | >; | 31 | | "forgejo" |
| 32 | | "bitbucket" | ||
| 33 | | "azure-devops"; | ||
| 32 | export type AddProjectRemoteSource = AddProjectRemoteProviderKind | "url"; | 34 | export type AddProjectRemoteSource = AddProjectRemoteProviderKind | "url"; |
| 33 | 35 | ||
| 34 | export function canCreateProjectInEnvironment( | 36 | export function canCreateProjectInEnvironment( |
| ... | @@ -190,7 +192,7 @@ export function buildAddProjectRemoteSourceReadiness( | ... | @@ -190,7 +192,7 @@ export function buildAddProjectRemoteSourceReadiness( |
| 190 | for (const source of ADD_PROJECT_REMOTE_SOURCES) { | 192 | for (const source of ADD_PROJECT_REMOTE_SOURCES) { |
| 191 | const kind = addProjectRemoteSourceProvider(source); | 193 | const kind = addProjectRemoteSourceProvider(source); |
| 192 | if (!kind) continue; | 194 | if (!kind) continue; |
| 193 | const provider = providerByKind.get(kind); | 195 | const provider = providerByKind.get(SourceControlProviderKind.make(kind)); |
| 194 | if (!provider) { | 196 | if (!provider) { |
| 195 | readiness[source] = unavailable; | 197 | readiness[source] = unavailable; |
| 196 | continue; | 198 | continue; |
packages/client-runtime/src/state/sourceControl.test.ts+4-3| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | ||
| 1 | import { vi } from "vite-plus/test"; | 2 | import { vi } from "vite-plus/test"; |
| 2 | import { | 3 | import { |
| 3 | EnvironmentId, | 4 | EnvironmentId, |
| ... | @@ -36,7 +37,7 @@ const TARGET = new PrimaryConnectionTarget({ | ... | @@ -36,7 +37,7 @@ const TARGET = new PrimaryConnectionTarget({ |
| 36 | 37 | ||
| 37 | const PUBLISH_RESULT: SourceControlPublishRepositoryResult = { | 38 | const PUBLISH_RESULT: SourceControlPublishRepositoryResult = { |
| 38 | repository: { | 39 | repository: { |
| 39 | provider: "github", | 40 | provider: SourceControlProviderKind.make("github"), |
| 40 | nameWithOwner: "t3tools/t3code", | 41 | nameWithOwner: "t3tools/t3code", |
| 41 | url: "https://github.com/t3tools/t3code", | 42 | url: "https://github.com/t3tools/t3code", |
| 42 | sshUrl: "git@github.com:t3tools/t3code.git", | 43 | sshUrl: "git@github.com:t3tools/t3code.git", |
| ... | @@ -140,7 +141,7 @@ describe("source control environment atoms", () => { | ... | @@ -140,7 +141,7 @@ describe("source control environment atoms", () => { |
| 140 | environmentId: TARGET.environmentId, | 141 | environmentId: TARGET.environmentId, |
| 141 | input: { | 142 | input: { |
| 142 | cwd: "/repo", | 143 | cwd: "/repo", |
| 143 | provider: "github", | 144 | provider: SourceControlProviderKind.make("github"), |
| 144 | repository: "t3tools/t3code", | 145 | repository: "t3tools/t3code", |
| 145 | visibility: "private", | 146 | visibility: "private", |
| 146 | }, | 147 | }, |
| ... | @@ -156,7 +157,7 @@ describe("source control environment atoms", () => { | ... | @@ -156,7 +157,7 @@ describe("source control environment atoms", () => { |
| 156 | environmentId: TARGET.environmentId, | 157 | environmentId: TARGET.environmentId, |
| 157 | input: { | 158 | input: { |
| 158 | cwd: "/repo", | 159 | cwd: "/repo", |
| 159 | provider: "github", | 160 | provider: SourceControlProviderKind.make("github"), |
| 160 | repository: "t3tools/t3code", | 161 | repository: "t3tools/t3code", |
| 161 | visibility: "private", | 162 | visibility: "private", |
| 162 | }, | 163 | }, |
packages/contracts/src/pullRequest.test.ts+11-6| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "./sourceControl.ts"; | ||
| 1 | import * as Schema from "effect/Schema"; | 2 | import * as Schema from "effect/Schema"; |
| 2 | import { describe, expect, it } from "vite-plus/test"; | 3 | import { describe, expect, it } from "vite-plus/test"; |
| 3 | 4 | ||
| ... | @@ -25,7 +26,7 @@ const LIST_RESULT: PullRequestListResult = { | ... | @@ -25,7 +26,7 @@ const LIST_RESULT: PullRequestListResult = { |
| 25 | providers: [ | 26 | providers: [ |
| 26 | { | 27 | { |
| 27 | host: "github.com", | 28 | host: "github.com", |
| 28 | kind: "github", | 29 | kind: SourceControlProviderKind.make("github"), |
| 29 | searchesOnHost: true, | 30 | searchesOnHost: true, |
| 30 | projectCount: 1, | 31 | projectCount: 1, |
| 31 | configured: true, | 32 | configured: true, |
| ... | @@ -33,7 +34,7 @@ const LIST_RESULT: PullRequestListResult = { | ... | @@ -33,7 +34,7 @@ const LIST_RESULT: PullRequestListResult = { |
| 33 | }, | 34 | }, |
| 34 | { | 35 | { |
| 35 | host: "gitlab.com", | 36 | host: "gitlab.com", |
| 36 | kind: "gitlab", | 37 | kind: SourceControlProviderKind.make("gitlab"), |
| 37 | searchesOnHost: true, | 38 | searchesOnHost: true, |
| 38 | projectCount: 1, | 39 | projectCount: 1, |
| 39 | configured: false, | 40 | configured: false, |
| ... | @@ -42,7 +43,7 @@ const LIST_RESULT: PullRequestListResult = { | ... | @@ -42,7 +43,7 @@ const LIST_RESULT: PullRequestListResult = { |
| 42 | ], | 43 | ], |
| 43 | entries: [ | 44 | entries: [ |
| 44 | { | 45 | { |
| 45 | provider: "github", | 46 | provider: SourceControlProviderKind.make("github"), |
| 46 | host: "github.com", | 47 | host: "github.com", |
| 47 | projectId: "project-1" as PullRequestListResult["entries"][number]["projectId"], | 48 | projectId: "project-1" as PullRequestListResult["entries"][number]["projectId"], |
| 48 | projectTitle: "t3code", | 49 | projectTitle: "t3code", |
| ... | @@ -75,12 +76,16 @@ describe("PullRequestListResult", () => { | ... | @@ -75,12 +76,16 @@ describe("PullRequestListResult", () => { |
| 75 | canonicalKey: "forge.example/team/repo", | 76 | canonicalKey: "forge.example/team/repo", |
| 76 | locator: { remoteUrl: "http://forge.example:3000/team/repo.git" }, | 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, SourceControlProviderKind.make("forgejo"))).toBe( |
| 79 | expect(pullRequestHostOf(identity, "gitlab")).toBe("forge.example"); | 80 | "forge.example:3000", |
| 81 | ); | ||
| 82 | expect(pullRequestHostOf(identity, SourceControlProviderKind.make("gitlab"))).toBe( | ||
| 83 | "forge.example", | ||
| 84 | ); | ||
| 80 | expect( | 85 | expect( |
| 81 | pullRequestHostOf( | 86 | pullRequestHostOf( |
| 82 | { ...identity, locator: { remoteUrl: "ssh://git@forge.example:2222/team/repo.git" } }, | 87 | { ...identity, locator: { remoteUrl: "ssh://git@forge.example:2222/team/repo.git" } }, |
| 83 | "forgejo", | 88 | SourceControlProviderKind.make("forgejo"), |
| 84 | ), | 89 | ), |
| 85 | ).toBe("forge.example"); | 90 | ).toBe("forge.example"); |
| 86 | }); | 91 | }); |
packages/contracts/src/pullRequest.ts+50-34| ... | @@ -1301,38 +1301,54 @@ export type PullRequestUnavailableReason = typeof PullRequestUnavailableReason.T | ... | @@ -1301,38 +1301,54 @@ export type PullRequestUnavailableReason = typeof PullRequestUnavailableReason.T |
| 1301 | * symptom. The reason names keep their `cli-` prefix for wire compatibility; for GitHub and | 1301 | * symptom. The reason names keep their `cli-` prefix for wire compatibility; for GitHub and |
| 1302 | * Bitbucket they mean "no credential" and "a refused credential", not a missing tool. | 1302 | * Bitbucket they mean "no credential" and "a refused credential", not a missing tool. |
| 1303 | */ | 1303 | */ |
| 1304 | const PROVIDER_REQUIREMENT: Partial< | 1304 | const PROVIDER_REQUIREMENT = new Map< |
| 1305 | Record<SourceControlProviderKind, { readonly missing: string; readonly unauthenticated: string }> | 1305 | string, |
| 1306 | > = { | 1306 | { readonly missing: string; readonly unauthenticated: string } |
| 1307 | github: { | 1307 | >([ |
| 1308 | missing: | 1308 | [ |
| 1309 | "No GitHub credential on the server. Set GH_TOKEN, or install the GitHub CLI (https://cli.github.com/) and run `gh auth login`.", | 1309 | "github", |
| 1310 | unauthenticated: | 1310 | { |
| 1311 | "GitHub has no working credential for this host. Run `gh auth login`, or check the account and hosts in Settings → Source Control.", | 1311 | missing: |
| 1312 | }, | 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 | forgejo: { | 1313 | unauthenticated: |
| 1314 | missing: | 1314 | "GitHub has no working credential for this host. Run `gh auth login`, or check the account and hosts in Settings → Source Control.", |
| 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.", | 1315 | }, |
| 1316 | unauthenticated: | 1316 | ], |
| 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.", | 1317 | [ |
| 1318 | }, | 1318 | "forgejo", |
| 1319 | gitlab: { | 1319 | { |
| 1320 | missing: | 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.", | 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: "GitLab CLI is not authenticated. Run `glab auth login` and retry.", | 1322 | unauthenticated: |
| 1323 | }, | 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 | "azure-devops": { | 1324 | }, |
| 1325 | missing: | 1325 | ], |
| 1326 | "Azure CLI (`az`) with the Azure DevOps extension is required. Install `az`, then run `az extension add --name azure-devops`.", | 1326 | [ |
| 1327 | unauthenticated: "Azure CLI is not signed in. Run `az login` and retry.", | 1327 | "gitlab", |
| 1328 | }, | 1328 | { |
| 1329 | bitbucket: { | 1329 | missing: |
| 1330 | 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 | "Bitbucket needs API credentials on the server. Add them in Settings → Source Control.", | 1331 | unauthenticated: "GitLab CLI is not authenticated. Run `glab auth login` and retry.", |
| 1332 | unauthenticated: | 1332 | }, |
| 1333 | "Bitbucket rejected the configured credentials. Check them in Settings → Source Control.", | 1333 | ], |
| 1334 | }, | 1334 | [ |
| 1335 | }; | 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 | * The host a project's repository is addressed below. `canonicalKey` is the normalized remote, | 1354 | * The host a project's repository is addressed below. `canonicalKey` is the normalized remote, |
| ... | @@ -1394,7 +1410,7 @@ export function pullRequestProviderRequirement( | ... | @@ -1394,7 +1410,7 @@ export function pullRequestProviderRequirement( |
| 1394 | provider: SourceControlProviderKind, | 1410 | provider: SourceControlProviderKind, |
| 1395 | reason: PullRequestUnavailableReason, | 1411 | reason: PullRequestUnavailableReason, |
| 1396 | ): string | null { | 1412 | ): string | null { |
| 1397 | const requirement = PROVIDER_REQUIREMENT[provider]; | 1413 | const requirement = PROVIDER_REQUIREMENT.get(provider); |
| 1398 | if (requirement === undefined) return null; | 1414 | if (requirement === undefined) return null; |
| 1399 | switch (reason) { | 1415 | switch (reason) { |
| 1400 | case "cli-missing": | 1416 | case "cli-missing": |
| ... | @@ -1427,7 +1443,7 @@ export class PullRequestUnavailableError extends Schema.TaggedError<PullRequestU | ... | @@ -1427,7 +1443,7 @@ export class PullRequestUnavailableError extends Schema.TaggedError<PullRequestU |
| 1427 | 1443 | ||
| 1428 | override get message(): string { | 1444 | override get message(): string { |
| 1429 | const requirement = | 1445 | const requirement = |
| 1430 | this.provider === undefined ? undefined : PROVIDER_REQUIREMENT[this.provider]; | 1446 | this.provider === undefined ? undefined : PROVIDER_REQUIREMENT.get(this.provider); |
| 1431 | switch (this.reason) { | 1447 | switch (this.reason) { |
| 1432 | case "cli-missing": | 1448 | case "cli-missing": |
| 1433 | return ( | 1449 | return ( |
packages/contracts/src/sourceControl.ts+11-9| ... | @@ -2,15 +2,17 @@ import * as Schema from "effect/Schema"; | ... | @@ -2,15 +2,17 @@ import * as Schema from "effect/Schema"; |
| 2 | import { PositiveInt, TrimmedNonEmptyString } from "./baseSchemas.ts"; | 2 | import { PositiveInt, TrimmedNonEmptyString } from "./baseSchemas.ts"; |
| 3 | import { VcsDriverKind } from "./vcs.ts"; | 3 | import { VcsDriverKind } from "./vcs.ts"; |
| 4 | 4 | ||
| 5 | export const SourceControlProviderKind = Schema.Literals([ | 5 | /** |
| 6 | "github", | 6 | * `SourceControlProviderKind` — open branded slug naming a source control host, such as `github`. |
| 7 | "gitlab", | 7 | * |
| 8 | "forgejo", | 8 | * Open on purpose, like `ProviderDriverKind`: a server can report a host this client does not |
| 9 | "azure-devops", | 9 | * ship a definition for, and clients fall back to a generic presentation instead of failing to |
| 10 | "bitbucket", | 10 | * decode. `unknown` is what the server reports for a remote it cannot attribute to any host. |
| 11 | "gitcafe", | 11 | */ |
| 12 | "unknown", | 12 | export const SourceControlProviderKind = TrimmedNonEmptyString.check( |
| 13 | ]); | 13 | Schema.isMaxLength(64), |
| 14 | Schema.isPattern(/^[a-z][a-z0-9-]*$/), | ||
| 15 | ).pipe(Schema.brand("SourceControlProviderKind")); | ||
| 14 | export type SourceControlProviderKind = typeof SourceControlProviderKind.Type; | 16 | export type SourceControlProviderKind = typeof SourceControlProviderKind.Type; |
| 15 | 17 | ||
| 16 | export const SourceControlProviderInfo = Schema.Struct({ | 18 | export const SourceControlProviderInfo = Schema.Struct({ |
packages/shared/src/git.test.ts+2-1| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | ||
| 1 | import type { VcsStatusRemoteResult, VcsStatusResult } from "@t3tools/contracts"; | 2 | import type { VcsStatusRemoteResult, VcsStatusResult } from "@t3tools/contracts"; |
| 2 | import { describe, expect, it } from "vite-plus/test"; | 3 | import { describe, expect, it } from "vite-plus/test"; |
| 3 | 4 | ||
| ... | @@ -257,7 +258,7 @@ describe("applyGitStatusStreamEvent", () => { | ... | @@ -257,7 +258,7 @@ describe("applyGitStatusStreamEvent", () => { |
| 257 | const current: VcsStatusResult = { | 258 | const current: VcsStatusResult = { |
| 258 | isRepo: true, | 259 | isRepo: true, |
| 259 | sourceControlProvider: { | 260 | sourceControlProvider: { |
| 260 | kind: "github", | 261 | kind: SourceControlProviderKind.make("github"), |
| 261 | name: "GitHub", | 262 | name: "GitHub", |
| 262 | baseUrl: "https://github.com", | 263 | baseUrl: "https://github.com", |
| 263 | }, | 264 | }, |
packages/shared/src/sourceControl.test.ts+18-9| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | ||
| 1 | import { describe, expect, it } from "vite-plus/test"; | 2 | import { describe, expect, it } from "vite-plus/test"; |
| 2 | 3 | ||
| 3 | import { | 4 | import { |
| ... | @@ -10,30 +11,38 @@ import { | ... | @@ -10,30 +11,38 @@ import { |
| 10 | 11 | ||
| 11 | describe("source control presentation", () => { | 12 | describe("source control presentation", () => { |
| 12 | it("uses merge request terminology for GitLab", () => { | 13 | it("uses merge request terminology for GitLab", () => { |
| 13 | expect(getChangeRequestTerminologyForKind("gitlab")).toEqual({ | 14 | expect(getChangeRequestTerminologyForKind(SourceControlProviderKind.make("gitlab"))).toEqual({ |
| 14 | shortLabel: "MR", | 15 | shortLabel: "MR", |
| 15 | singular: "merge request", | 16 | singular: "merge request", |
| 16 | }); | 17 | }); |
| 17 | }); | 18 | }); |
| 18 | 19 | ||
| 19 | it("uses pull request terminology for GitHub-compatible providers", () => { | 20 | it("uses pull request terminology for GitHub-compatible providers", () => { |
| 20 | expect(getChangeRequestTerminologyForKind("github")).toEqual({ | 21 | expect(getChangeRequestTerminologyForKind(SourceControlProviderKind.make("github"))).toEqual({ |
| 21 | shortLabel: "PR", | 22 | shortLabel: "PR", |
| 22 | singular: "pull request", | 23 | singular: "pull request", |
| 23 | }); | 24 | }); |
| 24 | expect(getChangeRequestTerminologyForKind("azure-devops")).toEqual({ | 25 | expect( |
| 25 | shortLabel: "PR", | 26 | getChangeRequestTerminologyForKind(SourceControlProviderKind.make("azure-devops")), |
| 26 | singular: "pull request", | 27 | ).toEqual({ |
| 27 | }); | ||
| 28 | expect(getChangeRequestTerminologyForKind("bitbucket")).toEqual({ | ||
| 29 | shortLabel: "PR", | 28 | shortLabel: "PR", |
| 30 | singular: "pull request", | 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 | it("falls back to generic change request copy for unknown providers", () => { | 39 | it("falls back to generic change request copy for unknown providers", () => { |
| 35 | expect( | 40 | expect( |
| 36 | resolveChangeRequestPresentation({ kind: "unknown", name: "forge", baseUrl: "" }), | 41 | resolveChangeRequestPresentation({ |
| 42 | kind: SourceControlProviderKind.make("unknown"), | ||
| 43 | name: "forge", | ||
| 44 | baseUrl: "", | ||
| 45 | }), | ||
| 37 | ).toEqual( | 46 | ).toEqual( |
| 38 | expect.objectContaining({ | 47 | expect.objectContaining({ |
| 39 | shortName: "change request", | 48 | shortName: "change request", |
| ... | @@ -69,7 +78,7 @@ describe("detectSourceControlProviderFromRemoteUrl", () => { | ... | @@ -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 | shortLabel: "PR", | 82 | shortLabel: "PR", |
| 74 | singular: "pull request", | 83 | singular: "pull request", |
| 75 | }); | 84 | }); |
packages/shared/src/sourceControl.ts+14-13| ... | @@ -1,8 +1,5 @@ | ... | @@ -1,8 +1,5 @@ |
| 1 | import type { | 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; |
| 2 | RepositoryIdentity, | 2 | import type { RepositoryIdentity, SourceControlProviderInfo } from "@t3tools/contracts"; |
| 3 | SourceControlProviderInfo, | ||
| 4 | SourceControlProviderKind, | ||
| 5 | } from "@t3tools/contracts"; | ||
| 6 | 3 | ||
| 7 | export interface ChangeRequestPresentation { | 4 | export interface ChangeRequestPresentation { |
| 8 | readonly icon: | 5 | readonly icon: |
| ... | @@ -124,7 +121,7 @@ export function resolveChangeRequestPresentation( | ... | @@ -124,7 +121,7 @@ export function resolveChangeRequestPresentation( |
| 124 | return BITBUCKET_CHANGE_REQUEST_PRESENTATION; | 121 | return BITBUCKET_CHANGE_REQUEST_PRESENTATION; |
| 125 | case "gitcafe": | 122 | case "gitcafe": |
| 126 | return GITCAFE_CHANGE_REQUEST_PRESENTATION; | 123 | return GITCAFE_CHANGE_REQUEST_PRESENTATION; |
| 127 | case "unknown": | 124 | default: |
| 128 | return GENERIC_CHANGE_REQUEST_PRESENTATION; | 125 | return GENERIC_CHANGE_REQUEST_PRESENTATION; |
| 129 | } | 126 | } |
| 130 | } | 127 | } |
| ... | @@ -244,7 +241,11 @@ export function detectSourceControlProviderFromRemoteUrl( | ... | @@ -244,7 +241,11 @@ export function detectSourceControlProviderFromRemoteUrl( |
| 244 | const hostname = parseHostName(host); | 241 | const hostname = parseHostName(host); |
| 245 | 242 | ||
| 246 | if (isGitCafeHost(hostname)) { | 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 | if ( | 251 | if ( |
| ... | @@ -253,7 +254,7 @@ export function detectSourceControlProviderFromRemoteUrl( | ... | @@ -253,7 +254,7 @@ export function detectSourceControlProviderFromRemoteUrl( |
| 253 | hasDnsLabel(hostname, "gitea") | 254 | hasDnsLabel(hostname, "gitea") |
| 254 | ) { | 255 | ) { |
| 255 | return { | 256 | return { |
| 256 | kind: "forgejo", | 257 | kind: SourceControlProviderKind.make("forgejo"), |
| 257 | name: "Forgejo", | 258 | name: "Forgejo", |
| 258 | baseUrl: /^https?:/iu.test(remoteUrl.trim()) | 259 | baseUrl: /^https?:/iu.test(remoteUrl.trim()) |
| 259 | ? new URL(remoteUrl.trim()).origin | 260 | ? new URL(remoteUrl.trim()).origin |
| ... | @@ -263,7 +264,7 @@ export function detectSourceControlProviderFromRemoteUrl( | ... | @@ -263,7 +264,7 @@ export function detectSourceControlProviderFromRemoteUrl( |
| 263 | 264 | ||
| 264 | if (isGitHubHost(hostname)) { | 265 | if (isGitHubHost(hostname)) { |
| 265 | return { | 266 | return { |
| 266 | kind: "github", | 267 | kind: SourceControlProviderKind.make("github"), |
| 267 | name: hostname === "github.com" ? "GitHub" : "GitHub Self-Hosted", | 268 | name: hostname === "github.com" ? "GitHub" : "GitHub Self-Hosted", |
| 268 | baseUrl: toBaseUrl(host), | 269 | baseUrl: toBaseUrl(host), |
| 269 | }; | 270 | }; |
| ... | @@ -271,7 +272,7 @@ export function detectSourceControlProviderFromRemoteUrl( | ... | @@ -271,7 +272,7 @@ export function detectSourceControlProviderFromRemoteUrl( |
| 271 | 272 | ||
| 272 | if (isGitLabHost(hostname)) { | 273 | if (isGitLabHost(hostname)) { |
| 273 | return { | 274 | return { |
| 274 | kind: "gitlab", | 275 | kind: SourceControlProviderKind.make("gitlab"), |
| 275 | name: hostname === "gitlab.com" ? "GitLab" : "GitLab Self-Hosted", | 276 | name: hostname === "gitlab.com" ? "GitLab" : "GitLab Self-Hosted", |
| 276 | baseUrl: toBaseUrl(host), | 277 | baseUrl: toBaseUrl(host), |
| 277 | }; | 278 | }; |
| ... | @@ -279,7 +280,7 @@ export function detectSourceControlProviderFromRemoteUrl( | ... | @@ -279,7 +280,7 @@ export function detectSourceControlProviderFromRemoteUrl( |
| 279 | 280 | ||
| 280 | if (isAzureDevOpsHost(hostname)) { | 281 | if (isAzureDevOpsHost(hostname)) { |
| 281 | return { | 282 | return { |
| 282 | kind: "azure-devops", | 283 | kind: SourceControlProviderKind.make("azure-devops"), |
| 283 | name: "Azure DevOps", | 284 | name: "Azure DevOps", |
| 284 | baseUrl: toBaseUrl(host), | 285 | baseUrl: toBaseUrl(host), |
| 285 | }; | 286 | }; |
| ... | @@ -287,14 +288,14 @@ export function detectSourceControlProviderFromRemoteUrl( | ... | @@ -287,14 +288,14 @@ export function detectSourceControlProviderFromRemoteUrl( |
| 287 | 288 | ||
| 288 | if (isBitbucketHost(hostname)) { | 289 | if (isBitbucketHost(hostname)) { |
| 289 | return { | 290 | return { |
| 290 | kind: "bitbucket", | 291 | kind: SourceControlProviderKind.make("bitbucket"), |
| 291 | name: hostname === "bitbucket.org" ? "Bitbucket" : "Bitbucket Self-Hosted", | 292 | name: hostname === "bitbucket.org" ? "Bitbucket" : "Bitbucket Self-Hosted", |
| 292 | baseUrl: toBaseUrl(host), | 293 | baseUrl: toBaseUrl(host), |
| 293 | }; | 294 | }; |
| 294 | } | 295 | } |
| 295 | 296 | ||
| 296 | return { | 297 | return { |
| 297 | kind: "unknown", | 298 | kind: SourceControlProviderKind.make("unknown"), |
| 298 | name: host, | 299 | name: host, |
| 299 | baseUrl: toBaseUrl(host), | 300 | baseUrl: toBaseUrl(host), |
| 300 | }; | 301 | }; |
packages/source-control-azure-devops/src/server/AzureDevOpsPullRequestProvider.ts+5-4| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | ||
| 1 | import * as Effect from "effect/Effect"; | 2 | import * as Effect from "effect/Effect"; |
| 2 | import * as Semaphore from "effect/Semaphore"; | 3 | import * as Semaphore from "effect/Semaphore"; |
| 3 | import type { PullRequestCapabilities, PullRequestViewerPermissions } from "@t3tools/contracts"; | 4 | import type { PullRequestCapabilities, PullRequestViewerPermissions } from "@t3tools/contracts"; |
| ... | @@ -157,7 +158,7 @@ export const make = Effect.gen(function* () { | ... | @@ -157,7 +158,7 @@ export const make = Effect.gen(function* () { |
| 157 | const fail = | 158 | const fail = |
| 158 | (operation: string) => (error: AzureDevOpsPullRequestCli.AzureDevOpsPullRequestCliError) => | 159 | (operation: string) => (error: AzureDevOpsPullRequestCli.AzureDevOpsPullRequestCliError) => |
| 159 | new PullRequestProviderError({ | 160 | new PullRequestProviderError({ |
| 160 | provider: "azure-devops", | 161 | provider: SourceControlProviderKind.make("azure-devops"), |
| 161 | operation, | 162 | operation, |
| 162 | ...azureDevOpsProviderFailure(error), | 163 | ...azureDevOpsProviderFailure(error), |
| 163 | detail: error.detail, | 164 | detail: error.detail, |
| ... | @@ -168,7 +169,7 @@ export const make = Effect.gen(function* () { | ... | @@ -168,7 +169,7 @@ export const make = Effect.gen(function* () { |
| 168 | const unsupported = (operation: string) => | 169 | const unsupported = (operation: string) => |
| 169 | Effect.fail( | 170 | Effect.fail( |
| 170 | new PullRequestProviderError({ | 171 | new PullRequestProviderError({ |
| 171 | provider: "azure-devops", | 172 | provider: SourceControlProviderKind.make("azure-devops"), |
| 172 | operation, | 173 | operation, |
| 173 | reason: "failed", | 174 | reason: "failed", |
| 174 | detail: "Azure DevOps reviews cannot be written from here yet.", | 175 | detail: "Azure DevOps reviews cannot be written from here yet.", |
| ... | @@ -296,7 +297,7 @@ export const make = Effect.gen(function* () { | ... | @@ -296,7 +297,7 @@ export const make = Effect.gen(function* () { |
| 296 | }; | 297 | }; |
| 297 | 298 | ||
| 298 | const provider: PullRequestProviderApi = { | 299 | const provider: PullRequestProviderApi = { |
| 299 | kind: "azure-devops", | 300 | kind: SourceControlProviderKind.make("azure-devops"), |
| 300 | capabilities: CAPABILITIES, | 301 | capabilities: CAPABILITIES, |
| 301 | // The bare repository name repeats across an organization; the project path does not. | 302 | // The bare repository name repeats across an organization; the project path does not. |
| 302 | repositoryKey: ({ canonicalKey }) => canonicalKey, | 303 | repositoryKey: ({ canonicalKey }) => canonicalKey, |
| ... | @@ -612,7 +613,7 @@ export const make = Effect.gen(function* () { | ... | @@ -612,7 +613,7 @@ export const make = Effect.gen(function* () { |
| 612 | listReviewerCandidates: () => | 613 | listReviewerCandidates: () => |
| 613 | Effect.fail( | 614 | Effect.fail( |
| 614 | new PullRequestProviderError({ | 615 | new PullRequestProviderError({ |
| 615 | provider: "azure-devops", | 616 | provider: SourceControlProviderKind.make("azure-devops"), |
| 616 | operation: "listReviewerCandidates", | 617 | operation: "listReviewerCandidates", |
| 617 | reason: "failed", | 618 | reason: "failed", |
| 618 | detail: "Azure DevOps cannot say who may review a pull request.", | 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,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | ||
| 1 | import { assert, it } from "@effect/vitest"; | 2 | import { assert, it } from "@effect/vitest"; |
| 2 | import * as Effect from "effect/Effect"; | 3 | import * as Effect from "effect/Effect"; |
| 3 | import * as Layer from "effect/Layer"; | 4 | import * as Layer from "effect/Layer"; |
| ... | @@ -35,7 +36,7 @@ it.effect("maps Azure DevOps PR summaries into provider-neutral change requests" | ... | @@ -35,7 +36,7 @@ it.effect("maps Azure DevOps PR summaries into provider-neutral change requests" |
| 35 | }); | 36 | }); |
| 36 | 37 | ||
| 37 | assert.deepStrictEqual(changeRequest, { | 38 | assert.deepStrictEqual(changeRequest, { |
| 38 | provider: "azure-devops", | 39 | provider: SourceControlProviderKind.make("azure-devops"), |
| 39 | number: 42, | 40 | number: 42, |
| 40 | title: "Add Azure provider", | 41 | title: "Add Azure provider", |
| 41 | url: "https://dev.azure.com/acme/project/_git/repo/pullrequest/42", | 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,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | ||
| 1 | import * as Effect from "effect/Effect"; | 2 | import * as Effect from "effect/Effect"; |
| 2 | import { SourceControlProviderError, type ChangeRequest } from "@t3tools/contracts"; | 3 | import { SourceControlProviderError, type ChangeRequest } from "@t3tools/contracts"; |
| 3 | 4 | ||
| ... | @@ -39,7 +40,7 @@ function parseAzureAuth(input: SourceControlAuthProbeInput) { | ... | @@ -39,7 +40,7 @@ function parseAzureAuth(input: SourceControlAuthProbeInput) { |
| 39 | 40 | ||
| 40 | export const discovery = { | 41 | export const discovery = { |
| 41 | type: "cli", | 42 | type: "cli", |
| 42 | kind: "azure-devops", | 43 | kind: SourceControlProviderKind.make("azure-devops"), |
| 43 | label: "Azure DevOps", | 44 | label: "Azure DevOps", |
| 44 | executable: "az", | 45 | executable: "az", |
| 45 | versionArgs: ["--version"], | 46 | versionArgs: ["--version"], |
| ... | @@ -67,7 +68,7 @@ function toChangeRequest(summary: { | ... | @@ -67,7 +68,7 @@ function toChangeRequest(summary: { |
| 67 | readonly updatedAt: ChangeRequest["updatedAt"]; | 68 | readonly updatedAt: ChangeRequest["updatedAt"]; |
| 68 | }): ChangeRequest { | 69 | }): ChangeRequest { |
| 69 | return { | 70 | return { |
| 70 | provider: "azure-devops", | 71 | provider: SourceControlProviderKind.make("azure-devops"), |
| 71 | number: summary.number, | 72 | number: summary.number, |
| 72 | title: summary.title, | 73 | title: summary.title, |
| 73 | url: summary.url, | 74 | url: summary.url, |
| ... | @@ -86,7 +87,7 @@ export const make = Effect.gen(function* () { | ... | @@ -86,7 +87,7 @@ export const make = Effect.gen(function* () { |
| 86 | const azure = yield* AzureDevOpsCli.AzureDevOpsCli; | 87 | const azure = yield* AzureDevOpsCli.AzureDevOpsCli; |
| 87 | 88 | ||
| 88 | return SourceControlProvider.SourceControlProvider.of({ | 89 | return SourceControlProvider.SourceControlProvider.of({ |
| 89 | kind: "azure-devops", | 90 | kind: SourceControlProviderKind.make("azure-devops"), |
| 90 | listChangeRequests: (input) => { | 91 | listChangeRequests: (input) => { |
| 91 | const source = SourceControlProvider.sourceControlRefFromInput(input); | 92 | const source = SourceControlProvider.sourceControlRefFromInput(input); |
| 92 | return azure | 93 | return azure |
| ... | @@ -102,7 +103,7 @@ export const make = Effect.gen(function* () { | ... | @@ -102,7 +103,7 @@ export const make = Effect.gen(function* () { |
| 102 | Effect.mapError( | 103 | Effect.mapError( |
| 103 | (error) => | 104 | (error) => |
| 104 | new SourceControlProviderError({ | 105 | new SourceControlProviderError({ |
| 105 | provider: "azure-devops", | 106 | provider: SourceControlProviderKind.make("azure-devops"), |
| 106 | operation: "listChangeRequests", | 107 | operation: "listChangeRequests", |
| 107 | command: error.command, | 108 | command: error.command, |
| 108 | cwd: input.cwd, | 109 | cwd: input.cwd, |
| ... | @@ -121,7 +122,7 @@ export const make = Effect.gen(function* () { | ... | @@ -121,7 +122,7 @@ export const make = Effect.gen(function* () { |
| 121 | Effect.mapError( | 122 | Effect.mapError( |
| 122 | (error) => | 123 | (error) => |
| 123 | new SourceControlProviderError({ | 124 | new SourceControlProviderError({ |
| 124 | provider: "azure-devops", | 125 | provider: SourceControlProviderKind.make("azure-devops"), |
| 125 | operation: "getChangeRequest", | 126 | operation: "getChangeRequest", |
| 126 | command: error.command, | 127 | command: error.command, |
| 127 | cwd: input.cwd, | 128 | cwd: input.cwd, |
| ... | @@ -149,7 +150,7 @@ export const make = Effect.gen(function* () { | ... | @@ -149,7 +150,7 @@ export const make = Effect.gen(function* () { |
| 149 | Effect.mapError( | 150 | Effect.mapError( |
| 150 | (error) => | 151 | (error) => |
| 151 | new SourceControlProviderError({ | 152 | new SourceControlProviderError({ |
| 152 | provider: "azure-devops", | 153 | provider: SourceControlProviderKind.make("azure-devops"), |
| 153 | operation: "createChangeRequest", | 154 | operation: "createChangeRequest", |
| 154 | command: error.command, | 155 | command: error.command, |
| 155 | cwd: input.cwd, | 156 | cwd: input.cwd, |
| ... | @@ -167,7 +168,7 @@ export const make = Effect.gen(function* () { | ... | @@ -167,7 +168,7 @@ export const make = Effect.gen(function* () { |
| 167 | Effect.mapError( | 168 | Effect.mapError( |
| 168 | (error) => | 169 | (error) => |
| 169 | new SourceControlProviderError({ | 170 | new SourceControlProviderError({ |
| 170 | provider: "azure-devops", | 171 | provider: SourceControlProviderKind.make("azure-devops"), |
| 171 | operation: "getRepositoryCloneUrls", | 172 | operation: "getRepositoryCloneUrls", |
| 172 | command: error.command, | 173 | command: error.command, |
| 173 | cwd: input.cwd, | 174 | cwd: input.cwd, |
| ... | @@ -184,7 +185,7 @@ export const make = Effect.gen(function* () { | ... | @@ -184,7 +185,7 @@ export const make = Effect.gen(function* () { |
| 184 | Effect.mapError( | 185 | Effect.mapError( |
| 185 | (error) => | 186 | (error) => |
| 186 | new SourceControlProviderError({ | 187 | new SourceControlProviderError({ |
| 187 | provider: "azure-devops", | 188 | provider: SourceControlProviderKind.make("azure-devops"), |
| 188 | operation: "createRepository", | 189 | operation: "createRepository", |
| 189 | command: error.command, | 190 | command: error.command, |
| 190 | cwd: input.cwd, | 191 | cwd: input.cwd, |
| ... | @@ -201,7 +202,7 @@ export const make = Effect.gen(function* () { | ... | @@ -201,7 +202,7 @@ export const make = Effect.gen(function* () { |
| 201 | Effect.mapError( | 202 | Effect.mapError( |
| 202 | (error) => | 203 | (error) => |
| 203 | new SourceControlProviderError({ | 204 | new SourceControlProviderError({ |
| 204 | provider: "azure-devops", | 205 | provider: SourceControlProviderKind.make("azure-devops"), |
| 205 | operation: "getDefaultBranch", | 206 | operation: "getDefaultBranch", |
| 206 | command: error.command, | 207 | command: error.command, |
| 207 | cwd: input.cwd, | 208 | cwd: input.cwd, |
| ... | @@ -221,7 +222,7 @@ export const make = Effect.gen(function* () { | ... | @@ -221,7 +222,7 @@ export const make = Effect.gen(function* () { |
| 221 | Effect.mapError( | 222 | Effect.mapError( |
| 222 | (error) => | 223 | (error) => |
| 223 | new SourceControlProviderError({ | 224 | new SourceControlProviderError({ |
| 224 | provider: "azure-devops", | 225 | provider: SourceControlProviderKind.make("azure-devops"), |
| 225 | operation: "checkoutChangeRequest", | 226 | operation: "checkoutChangeRequest", |
| 226 | command: error.command, | 227 | command: error.command, |
| 227 | cwd: input.cwd, | 228 | cwd: input.cwd, |
packages/source-control-azure-devops/src/server/driver.ts+2-1| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | ||
| 1 | /** | 2 | /** |
| 2 | * The Azure DevOps driver: repository operations, discovery, and pull requests through `az`. | 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,7 +11,7 @@ import * as AzureDevOpsPullRequestProvider from "./AzureDevOpsPullRequestProvide |
| 10 | import * as AzureDevOpsSourceControlProvider from "./AzureDevOpsSourceControlProvider.ts"; | 11 | import * as AzureDevOpsSourceControlProvider from "./AzureDevOpsSourceControlProvider.ts"; |
| 11 | 12 | ||
| 12 | export const driver = defineSourceControlDriver({ | 13 | export const driver = defineSourceControlDriver({ |
| 13 | kind: "azure-devops", | 14 | kind: SourceControlProviderKind.make("azure-devops"), |
| 14 | make: Effect.all({ | 15 | make: Effect.all({ |
| 15 | sourceControl: AzureDevOpsSourceControlProvider.make, | 16 | sourceControl: AzureDevOpsSourceControlProvider.make, |
| 16 | discovery: Effect.succeed(AzureDevOpsSourceControlProvider.discovery), | 17 | discovery: Effect.succeed(AzureDevOpsSourceControlProvider.discovery), |
packages/source-control-bitbucket/src/server/BitbucketPullRequestProvider.ts+4-3| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | ||
| 1 | import * as Effect from "effect/Effect"; | 2 | import * as Effect from "effect/Effect"; |
| 2 | import type { PullRequestCapabilities, PullRequestViewerPermissions } from "@t3tools/contracts"; | 3 | import type { PullRequestCapabilities, PullRequestViewerPermissions } from "@t3tools/contracts"; |
| 3 | 4 | ||
| ... | @@ -122,7 +123,7 @@ export const make = Effect.gen(function* () { | ... | @@ -122,7 +123,7 @@ export const make = Effect.gen(function* () { |
| 122 | const fail = | 123 | const fail = |
| 123 | (operation: string) => (error: BitbucketPullRequestApi.BitbucketPullRequestApiError) => | 124 | (operation: string) => (error: BitbucketPullRequestApi.BitbucketPullRequestApiError) => |
| 124 | new PullRequestProviderError({ | 125 | new PullRequestProviderError({ |
| 125 | provider: "bitbucket", | 126 | provider: SourceControlProviderKind.make("bitbucket"), |
| 126 | operation, | 127 | operation, |
| 127 | ...bitbucketProviderFailure(error), | 128 | ...bitbucketProviderFailure(error), |
| 128 | // Every Bitbucket failure states its own fact; this names the operation around it, so | 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,7 +158,7 @@ export const make = Effect.gen(function* () { |
| 157 | }; | 158 | }; |
| 158 | 159 | ||
| 159 | const provider: PullRequestProviderApi = { | 160 | const provider: PullRequestProviderApi = { |
| 160 | kind: "bitbucket", | 161 | kind: SourceControlProviderKind.make("bitbucket"), |
| 161 | capabilities: CAPABILITIES, | 162 | capabilities: CAPABILITIES, |
| 162 | 163 | ||
| 163 | // Bitbucket credentials come from the server's environment rather than a checkout, so the | 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,7 +364,7 @@ export const make = Effect.gen(function* () { |
| 363 | setReaction: () => | 364 | setReaction: () => |
| 364 | Effect.fail( | 365 | Effect.fail( |
| 365 | new PullRequestProviderError({ | 366 | new PullRequestProviderError({ |
| 366 | provider: "bitbucket", | 367 | provider: SourceControlProviderKind.make("bitbucket"), |
| 367 | operation: "setReaction", | 368 | operation: "setReaction", |
| 368 | reason: "failed", | 369 | reason: "failed", |
| 369 | detail: "Bitbucket does not support reactions.", | 370 | detail: "Bitbucket does not support reactions.", |
packages/source-control-bitbucket/src/server/BitbucketSourceControlProvider.test.ts+2-1| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | ||
| 1 | import { assert, it } from "@effect/vitest"; | 2 | import { assert, it } from "@effect/vitest"; |
| 2 | import * as Effect from "effect/Effect"; | 3 | import * as Effect from "effect/Effect"; |
| 3 | import * as Layer from "effect/Layer"; | 4 | import * as Layer from "effect/Layer"; |
| ... | @@ -36,7 +37,7 @@ it.effect("maps Bitbucket PR summaries into provider-neutral change requests", ( | ... | @@ -36,7 +37,7 @@ it.effect("maps Bitbucket PR summaries into provider-neutral change requests", ( |
| 36 | }); | 37 | }); |
| 37 | 38 | ||
| 38 | assert.deepStrictEqual(changeRequest, { | 39 | assert.deepStrictEqual(changeRequest, { |
| 39 | provider: "bitbucket", | 40 | provider: SourceControlProviderKind.make("bitbucket"), |
| 40 | number: 42, | 41 | number: 42, |
| 41 | title: "Add Bitbucket provider", | 42 | title: "Add Bitbucket provider", |
| 42 | url: "https://bitbucket.org/pingdotgg/t3code/pull-requests/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,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | ||
| 1 | import * as Effect from "effect/Effect"; | 2 | import * as Effect from "effect/Effect"; |
| 2 | import * as Option from "effect/Option"; | 3 | import * as Option from "effect/Option"; |
| 3 | import { SourceControlProviderError, type ChangeRequest } from "@t3tools/contracts"; | 4 | import { SourceControlProviderError, type ChangeRequest } from "@t3tools/contracts"; |
| ... | @@ -9,7 +10,7 @@ import type { SourceControlApiDiscoverySpec } from "@t3tools/source-control-core | ... | @@ -9,7 +10,7 @@ import type { SourceControlApiDiscoverySpec } from "@t3tools/source-control-core |
| 9 | 10 | ||
| 10 | function toChangeRequest(summary: NormalizedBitbucketPullRequestRecord): ChangeRequest { | 11 | function toChangeRequest(summary: NormalizedBitbucketPullRequestRecord): ChangeRequest { |
| 11 | return { | 12 | return { |
| 12 | provider: "bitbucket", | 13 | provider: SourceControlProviderKind.make("bitbucket"), |
| 13 | number: summary.number, | 14 | number: summary.number, |
| 14 | title: summary.title, | 15 | title: summary.title, |
| 15 | url: summary.url, | 16 | url: summary.url, |
| ... | @@ -34,7 +35,7 @@ export const make = Effect.gen(function* () { | ... | @@ -34,7 +35,7 @@ export const make = Effect.gen(function* () { |
| 34 | const bitbucket = yield* BitbucketApi.BitbucketApi; | 35 | const bitbucket = yield* BitbucketApi.BitbucketApi; |
| 35 | 36 | ||
| 36 | return SourceControlProvider.SourceControlProvider.of({ | 37 | return SourceControlProvider.SourceControlProvider.of({ |
| 37 | kind: "bitbucket", | 38 | kind: SourceControlProviderKind.make("bitbucket"), |
| 38 | listChangeRequests: (input) => { | 39 | listChangeRequests: (input) => { |
| 39 | const source = SourceControlProvider.sourceControlRefFromInput(input); | 40 | const source = SourceControlProvider.sourceControlRefFromInput(input); |
| 40 | return bitbucket | 41 | return bitbucket |
| ... | @@ -51,7 +52,7 @@ export const make = Effect.gen(function* () { | ... | @@ -51,7 +52,7 @@ export const make = Effect.gen(function* () { |
| 51 | Effect.mapError( | 52 | Effect.mapError( |
| 52 | (error) => | 53 | (error) => |
| 53 | new SourceControlProviderError({ | 54 | new SourceControlProviderError({ |
| 54 | provider: "bitbucket", | 55 | provider: SourceControlProviderKind.make("bitbucket"), |
| 55 | operation: "listChangeRequests", | 56 | operation: "listChangeRequests", |
| 56 | cwd: input.cwd, | 57 | cwd: input.cwd, |
| 57 | reference: SourceControlProvider.transportSafeSourceControlErrorValue( | 58 | reference: SourceControlProvider.transportSafeSourceControlErrorValue( |
| ... | @@ -69,7 +70,7 @@ export const make = Effect.gen(function* () { | ... | @@ -69,7 +70,7 @@ export const make = Effect.gen(function* () { |
| 69 | Effect.mapError( | 70 | Effect.mapError( |
| 70 | (error) => | 71 | (error) => |
| 71 | new SourceControlProviderError({ | 72 | new SourceControlProviderError({ |
| 72 | provider: "bitbucket", | 73 | provider: SourceControlProviderKind.make("bitbucket"), |
| 73 | operation: "getChangeRequest", | 74 | operation: "getChangeRequest", |
| 74 | cwd: input.cwd, | 75 | cwd: input.cwd, |
| 75 | reference: SourceControlProvider.transportSafeSourceControlErrorValue( | 76 | reference: SourceControlProvider.transportSafeSourceControlErrorValue( |
| ... | @@ -97,7 +98,7 @@ export const make = Effect.gen(function* () { | ... | @@ -97,7 +98,7 @@ export const make = Effect.gen(function* () { |
| 97 | Effect.mapError( | 98 | Effect.mapError( |
| 98 | (error) => | 99 | (error) => |
| 99 | new SourceControlProviderError({ | 100 | new SourceControlProviderError({ |
| 100 | provider: "bitbucket", | 101 | provider: SourceControlProviderKind.make("bitbucket"), |
| 101 | operation: "createChangeRequest", | 102 | operation: "createChangeRequest", |
| 102 | cwd: input.cwd, | 103 | cwd: input.cwd, |
| 103 | reference: SourceControlProvider.transportSafeSourceControlErrorValue( | 104 | reference: SourceControlProvider.transportSafeSourceControlErrorValue( |
| ... | @@ -114,7 +115,7 @@ export const make = Effect.gen(function* () { | ... | @@ -114,7 +115,7 @@ export const make = Effect.gen(function* () { |
| 114 | Effect.mapError( | 115 | Effect.mapError( |
| 115 | (error) => | 116 | (error) => |
| 116 | new SourceControlProviderError({ | 117 | new SourceControlProviderError({ |
| 117 | provider: "bitbucket", | 118 | provider: SourceControlProviderKind.make("bitbucket"), |
| 118 | operation: "getRepositoryCloneUrls", | 119 | operation: "getRepositoryCloneUrls", |
| 119 | cwd: input.cwd, | 120 | cwd: input.cwd, |
| 120 | repository: SourceControlProvider.transportSafeSourceControlErrorValue( | 121 | repository: SourceControlProvider.transportSafeSourceControlErrorValue( |
| ... | @@ -130,7 +131,7 @@ export const make = Effect.gen(function* () { | ... | @@ -130,7 +131,7 @@ export const make = Effect.gen(function* () { |
| 130 | Effect.mapError( | 131 | Effect.mapError( |
| 131 | (error) => | 132 | (error) => |
| 132 | new SourceControlProviderError({ | 133 | new SourceControlProviderError({ |
| 133 | provider: "bitbucket", | 134 | provider: SourceControlProviderKind.make("bitbucket"), |
| 134 | operation: "createRepository", | 135 | operation: "createRepository", |
| 135 | cwd: input.cwd, | 136 | cwd: input.cwd, |
| 136 | repository: SourceControlProvider.transportSafeSourceControlErrorValue( | 137 | repository: SourceControlProvider.transportSafeSourceControlErrorValue( |
| ... | @@ -151,7 +152,7 @@ export const make = Effect.gen(function* () { | ... | @@ -151,7 +152,7 @@ export const make = Effect.gen(function* () { |
| 151 | Effect.mapError( | 152 | Effect.mapError( |
| 152 | (error) => | 153 | (error) => |
| 153 | new SourceControlProviderError({ | 154 | new SourceControlProviderError({ |
| 154 | provider: "bitbucket", | 155 | provider: SourceControlProviderKind.make("bitbucket"), |
| 155 | operation: "getDefaultBranch", | 156 | operation: "getDefaultBranch", |
| 156 | cwd: input.cwd, | 157 | cwd: input.cwd, |
| 157 | detail: "Failed to get default branch.", | 158 | detail: "Failed to get default branch.", |
| ... | @@ -171,7 +172,7 @@ export const make = Effect.gen(function* () { | ... | @@ -171,7 +172,7 @@ export const make = Effect.gen(function* () { |
| 171 | Effect.mapError( | 172 | Effect.mapError( |
| 172 | (error) => | 173 | (error) => |
| 173 | new SourceControlProviderError({ | 174 | new SourceControlProviderError({ |
| 174 | provider: "bitbucket", | 175 | provider: SourceControlProviderKind.make("bitbucket"), |
| 175 | operation: "checkoutChangeRequest", | 176 | operation: "checkoutChangeRequest", |
| 176 | cwd: input.cwd, | 177 | cwd: input.cwd, |
| 177 | reference: SourceControlProvider.transportSafeSourceControlErrorValue( | 178 | reference: SourceControlProvider.transportSafeSourceControlErrorValue( |
| ... | @@ -190,7 +191,7 @@ export const makeDiscovery = Effect.gen(function* () { | ... | @@ -190,7 +191,7 @@ export const makeDiscovery = Effect.gen(function* () { |
| 190 | 191 | ||
| 191 | return { | 192 | return { |
| 192 | type: "api", | 193 | type: "api", |
| 193 | kind: "bitbucket", | 194 | kind: SourceControlProviderKind.make("bitbucket"), |
| 194 | label: "Bitbucket", | 195 | label: "Bitbucket", |
| 195 | installHint: "Add a Bitbucket token in Settings → Source Control.", | 196 | installHint: "Add a Bitbucket token in Settings → Source Control.", |
| 196 | probeAuth: bitbucket.probeAuth, | 197 | probeAuth: bitbucket.probeAuth, |
packages/source-control-bitbucket/src/server/driver.ts+2-1| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | ||
| 1 | /** | 2 | /** |
| 2 | * The Bitbucket Cloud driver: repository operations, discovery, and pull requests through the | 3 | * The Bitbucket Cloud driver: repository operations, discovery, and pull requests through the |
| 3 | * Bitbucket REST API. | 4 | * Bitbucket REST API. |
| ... | @@ -11,7 +12,7 @@ import * as BitbucketPullRequestProvider from "./BitbucketPullRequestProvider.ts | ... | @@ -11,7 +12,7 @@ import * as BitbucketPullRequestProvider from "./BitbucketPullRequestProvider.ts |
| 11 | import * as BitbucketSourceControlProvider from "./BitbucketSourceControlProvider.ts"; | 12 | import * as BitbucketSourceControlProvider from "./BitbucketSourceControlProvider.ts"; |
| 12 | 13 | ||
| 13 | export const driver = defineSourceControlDriver({ | 14 | export const driver = defineSourceControlDriver({ |
| 14 | kind: "bitbucket", | 15 | kind: SourceControlProviderKind.make("bitbucket"), |
| 15 | make: Effect.all({ | 16 | make: Effect.all({ |
| 16 | sourceControl: BitbucketSourceControlProvider.make, | 17 | sourceControl: BitbucketSourceControlProvider.make, |
| 17 | discovery: BitbucketSourceControlProvider.makeDiscovery, | 18 | discovery: BitbucketSourceControlProvider.makeDiscovery, |
packages/source-control-core/src/server/SourceControlRateLimit.test.ts+17-4| ... | @@ -1,10 +1,11 @@ | ... | @@ -1,10 +1,11 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | ||
| 1 | import { assert, it } from "@effect/vitest"; | 2 | import { assert, it } from "@effect/vitest"; |
| 2 | import * as Effect from "effect/Effect"; | 3 | import * as Effect from "effect/Effect"; |
| 3 | import * as TestClock from "effect/testing/TestClock"; | 4 | import * as TestClock from "effect/testing/TestClock"; |
| 4 | 5 | ||
| 5 | import * as SourceControlRateLimit from "./SourceControlRateLimit.ts"; | 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 | it.effect("isolates cooldowns for verified credentials on the same host", () => | 10 | it.effect("isolates cooldowns for verified credentials on the same host", () => |
| 10 | Effect.gen(function* () { | 11 | Effect.gen(function* () { |
| ... | @@ -41,7 +42,7 @@ it.effect("backs off repeated rate limits until a successful request", () => | ... | @@ -41,7 +42,7 @@ it.effect("backs off repeated rate limits until a successful request", () => |
| 41 | const firstPause = yield* Effect.flip(limits.check(github)); | 42 | const firstPause = yield* Effect.flip(limits.check(github)); |
| 42 | assert.deepInclude(firstPause, { | 43 | assert.deepInclude(firstPause, { |
| 43 | _tag: "SourceControlRateLimitPausedError", | 44 | _tag: "SourceControlRateLimitPausedError", |
| 44 | provider: "github", | 45 | provider: SourceControlProviderKind.make("github"), |
| 45 | host: "github.com", | 46 | host: "github.com", |
| 46 | retryAt: 30_000, | 47 | retryAt: 30_000, |
| 47 | }); | 48 | }); |
| ... | @@ -105,8 +106,20 @@ it.effect("keeps providers and hosts isolated", () => | ... | @@ -105,8 +106,20 @@ it.effect("keeps providers and hosts isolated", () => |
| 105 | const lease = yield* limits.check(github); | 106 | const lease = yield* limits.check(github); |
| 106 | yield* limits.recordRateLimit({ ...github, lease }); | 107 | yield* limits.recordRateLimit({ ...github, lease }); |
| 107 | 108 | ||
| 108 | assert.equal(yield* limits.check({ provider: "gitlab", host: "github.com" }), 0); | 109 | assert.equal( |
| 109 | assert.equal(yield* limits.check({ provider: "github", host: "github.example.com" }), 0); | 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 | }).pipe(Effect.provide(SourceControlRateLimit.layer)), | 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,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | ||
| 1 | import * as Effect from "effect/Effect"; | 2 | import * as Effect from "effect/Effect"; |
| 2 | import * as Schema from "effect/Schema"; | 3 | import * as Schema from "effect/Schema"; |
| 3 | import * as Result from "effect/Result"; | 4 | import * as Result from "effect/Result"; |
| ... | @@ -67,7 +68,7 @@ export const make = Effect.gen(function* () { | ... | @@ -67,7 +68,7 @@ export const make = Effect.gen(function* () { |
| 67 | const cli = yield* ForgejoCli.ForgejoCli; | 68 | const cli = yield* ForgejoCli.ForgejoCli; |
| 68 | const failure = (operation: string, detail: string, cause?: unknown) => | 69 | const failure = (operation: string, detail: string, cause?: unknown) => |
| 69 | new PullRequestProviderError({ | 70 | new PullRequestProviderError({ |
| 70 | provider: "forgejo", | 71 | provider: SourceControlProviderKind.make("forgejo"), |
| 71 | operation, | 72 | operation, |
| 72 | reason: "failed", | 73 | reason: "failed", |
| 73 | detail, | 74 | detail, |
| ... | @@ -78,7 +79,7 @@ export const make = Effect.gen(function* () { | ... | @@ -78,7 +79,7 @@ export const make = Effect.gen(function* () { |
| 78 | Effect.mapError( | 79 | Effect.mapError( |
| 79 | (error) => | 80 | (error) => |
| 80 | new PullRequestProviderError({ | 81 | new PullRequestProviderError({ |
| 81 | provider: "forgejo", | 82 | provider: SourceControlProviderKind.make("forgejo"), |
| 82 | operation: input.path, | 83 | operation: input.path, |
| 83 | detail: error.detail, | 84 | detail: error.detail, |
| 84 | cause: error, | 85 | cause: error, |
| ... | @@ -203,7 +204,7 @@ export const make = Effect.gen(function* () { | ... | @@ -203,7 +204,7 @@ export const make = Effect.gen(function* () { |
| 203 | const unsupported = (operation: string) => | 204 | const unsupported = (operation: string) => |
| 204 | Effect.fail(failure(operation, `Forgejo does not expose ${operation} through its API.`)); | 205 | Effect.fail(failure(operation, `Forgejo does not expose ${operation} through its API.`)); |
| 205 | const provider: PullRequestProviderApi = { | 206 | const provider: PullRequestProviderApi = { |
| 206 | kind: "forgejo", | 207 | kind: SourceControlProviderKind.make("forgejo"), |
| 207 | capabilities: CAPABILITIES, | 208 | capabilities: CAPABILITIES, |
| 208 | getViewer, | 209 | getViewer, |
| 209 | listChangeRequests: Effect.fn("ForgejoPullRequestProvider.listChangeRequests")( | 210 | listChangeRequests: Effect.fn("ForgejoPullRequestProvider.listChangeRequests")( |
packages/source-control-forgejo/src/server/ForgejoSourceControlProvider.test.ts+25-8| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | ||
| 1 | import { assert, it } from "@effect/vitest"; | 2 | import { assert, it } from "@effect/vitest"; |
| 2 | import * as Effect from "effect/Effect"; | 3 | import * as Effect from "effect/Effect"; |
| 3 | import * as FileSystem from "effect/FileSystem"; | 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,7 +259,11 @@ it.effect("gives Forgejo identities the browser URL of the login that serves the |
| 258 | }); | 259 | }); |
| 259 | const served = (baseUrl: string) => () => | 260 | const served = (baseUrl: string) => () => |
| 260 | Effect.succeed({ | 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 | remoteName: "origin", | 267 | remoteName: "origin", |
| 263 | remoteUrl: "", | 268 | remoteUrl: "", |
| 264 | }); | 269 | }); |
| ... | @@ -271,7 +276,7 @@ it.effect("gives Forgejo identities the browser URL of the login that serves the | ... | @@ -271,7 +276,7 @@ it.effect("gives Forgejo identities the browser URL of the login that serves the |
| 271 | }), | 276 | }), |
| 272 | { | 277 | { |
| 273 | ...identity("http://forge.test:3000/git/team/repo.git"), | 278 | ...identity("http://forge.test:3000/git/team/repo.git"), |
| 274 | provider: "forgejo", | 279 | provider: SourceControlProviderKind.make("forgejo"), |
| 275 | webUrl: "http://forge.test:3000/git/team/repo", | 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,7 +562,7 @@ it.effect("discovers Forgejo accounts and retains the server port", () => |
| 557 | cwd: "/repo", | 562 | cwd: "/repo", |
| 558 | context: { | 563 | context: { |
| 559 | provider: { | 564 | provider: { |
| 560 | kind: "unknown", | 565 | kind: SourceControlProviderKind.make("unknown"), |
| 561 | name: "git.forgejo.local", | 566 | name: "git.forgejo.local", |
| 562 | baseUrl: "https://git.forgejo.local", | 567 | baseUrl: "https://git.forgejo.local", |
| 563 | }, | 568 | }, |
| ... | @@ -577,7 +582,7 @@ it.effect("discovers Forgejo accounts and retains the server port", () => | ... | @@ -577,7 +582,7 @@ it.effect("discovers Forgejo accounts and retains the server port", () => |
| 577 | ), | 582 | ), |
| 578 | }); | 583 | }); |
| 579 | assert.deepStrictEqual(refined, { | 584 | assert.deepStrictEqual(refined, { |
| 580 | kind: "forgejo", | 585 | kind: SourceControlProviderKind.make("forgejo"), |
| 581 | name: "Forgejo / Gitea", | 586 | name: "Forgejo / Gitea", |
| 582 | baseUrl: "http://forgejo.local:3000", | 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,7 +627,11 @@ it.effect("does not choose a default Forgejo login across ambiguous SSH server p |
| 622 | const refined = ForgejoSourceControlProvider.discovery.refineUnknownRemote({ | 627 | const refined = ForgejoSourceControlProvider.discovery.refineUnknownRemote({ |
| 623 | cwd: "/repo", | 628 | cwd: "/repo", |
| 624 | context: { | 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 | remoteName: "origin", | 635 | remoteName: "origin", |
| 627 | remoteUrl: "git@forgejo.local:maria/project.git", | 636 | remoteUrl: "git@forgejo.local:maria/project.git", |
| 628 | requestedHost: "forgejo.local:4000", | 637 | requestedHost: "forgejo.local:4000", |
| ... | @@ -732,7 +741,11 @@ it.effect("routes mounted Forgejo repositories without repeating the mount in AP | ... | @@ -732,7 +741,11 @@ it.effect("routes mounted Forgejo repositories without repeating the mount in AP |
| 732 | cwd: "/repo", | 741 | cwd: "/repo", |
| 733 | repository: "forgejo/maria/project", | 742 | repository: "forgejo/maria/project", |
| 734 | context: { | 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 | remoteName: "origin", | 749 | remoteName: "origin", |
| 737 | remoteUrl: "https://code.test/forgejo/maria/project.git", | 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,7 +757,11 @@ it.effect("routes mounted Forgejo repositories without repeating the mount in AP |
| 744 | cwd: "/repo", | 757 | cwd: "/repo", |
| 745 | repository: "forgejo/project", | 758 | repository: "forgejo/project", |
| 746 | context: { | 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 | remoteName: "origin", | 765 | remoteName: "origin", |
| 749 | remoteUrl: "ssh://git@code.test/forgejo/project.git", | 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,7 +844,7 @@ it.effect("prefers fj for HTTP and ported SSH aliases on root servers", () => { |
| 827 | repository: "maria/project", | 844 | repository: "maria/project", |
| 828 | context: { | 845 | context: { |
| 829 | provider: { | 846 | provider: { |
| 830 | kind: "forgejo", | 847 | kind: SourceControlProviderKind.make("forgejo"), |
| 831 | name: "Forgejo", | 848 | name: "Forgejo", |
| 832 | baseUrl: "http://forgejo.local:3000", | 849 | baseUrl: "http://forgejo.local:3000", |
| 833 | }, | 850 | }, |
packages/source-control-forgejo/src/server/ForgejoSourceControlProvider.ts+24-8| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | ||
| 1 | import * as Effect from "effect/Effect"; | 2 | import * as Effect from "effect/Effect"; |
| 2 | import * as FileSystem from "effect/FileSystem"; | 3 | import * as FileSystem from "effect/FileSystem"; |
| 3 | import * as Schema from "effect/Schema"; | 4 | import * as Schema from "effect/Schema"; |
| ... | @@ -18,7 +19,7 @@ const isForgejoCliError = Schema.is(ForgejoCli.ForgejoCliError); | ... | @@ -18,7 +19,7 @@ const isForgejoCliError = Schema.is(ForgejoCli.ForgejoCliError); |
| 18 | 19 | ||
| 19 | export const discovery = { | 20 | export const discovery = { |
| 20 | type: "cli", | 21 | type: "cli", |
| 21 | kind: "forgejo", | 22 | kind: SourceControlProviderKind.make("forgejo"), |
| 22 | label: "Forgejo / Gitea", | 23 | label: "Forgejo / Gitea", |
| 23 | executable: "tea", | 24 | executable: "tea", |
| 24 | versionArgs: ["--version"], | 25 | versionArgs: ["--version"], |
| ... | @@ -47,7 +48,13 @@ export const discovery = { | ... | @@ -47,7 +48,13 @@ export const discovery = { |
| 47 | remote, | 48 | remote, |
| 48 | input.context.requestedHost, | 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 | installHint: | 59 | installHint: |
| 53 | "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 | "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,7 +67,7 @@ export const makeDiscovery = Effect.gen(function* () { |
| 60 | if (!listLogins) return discovery; | 67 | if (!listLogins) return discovery; |
| 61 | return { | 68 | return { |
| 62 | type: "managed-cli", | 69 | type: "managed-cli", |
| 63 | kind: "forgejo", | 70 | kind: SourceControlProviderKind.make("forgejo"), |
| 64 | label: discovery.label, | 71 | label: discovery.label, |
| 65 | installHint: discovery.installHint, | 72 | installHint: discovery.installHint, |
| 66 | probe: Effect.fn("ForgejoSourceControlProvider.discovery")(function* (cwd: string) { | 73 | probe: Effect.fn("ForgejoSourceControlProvider.discovery")(function* (cwd: string) { |
| ... | @@ -150,7 +157,12 @@ export const makeDiscovery = Effect.gen(function* () { | ... | @@ -150,7 +157,12 @@ export const makeDiscovery = Effect.gen(function* () { |
| 150 | remoteUrl: input.context.remoteUrl, | 157 | remoteUrl: input.context.remoteUrl, |
| 151 | }).pipe(Effect.orElseSucceed(() => [])); | 158 | }).pipe(Effect.orElseSucceed(() => [])); |
| 152 | const login = ForgejoCli.matchForgejoLogin(logins, remote, input.context.requestedHost); | 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 | return null; | 167 | return null; |
| 156 | }, | 168 | }, |
| ... | @@ -188,7 +200,7 @@ const refineRepositoryIdentity: NonNullable< | ... | @@ -188,7 +200,7 @@ const refineRepositoryIdentity: NonNullable< |
| 188 | const context = yield* resolveContext({ | 200 | const context = yield* resolveContext({ |
| 189 | cwd: identity.rootPath, | 201 | cwd: identity.rootPath, |
| 190 | context: { | 202 | context: { |
| 191 | provider: { kind: "unknown", name: "Unknown", baseUrl: "" }, | 203 | provider: { kind: SourceControlProviderKind.make("unknown"), name: "Unknown", baseUrl: "" }, |
| 192 | remoteName: identity.locator.remoteName, | 204 | remoteName: identity.locator.remoteName, |
| 193 | remoteUrl: identity.locator.remoteUrl, | 205 | remoteUrl: identity.locator.remoteUrl, |
| 194 | }, | 206 | }, |
| ... | @@ -200,7 +212,11 @@ const refineRepositoryIdentity: NonNullable< | ... | @@ -200,7 +212,11 @@ const refineRepositoryIdentity: NonNullable< |
| 200 | !remote.ssh && basePath && remote.path.startsWith(`${basePath}/`) | 212 | !remote.ssh && basePath && remote.path.startsWith(`${basePath}/`) |
| 201 | ? remote.path.slice(basePath.length + 1) | 213 | ? remote.path.slice(basePath.length + 1) |
| 202 | : remote.path; | 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 | const RepositorySchema = Schema.Struct({ | 222 | const RepositorySchema = Schema.Struct({ |
| ... | @@ -245,7 +261,7 @@ export const make = Effect.gen(function* () { | ... | @@ -245,7 +261,7 @@ export const make = Effect.gen(function* () { |
| 245 | Effect.mapError( | 261 | Effect.mapError( |
| 246 | (cause: unknown) => | 262 | (cause: unknown) => |
| 247 | new SourceControlProviderError({ | 263 | new SourceControlProviderError({ |
| 248 | provider: "forgejo", | 264 | provider: SourceControlProviderKind.make("forgejo"), |
| 249 | operation, | 265 | operation, |
| 250 | cwd, | 266 | cwd, |
| 251 | ...(isForgejoCliError(cause) ? { command: cause.command } : {}), | 267 | ...(isForgejoCliError(cause) ? { command: cause.command } : {}), |
| ... | @@ -272,7 +288,7 @@ export const make = Effect.gen(function* () { | ... | @@ -272,7 +288,7 @@ export const make = Effect.gen(function* () { |
| 272 | ); | 288 | ); |
| 273 | }); | 289 | }); |
| 274 | return SourceControlProvider.SourceControlProvider.of({ | 290 | return SourceControlProvider.SourceControlProvider.of({ |
| 275 | kind: "forgejo", | 291 | kind: SourceControlProviderKind.make("forgejo"), |
| 276 | repositoryNameFromRemoteUrl, | 292 | repositoryNameFromRemoteUrl, |
| 277 | refineRepositoryIdentity, | 293 | refineRepositoryIdentity, |
| 278 | listChangeRequests: (input) => | 294 | listChangeRequests: (input) => |
packages/source-control-forgejo/src/server/driver.ts+2-1| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | ||
| 1 | /** | 2 | /** |
| 2 | * The Forgejo / Gitea driver: repository operations, discovery, and pull requests through | 3 | * The Forgejo / Gitea driver: repository operations, discovery, and pull requests through |
| 3 | * `fj` or `tea`. | 4 | * `fj` or `tea`. |
| ... | @@ -11,7 +12,7 @@ import * as ForgejoPullRequestProvider from "./ForgejoPullRequestProvider.ts"; | ... | @@ -11,7 +12,7 @@ import * as ForgejoPullRequestProvider from "./ForgejoPullRequestProvider.ts"; |
| 11 | import * as ForgejoSourceControlProvider from "./ForgejoSourceControlProvider.ts"; | 12 | import * as ForgejoSourceControlProvider from "./ForgejoSourceControlProvider.ts"; |
| 12 | 13 | ||
| 13 | export const driver = defineSourceControlDriver({ | 14 | export const driver = defineSourceControlDriver({ |
| 14 | kind: "forgejo", | 15 | kind: SourceControlProviderKind.make("forgejo"), |
| 15 | make: Effect.all({ | 16 | make: Effect.all({ |
| 16 | sourceControl: ForgejoSourceControlProvider.make, | 17 | sourceControl: ForgejoSourceControlProvider.make, |
| 17 | discovery: ForgejoSourceControlProvider.makeDiscovery, | 18 | discovery: ForgejoSourceControlProvider.makeDiscovery, |
packages/source-control-forgejo/src/server/forgejoPullRequests.ts+2-1| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | ||
| 1 | import * as Schema from "effect/Schema"; | 2 | import * as Schema from "effect/Schema"; |
| 2 | import * as Option from "effect/Option"; | 3 | import * as Option from "effect/Option"; |
| 3 | import type { ChangeRequest } from "@t3tools/contracts"; | 4 | import type { ChangeRequest } from "@t3tools/contracts"; |
| ... | @@ -26,7 +27,7 @@ export const ForgejoPullRequestSchema = Schema.Struct({ | ... | @@ -26,7 +27,7 @@ export const ForgejoPullRequestSchema = Schema.Struct({ |
| 26 | }); | 27 | }); |
| 27 | export function toForgejoChangeRequest(raw: typeof ForgejoPullRequestSchema.Type): ChangeRequest { | 28 | export function toForgejoChangeRequest(raw: typeof ForgejoPullRequestSchema.Type): ChangeRequest { |
| 28 | return { | 29 | return { |
| 29 | provider: "forgejo", | 30 | provider: SourceControlProviderKind.make("forgejo"), |
| 30 | number: raw.number, | 31 | number: raw.number, |
| 31 | title: raw.title, | 32 | title: raw.title, |
| 32 | url: raw.html_url, | 33 | url: raw.html_url, |
packages/source-control-gitcafe/src/server/GitCafePullRequestProvider.ts+4-3| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | ||
| 1 | import * as Effect from "effect/Effect"; | 2 | import * as Effect from "effect/Effect"; |
| 2 | import * as Option from "effect/Option"; | 3 | import * as Option from "effect/Option"; |
| 3 | import * as Schema from "effect/Schema"; | 4 | import * as Schema from "effect/Schema"; |
| ... | @@ -176,7 +177,7 @@ export const make = Effect.gen(function* () { | ... | @@ -176,7 +177,7 @@ export const make = Effect.gen(function* () { |
| 176 | 177 | ||
| 177 | const failure = (operation: string, detail: string, cause?: unknown) => | 178 | const failure = (operation: string, detail: string, cause?: unknown) => |
| 178 | new PullRequestProviderError({ | 179 | new PullRequestProviderError({ |
| 179 | provider: "gitcafe", | 180 | provider: SourceControlProviderKind.make("gitcafe"), |
| 180 | operation, | 181 | operation, |
| 181 | reason: "failed", | 182 | reason: "failed", |
| 182 | detail, | 183 | detail, |
| ... | @@ -207,7 +208,7 @@ export const make = Effect.gen(function* () { | ... | @@ -207,7 +208,7 @@ export const make = Effect.gen(function* () { |
| 207 | Effect.mapError( | 208 | Effect.mapError( |
| 208 | (error) => | 209 | (error) => |
| 209 | new PullRequestProviderError({ | 210 | new PullRequestProviderError({ |
| 210 | provider: "gitcafe", | 211 | provider: SourceControlProviderKind.make("gitcafe"), |
| 211 | operation, | 212 | operation, |
| 212 | reason: error.reason, | 213 | reason: error.reason, |
| 213 | detail: error.detail, | 214 | detail: error.detail, |
| ... | @@ -475,7 +476,7 @@ export const make = Effect.gen(function* () { | ... | @@ -475,7 +476,7 @@ export const make = Effect.gen(function* () { |
| 475 | }); | 476 | }); |
| 476 | 477 | ||
| 477 | const provider: PullRequestProviderApi = { | 478 | const provider: PullRequestProviderApi = { |
| 478 | kind: "gitcafe", | 479 | kind: SourceControlProviderKind.make("gitcafe"), |
| 479 | capabilities: CAPABILITIES, | 480 | capabilities: CAPABILITIES, |
| 480 | getViewer: (input) => | 481 | getViewer: (input) => |
| 481 | read( | 482 | read( |
packages/source-control-gitcafe/src/server/GitCafeSourceControlProvider.test.ts+11-2| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | ||
| 1 | /** | 2 | /** |
| 2 | * Pins `./GitCafeSourceControlProvider.ts`, which pass 2 creates: | 3 | * Pins `./GitCafeSourceControlProvider.ts`, which pass 2 creates: |
| 3 | * | 4 | * |
| ... | @@ -87,7 +88,11 @@ describe("GitCafeSourceControlProvider", () => { | ... | @@ -87,7 +88,11 @@ describe("GitCafeSourceControlProvider", () => { |
| 87 | ); | 88 | ); |
| 88 | }); | 89 | }); |
| 89 | const context = { | 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 | remoteName: "origin", | 96 | remoteName: "origin", |
| 92 | remoteUrl, | 97 | remoteUrl, |
| 93 | }; | 98 | }; |
| ... | @@ -220,7 +225,11 @@ describe("GitCafeSourceControlProvider", () => { | ... | @@ -220,7 +225,11 @@ describe("GitCafeSourceControlProvider", () => { |
| 220 | remoteUrl === undefined | 225 | remoteUrl === undefined |
| 221 | ? undefined | 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 | remoteName: "origin", | 233 | remoteName: "origin", |
| 225 | remoteUrl, | 234 | remoteUrl, |
| 226 | }; | 235 | }; |
packages/source-control-gitcafe/src/server/GitCafeSourceControlProvider.ts+5-4| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | ||
| 1 | import * as Effect from "effect/Effect"; | 2 | import * as Effect from "effect/Effect"; |
| 2 | import * as Option from "effect/Option"; | 3 | import * as Option from "effect/Option"; |
| 3 | import * as Result from "effect/Result"; | 4 | import * as Result from "effect/Result"; |
| ... | @@ -59,7 +60,7 @@ const cliFailure = (output: string) => | ... | @@ -59,7 +60,7 @@ const cliFailure = (output: string) => |
| 59 | 60 | ||
| 60 | export const discovery = { | 61 | export const discovery = { |
| 61 | type: "cli", | 62 | type: "cli", |
| 62 | kind: "gitcafe", | 63 | kind: SourceControlProviderKind.make("gitcafe"), |
| 63 | label: "GitCafe", | 64 | label: "GitCafe", |
| 64 | executable: "cafe", | 65 | executable: "cafe", |
| 65 | versionArgs: ["--version"], | 66 | versionArgs: ["--version"], |
| ... | @@ -150,7 +151,7 @@ export const make = Effect.gen(function* () { | ... | @@ -150,7 +151,7 @@ export const make = Effect.gen(function* () { |
| 150 | 151 | ||
| 151 | const error = (operation: string, cwd: string, detail: string, cause?: unknown) => | 152 | const error = (operation: string, cwd: string, detail: string, cause?: unknown) => |
| 152 | new SourceControlProviderError({ | 153 | new SourceControlProviderError({ |
| 153 | provider: "gitcafe", | 154 | provider: SourceControlProviderKind.make("gitcafe"), |
| 154 | operation, | 155 | operation, |
| 155 | cwd, | 156 | cwd, |
| 156 | detail, | 157 | detail, |
| ... | @@ -269,7 +270,7 @@ export const make = Effect.gen(function* () { | ... | @@ -269,7 +270,7 @@ export const make = Effect.gen(function* () { |
| 269 | ); | 270 | ); |
| 270 | const owner = target.repository.split("/")[0] ?? null; | 271 | const owner = target.repository.split("/")[0] ?? null; |
| 271 | return { | 272 | return { |
| 272 | provider: "gitcafe", | 273 | provider: SourceControlProviderKind.make("gitcafe"), |
| 273 | number: pull.number, | 274 | number: pull.number, |
| 274 | title: pull.title, | 275 | title: pull.title, |
| 275 | url: `https://${target.host}/${target.repository}/pulls/${pull.number}`, | 276 | url: `https://${target.host}/${target.repository}/pulls/${pull.number}`, |
| ... | @@ -290,7 +291,7 @@ export const make = Effect.gen(function* () { | ... | @@ -290,7 +291,7 @@ export const make = Effect.gen(function* () { |
| 290 | }); | 291 | }); |
| 291 | 292 | ||
| 292 | return SourceControlProvider.SourceControlProvider.of({ | 293 | return SourceControlProvider.SourceControlProvider.of({ |
| 293 | kind: "gitcafe", | 294 | kind: SourceControlProviderKind.make("gitcafe"), |
| 294 | repositoryNameFromRemoteUrl: (url) => GitCafeHosts.parseGitCafeRemote(url)?.repository ?? null, | 295 | repositoryNameFromRemoteUrl: (url) => GitCafeHosts.parseGitCafeRemote(url)?.repository ?? null, |
| 295 | getChangeRequest: getPull, | 296 | getChangeRequest: getPull, |
| 296 | listChangeRequests: Effect.fn("GitCafeSourceControlProvider.listChangeRequests")( | 297 | listChangeRequests: Effect.fn("GitCafeSourceControlProvider.listChangeRequests")( |
packages/source-control-gitcafe/src/server/driver.ts+2-1| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | ||
| 1 | /** | 2 | /** |
| 2 | * The GitCafe driver: repository operations and discovery through `cafe` and git, and pull | 3 | * The GitCafe driver: repository operations and discovery through `cafe` and git, and pull |
| 3 | * requests through GitCafe's REST API on git.cafe and staging.git.cafe. | 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,7 +12,7 @@ import * as GitCafePullRequestProvider from "./GitCafePullRequestProvider.ts"; |
| 11 | import * as GitCafeSourceControlProvider from "./GitCafeSourceControlProvider.ts"; | 12 | import * as GitCafeSourceControlProvider from "./GitCafeSourceControlProvider.ts"; |
| 12 | 13 | ||
| 13 | export const driver = defineSourceControlDriver({ | 14 | export const driver = defineSourceControlDriver({ |
| 14 | kind: "gitcafe", | 15 | kind: SourceControlProviderKind.make("gitcafe"), |
| 15 | make: Effect.all({ | 16 | make: Effect.all({ |
| 16 | sourceControl: GitCafeSourceControlProvider.make, | 17 | sourceControl: GitCafeSourceControlProvider.make, |
| 17 | discovery: Effect.succeed(GitCafeSourceControlProvider.discovery), | 18 | discovery: Effect.succeed(GitCafeSourceControlProvider.discovery), |
packages/source-control-github/src/server/GitHubApi.ts+2-1| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | ||
| 1 | import * as Clock from "effect/Clock"; | 2 | import * as Clock from "effect/Clock"; |
| 2 | import * as Context from "effect/Context"; | 3 | import * as Context from "effect/Context"; |
| 3 | import * as Data from "effect/Data"; | 4 | import * as Data from "effect/Data"; |
| ... | @@ -389,7 +390,7 @@ export const make = Effect.gen(function* () { | ... | @@ -389,7 +390,7 @@ export const make = Effect.gen(function* () { |
| 389 | // this reads today is `core`; a `search/` read would need its own resource here. A refusal | 390 | // this reads today is `core`; a `search/` read would need its own resource here. A refusal |
| 390 | // still pauses the whole host, the key PullRequestService records its own backoff under. | 391 | // still pauses the whole host, the key PullRequestService records its own backoff under. |
| 391 | const resource = input.graphql === true ? "graphql" : "core"; | 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 | const run = Effect.gen(function* () { | 394 | const run = Effect.gen(function* () { |
| 394 | const lease = yield* quota | 395 | const lease = yield* quota |
| 395 | .admit(host, resource, { allowReserve: input.allowReserve }) | 396 | .admit(host, resource, { allowReserve: input.allowReserve }) |
packages/source-control-github/src/server/GitHubPullRequestProvider.ts+3-2| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | ||
| 1 | import { removeAgentCredits } from "@t3tools/source-control-core/server/mergeMessage"; | 2 | import { removeAgentCredits } from "@t3tools/source-control-core/server/mergeMessage"; |
| 2 | import * as Effect from "effect/Effect"; | 3 | import * as Effect from "effect/Effect"; |
| 3 | import type { | 4 | import type { |
| ... | @@ -202,7 +203,7 @@ export const make = Effect.gen(function* () { | ... | @@ -202,7 +203,7 @@ export const make = Effect.gen(function* () { |
| 202 | 203 | ||
| 203 | const fail = (operation: string) => (error: GitHubPullRequestApi.GitHubPullRequestApiError) => | 204 | const fail = (operation: string) => (error: GitHubPullRequestApi.GitHubPullRequestApiError) => |
| 204 | new PullRequestProviderError({ | 205 | new PullRequestProviderError({ |
| 205 | provider: "github", | 206 | provider: SourceControlProviderKind.make("github"), |
| 206 | operation, | 207 | operation, |
| 207 | ...gitHubProviderFailure(error), | 208 | ...gitHubProviderFailure(error), |
| 208 | detail: error.message, | 209 | detail: error.message, |
| ... | @@ -261,7 +262,7 @@ export const make = Effect.gen(function* () { | ... | @@ -261,7 +262,7 @@ export const make = Effect.gen(function* () { |
| 261 | ); | 262 | ); |
| 262 | 263 | ||
| 263 | const provider: PullRequestProviderApi = { | 264 | const provider: PullRequestProviderApi = { |
| 264 | kind: "github", | 265 | kind: SourceControlProviderKind.make("github"), |
| 265 | capabilities: CAPABILITIES, | 266 | capabilities: CAPABILITIES, |
| 266 | mergeMessageRewrite: removeAgentCredits, | 267 | mergeMessageRewrite: removeAgentCredits, |
| 267 | getRoutingIdentity: (input) => | 268 | getRoutingIdentity: (input) => |
packages/source-control-github/src/server/GitHubQuota.ts+2-1| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | ||
| 1 | import * as Clock from "effect/Clock"; | 2 | import * as Clock from "effect/Clock"; |
| 2 | import * as Context from "effect/Context"; | 3 | import * as Context from "effect/Context"; |
| 3 | import * as Effect from "effect/Effect"; | 4 | import * as Effect from "effect/Effect"; |
| ... | @@ -86,7 +87,7 @@ const make = Effect.gen(function* () { | ... | @@ -86,7 +87,7 @@ const make = Effect.gen(function* () { |
| 86 | const floor = options?.allowReserve === true ? 1 : snapshot.limit * RESERVE_RATIO; | 87 | const floor = options?.allowReserve === true ? 1 : snapshot.limit * RESERVE_RATIO; |
| 87 | if (snapshot.remaining >= floor) return; | 88 | if (snapshot.remaining >= floor) return; |
| 88 | return yield* new SourceControlRateLimit.SourceControlRateLimitPausedError({ | 89 | return yield* new SourceControlRateLimit.SourceControlRateLimitPausedError({ |
| 89 | provider: "github", | 90 | provider: SourceControlProviderKind.make("github"), |
| 90 | host: host.trim().toLowerCase(), | 91 | host: host.trim().toLowerCase(), |
| 91 | retryAt: snapshot.resetAtMs, | 92 | retryAt: snapshot.resetAtMs, |
| 92 | }); | 93 | }); |
packages/source-control-github/src/server/GitHubSourceControlProvider.test.ts+12-3| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | ||
| 1 | import * as HostProcess from "@t3tools/shared/HostProcess"; | 2 | import * as HostProcess from "@t3tools/shared/HostProcess"; |
| 2 | import { assert, describe, it } from "@effect/vitest"; | 3 | import { assert, describe, it } from "@effect/vitest"; |
| 3 | import * as NodeServices from "@effect/platform-node/NodeServices"; | 4 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| ... | @@ -137,7 +138,11 @@ const makeProvider = (api: Partial<GitHubApi.GitHubApi["Service"]>) => | ... | @@ -137,7 +138,11 @@ const makeProvider = (api: Partial<GitHubApi.GitHubApi["Service"]>) => |
| 137 | GitHubSourceControlProvider.make.pipe(Effect.provide(harness({ remotes: "", api }).layer)); | 138 | GitHubSourceControlProvider.make.pipe(Effect.provide(harness({ remotes: "", api }).layer)); |
| 138 | 139 | ||
| 139 | const githubContext = (host: string) => ({ | 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 | remoteName: "origin", | 146 | remoteName: "origin", |
| 142 | remoteUrl: `git@${host}:acme/web.git`, | 147 | remoteUrl: `git@${host}:acme/web.git`, |
| 143 | }); | 148 | }); |
| ... | @@ -1067,7 +1072,7 @@ it.live.each([ | ... | @@ -1067,7 +1072,7 @@ it.live.each([ |
| 1067 | cwd: "/repo", | 1072 | cwd: "/repo", |
| 1068 | context: { | 1073 | context: { |
| 1069 | provider: { | 1074 | provider: { |
| 1070 | kind: "github" as const, | 1075 | kind: SourceControlProviderKind.make("github"), |
| 1071 | name: "GitHub Self-Hosted", | 1076 | name: "GitHub Self-Hosted", |
| 1072 | baseUrl: "https://code.example.test", | 1077 | baseUrl: "https://code.example.test", |
| 1073 | }, | 1078 | }, |
| ... | @@ -1184,7 +1189,11 @@ it.effect.each([ | ... | @@ -1184,7 +1189,11 @@ it.effect.each([ |
| 1184 | const input = { | 1189 | const input = { |
| 1185 | cwd: "/repo", | 1190 | cwd: "/repo", |
| 1186 | context: { | 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 | remoteName: "origin", | 1197 | remoteName: "origin", |
| 1189 | remoteUrl: `git@${host}:me/web.git`, | 1198 | remoteUrl: `git@${host}:me/web.git`, |
| 1190 | }, | 1199 | }, |
packages/source-control-github/src/server/GitHubSourceControlProvider.ts+8-7| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | ||
| 1 | import * as Context from "effect/Context"; | 2 | import * as Context from "effect/Context"; |
| 2 | import * as Data from "effect/Data"; | 3 | import * as Data from "effect/Data"; |
| 3 | import * as Effect from "effect/Effect"; | 4 | import * as Effect from "effect/Effect"; |
| ... | @@ -62,7 +63,7 @@ const decodeLinkSubject = Schema.decodeUnknownEffect( | ... | @@ -62,7 +63,7 @@ const decodeLinkSubject = Schema.decodeUnknownEffect( |
| 62 | ); | 63 | ); |
| 63 | 64 | ||
| 64 | function toChangeRequest(record: NormalizedGitHubPullRequestRecord): ChangeRequest { | 65 | function toChangeRequest(record: NormalizedGitHubPullRequestRecord): ChangeRequest { |
| 65 | return { provider: "github", ...record }; | 66 | return { provider: SourceControlProviderKind.make("github"), ...record }; |
| 66 | } | 67 | } |
| 67 | 68 | ||
| 68 | function authAccounts(accounts: ReadonlyArray<GitHubAuthStatusAccount>) { | 69 | function authAccounts(accounts: ReadonlyArray<GitHubAuthStatusAccount>) { |
| ... | @@ -164,7 +165,7 @@ export function parseGitHubAuth( | ... | @@ -164,7 +165,7 @@ export function parseGitHubAuth( |
| 164 | 165 | ||
| 165 | export const discovery = { | 166 | export const discovery = { |
| 166 | type: "cli", | 167 | type: "cli", |
| 167 | kind: "github", | 168 | kind: SourceControlProviderKind.make("github"), |
| 168 | label: "GitHub", | 169 | label: "GitHub", |
| 169 | executable: "gh", | 170 | executable: "gh", |
| 170 | versionArgs: ["--version"], | 171 | versionArgs: ["--version"], |
| ... | @@ -271,7 +272,7 @@ export const makeDiscovery = Effect.gen(function* () { | ... | @@ -271,7 +272,7 @@ export const makeDiscovery = Effect.gen(function* () { |
| 271 | Effect.map((known) => | 272 | Effect.map((known) => |
| 272 | known | 273 | known |
| 273 | ? ({ | 274 | ? ({ |
| 274 | kind: "github", | 275 | kind: SourceControlProviderKind.make("github"), |
| 275 | name: "GitHub Self-Hosted", | 276 | name: "GitHub Self-Hosted", |
| 276 | baseUrl: context.provider.baseUrl, | 277 | baseUrl: context.provider.baseUrl, |
| 277 | } as const) | 278 | } as const) |
| ... | @@ -878,7 +879,7 @@ export const make = Effect.gen(function* () { | ... | @@ -878,7 +879,7 @@ export const make = Effect.gen(function* () { |
| 878 | ) => | 879 | ) => |
| 879 | (error: GitHubFailure) => | 880 | (error: GitHubFailure) => |
| 880 | new SourceControlProviderError({ | 881 | new SourceControlProviderError({ |
| 881 | provider: "github", | 882 | provider: SourceControlProviderKind.make("github"), |
| 882 | operation, | 883 | operation, |
| 883 | cwd, | 884 | cwd, |
| 884 | ...(context?.reference === undefined | 885 | ...(context?.reference === undefined |
| ... | @@ -915,7 +916,7 @@ export const make = Effect.gen(function* () { | ... | @@ -915,7 +916,7 @@ export const make = Effect.gen(function* () { |
| 915 | Effect.mapError( | 916 | Effect.mapError( |
| 916 | (cause) => | 917 | (cause) => |
| 917 | new SourceControlProviderError({ | 918 | new SourceControlProviderError({ |
| 918 | provider: "github", | 919 | provider: SourceControlProviderKind.make("github"), |
| 919 | operation: "resolveLink", | 920 | operation: "resolveLink", |
| 920 | cwd: input.cwd, | 921 | cwd: input.cwd, |
| 921 | detail: "The linked subject could not be read.", | 922 | detail: "The linked subject could not be read.", |
| ... | @@ -927,7 +928,7 @@ export const make = Effect.gen(function* () { | ... | @@ -927,7 +928,7 @@ export const make = Effect.gen(function* () { |
| 927 | Effect.mapError( | 928 | Effect.mapError( |
| 928 | (cause) => | 929 | (cause) => |
| 929 | new SourceControlProviderError({ | 930 | new SourceControlProviderError({ |
| 930 | provider: "github", | 931 | provider: SourceControlProviderKind.make("github"), |
| 931 | operation: "resolveLink.decode", | 932 | operation: "resolveLink.decode", |
| 932 | cwd: input.cwd, | 933 | cwd: input.cwd, |
| 933 | detail: "The linked subject could not be read.", | 934 | detail: "The linked subject could not be read.", |
| ... | @@ -939,7 +940,7 @@ export const make = Effect.gen(function* () { | ... | @@ -939,7 +940,7 @@ export const make = Effect.gen(function* () { |
| 939 | }); | 940 | }); |
| 940 | 941 | ||
| 941 | return SourceControlProvider.SourceControlProvider.of({ | 942 | return SourceControlProvider.SourceControlProvider.of({ |
| 942 | kind: "github", | 943 | kind: SourceControlProviderKind.make("github"), |
| 943 | // `gh pr list --head` filters on the head ref name alone and accepts anything, so an | 944 | // `gh pr list --head` filters on the head ref name alone and accepts anything, so an |
| 944 | // `owner:branch` or `remote:branch` selector silently lists zero pull requests while | 945 | // `owner:branch` or `remote:branch` selector silently lists zero pull requests while |
| 945 | // spending a GraphQL call; the bare branch is always among the selectors. Without the owner, | 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,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | ||
| 1 | /** | 2 | /** |
| 2 | * The GitHub driver: repository operations and discovery through `gh` and git, and pull requests | 3 | * The GitHub driver: repository operations and discovery through `gh` and git, and pull requests |
| 3 | * through the GitHub REST and GraphQL APIs. | 4 | * through the GitHub REST and GraphQL APIs. |
| ... | @@ -11,7 +12,7 @@ import * as GitHubPullRequestProvider from "./GitHubPullRequestProvider.ts"; | ... | @@ -11,7 +12,7 @@ import * as GitHubPullRequestProvider from "./GitHubPullRequestProvider.ts"; |
| 11 | import * as GitHubSourceControlProvider from "./GitHubSourceControlProvider.ts"; | 12 | import * as GitHubSourceControlProvider from "./GitHubSourceControlProvider.ts"; |
| 12 | 13 | ||
| 13 | export const driver = defineSourceControlDriver({ | 14 | export const driver = defineSourceControlDriver({ |
| 14 | kind: "github", | 15 | kind: SourceControlProviderKind.make("github"), |
| 15 | make: Effect.all({ | 16 | make: Effect.all({ |
| 16 | sourceControl: GitHubSourceControlProvider.make, | 17 | sourceControl: GitHubSourceControlProvider.make, |
| 17 | discovery: GitHubSourceControlProvider.makeDiscovery, | 18 | discovery: GitHubSourceControlProvider.makeDiscovery, |
packages/source-control-gitlab/src/server/GitLabPullRequestProvider.ts+3-2| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | ||
| 1 | import * as Effect from "effect/Effect"; | 2 | import * as Effect from "effect/Effect"; |
| 2 | import type { | 3 | import type { |
| 3 | PullRequestCapabilities, | 4 | PullRequestCapabilities, |
| ... | @@ -111,7 +112,7 @@ export const make = Effect.gen(function* () { | ... | @@ -111,7 +112,7 @@ export const make = Effect.gen(function* () { |
| 111 | 112 | ||
| 112 | const fail = (operation: string) => (error: GitLabPullRequestCli.GitLabPullRequestCliError) => | 113 | const fail = (operation: string) => (error: GitLabPullRequestCli.GitLabPullRequestCliError) => |
| 113 | new PullRequestProviderError({ | 114 | new PullRequestProviderError({ |
| 114 | provider: "gitlab", | 115 | provider: SourceControlProviderKind.make("gitlab"), |
| 115 | operation, | 116 | operation, |
| 116 | ...gitLabProviderFailure(error), | 117 | ...gitLabProviderFailure(error), |
| 117 | detail: error.detail, | 118 | detail: error.detail, |
| ... | @@ -119,7 +120,7 @@ export const make = Effect.gen(function* () { | ... | @@ -119,7 +120,7 @@ export const make = Effect.gen(function* () { |
| 119 | }); | 120 | }); |
| 120 | 121 | ||
| 121 | const provider: PullRequestProviderApi = { | 122 | const provider: PullRequestProviderApi = { |
| 122 | kind: "gitlab", | 123 | kind: SourceControlProviderKind.make("gitlab"), |
| 123 | capabilities: CAPABILITIES, | 124 | capabilities: CAPABILITIES, |
| 124 | 125 | ||
| 125 | getViewer: (input) => | 126 | getViewer: (input) => |
packages/source-control-gitlab/src/server/GitLabSourceControlProvider.test.ts+3-2| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | ||
| 1 | import { assert, it } from "@effect/vitest"; | 2 | import { assert, it } from "@effect/vitest"; |
| 2 | import * as Effect from "effect/Effect"; | 3 | import * as Effect from "effect/Effect"; |
| 3 | import * as Layer from "effect/Layer"; | 4 | import * as Layer from "effect/Layer"; |
| ... | @@ -38,7 +39,7 @@ it.effect("maps GitLab MR summaries into provider-neutral change requests", () = | ... | @@ -38,7 +39,7 @@ it.effect("maps GitLab MR summaries into provider-neutral change requests", () = |
| 38 | }); | 39 | }); |
| 39 | 40 | ||
| 40 | assert.deepStrictEqual(changeRequest, { | 41 | assert.deepStrictEqual(changeRequest, { |
| 41 | provider: "gitlab", | 42 | provider: SourceControlProviderKind.make("gitlab"), |
| 42 | number: 42, | 43 | number: 42, |
| 43 | title: "Add GitLab provider", | 44 | title: "Add GitLab provider", |
| 44 | url: "https://gitlab.com/pingdotgg/t3code/-/merge_requests/42", | 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,7 +190,7 @@ it("refines unknown GitLab remotes with mixed-case provider hosts", () => { |
| 189 | cwd: "/repo", | 190 | cwd: "/repo", |
| 190 | context: { | 191 | context: { |
| 191 | provider: { | 192 | provider: { |
| 192 | kind: "unknown", | 193 | kind: SourceControlProviderKind.make("unknown"), |
| 193 | name: "Self-Hosted.Example.Test", | 194 | name: "Self-Hosted.Example.Test", |
| 194 | baseUrl: "https://Self-Hosted.Example.Test", | 195 | baseUrl: "https://Self-Hosted.Example.Test", |
| 195 | }, | 196 | }, |
packages/source-control-gitlab/src/server/GitLabSourceControlProvider.ts+14-13| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | ||
| 1 | import * as Schema from "effect/Schema"; | 2 | import * as Schema from "effect/Schema"; |
| 2 | import * as Effect from "effect/Effect"; | 3 | import * as Effect from "effect/Effect"; |
| 3 | import * as Option from "effect/Option"; | 4 | import * as Option from "effect/Option"; |
| ... | @@ -25,7 +26,7 @@ const decodeLinkSubject = Schema.decodeUnknownEffect( | ... | @@ -25,7 +26,7 @@ const decodeLinkSubject = Schema.decodeUnknownEffect( |
| 25 | 26 | ||
| 26 | function toChangeRequest(summary: GitLabCli.GitLabMergeRequestSummary): ChangeRequest { | 27 | function toChangeRequest(summary: GitLabCli.GitLabMergeRequestSummary): ChangeRequest { |
| 27 | return { | 28 | return { |
| 28 | provider: "gitlab", | 29 | provider: SourceControlProviderKind.make("gitlab"), |
| 29 | number: summary.number, | 30 | number: summary.number, |
| 30 | title: summary.title, | 31 | title: summary.title, |
| 31 | url: summary.url, | 32 | url: summary.url, |
| ... | @@ -90,7 +91,7 @@ function refineUnknownGitLabRemote(input: SourceControlUnknownRemoteRefinementIn | ... | @@ -90,7 +91,7 @@ function refineUnknownGitLabRemote(input: SourceControlUnknownRemoteRefinementIn |
| 90 | } | 91 | } |
| 91 | 92 | ||
| 92 | return { | 93 | return { |
| 93 | kind: "gitlab", | 94 | kind: SourceControlProviderKind.make("gitlab"), |
| 94 | name: "GitLab Self-Hosted", | 95 | name: "GitLab Self-Hosted", |
| 95 | baseUrl: input.context.provider.baseUrl, | 96 | baseUrl: input.context.provider.baseUrl, |
| 96 | } as const; | 97 | } as const; |
| ... | @@ -98,7 +99,7 @@ function refineUnknownGitLabRemote(input: SourceControlUnknownRemoteRefinementIn | ... | @@ -98,7 +99,7 @@ function refineUnknownGitLabRemote(input: SourceControlUnknownRemoteRefinementIn |
| 98 | 99 | ||
| 99 | export const discovery = { | 100 | export const discovery = { |
| 100 | type: "cli", | 101 | type: "cli", |
| 101 | kind: "gitlab", | 102 | kind: SourceControlProviderKind.make("gitlab"), |
| 102 | label: "GitLab", | 103 | label: "GitLab", |
| 103 | executable: "glab", | 104 | executable: "glab", |
| 104 | versionArgs: ["--version"], | 105 | versionArgs: ["--version"], |
| ... | @@ -127,7 +128,7 @@ export const make = Effect.gen(function* () { | ... | @@ -127,7 +128,7 @@ export const make = Effect.gen(function* () { |
| 127 | Effect.mapError( | 128 | Effect.mapError( |
| 128 | (cause) => | 129 | (cause) => |
| 129 | new SourceControlProviderError({ | 130 | new SourceControlProviderError({ |
| 130 | provider: "gitlab", | 131 | provider: SourceControlProviderKind.make("gitlab"), |
| 131 | operation: "resolveLink", | 132 | operation: "resolveLink", |
| 132 | cwd: input.cwd, | 133 | cwd: input.cwd, |
| 133 | detail: "The linked subject could not be read.", | 134 | detail: "The linked subject could not be read.", |
| ... | @@ -139,7 +140,7 @@ export const make = Effect.gen(function* () { | ... | @@ -139,7 +140,7 @@ export const make = Effect.gen(function* () { |
| 139 | Effect.mapError( | 140 | Effect.mapError( |
| 140 | (cause) => | 141 | (cause) => |
| 141 | new SourceControlProviderError({ | 142 | new SourceControlProviderError({ |
| 142 | provider: "gitlab", | 143 | provider: SourceControlProviderKind.make("gitlab"), |
| 143 | operation: "resolveLink.decode", | 144 | operation: "resolveLink.decode", |
| 144 | cwd: input.cwd, | 145 | cwd: input.cwd, |
| 145 | detail: "The linked subject could not be read.", | 146 | detail: "The linked subject could not be read.", |
| ... | @@ -151,7 +152,7 @@ export const make = Effect.gen(function* () { | ... | @@ -151,7 +152,7 @@ export const make = Effect.gen(function* () { |
| 151 | }); | 152 | }); |
| 152 | 153 | ||
| 153 | return SourceControlProvider.SourceControlProvider.of({ | 154 | return SourceControlProvider.SourceControlProvider.of({ |
| 154 | kind: "gitlab", | 155 | kind: SourceControlProviderKind.make("gitlab"), |
| 155 | resolveLink: (input) => { | 156 | resolveLink: (input) => { |
| 156 | // Automatic enrichment must not send ambient CLI credentials to a host from message text. | 157 | // Automatic enrichment must not send ambient CLI credentials to a host from message text. |
| 157 | if (input.url.host !== "gitlab.com") return undefined; | 158 | if (input.url.host !== "gitlab.com") return undefined; |
| ... | @@ -179,7 +180,7 @@ export const make = Effect.gen(function* () { | ... | @@ -179,7 +180,7 @@ export const make = Effect.gen(function* () { |
| 179 | Effect.mapError( | 180 | Effect.mapError( |
| 180 | (error) => | 181 | (error) => |
| 181 | new SourceControlProviderError({ | 182 | new SourceControlProviderError({ |
| 182 | provider: "gitlab", | 183 | provider: SourceControlProviderKind.make("gitlab"), |
| 183 | operation: "listChangeRequests", | 184 | operation: "listChangeRequests", |
| 184 | command: error.command, | 185 | command: error.command, |
| 185 | cwd: input.cwd, | 186 | cwd: input.cwd, |
| ... | @@ -198,7 +199,7 @@ export const make = Effect.gen(function* () { | ... | @@ -198,7 +199,7 @@ export const make = Effect.gen(function* () { |
| 198 | Effect.mapError( | 199 | Effect.mapError( |
| 199 | (error) => | 200 | (error) => |
| 200 | new SourceControlProviderError({ | 201 | new SourceControlProviderError({ |
| 201 | provider: "gitlab", | 202 | provider: SourceControlProviderKind.make("gitlab"), |
| 202 | operation: "getChangeRequest", | 203 | operation: "getChangeRequest", |
| 203 | command: error.command, | 204 | command: error.command, |
| 204 | cwd: input.cwd, | 205 | cwd: input.cwd, |
| ... | @@ -226,7 +227,7 @@ export const make = Effect.gen(function* () { | ... | @@ -226,7 +227,7 @@ export const make = Effect.gen(function* () { |
| 226 | Effect.mapError( | 227 | Effect.mapError( |
| 227 | (error) => | 228 | (error) => |
| 228 | new SourceControlProviderError({ | 229 | new SourceControlProviderError({ |
| 229 | provider: "gitlab", | 230 | provider: SourceControlProviderKind.make("gitlab"), |
| 230 | operation: "createChangeRequest", | 231 | operation: "createChangeRequest", |
| 231 | command: error.command, | 232 | command: error.command, |
| 232 | cwd: input.cwd, | 233 | cwd: input.cwd, |
| ... | @@ -244,7 +245,7 @@ export const make = Effect.gen(function* () { | ... | @@ -244,7 +245,7 @@ export const make = Effect.gen(function* () { |
| 244 | Effect.mapError( | 245 | Effect.mapError( |
| 245 | (error) => | 246 | (error) => |
| 246 | new SourceControlProviderError({ | 247 | new SourceControlProviderError({ |
| 247 | provider: "gitlab", | 248 | provider: SourceControlProviderKind.make("gitlab"), |
| 248 | operation: "getRepositoryCloneUrls", | 249 | operation: "getRepositoryCloneUrls", |
| 249 | command: error.command, | 250 | command: error.command, |
| 250 | cwd: input.cwd, | 251 | cwd: input.cwd, |
| ... | @@ -261,7 +262,7 @@ export const make = Effect.gen(function* () { | ... | @@ -261,7 +262,7 @@ export const make = Effect.gen(function* () { |
| 261 | Effect.mapError( | 262 | Effect.mapError( |
| 262 | (error) => | 263 | (error) => |
| 263 | new SourceControlProviderError({ | 264 | new SourceControlProviderError({ |
| 264 | provider: "gitlab", | 265 | provider: SourceControlProviderKind.make("gitlab"), |
| 265 | operation: "createRepository", | 266 | operation: "createRepository", |
| 266 | command: error.command, | 267 | command: error.command, |
| 267 | cwd: input.cwd, | 268 | cwd: input.cwd, |
| ... | @@ -278,7 +279,7 @@ export const make = Effect.gen(function* () { | ... | @@ -278,7 +279,7 @@ export const make = Effect.gen(function* () { |
| 278 | Effect.mapError( | 279 | Effect.mapError( |
| 279 | (error) => | 280 | (error) => |
| 280 | new SourceControlProviderError({ | 281 | new SourceControlProviderError({ |
| 281 | provider: "gitlab", | 282 | provider: SourceControlProviderKind.make("gitlab"), |
| 282 | operation: "getDefaultBranch", | 283 | operation: "getDefaultBranch", |
| 283 | command: error.command, | 284 | command: error.command, |
| 284 | cwd: input.cwd, | 285 | cwd: input.cwd, |
| ... | @@ -292,7 +293,7 @@ export const make = Effect.gen(function* () { | ... | @@ -292,7 +293,7 @@ export const make = Effect.gen(function* () { |
| 292 | Effect.mapError( | 293 | Effect.mapError( |
| 293 | (error) => | 294 | (error) => |
| 294 | new SourceControlProviderError({ | 295 | new SourceControlProviderError({ |
| 295 | provider: "gitlab", | 296 | provider: SourceControlProviderKind.make("gitlab"), |
| 296 | operation: "checkoutChangeRequest", | 297 | operation: "checkoutChangeRequest", |
| 297 | command: error.command, | 298 | command: error.command, |
| 298 | cwd: input.cwd, | 299 | cwd: input.cwd, |
packages/source-control-gitlab/src/server/driver.ts+2-1| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | ||
| 1 | /** | 2 | /** |
| 2 | * The GitLab driver: repository operations, discovery, and merge requests through `glab`. | 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,7 +11,7 @@ import * as GitLabPullRequestProvider from "./GitLabPullRequestProvider.ts"; |
| 10 | import * as GitLabSourceControlProvider from "./GitLabSourceControlProvider.ts"; | 11 | import * as GitLabSourceControlProvider from "./GitLabSourceControlProvider.ts"; |
| 11 | 12 | ||
| 12 | export const driver = defineSourceControlDriver({ | 13 | export const driver = defineSourceControlDriver({ |
| 13 | kind: "gitlab", | 14 | kind: SourceControlProviderKind.make("gitlab"), |
| 14 | make: Effect.all({ | 15 | make: Effect.all({ |
| 15 | sourceControl: GitLabSourceControlProvider.make, | 16 | sourceControl: GitLabSourceControlProvider.make, |
| 16 | discovery: Effect.succeed(GitLabSourceControlProvider.discovery), | 17 | discovery: Effect.succeed(GitLabSourceControlProvider.discovery), |