| author | |
| committer | |
| log | d9e95398622e158f78cffba5367a9d154ccc55e4 |
| tree | 559ce3b1f3654c81072bd1fe07cabd1828633ea1 |
| parent | 7118e43d16b157dd3079f354bd66ca5d772a9a93 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: codex <codex@users.noreply.github.com>42 files changed, 1336 insertions(+), 1338 deletions(-)
apps/server/src/git/GitManager.test.ts+11-13| ... | @@ -376,7 +376,7 @@ function createTextGeneration( | ... | @@ -376,7 +376,7 @@ function createTextGeneration( |
| 376 | } | 376 | } |
| 377 | 377 | ||
| 378 | function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): { | 378 | function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): { |
| 379 | service: GitHubCli.GitHubCliShape; | 379 | service: GitHubCli.GitHubCli["Service"]; |
| 380 | ghCalls: string[]; | 380 | ghCalls: string[]; |
| 381 | } { | 381 | } { |
| 382 | const prListQueue = [...(scenario.prListSequence ?? [])]; | 382 | const prListQueue = [...(scenario.prListSequence ?? [])]; |
| ... | @@ -388,7 +388,7 @@ function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): { | ... | @@ -388,7 +388,7 @@ function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): { |
| 388 | ); | 388 | ); |
| 389 | const ghCalls: string[] = []; | 389 | const ghCalls: string[] = []; |
| 390 | 390 | ||
| 391 | const execute: GitHubCli.GitHubCliShape["execute"] = (input) => { | 391 | const execute: GitHubCli.GitHubCli["Service"]["execute"] = (input) => { |
| 392 | const args = [...input.args]; | 392 | const args = [...input.args]; |
| 393 | ghCalls.push(args.join(" ")); | 393 | ghCalls.push(args.join(" ")); |
| 394 | 394 | ||
| ... | @@ -609,7 +609,7 @@ function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): { | ... | @@ -609,7 +609,7 @@ function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): { |
| 609 | } | 609 | } |
| 610 | 610 | ||
| 611 | function runStackedAction( | 611 | function runStackedAction( |
| 612 | manager: GitManager.GitManagerShape, | 612 | manager: GitManager.GitManager["Service"], |
| 613 | input: { | 613 | input: { |
| 614 | cwd: string; | 614 | cwd: string; |
| 615 | action: "commit" | "push" | "create_pr" | "commit_push" | "commit_push_pr"; | 615 | action: "commit" | "push" | "create_pr" | "commit_push" | "commit_push_pr"; |
| ... | @@ -618,7 +618,7 @@ function runStackedAction( | ... | @@ -618,7 +618,7 @@ function runStackedAction( |
| 618 | featureBranch?: boolean; | 618 | featureBranch?: boolean; |
| 619 | filePaths?: readonly string[]; | 619 | filePaths?: readonly string[]; |
| 620 | }, | 620 | }, |
| 621 | options?: Parameters<GitManager.GitManagerShape["runStackedAction"]>[1], | 621 | options?: Parameters<GitManager.GitManager["Service"]["runStackedAction"]>[1], |
| 622 | ) { | 622 | ) { |
| 623 | return manager.runStackedAction( | 623 | return manager.runStackedAction( |
| 624 | { | 624 | { |
| ... | @@ -630,14 +630,14 @@ function runStackedAction( | ... | @@ -630,14 +630,14 @@ function runStackedAction( |
| 630 | } | 630 | } |
| 631 | 631 | ||
| 632 | function resolvePullRequest( | 632 | function resolvePullRequest( |
| 633 | manager: GitManager.GitManagerShape, | 633 | manager: GitManager.GitManager["Service"], |
| 634 | input: { cwd: string; reference: string }, | 634 | input: { cwd: string; reference: string }, |
| 635 | ) { | 635 | ) { |
| 636 | return manager.resolvePullRequest(input); | 636 | return manager.resolvePullRequest(input); |
| 637 | } | 637 | } |
| 638 | 638 | ||
| 639 | function preparePullRequestThread( | 639 | function preparePullRequestThread( |
| 640 | manager: GitManager.GitManagerShape, | 640 | manager: GitManager.GitManager["Service"], |
| 641 | input: GitPreparePullRequestThreadInput, | 641 | input: GitPreparePullRequestThreadInput, |
| 642 | ) { | 642 | ) { |
| 643 | return manager.preparePullRequestThread(input); | 643 | return manager.preparePullRequestThread(input); |
| ... | @@ -646,11 +646,11 @@ function preparePullRequestThread( | ... | @@ -646,11 +646,11 @@ function preparePullRequestThread( |
| 646 | function makeManager(input?: { | 646 | function makeManager(input?: { |
| 647 | ghScenario?: FakeGhScenario; | 647 | ghScenario?: FakeGhScenario; |
| 648 | textGeneration?: Partial<FakeGitTextGeneration>; | 648 | textGeneration?: Partial<FakeGitTextGeneration>; |
| 649 | setupScriptRunner?: ProjectSetupScriptRunner.ProjectSetupScriptRunnerShape; | 649 | setupScriptRunner?: ProjectSetupScriptRunner.ProjectSetupScriptRunner["Service"]; |
| 650 | }) { | 650 | }) { |
| 651 | const { service: gitHubCli, ghCalls } = createGitHubCliWithFakeGh(input?.ghScenario); | 651 | const { service: gitHubCli, ghCalls } = createGitHubCliWithFakeGh(input?.ghScenario); |
| 652 | const textGeneration = createTextGeneration(input?.textGeneration); | 652 | const textGeneration = createTextGeneration(input?.textGeneration); |
| 653 | const serverConfigLayer = ServerConfig.ServerConfig.layerTest(process.cwd(), { | 653 | const serverConfigLayer = ServerConfig.layerTest(process.cwd(), { |
| 654 | prefix: "t3-git-manager-test-", | 654 | prefix: "t3-git-manager-test-", |
| 655 | }); | 655 | }); |
| 656 | 656 | ||
| ... | @@ -663,7 +663,7 @@ function makeManager(input?: { | ... | @@ -663,7 +663,7 @@ function makeManager(input?: { |
| 663 | ); | 663 | ); |
| 664 | const sourceControlRegistryLayer = Layer.effect( | 664 | const sourceControlRegistryLayer = Layer.effect( |
| 665 | SourceControlProviderRegistry.SourceControlProviderRegistry, | 665 | SourceControlProviderRegistry.SourceControlProviderRegistry, |
| 666 | GitHubSourceControlProvider.make().pipe( | 666 | GitHubSourceControlProvider.make.pipe( |
| 667 | Effect.map((provider) => | 667 | Effect.map((provider) => |
| 668 | SourceControlProviderRegistry.SourceControlProviderRegistry.of({ | 668 | SourceControlProviderRegistry.SourceControlProviderRegistry.of({ |
| 669 | get: () => Effect.succeed(provider), | 669 | get: () => Effect.succeed(provider), |
| ... | @@ -688,7 +688,7 @@ function makeManager(input?: { | ... | @@ -688,7 +688,7 @@ function makeManager(input?: { |
| 688 | serverSettingsLayer, | 688 | serverSettingsLayer, |
| 689 | ).pipe(Layer.provideMerge(sourceControlRegistryLayer), Layer.provideMerge(NodeServices.layer)); | 689 | ).pipe(Layer.provideMerge(sourceControlRegistryLayer), Layer.provideMerge(NodeServices.layer)); |
| 690 | 690 | ||
| 691 | return GitManager.makeGitManager().pipe( | 691 | return GitManager.make.pipe( |
| 692 | Effect.provide(managerLayer), | 692 | Effect.provide(managerLayer), |
| 693 | Effect.map((manager) => ({ manager, ghCalls })), | 693 | Effect.map((manager) => ({ manager, ghCalls })), |
| 694 | ); | 694 | ); |
| ... | @@ -697,9 +697,7 @@ function makeManager(input?: { | ... | @@ -697,9 +697,7 @@ function makeManager(input?: { |
| 697 | const asThreadId = (threadId: string) => threadId as ThreadId; | 697 | const asThreadId = (threadId: string) => threadId as ThreadId; |
| 698 | 698 | ||
| 699 | const GitManagerTestLayer = GitVcsDriver.layer.pipe( | 699 | const GitManagerTestLayer = GitVcsDriver.layer.pipe( |
| 700 | Layer.provide( | 700 | Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-git-manager-test-" })), |
| 701 | ServerConfig.ServerConfig.layerTest(process.cwd(), { prefix: "t3-git-manager-test-" }), | ||
| 702 | ), | ||
| 703 | Layer.provideMerge(VcsProcess.layer), | 701 | Layer.provideMerge(VcsProcess.layer), |
| 704 | Layer.provideMerge(NodeServices.layer), | 702 | Layer.provideMerge(NodeServices.layer), |
| 705 | ); | 703 | ); |
apps/server/src/git/GitManager.ts+68-71| ... | @@ -42,17 +42,13 @@ import { | ... | @@ -42,17 +42,13 @@ import { |
| 42 | } from "@t3tools/shared/sourceControl"; | 42 | } from "@t3tools/shared/sourceControl"; |
| 43 | 43 | ||
| 44 | import { GitManagerError } from "@t3tools/contracts"; | 44 | import { GitManagerError } from "@t3tools/contracts"; |
| 45 | import { TextGeneration } from "../textGeneration/TextGeneration.ts"; | 45 | import * as TextGeneration from "../textGeneration/TextGeneration.ts"; |
| 46 | import { ProjectSetupScriptRunner } from "../project/Services/ProjectSetupScriptRunner.ts"; | 46 | import * as ProjectSetupScriptRunner from "../project/Services/ProjectSetupScriptRunner.ts"; |
| 47 | import { extractBranchNameFromRemoteRef } from "./remoteRefs.ts"; | 47 | import { extractBranchNameFromRemoteRef } from "./remoteRefs.ts"; |
| 48 | import { ServerSettingsService } from "../serverSettings.ts"; | 48 | import * as ServerSettings from "../serverSettings.ts"; |
| 49 | import type { GitManagerServiceError } from "@t3tools/contracts"; | 49 | import type { GitManagerServiceError } from "@t3tools/contracts"; |
| 50 | import { | 50 | import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; |
| 51 | GitVcsDriver, | 51 | import * as SourceControlProviderRegistry from "../sourceControl/SourceControlProviderRegistry.ts"; |
| 52 | type GitRemoteStatusOptions, | ||
| 53 | type GitStatusDetails, | ||
| 54 | } from "../vcs/GitVcsDriver.ts"; | ||
| 55 | import { SourceControlProviderRegistry } from "../sourceControl/SourceControlProviderRegistry.ts"; | ||
| 56 | import type { ChangeRequest } from "@t3tools/contracts"; | 52 | import type { ChangeRequest } from "@t3tools/contracts"; |
| 57 | 53 | ||
| 58 | export interface GitActionProgressReporter { | 54 | export interface GitActionProgressReporter { |
| ... | @@ -64,35 +60,34 @@ export interface GitRunStackedActionOptions { | ... | @@ -64,35 +60,34 @@ export interface GitRunStackedActionOptions { |
| 64 | readonly progressReporter?: GitActionProgressReporter; | 60 | readonly progressReporter?: GitActionProgressReporter; |
| 65 | } | 61 | } |
| 66 | 62 | ||
| 67 | export interface GitManagerShape { | 63 | export class GitManager extends Context.Service< |
| 68 | readonly status: ( | 64 | GitManager, |
| 69 | input: VcsStatusInput, | 65 | { |
| 70 | ) => Effect.Effect<VcsStatusResult, GitManagerServiceError>; | 66 | readonly status: ( |
| 71 | readonly localStatus: ( | 67 | input: VcsStatusInput, |
| 72 | input: VcsStatusInput, | 68 | ) => Effect.Effect<VcsStatusResult, GitManagerServiceError>; |
| 73 | ) => Effect.Effect<VcsStatusLocalResult, GitManagerServiceError>; | 69 | readonly localStatus: ( |
| 74 | readonly remoteStatus: ( | 70 | input: VcsStatusInput, |
| 75 | input: VcsStatusInput, | 71 | ) => Effect.Effect<VcsStatusLocalResult, GitManagerServiceError>; |
| 76 | options?: GitRemoteStatusOptions, | 72 | readonly remoteStatus: ( |
| 77 | ) => Effect.Effect<VcsStatusRemoteResult | null, GitManagerServiceError>; | 73 | input: VcsStatusInput, |
| 78 | readonly invalidateLocalStatus: (cwd: string) => Effect.Effect<void, never>; | 74 | options?: GitVcsDriver.GitRemoteStatusOptions, |
| 79 | readonly invalidateRemoteStatus: (cwd: string) => Effect.Effect<void, never>; | 75 | ) => Effect.Effect<VcsStatusRemoteResult | null, GitManagerServiceError>; |
| 80 | readonly invalidateStatus: (cwd: string) => Effect.Effect<void, never>; | 76 | readonly invalidateLocalStatus: (cwd: string) => Effect.Effect<void, never>; |
| 81 | readonly resolvePullRequest: ( | 77 | readonly invalidateRemoteStatus: (cwd: string) => Effect.Effect<void, never>; |
| 82 | input: GitPullRequestRefInput, | 78 | readonly invalidateStatus: (cwd: string) => Effect.Effect<void, never>; |
| 83 | ) => Effect.Effect<GitResolvePullRequestResult, GitManagerServiceError>; | 79 | readonly resolvePullRequest: ( |
| 84 | readonly preparePullRequestThread: ( | 80 | input: GitPullRequestRefInput, |
| 85 | input: GitPreparePullRequestThreadInput, | 81 | ) => Effect.Effect<GitResolvePullRequestResult, GitManagerServiceError>; |
| 86 | ) => Effect.Effect<GitPreparePullRequestThreadResult, GitManagerServiceError>; | 82 | readonly preparePullRequestThread: ( |
| 87 | readonly runStackedAction: ( | 83 | input: GitPreparePullRequestThreadInput, |
| 88 | input: GitRunStackedActionInput, | 84 | ) => Effect.Effect<GitPreparePullRequestThreadResult, GitManagerServiceError>; |
| 89 | options?: GitRunStackedActionOptions, | 85 | readonly runStackedAction: ( |
| 90 | ) => Effect.Effect<GitRunStackedActionResult, GitManagerServiceError>; | 86 | input: GitRunStackedActionInput, |
| 91 | } | 87 | options?: GitRunStackedActionOptions, |
| 92 | 88 | ) => Effect.Effect<GitRunStackedActionResult, GitManagerServiceError>; | |
| 93 | export class GitManager extends Context.Service<GitManager, GitManagerShape>()( | 89 | } |
| 94 | "t3/git/GitManager", | 90 | >()("t3/git/GitManager") {} |
| 95 | ) {} | ||
| 96 | 91 | ||
| 97 | const COMMIT_TIMEOUT_MS = 10 * 60_000; | 92 | const COMMIT_TIMEOUT_MS = 10 * 60_000; |
| 98 | const MAX_PROGRESS_TEXT_LENGTH = 500; | 93 | const MAX_PROGRESS_TEXT_LENGTH = 500; |
| ... | @@ -531,15 +526,15 @@ function toPullRequestHeadRemoteInfo(pr: { | ... | @@ -531,15 +526,15 @@ function toPullRequestHeadRemoteInfo(pr: { |
| 531 | }; | 526 | }; |
| 532 | } | 527 | } |
| 533 | 528 | ||
| 534 | export const makeGitManager = Effect.fn("makeGitManager")(function* () { | 529 | export const make = Effect.gen(function* () { |
| 535 | const gitCore = yield* GitVcsDriver; | 530 | const gitCore = yield* GitVcsDriver.GitVcsDriver; |
| 536 | const sourceControlProviders = yield* SourceControlProviderRegistry; | 531 | const sourceControlProviders = yield* SourceControlProviderRegistry.SourceControlProviderRegistry; |
| 537 | const textGeneration = yield* TextGeneration; | 532 | const textGeneration = yield* TextGeneration.TextGeneration; |
| 538 | const projectSetupScriptRunner = yield* ProjectSetupScriptRunner; | 533 | const projectSetupScriptRunner = yield* ProjectSetupScriptRunner.ProjectSetupScriptRunner; |
| 539 | const crypto = yield* Crypto.Crypto; | 534 | const crypto = yield* Crypto.Crypto; |
| 540 | 535 | ||
| 541 | const sourceControlProvider = (cwd: string) => sourceControlProviders.resolve({ cwd }); | 536 | const sourceControlProvider = (cwd: string) => sourceControlProviders.resolve({ cwd }); |
| 542 | const serverSettingsService = yield* ServerSettingsService; | 537 | const serverSettingsService = yield* ServerSettings.ServerSettingsService; |
| 543 | const randomUUIDv4 = crypto.randomUUIDv4.pipe( | 538 | const randomUUIDv4 = crypto.randomUUIDv4.pipe( |
| 544 | Effect.mapError((cause) => | 539 | Effect.mapError((cause) => |
| 545 | gitManagerError("randomUUIDv4", "Failed to generate Git operation identifier.", cause), | 540 | gitManagerError("randomUUIDv4", "Failed to generate Git operation identifier.", cause), |
| ... | @@ -721,7 +716,7 @@ export const makeGitManager = Effect.fn("makeGitManager")(function* () { | ... | @@ -721,7 +716,7 @@ export const makeGitManager = Effect.fn("makeGitManager")(function* () { |
| 721 | aheadCount: 0, | 716 | aheadCount: 0, |
| 722 | behindCount: 0, | 717 | behindCount: 0, |
| 723 | aheadOfDefaultCount: 0, | 718 | aheadOfDefaultCount: 0, |
| 724 | } satisfies GitStatusDetails; | 719 | } satisfies GitVcsDriver.GitStatusDetails; |
| 725 | const readLocalStatus = Effect.fn("readLocalStatus")(function* (cwd: string) { | 720 | const readLocalStatus = Effect.fn("readLocalStatus")(function* (cwd: string) { |
| 726 | const details = yield* gitCore | 721 | const details = yield* gitCore |
| 727 | .statusDetailsLocal(cwd) | 722 | .statusDetailsLocal(cwd) |
| ... | @@ -752,7 +747,7 @@ export const makeGitManager = Effect.fn("makeGitManager")(function* () { | ... | @@ -752,7 +747,7 @@ export const makeGitManager = Effect.fn("makeGitManager")(function* () { |
| 752 | ); | 747 | ); |
| 753 | const readRemoteStatus = Effect.fn("readRemoteStatus")(function* ( | 748 | const readRemoteStatus = Effect.fn("readRemoteStatus")(function* ( |
| 754 | cwd: string, | 749 | cwd: string, |
| 755 | options?: GitRemoteStatusOptions, | 750 | options?: GitVcsDriver.GitRemoteStatusOptions, |
| 756 | ) { | 751 | ) { |
| 757 | const details = yield* gitCore | 752 | const details = yield* gitCore |
| 758 | .statusDetailsRemote(cwd, options) | 753 | .statusDetailsRemote(cwd, options) |
| ... | @@ -1358,11 +1353,13 @@ export const makeGitManager = Effect.fn("makeGitManager")(function* () { | ... | @@ -1358,11 +1353,13 @@ export const makeGitManager = Effect.fn("makeGitManager")(function* () { |
| 1358 | }; | 1353 | }; |
| 1359 | }); | 1354 | }); |
| 1360 | 1355 | ||
| 1361 | const localStatus: GitManagerShape["localStatus"] = Effect.fn("localStatus")(function* (input) { | 1356 | const localStatus: GitManager["Service"]["localStatus"] = Effect.fn("localStatus")( |
| 1362 | const cacheKey = yield* normalizeStatusCacheKey(input.cwd); | 1357 | function* (input) { |
| 1363 | return yield* Cache.get(localStatusResultCache, cacheKey); | 1358 | const cacheKey = yield* normalizeStatusCacheKey(input.cwd); |
| 1364 | }); | 1359 | return yield* Cache.get(localStatusResultCache, cacheKey); |
| 1365 | const remoteStatus: GitManagerShape["remoteStatus"] = Effect.fn("remoteStatus")( | 1360 | }, |
| 1361 | ); | ||
| 1362 | const remoteStatus: GitManager["Service"]["remoteStatus"] = Effect.fn("remoteStatus")( | ||
| 1366 | function* (input, options) { | 1363 | function* (input, options) { |
| 1367 | const cacheKey = yield* normalizeStatusCacheKey(input.cwd); | 1364 | const cacheKey = yield* normalizeStatusCacheKey(input.cwd); |
| 1368 | if (options?.refreshUpstream === false) { | 1365 | if (options?.refreshUpstream === false) { |
| ... | @@ -1371,43 +1368,43 @@ export const makeGitManager = Effect.fn("makeGitManager")(function* () { | ... | @@ -1371,43 +1368,43 @@ export const makeGitManager = Effect.fn("makeGitManager")(function* () { |
| 1371 | return yield* Cache.get(remoteStatusResultCache, cacheKey); | 1368 | return yield* Cache.get(remoteStatusResultCache, cacheKey); |
| 1372 | }, | 1369 | }, |
| 1373 | ); | 1370 | ); |
| 1374 | const status: GitManagerShape["status"] = Effect.fn("status")(function* (input) { | 1371 | const status: GitManager["Service"]["status"] = Effect.fn("status")(function* (input) { |
| 1375 | const [local, remote] = yield* Effect.all([localStatus(input), remoteStatus(input)], { | 1372 | const [local, remote] = yield* Effect.all([localStatus(input), remoteStatus(input)], { |
| 1376 | concurrency: "unbounded", | 1373 | concurrency: "unbounded", |
| 1377 | }); | 1374 | }); |
| 1378 | return mergeGitStatusParts(local, remote); | 1375 | return mergeGitStatusParts(local, remote); |
| 1379 | }); | 1376 | }); |
| 1380 | const invalidateLocalStatus: GitManagerShape["invalidateLocalStatus"] = Effect.fn( | 1377 | const invalidateLocalStatus: GitManager["Service"]["invalidateLocalStatus"] = Effect.fn( |
| 1381 | "invalidateLocalStatus", | 1378 | "invalidateLocalStatus", |
| 1382 | )(function* (cwd) { | 1379 | )(function* (cwd) { |
| 1383 | yield* invalidateLocalStatusResultCache(cwd); | 1380 | yield* invalidateLocalStatusResultCache(cwd); |
| 1384 | }); | 1381 | }); |
| 1385 | const invalidateRemoteStatus: GitManagerShape["invalidateRemoteStatus"] = Effect.fn( | 1382 | const invalidateRemoteStatus: GitManager["Service"]["invalidateRemoteStatus"] = Effect.fn( |
| 1386 | "invalidateRemoteStatus", | 1383 | "invalidateRemoteStatus", |
| 1387 | )(function* (cwd) { | 1384 | )(function* (cwd) { |
| 1388 | yield* invalidateRemoteStatusResultCache(cwd); | 1385 | yield* invalidateRemoteStatusResultCache(cwd); |
| 1389 | }); | 1386 | }); |
| 1390 | const invalidateStatus: GitManagerShape["invalidateStatus"] = Effect.fn("invalidateStatus")( | 1387 | const invalidateStatus: GitManager["Service"]["invalidateStatus"] = Effect.fn("invalidateStatus")( |
| 1391 | function* (cwd) { | 1388 | function* (cwd) { |
| 1392 | yield* invalidateLocalStatusResultCache(cwd); | 1389 | yield* invalidateLocalStatusResultCache(cwd); |
| 1393 | yield* invalidateRemoteStatusResultCache(cwd); | 1390 | yield* invalidateRemoteStatusResultCache(cwd); |
| 1394 | }, | 1391 | }, |
| 1395 | ); | 1392 | ); |
| 1396 | 1393 | ||
| 1397 | const resolvePullRequest: GitManagerShape["resolvePullRequest"] = Effect.fn("resolvePullRequest")( | 1394 | const resolvePullRequest: GitManager["Service"]["resolvePullRequest"] = Effect.fn( |
| 1398 | function* (input) { | 1395 | "resolvePullRequest", |
| 1399 | const pullRequest = yield* (yield* sourceControlProvider(input.cwd)) | 1396 | )(function* (input) { |
| 1400 | .getChangeRequest({ | 1397 | const pullRequest = yield* (yield* sourceControlProvider(input.cwd)) |
| 1401 | cwd: input.cwd, | 1398 | .getChangeRequest({ |
| 1402 | reference: normalizePullRequestReference(input.reference), | 1399 | cwd: input.cwd, |
| 1403 | }) | 1400 | reference: normalizePullRequestReference(input.reference), |
| 1404 | .pipe(Effect.map((resolved) => toResolvedPullRequest(resolved))); | 1401 | }) |
| 1402 | .pipe(Effect.map((resolved) => toResolvedPullRequest(resolved))); | ||
| 1405 | 1403 | ||
| 1406 | return { pullRequest }; | 1404 | return { pullRequest }; |
| 1407 | }, | 1405 | }); |
| 1408 | ); | ||
| 1409 | 1406 | ||
| 1410 | const preparePullRequestThread: GitManagerShape["preparePullRequestThread"] = Effect.fn( | 1407 | const preparePullRequestThread: GitManager["Service"]["preparePullRequestThread"] = Effect.fn( |
| 1411 | "preparePullRequestThread", | 1408 | "preparePullRequestThread", |
| 1412 | )(function* (input) { | 1409 | )(function* (input) { |
| 1413 | const maybeRunSetupScript = (worktreePath: string) => { | 1410 | const maybeRunSetupScript = (worktreePath: string) => { |
| ... | @@ -1608,7 +1605,7 @@ export const makeGitManager = Effect.fn("makeGitManager")(function* () { | ... | @@ -1608,7 +1605,7 @@ export const makeGitManager = Effect.fn("makeGitManager")(function* () { |
| 1608 | }; | 1605 | }; |
| 1609 | }); | 1606 | }); |
| 1610 | 1607 | ||
| 1611 | const runStackedAction: GitManagerShape["runStackedAction"] = Effect.fn("runStackedAction")( | 1608 | const runStackedAction: GitManager["Service"]["runStackedAction"] = Effect.fn("runStackedAction")( |
| 1612 | function* (input, options) { | 1609 | function* (input, options) { |
| 1613 | const progress = yield* createProgressEmitter(input, options); | 1610 | const progress = yield* createProgressEmitter(input, options); |
| 1614 | const currentPhase = yield* Ref.make<Option.Option<GitActionProgressPhase>>(Option.none()); | 1611 | const currentPhase = yield* Ref.make<Option.Option<GitActionProgressPhase>>(Option.none()); |
| ... | @@ -1787,7 +1784,7 @@ export const makeGitManager = Effect.fn("makeGitManager")(function* () { | ... | @@ -1787,7 +1784,7 @@ export const makeGitManager = Effect.fn("makeGitManager")(function* () { |
| 1787 | }, | 1784 | }, |
| 1788 | ); | 1785 | ); |
| 1789 | 1786 | ||
| 1790 | return { | 1787 | return GitManager.of({ |
| 1791 | localStatus, | 1788 | localStatus, |
| 1792 | remoteStatus, | 1789 | remoteStatus, |
| 1793 | status, | 1790 | status, |
| ... | @@ -1797,7 +1794,7 @@ export const makeGitManager = Effect.fn("makeGitManager")(function* () { | ... | @@ -1797,7 +1794,7 @@ export const makeGitManager = Effect.fn("makeGitManager")(function* () { |
| 1797 | resolvePullRequest, | 1794 | resolvePullRequest, |
| 1798 | preparePullRequestThread, | 1795 | preparePullRequestThread, |
| 1799 | runStackedAction, | 1796 | runStackedAction, |
| 1800 | } satisfies GitManagerShape; | 1797 | }); |
| 1801 | }); | 1798 | }); |
| 1802 | 1799 | ||
| 1803 | export const layer = Layer.effect(GitManager, makeGitManager()); | 1800 | export const layer = Layer.effect(GitManager, make); |
apps/server/src/git/GitWorkflowService.test.ts+3-1| ... | @@ -7,7 +7,9 @@ import * as GitWorkflowService from "./GitWorkflowService.ts"; | ... | @@ -7,7 +7,9 @@ import * as GitWorkflowService from "./GitWorkflowService.ts"; |
| 7 | import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; | 7 | import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; |
| 8 | import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts"; | 8 | import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts"; |
| 9 | 9 | ||
| 10 | function makeLayer(input: { readonly detect: VcsDriverRegistry.VcsDriverRegistryShape["detect"] }) { | 10 | function makeLayer(input: { |
| 11 | readonly detect: VcsDriverRegistry.VcsDriverRegistry["Service"]["detect"]; | ||
| 12 | }) { | ||
| 11 | return GitWorkflowService.layer.pipe( | 13 | return GitWorkflowService.layer.pipe( |
| 12 | Layer.provide( | 14 | Layer.provide( |
| 13 | Layer.mock(VcsDriverRegistry.VcsDriverRegistry)({ | 15 | Layer.mock(VcsDriverRegistry.VcsDriverRegistry)({ |
apps/server/src/git/GitWorkflowService.ts+66-64| ... | @@ -28,68 +28,70 @@ import { | ... | @@ -28,68 +28,70 @@ import { |
| 28 | type VcsStatusResult, | 28 | type VcsStatusResult, |
| 29 | } from "@t3tools/contracts"; | 29 | } from "@t3tools/contracts"; |
| 30 | 30 | ||
| 31 | import { GitManager, type GitRunStackedActionOptions } from "./GitManager.ts"; | 31 | import * as GitManager from "./GitManager.ts"; |
| 32 | import { GitVcsDriver, type GitRemoteStatusOptions } from "../vcs/GitVcsDriver.ts"; | 32 | import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; |
| 33 | import { VcsDriverRegistry } from "../vcs/VcsDriverRegistry.ts"; | 33 | import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts"; |
| 34 | |||
| 35 | export interface GitWorkflowServiceShape { | ||
| 36 | readonly status: ( | ||
| 37 | input: VcsStatusInput, | ||
| 38 | ) => Effect.Effect<VcsStatusResult, GitManagerServiceError>; | ||
| 39 | readonly localStatus: ( | ||
| 40 | input: VcsStatusInput, | ||
| 41 | ) => Effect.Effect<VcsStatusLocalResult, GitManagerServiceError>; | ||
| 42 | readonly remoteStatus: ( | ||
| 43 | input: VcsStatusInput, | ||
| 44 | options?: GitRemoteStatusOptions, | ||
| 45 | ) => Effect.Effect<VcsStatusRemoteResult | null, GitManagerServiceError>; | ||
| 46 | readonly invalidateLocalStatus: (cwd: string) => Effect.Effect<void, never>; | ||
| 47 | readonly invalidateRemoteStatus: (cwd: string) => Effect.Effect<void, never>; | ||
| 48 | readonly invalidateStatus: (cwd: string) => Effect.Effect<void, never>; | ||
| 49 | readonly pullCurrentBranch: (cwd: string) => Effect.Effect<VcsPullResult, GitCommandError>; | ||
| 50 | readonly runStackedAction: ( | ||
| 51 | input: GitRunStackedActionInput, | ||
| 52 | options?: GitRunStackedActionOptions, | ||
| 53 | ) => Effect.Effect<GitRunStackedActionResult, GitManagerServiceError>; | ||
| 54 | readonly resolvePullRequest: ( | ||
| 55 | input: GitPullRequestRefInput, | ||
| 56 | ) => Effect.Effect<GitResolvePullRequestResult, GitManagerServiceError>; | ||
| 57 | readonly preparePullRequestThread: ( | ||
| 58 | input: GitPreparePullRequestThreadInput, | ||
| 59 | ) => Effect.Effect<GitPreparePullRequestThreadResult, GitManagerServiceError>; | ||
| 60 | readonly listRefs: (input: VcsListRefsInput) => Effect.Effect<VcsListRefsResult, GitCommandError>; | ||
| 61 | readonly createWorktree: ( | ||
| 62 | input: VcsCreateWorktreeInput, | ||
| 63 | ) => Effect.Effect<VcsCreateWorktreeResult, GitCommandError>; | ||
| 64 | readonly fetchRemote: (input: { | ||
| 65 | readonly cwd: string; | ||
| 66 | readonly remoteName: string; | ||
| 67 | }) => Effect.Effect<void, GitCommandError>; | ||
| 68 | readonly resolveRemoteTrackingCommit: (input: { | ||
| 69 | readonly cwd: string; | ||
| 70 | readonly refName: string; | ||
| 71 | readonly fallbackRemoteName: string; | ||
| 72 | }) => Effect.Effect< | ||
| 73 | { readonly commitSha: string; readonly remoteRefName: string }, | ||
| 74 | GitCommandError | ||
| 75 | >; | ||
| 76 | readonly removeWorktree: (input: VcsRemoveWorktreeInput) => Effect.Effect<void, GitCommandError>; | ||
| 77 | readonly createRef: ( | ||
| 78 | input: VcsCreateRefInput, | ||
| 79 | ) => Effect.Effect<VcsCreateRefResult, GitCommandError>; | ||
| 80 | readonly switchRef: ( | ||
| 81 | input: VcsSwitchRefInput, | ||
| 82 | ) => Effect.Effect<VcsSwitchRefResult, GitCommandError>; | ||
| 83 | readonly renameBranch: (input: { | ||
| 84 | readonly cwd: string; | ||
| 85 | readonly oldBranch: string; | ||
| 86 | readonly newBranch: string; | ||
| 87 | }) => Effect.Effect<{ readonly branch: string }, GitManagerServiceError>; | ||
| 88 | } | ||
| 89 | 34 | ||
| 90 | export class GitWorkflowService extends Context.Service< | 35 | export class GitWorkflowService extends Context.Service< |
| 91 | GitWorkflowService, | 36 | GitWorkflowService, |
| 92 | GitWorkflowServiceShape | 37 | { |
| 38 | readonly status: ( | ||
| 39 | input: VcsStatusInput, | ||
| 40 | ) => Effect.Effect<VcsStatusResult, GitManagerServiceError>; | ||
| 41 | readonly localStatus: ( | ||
| 42 | input: VcsStatusInput, | ||
| 43 | ) => Effect.Effect<VcsStatusLocalResult, GitManagerServiceError>; | ||
| 44 | readonly remoteStatus: ( | ||
| 45 | input: VcsStatusInput, | ||
| 46 | options?: GitVcsDriver.GitRemoteStatusOptions, | ||
| 47 | ) => Effect.Effect<VcsStatusRemoteResult | null, GitManagerServiceError>; | ||
| 48 | readonly invalidateLocalStatus: (cwd: string) => Effect.Effect<void, never>; | ||
| 49 | readonly invalidateRemoteStatus: (cwd: string) => Effect.Effect<void, never>; | ||
| 50 | readonly invalidateStatus: (cwd: string) => Effect.Effect<void, never>; | ||
| 51 | readonly pullCurrentBranch: (cwd: string) => Effect.Effect<VcsPullResult, GitCommandError>; | ||
| 52 | readonly runStackedAction: ( | ||
| 53 | input: GitRunStackedActionInput, | ||
| 54 | options?: GitManager.GitRunStackedActionOptions, | ||
| 55 | ) => Effect.Effect<GitRunStackedActionResult, GitManagerServiceError>; | ||
| 56 | readonly resolvePullRequest: ( | ||
| 57 | input: GitPullRequestRefInput, | ||
| 58 | ) => Effect.Effect<GitResolvePullRequestResult, GitManagerServiceError>; | ||
| 59 | readonly preparePullRequestThread: ( | ||
| 60 | input: GitPreparePullRequestThreadInput, | ||
| 61 | ) => Effect.Effect<GitPreparePullRequestThreadResult, GitManagerServiceError>; | ||
| 62 | readonly listRefs: ( | ||
| 63 | input: VcsListRefsInput, | ||
| 64 | ) => Effect.Effect<VcsListRefsResult, GitCommandError>; | ||
| 65 | readonly createWorktree: ( | ||
| 66 | input: VcsCreateWorktreeInput, | ||
| 67 | ) => Effect.Effect<VcsCreateWorktreeResult, GitCommandError>; | ||
| 68 | readonly fetchRemote: (input: { | ||
| 69 | readonly cwd: string; | ||
| 70 | readonly remoteName: string; | ||
| 71 | }) => Effect.Effect<void, GitCommandError>; | ||
| 72 | readonly resolveRemoteTrackingCommit: (input: { | ||
| 73 | readonly cwd: string; | ||
| 74 | readonly refName: string; | ||
| 75 | readonly fallbackRemoteName: string; | ||
| 76 | }) => Effect.Effect< | ||
| 77 | { readonly commitSha: string; readonly remoteRefName: string }, | ||
| 78 | GitCommandError | ||
| 79 | >; | ||
| 80 | readonly removeWorktree: ( | ||
| 81 | input: VcsRemoveWorktreeInput, | ||
| 82 | ) => Effect.Effect<void, GitCommandError>; | ||
| 83 | readonly createRef: ( | ||
| 84 | input: VcsCreateRefInput, | ||
| 85 | ) => Effect.Effect<VcsCreateRefResult, GitCommandError>; | ||
| 86 | readonly switchRef: ( | ||
| 87 | input: VcsSwitchRefInput, | ||
| 88 | ) => Effect.Effect<VcsSwitchRefResult, GitCommandError>; | ||
| 89 | readonly renameBranch: (input: { | ||
| 90 | readonly cwd: string; | ||
| 91 | readonly oldBranch: string; | ||
| 92 | readonly newBranch: string; | ||
| 93 | }) => Effect.Effect<{ readonly branch: string }, GitManagerServiceError>; | ||
| 94 | } | ||
| 93 | >()("t3/git/GitWorkflowService") {} | 95 | >()("t3/git/GitWorkflowService") {} |
| 94 | 96 | ||
| 95 | const unsupportedGitWorkflow = (operation: string, cwd: string, detail: string) => | 97 | const unsupportedGitWorkflow = (operation: string, cwd: string, detail: string) => |
| ... | @@ -142,10 +144,10 @@ function nonRepositoryListRefs(): VcsListRefsResult { | ... | @@ -142,10 +144,10 @@ function nonRepositoryListRefs(): VcsListRefsResult { |
| 142 | }; | 144 | }; |
| 143 | } | 145 | } |
| 144 | 146 | ||
| 145 | export const make = Effect.fn("makeGitWorkflowService")(function* () { | 147 | export const make = Effect.gen(function* () { |
| 146 | const registry = yield* VcsDriverRegistry; | 148 | const registry = yield* VcsDriverRegistry.VcsDriverRegistry; |
| 147 | const git = yield* GitVcsDriver; | 149 | const git = yield* GitVcsDriver.GitVcsDriver; |
| 148 | const gitManager = yield* GitManager; | 150 | const gitManager = yield* GitManager.GitManager; |
| 149 | 151 | ||
| 150 | const ensureGit = Effect.fn("GitWorkflowService.ensureGit")(function* ( | 152 | const ensureGit = Effect.fn("GitWorkflowService.ensureGit")(function* ( |
| 151 | operation: string, | 153 | operation: string, |
| ... | @@ -334,4 +336,4 @@ export const make = Effect.fn("makeGitWorkflowService")(function* () { | ... | @@ -334,4 +336,4 @@ export const make = Effect.fn("makeGitWorkflowService")(function* () { |
| 334 | }); | 336 | }); |
| 335 | }); | 337 | }); |
| 336 | 338 | ||
| 337 | export const layer = Layer.effect(GitWorkflowService, make()); | 339 | export const layer = Layer.effect(GitWorkflowService, make); |
apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts+3-3| ... | @@ -59,7 +59,7 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; | ... | @@ -59,7 +59,7 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 59 | import * as Clock from "effect/Clock"; | 59 | import * as Clock from "effect/Clock"; |
| 60 | import { ServerSettingsService } from "../../serverSettings.ts"; | 60 | import { ServerSettingsService } from "../../serverSettings.ts"; |
| 61 | import { VcsStatusBroadcaster } from "../../vcs/VcsStatusBroadcaster.ts"; | 61 | import { VcsStatusBroadcaster } from "../../vcs/VcsStatusBroadcaster.ts"; |
| 62 | import { GitWorkflowService, type GitWorkflowServiceShape } from "../../git/GitWorkflowService.ts"; | 62 | import * as GitWorkflowService from "../../git/GitWorkflowService.ts"; |
| 63 | 63 | ||
| 64 | const asProjectId = (value: string): ProjectId => ProjectId.make(value); | 64 | const asProjectId = (value: string): ProjectId => ProjectId.make(value); |
| 65 | const asApprovalRequestId = (value: string): ApprovalRequestId => ApprovalRequestId.make(value); | 65 | const asApprovalRequestId = (value: string): ApprovalRequestId => ApprovalRequestId.make(value); |
| ... | @@ -348,9 +348,9 @@ describe("ProviderCommandReactor", () => { | ... | @@ -348,9 +348,9 @@ describe("ProviderCommandReactor", () => { |
| 348 | Layer.provideMerge(Layer.succeed(ProviderService, service)), | 348 | Layer.provideMerge(Layer.succeed(ProviderService, service)), |
| 349 | Layer.provideMerge(makeProviderRegistryLayer(providerSnapshots as never)), | 349 | Layer.provideMerge(makeProviderRegistryLayer(providerSnapshots as never)), |
| 350 | Layer.provideMerge( | 350 | Layer.provideMerge( |
| 351 | Layer.mock(GitWorkflowService)({ | 351 | Layer.mock(GitWorkflowService.GitWorkflowService)({ |
| 352 | renameBranch, | 352 | renameBranch, |
| 353 | } satisfies Partial<GitWorkflowServiceShape>), | 353 | } satisfies Partial<GitWorkflowService.GitWorkflowService["Service"]>), |
| 354 | ), | 354 | ), |
| 355 | Layer.provideMerge( | 355 | Layer.provideMerge( |
| 356 | Layer.succeed(VcsStatusBroadcaster, { | 356 | Layer.succeed(VcsStatusBroadcaster, { |
apps/server/src/sourceControl/AzureDevOpsCli.test.ts+1-1| ... | @@ -17,7 +17,7 @@ const processOutput = (stdout: string): VcsProcess.VcsProcessOutput => ({ | ... | @@ -17,7 +17,7 @@ const processOutput = (stdout: string): VcsProcess.VcsProcessOutput => ({ |
| 17 | stderrTruncated: false, | 17 | stderrTruncated: false, |
| 18 | }); | 18 | }); |
| 19 | 19 | ||
| 20 | const mockRun = vi.fn<VcsProcess.VcsProcessShape["run"]>(); | 20 | const mockRun = vi.fn<VcsProcess.VcsProcess["Service"]["run"]>(); |
| 21 | 21 | ||
| 22 | const supportLayer = Layer.mergeAll( | 22 | const supportLayer = Layer.mergeAll( |
| 23 | Layer.mock(VcsProcess.VcsProcess)({ | 23 | Layer.mock(VcsProcess.VcsProcess)({ |
apps/server/src/sourceControl/AzureDevOpsCli.ts+68-72| ... | @@ -11,7 +11,12 @@ import { | ... | @@ -11,7 +11,12 @@ import { |
| 11 | } from "@t3tools/contracts"; | 11 | } from "@t3tools/contracts"; |
| 12 | 12 | ||
| 13 | import * as VcsProcess from "../vcs/VcsProcess.ts"; | 13 | import * as VcsProcess from "../vcs/VcsProcess.ts"; |
| 14 | import * as AzureDevOpsPullRequests from "./azureDevOpsPullRequests.ts"; | 14 | import { |
| 15 | decodeAzureDevOpsPullRequestJson, | ||
| 16 | decodeAzureDevOpsPullRequestListJson, | ||
| 17 | formatAzureDevOpsJsonDecodeError, | ||
| 18 | type NormalizedAzureDevOpsPullRequestRecord, | ||
| 19 | } from "./azureDevOpsPullRequests.ts"; | ||
| 15 | import * as SourceControlProvider from "./SourceControlProvider.ts"; | 20 | import * as SourceControlProvider from "./SourceControlProvider.ts"; |
| 16 | 21 | ||
| 17 | const DEFAULT_TIMEOUT_MS = 30_000; | 22 | const DEFAULT_TIMEOUT_MS = 30_000; |
| ... | @@ -35,67 +40,60 @@ export interface AzureDevOpsRepositoryCloneUrls { | ... | @@ -35,67 +40,60 @@ export interface AzureDevOpsRepositoryCloneUrls { |
| 35 | readonly sshUrl: string; | 40 | readonly sshUrl: string; |
| 36 | } | 41 | } |
| 37 | 42 | ||
| 38 | export interface AzureDevOpsCliShape { | 43 | export class AzureDevOpsCli extends Context.Service< |
| 39 | readonly execute: (input: { | 44 | AzureDevOpsCli, |
| 40 | readonly cwd: string; | 45 | { |
| 41 | readonly args: ReadonlyArray<string>; | 46 | readonly execute: (input: { |
| 42 | readonly timeoutMs?: number; | 47 | readonly cwd: string; |
| 43 | }) => Effect.Effect<VcsProcess.VcsProcessOutput, AzureDevOpsCliError>; | 48 | readonly args: ReadonlyArray<string>; |
| 44 | 49 | readonly timeoutMs?: number; | |
| 45 | readonly listPullRequests: (input: { | 50 | }) => Effect.Effect<VcsProcess.VcsProcessOutput, AzureDevOpsCliError>; |
| 46 | readonly cwd: string; | 51 | |
| 47 | readonly headSelector: string; | 52 | readonly listPullRequests: (input: { |
| 48 | readonly source?: SourceControlProvider.SourceControlRefSelector; | 53 | readonly cwd: string; |
| 49 | readonly state: "open" | "closed" | "merged" | "all"; | 54 | readonly headSelector: string; |
| 50 | readonly limit?: number; | 55 | readonly source?: SourceControlProvider.SourceControlRefSelector; |
| 51 | }) => Effect.Effect< | 56 | readonly state: "open" | "closed" | "merged" | "all"; |
| 52 | ReadonlyArray<AzureDevOpsPullRequests.NormalizedAzureDevOpsPullRequestRecord>, | 57 | readonly limit?: number; |
| 53 | AzureDevOpsCliError | 58 | }) => Effect.Effect<ReadonlyArray<NormalizedAzureDevOpsPullRequestRecord>, AzureDevOpsCliError>; |
| 54 | >; | 59 | |
| 55 | 60 | readonly getPullRequest: (input: { | |
| 56 | readonly getPullRequest: (input: { | 61 | readonly cwd: string; |
| 57 | readonly cwd: string; | 62 | readonly reference: string; |
| 58 | readonly reference: string; | 63 | }) => Effect.Effect<NormalizedAzureDevOpsPullRequestRecord, AzureDevOpsCliError>; |
| 59 | }) => Effect.Effect< | 64 | |
| 60 | AzureDevOpsPullRequests.NormalizedAzureDevOpsPullRequestRecord, | 65 | readonly getRepositoryCloneUrls: (input: { |
| 61 | AzureDevOpsCliError | 66 | readonly cwd: string; |
| 62 | >; | 67 | readonly repository: string; |
| 63 | 68 | }) => Effect.Effect<AzureDevOpsRepositoryCloneUrls, AzureDevOpsCliError>; | |
| 64 | readonly getRepositoryCloneUrls: (input: { | 69 | |
| 65 | readonly cwd: string; | 70 | readonly createRepository: (input: { |
| 66 | readonly repository: string; | 71 | readonly cwd: string; |
| 67 | }) => Effect.Effect<AzureDevOpsRepositoryCloneUrls, AzureDevOpsCliError>; | 72 | readonly repository: string; |
| 68 | 73 | readonly visibility: SourceControlRepositoryVisibility; | |
| 69 | readonly createRepository: (input: { | 74 | }) => Effect.Effect<AzureDevOpsRepositoryCloneUrls, AzureDevOpsCliError>; |
| 70 | readonly cwd: string; | 75 | |
| 71 | readonly repository: string; | 76 | readonly createPullRequest: (input: { |
| 72 | readonly visibility: SourceControlRepositoryVisibility; | 77 | readonly cwd: string; |
| 73 | }) => Effect.Effect<AzureDevOpsRepositoryCloneUrls, AzureDevOpsCliError>; | 78 | readonly baseBranch: string; |
| 74 | 79 | readonly headSelector: string; | |
| 75 | readonly createPullRequest: (input: { | 80 | readonly source?: SourceControlProvider.SourceControlRefSelector; |
| 76 | readonly cwd: string; | 81 | readonly target?: SourceControlProvider.SourceControlRefSelector; |
| 77 | readonly baseBranch: string; | 82 | readonly title: string; |
| 78 | readonly headSelector: string; | 83 | readonly bodyFile: string; |
| 79 | readonly source?: SourceControlProvider.SourceControlRefSelector; | 84 | }) => Effect.Effect<void, AzureDevOpsCliError>; |
| 80 | readonly target?: SourceControlProvider.SourceControlRefSelector; | 85 | |
| 81 | readonly title: string; | 86 | readonly getDefaultBranch: (input: { |
| 82 | readonly bodyFile: string; | 87 | readonly cwd: string; |
| 83 | }) => Effect.Effect<void, AzureDevOpsCliError>; | 88 | }) => Effect.Effect<string | null, AzureDevOpsCliError>; |
| 84 | 89 | ||
| 85 | readonly getDefaultBranch: (input: { | 90 | readonly checkoutPullRequest: (input: { |
| 86 | readonly cwd: string; | 91 | readonly cwd: string; |
| 87 | }) => Effect.Effect<string | null, AzureDevOpsCliError>; | 92 | readonly reference: string; |
| 88 | 93 | readonly remoteName?: string; | |
| 89 | readonly checkoutPullRequest: (input: { | 94 | }) => Effect.Effect<void, AzureDevOpsCliError>; |
| 90 | readonly cwd: string; | 95 | } |
| 91 | readonly reference: string; | 96 | >()("t3/sourceControl/AzureDevOpsCli") {} |
| 92 | readonly remoteName?: string; | ||
| 93 | }) => Effect.Effect<void, AzureDevOpsCliError>; | ||
| 94 | } | ||
| 95 | |||
| 96 | export class AzureDevOpsCli extends Context.Service<AzureDevOpsCli, AzureDevOpsCliShape>()( | ||
| 97 | "t3/sourceControl/AzureDevOpsCli", | ||
| 98 | ) {} | ||
| 99 | 97 | ||
| 100 | function errorText(error: VcsError | unknown): string { | 98 | function errorText(error: VcsError | unknown): string { |
| 101 | if (typeof error === "object" && error !== null) { | 99 | if (typeof error === "object" && error !== null) { |
| ... | @@ -239,10 +237,10 @@ function decodeAzureDevOpsJson<S extends Schema.Top>( | ... | @@ -239,10 +237,10 @@ function decodeAzureDevOpsJson<S extends Schema.Top>( |
| 239 | ); | 237 | ); |
| 240 | } | 238 | } |
| 241 | 239 | ||
| 242 | export const make = Effect.fn("makeAzureDevOpsCli")(function* () { | 240 | export const make = Effect.gen(function* () { |
| 243 | const process = yield* VcsProcess.VcsProcess; | 241 | const process = yield* VcsProcess.VcsProcess; |
| 244 | 242 | ||
| 245 | const execute: AzureDevOpsCliShape["execute"] = (input) => | 243 | const execute: AzureDevOpsCli["Service"]["execute"] = (input) => |
| 246 | process | 244 | process |
| 247 | .run({ | 245 | .run({ |
| 248 | operation: "AzureDevOpsCli.execute", | 246 | operation: "AzureDevOpsCli.execute", |
| ... | @@ -253,7 +251,7 @@ export const make = Effect.fn("makeAzureDevOpsCli")(function* () { | ... | @@ -253,7 +251,7 @@ export const make = Effect.fn("makeAzureDevOpsCli")(function* () { |
| 253 | }) | 251 | }) |
| 254 | .pipe(Effect.mapError((error) => normalizeAzureDevOpsCliError("execute", error))); | 252 | .pipe(Effect.mapError((error) => normalizeAzureDevOpsCliError("execute", error))); |
| 255 | 253 | ||
| 256 | const executeJson = (input: Parameters<AzureDevOpsCliShape["execute"]>[0]) => | 254 | const executeJson = (input: Parameters<AzureDevOpsCli["Service"]["execute"]>[0]) => |
| 257 | execute({ | 255 | execute({ |
| 258 | ...input, | 256 | ...input, |
| 259 | args: [...input.args, "--only-show-errors", "--output", "json"], | 257 | args: [...input.args, "--only-show-errors", "--output", "json"], |
| ... | @@ -282,15 +280,13 @@ export const make = Effect.fn("makeAzureDevOpsCli")(function* () { | ... | @@ -282,15 +280,13 @@ export const make = Effect.fn("makeAzureDevOpsCli")(function* () { |
| 282 | Effect.flatMap((raw) => | 280 | Effect.flatMap((raw) => |
| 283 | raw.length === 0 | 281 | raw.length === 0 |
| 284 | ? Effect.succeed([]) | 282 | ? Effect.succeed([]) |
| 285 | : Effect.sync(() => | 283 | : Effect.sync(() => decodeAzureDevOpsPullRequestListJson(raw)).pipe( |
| 286 | AzureDevOpsPullRequests.decodeAzureDevOpsPullRequestListJson(raw), | ||
| 287 | ).pipe( | ||
| 288 | Effect.flatMap((decoded) => { | 284 | Effect.flatMap((decoded) => { |
| 289 | if (!Result.isSuccess(decoded)) { | 285 | if (!Result.isSuccess(decoded)) { |
| 290 | return Effect.fail( | 286 | return Effect.fail( |
| 291 | new AzureDevOpsCliError({ | 287 | new AzureDevOpsCliError({ |
| 292 | operation: "listPullRequests", | 288 | operation: "listPullRequests", |
| 293 | detail: `Azure DevOps CLI returned invalid PR list JSON: ${AzureDevOpsPullRequests.formatAzureDevOpsJsonDecodeError(decoded.failure)}`, | 289 | detail: `Azure DevOps CLI returned invalid PR list JSON: ${formatAzureDevOpsJsonDecodeError(decoded.failure)}`, |
| 294 | cause: decoded.failure, | 290 | cause: decoded.failure, |
| 295 | }), | 291 | }), |
| 296 | ); | 292 | ); |
| ... | @@ -316,13 +312,13 @@ export const make = Effect.fn("makeAzureDevOpsCli")(function* () { | ... | @@ -316,13 +312,13 @@ export const make = Effect.fn("makeAzureDevOpsCli")(function* () { |
| 316 | }).pipe( | 312 | }).pipe( |
| 317 | Effect.map((result) => result.stdout.trim()), | 313 | Effect.map((result) => result.stdout.trim()), |
| 318 | Effect.flatMap((raw) => | 314 | Effect.flatMap((raw) => |
| 319 | Effect.sync(() => AzureDevOpsPullRequests.decodeAzureDevOpsPullRequestJson(raw)).pipe( | 315 | Effect.sync(() => decodeAzureDevOpsPullRequestJson(raw)).pipe( |
| 320 | Effect.flatMap((decoded) => { | 316 | Effect.flatMap((decoded) => { |
| 321 | if (!Result.isSuccess(decoded)) { | 317 | if (!Result.isSuccess(decoded)) { |
| 322 | return Effect.fail( | 318 | return Effect.fail( |
| 323 | new AzureDevOpsCliError({ | 319 | new AzureDevOpsCliError({ |
| 324 | operation: "getPullRequest", | 320 | operation: "getPullRequest", |
| 325 | detail: `Azure DevOps CLI returned invalid pull request JSON: ${AzureDevOpsPullRequests.formatAzureDevOpsJsonDecodeError(decoded.failure)}`, | 321 | detail: `Azure DevOps CLI returned invalid pull request JSON: ${formatAzureDevOpsJsonDecodeError(decoded.failure)}`, |
| 326 | cause: decoded.failure, | 322 | cause: decoded.failure, |
| 327 | }), | 323 | }), |
| 328 | ); | 324 | ); |
| ... | @@ -434,4 +430,4 @@ export const make = Effect.fn("makeAzureDevOpsCli")(function* () { | ... | @@ -434,4 +430,4 @@ export const make = Effect.fn("makeAzureDevOpsCli")(function* () { |
| 434 | }); | 430 | }); |
| 435 | }); | 431 | }); |
| 436 | 432 | ||
| 437 | export const layer = Layer.effect(AzureDevOpsCli, make()); | 433 | export const layer = Layer.effect(AzureDevOpsCli, make); |
apps/server/src/sourceControl/AzureDevOpsSourceControlProvider.test.ts+5-4| ... | @@ -6,8 +6,8 @@ import * as Option from "effect/Option"; | ... | @@ -6,8 +6,8 @@ import * as Option from "effect/Option"; |
| 6 | import * as AzureDevOpsCli from "./AzureDevOpsCli.ts"; | 6 | import * as AzureDevOpsCli from "./AzureDevOpsCli.ts"; |
| 7 | import * as AzureDevOpsSourceControlProvider from "./AzureDevOpsSourceControlProvider.ts"; | 7 | import * as AzureDevOpsSourceControlProvider from "./AzureDevOpsSourceControlProvider.ts"; |
| 8 | 8 | ||
| 9 | function makeProvider(azure: Partial<AzureDevOpsCli.AzureDevOpsCliShape>) { | 9 | function makeProvider(azure: Partial<AzureDevOpsCli.AzureDevOpsCli["Service"]>) { |
| 10 | return AzureDevOpsSourceControlProvider.make().pipe( | 10 | return AzureDevOpsSourceControlProvider.make.pipe( |
| 11 | Effect.provide(Layer.mock(AzureDevOpsCli.AzureDevOpsCli)(azure)), | 11 | Effect.provide(Layer.mock(AzureDevOpsCli.AzureDevOpsCli)(azure)), |
| 12 | ); | 12 | ); |
| 13 | } | 13 | } |
| ... | @@ -48,8 +48,9 @@ it.effect("maps Azure DevOps PR summaries into provider-neutral change requests" | ... | @@ -48,8 +48,9 @@ it.effect("maps Azure DevOps PR summaries into provider-neutral change requests" |
| 48 | 48 | ||
| 49 | it.effect("creates Azure DevOps PRs through provider-neutral input names", () => | 49 | it.effect("creates Azure DevOps PRs through provider-neutral input names", () => |
| 50 | Effect.gen(function* () { | 50 | Effect.gen(function* () { |
| 51 | let createInput: Parameters<AzureDevOpsCli.AzureDevOpsCliShape["createPullRequest"]>[0] | null = | 51 | let createInput: |
| 52 | null; | 52 | | Parameters<AzureDevOpsCli.AzureDevOpsCli["Service"]["createPullRequest"]>[0] |
| 53 | | null = null; | ||
| 53 | const provider = yield* makeProvider({ | 54 | const provider = yield* makeProvider({ |
| 54 | createPullRequest: (input) => { | 55 | createPullRequest: (input) => { |
| 55 | createInput = input; | 56 | createInput = input; |
apps/server/src/sourceControl/AzureDevOpsSourceControlProvider.ts+15-11| ... | @@ -4,7 +4,13 @@ import { SourceControlProviderError, type ChangeRequest } from "@t3tools/contrac | ... | @@ -4,7 +4,13 @@ import { SourceControlProviderError, type ChangeRequest } from "@t3tools/contrac |
| 4 | 4 | ||
| 5 | import * as AzureDevOpsCli from "./AzureDevOpsCli.ts"; | 5 | import * as AzureDevOpsCli from "./AzureDevOpsCli.ts"; |
| 6 | import * as SourceControlProvider from "./SourceControlProvider.ts"; | 6 | import * as SourceControlProvider from "./SourceControlProvider.ts"; |
| 7 | import * as SourceControlProviderDiscovery from "./SourceControlProviderDiscovery.ts"; | 7 | import { |
| 8 | combinedAuthOutput, | ||
| 9 | firstSafeAuthLine, | ||
| 10 | providerAuth, | ||
| 11 | type SourceControlAuthProbeInput, | ||
| 12 | type SourceControlCliDiscoverySpec, | ||
| 13 | } from "./SourceControlProviderDiscovery.ts"; | ||
| 8 | 14 | ||
| 9 | function providerError( | 15 | function providerError( |
| 10 | operation: string, | 16 | operation: string, |
| ... | @@ -18,28 +24,26 @@ function providerError( | ... | @@ -18,28 +24,26 @@ function providerError( |
| 18 | }); | 24 | }); |
| 19 | } | 25 | } |
| 20 | 26 | ||
| 21 | function parseAzureAuth(input: SourceControlProviderDiscovery.SourceControlAuthProbeInput) { | 27 | function parseAzureAuth(input: SourceControlAuthProbeInput) { |
| 22 | const account = input.stdout.trim().split(/\r?\n/)[0]?.trim(); | 28 | const account = input.stdout.trim().split(/\r?\n/)[0]?.trim(); |
| 23 | 29 | ||
| 24 | if (input.exitCode !== 0) { | 30 | if (input.exitCode !== 0) { |
| 25 | return SourceControlProviderDiscovery.providerAuth({ | 31 | return providerAuth({ |
| 26 | status: "unauthenticated", | 32 | status: "unauthenticated", |
| 27 | detail: | 33 | detail: |
| 28 | SourceControlProviderDiscovery.firstSafeAuthLine( | 34 | firstSafeAuthLine(combinedAuthOutput(input)) ?? "Run `az login` to authenticate Azure CLI.", |
| 29 | SourceControlProviderDiscovery.combinedAuthOutput(input), | ||
| 30 | ) ?? "Run `az login` to authenticate Azure CLI.", | ||
| 31 | }); | 35 | }); |
| 32 | } | 36 | } |
| 33 | 37 | ||
| 34 | if (account !== undefined && account.length > 0) { | 38 | if (account !== undefined && account.length > 0) { |
| 35 | return SourceControlProviderDiscovery.providerAuth({ | 39 | return providerAuth({ |
| 36 | status: "authenticated", | 40 | status: "authenticated", |
| 37 | account, | 41 | account, |
| 38 | host: "dev.azure.com", | 42 | host: "dev.azure.com", |
| 39 | }); | 43 | }); |
| 40 | } | 44 | } |
| 41 | 45 | ||
| 42 | return SourceControlProviderDiscovery.providerAuth({ | 46 | return providerAuth({ |
| 43 | status: "unknown", | 47 | status: "unknown", |
| 44 | host: "dev.azure.com", | 48 | host: "dev.azure.com", |
| 45 | detail: "Azure CLI account status could not be parsed.", | 49 | detail: "Azure CLI account status could not be parsed.", |
| ... | @@ -56,7 +60,7 @@ export const discovery = { | ... | @@ -56,7 +60,7 @@ export const discovery = { |
| 56 | parseAuth: parseAzureAuth, | 60 | parseAuth: parseAzureAuth, |
| 57 | installHint: | 61 | installHint: |
| 58 | "Install the Azure command-line tools (`az`), then enable Azure DevOps support with `az extension add --name azure-devops`.", | 62 | "Install the Azure command-line tools (`az`), then enable Azure DevOps support with `az extension add --name azure-devops`.", |
| 59 | } satisfies SourceControlProviderDiscovery.SourceControlCliDiscoverySpec; | 63 | } satisfies SourceControlCliDiscoverySpec; |
| 60 | 64 | ||
| 61 | function toChangeRequest(summary: { | 65 | function toChangeRequest(summary: { |
| 62 | readonly number: number; | 66 | readonly number: number; |
| ... | @@ -80,7 +84,7 @@ function toChangeRequest(summary: { | ... | @@ -80,7 +84,7 @@ function toChangeRequest(summary: { |
| 80 | }; | 84 | }; |
| 81 | } | 85 | } |
| 82 | 86 | ||
| 83 | export const make = Effect.fn("makeAzureDevOpsSourceControlProvider")(function* () { | 87 | export const make = Effect.gen(function* () { |
| 84 | const azure = yield* AzureDevOpsCli.AzureDevOpsCli; | 88 | const azure = yield* AzureDevOpsCli.AzureDevOpsCli; |
| 85 | 89 | ||
| 86 | return SourceControlProvider.SourceControlProvider.of({ | 90 | return SourceControlProvider.SourceControlProvider.of({ |
| ... | @@ -142,4 +146,4 @@ export const make = Effect.fn("makeAzureDevOpsSourceControlProvider")(function* | ... | @@ -142,4 +146,4 @@ export const make = Effect.fn("makeAzureDevOpsSourceControlProvider")(function* |
| 142 | }); | 146 | }); |
| 143 | }); | 147 | }); |
| 144 | 148 | ||
| 145 | export const layer = Layer.effect(SourceControlProvider.SourceControlProvider, make()); | 149 | export const layer = Layer.effect(SourceControlProvider.SourceControlProvider, make); |
apps/server/src/sourceControl/BitbucketApi.test.ts+16-16| ... | @@ -53,41 +53,41 @@ const repositoryJson = { | ... | @@ -53,41 +53,41 @@ const repositoryJson = { |
| 53 | 53 | ||
| 54 | function makeLayer(input: { | 54 | function makeLayer(input: { |
| 55 | readonly response: (request: HttpClientRequest.HttpClientRequest) => Response; | 55 | readonly response: (request: HttpClientRequest.HttpClientRequest) => Response; |
| 56 | readonly git?: Partial<GitVcsDriver.GitVcsDriverShape>; | 56 | readonly git?: Partial<GitVcsDriver.GitVcsDriver["Service"]>; |
| 57 | }) { | 57 | }) { |
| 58 | const execute = vi.fn((request: HttpClientRequest.HttpClientRequest) => | 58 | const execute = vi.fn((request: HttpClientRequest.HttpClientRequest) => |
| 59 | Effect.succeed(HttpClientResponse.fromWeb(request, input.response(request))), | 59 | Effect.succeed(HttpClientResponse.fromWeb(request, input.response(request))), |
| 60 | ); | 60 | ); |
| 61 | const gitMock = { | 61 | const gitMock = { |
| 62 | readConfigValue: vi.fn<GitVcsDriver.GitVcsDriverShape["readConfigValue"]>(() => | 62 | readConfigValue: vi.fn<GitVcsDriver.GitVcsDriver["Service"]["readConfigValue"]>(() => |
| 63 | Effect.succeed<string | null>("git@bitbucket.org:pingdotgg/t3code.git"), | 63 | Effect.succeed<string | null>("git@bitbucket.org:pingdotgg/t3code.git"), |
| 64 | ), | 64 | ), |
| 65 | resolvePrimaryRemoteName: vi.fn<GitVcsDriver.GitVcsDriverShape["resolvePrimaryRemoteName"]>( | 65 | resolvePrimaryRemoteName: vi.fn< |
| 66 | () => Effect.succeed("origin"), | 66 | GitVcsDriver.GitVcsDriver["Service"]["resolvePrimaryRemoteName"] |
| 67 | ), | 67 | >(() => Effect.succeed("origin")), |
| 68 | ensureRemote: vi.fn<GitVcsDriver.GitVcsDriverShape["ensureRemote"]>(() => | 68 | ensureRemote: vi.fn<GitVcsDriver.GitVcsDriver["Service"]["ensureRemote"]>(() => |
| 69 | Effect.succeed("octocat"), | 69 | Effect.succeed("octocat"), |
| 70 | ), | 70 | ), |
| 71 | fetchRemoteBranch: vi.fn<GitVcsDriver.GitVcsDriverShape["fetchRemoteBranch"]>( | 71 | fetchRemoteBranch: vi.fn<GitVcsDriver.GitVcsDriver["Service"]["fetchRemoteBranch"]>( |
| 72 | () => Effect.void, | ||
| 73 | ), | ||
| 74 | fetchRemoteTrackingBranch: vi.fn<GitVcsDriver.GitVcsDriverShape["fetchRemoteTrackingBranch"]>( | ||
| 75 | () => Effect.void, | 72 | () => Effect.void, |
| 76 | ), | 73 | ), |
| 77 | setBranchUpstream: vi.fn<GitVcsDriver.GitVcsDriverShape["setBranchUpstream"]>( | 74 | fetchRemoteTrackingBranch: vi.fn< |
| 75 | GitVcsDriver.GitVcsDriver["Service"]["fetchRemoteTrackingBranch"] | ||
| 76 | >(() => Effect.void), | ||
| 77 | setBranchUpstream: vi.fn<GitVcsDriver.GitVcsDriver["Service"]["setBranchUpstream"]>( | ||
| 78 | () => Effect.void, | 78 | () => Effect.void, |
| 79 | ), | 79 | ), |
| 80 | switchRef: vi.fn<GitVcsDriver.GitVcsDriverShape["switchRef"]>((request) => | 80 | switchRef: vi.fn<GitVcsDriver.GitVcsDriver["Service"]["switchRef"]>((request) => |
| 81 | Effect.succeed({ refName: request.refName }), | 81 | Effect.succeed({ refName: request.refName }), |
| 82 | ), | 82 | ), |
| 83 | listLocalBranchNames: vi.fn<GitVcsDriver.GitVcsDriverShape["listLocalBranchNames"]>(() => | 83 | listLocalBranchNames: vi.fn<GitVcsDriver.GitVcsDriver["Service"]["listLocalBranchNames"]>(() => |
| 84 | Effect.succeed([]), | 84 | Effect.succeed([]), |
| 85 | ), | 85 | ), |
| 86 | }; | 86 | }; |
| 87 | const git = { | 87 | const git = { |
| 88 | ...gitMock, | 88 | ...gitMock, |
| 89 | ...input.git, | 89 | ...input.git, |
| 90 | } satisfies Partial<GitVcsDriver.GitVcsDriverShape>; | 90 | } satisfies Partial<GitVcsDriver.GitVcsDriver["Service"]>; |
| 91 | 91 | ||
| 92 | const driver = { | 92 | const driver = { |
| 93 | listRemotes: () => | 93 | listRemotes: () => |
| ... | @@ -106,7 +106,7 @@ function makeLayer(input: { | ... | @@ -106,7 +106,7 @@ function makeLayer(input: { |
| 106 | expiresAt: Option.none(), | 106 | expiresAt: Option.none(), |
| 107 | }, | 107 | }, |
| 108 | }), | 108 | }), |
| 109 | } satisfies Partial<VcsDriver.VcsDriverShape>; | 109 | } satisfies Partial<VcsDriver.VcsDriver["Service"]>; |
| 110 | 110 | ||
| 111 | const layer = BitbucketApi.layer.pipe( | 111 | const layer = BitbucketApi.layer.pipe( |
| 112 | Layer.provide( | 112 | Layer.provide( |
| ... | @@ -130,7 +130,7 @@ function makeLayer(input: { | ... | @@ -130,7 +130,7 @@ function makeLayer(input: { |
| 130 | expiresAt: Option.none(), | 130 | expiresAt: Option.none(), |
| 131 | }, | 131 | }, |
| 132 | }, | 132 | }, |
| 133 | driver: driver as unknown as VcsDriver.VcsDriverShape, | 133 | driver: driver as unknown as VcsDriver.VcsDriver["Service"], |
| 134 | }), | 134 | }), |
| 135 | }), | 135 | }), |
| 136 | ), | 136 | ), |
apps/server/src/sourceControl/BitbucketApi.ts+64-76| ... | @@ -15,7 +15,12 @@ import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstab | ... | @@ -15,7 +15,12 @@ import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstab |
| 15 | import { sanitizeBranchFragment } from "@t3tools/shared/git"; | 15 | import { sanitizeBranchFragment } from "@t3tools/shared/git"; |
| 16 | import { detectSourceControlProviderFromRemoteUrl } from "@t3tools/shared/sourceControl"; | 16 | import { detectSourceControlProviderFromRemoteUrl } from "@t3tools/shared/sourceControl"; |
| 17 | 17 | ||
| 18 | import * as BitbucketPullRequests from "./bitbucketPullRequests.ts"; | 18 | import { |
| 19 | BitbucketPullRequestListSchema, | ||
| 20 | BitbucketPullRequestSchema, | ||
| 21 | normalizeBitbucketPullRequestRecord, | ||
| 22 | type NormalizedBitbucketPullRequestRecord, | ||
| 23 | } from "./bitbucketPullRequests.ts"; | ||
| 19 | import * as SourceControlProvider from "./SourceControlProvider.ts"; | 24 | import * as SourceControlProvider from "./SourceControlProvider.ts"; |
| 20 | import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; | 25 | import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; |
| 21 | import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts"; | 26 | import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts"; |
| ... | @@ -44,7 +49,7 @@ export class BitbucketApiError extends Schema.TaggedErrorClass<BitbucketApiError | ... | @@ -44,7 +49,7 @@ export class BitbucketApiError extends Schema.TaggedErrorClass<BitbucketApiError |
| 44 | return `Bitbucket API failed in ${this.operation}: ${this.detail}`; | 49 | return `Bitbucket API failed in ${this.operation}: ${this.detail}`; |
| 45 | } | 50 | } |
| 46 | } | 51 | } |
| 47 | const isBitbucketApiErrorValue = Schema.is(BitbucketApiError); | 52 | const isBitbucketApiError = Schema.is(BitbucketApiError); |
| 48 | 53 | ||
| 49 | const RawBitbucketRepositorySchema = Schema.Struct({ | 54 | const RawBitbucketRepositorySchema = Schema.Struct({ |
| 50 | full_name: TrimmedNonEmptyString, | 55 | full_name: TrimmedNonEmptyString, |
| ... | @@ -100,62 +105,55 @@ export interface BitbucketRepositoryLocator { | ... | @@ -100,62 +105,55 @@ export interface BitbucketRepositoryLocator { |
| 100 | readonly repoSlug: string; | 105 | readonly repoSlug: string; |
| 101 | } | 106 | } |
| 102 | 107 | ||
| 103 | export interface BitbucketApiShape { | 108 | export class BitbucketApi extends Context.Service< |
| 104 | readonly probeAuth: Effect.Effect<SourceControlProviderAuth, never>; | 109 | BitbucketApi, |
| 105 | readonly listPullRequests: (input: { | 110 | { |
| 106 | readonly cwd: string; | 111 | readonly probeAuth: Effect.Effect<SourceControlProviderAuth, never>; |
| 107 | readonly context?: SourceControlProvider.SourceControlProviderContext; | 112 | readonly listPullRequests: (input: { |
| 108 | readonly headSelector: string; | 113 | readonly cwd: string; |
| 109 | readonly source?: SourceControlProvider.SourceControlRefSelector; | 114 | readonly context?: SourceControlProvider.SourceControlProviderContext; |
| 110 | readonly state: "open" | "closed" | "merged" | "all"; | 115 | readonly headSelector: string; |
| 111 | readonly limit?: number; | 116 | readonly source?: SourceControlProvider.SourceControlRefSelector; |
| 112 | }) => Effect.Effect< | 117 | readonly state: "open" | "closed" | "merged" | "all"; |
| 113 | ReadonlyArray<BitbucketPullRequests.NormalizedBitbucketPullRequestRecord>, | 118 | readonly limit?: number; |
| 114 | BitbucketApiError | 119 | }) => Effect.Effect<ReadonlyArray<NormalizedBitbucketPullRequestRecord>, BitbucketApiError>; |
| 115 | >; | 120 | readonly getPullRequest: (input: { |
| 116 | readonly getPullRequest: (input: { | 121 | readonly cwd: string; |
| 117 | readonly cwd: string; | 122 | readonly context?: SourceControlProvider.SourceControlProviderContext; |
| 118 | readonly context?: SourceControlProvider.SourceControlProviderContext; | 123 | readonly reference: string; |
| 119 | readonly reference: string; | 124 | }) => Effect.Effect<NormalizedBitbucketPullRequestRecord, BitbucketApiError>; |
| 120 | }) => Effect.Effect< | 125 | readonly getRepositoryCloneUrls: (input: { |
| 121 | BitbucketPullRequests.NormalizedBitbucketPullRequestRecord, | 126 | readonly cwd: string; |
| 122 | BitbucketApiError | 127 | readonly context?: SourceControlProvider.SourceControlProviderContext; |
| 123 | >; | 128 | readonly repository: string; |
| 124 | readonly getRepositoryCloneUrls: (input: { | 129 | }) => Effect.Effect<SourceControlRepositoryCloneUrls, BitbucketApiError>; |
| 125 | readonly cwd: string; | 130 | readonly createRepository: (input: { |
| 126 | readonly context?: SourceControlProvider.SourceControlProviderContext; | 131 | readonly cwd: string; |
| 127 | readonly repository: string; | 132 | readonly repository: string; |
| 128 | }) => Effect.Effect<SourceControlRepositoryCloneUrls, BitbucketApiError>; | 133 | readonly visibility: SourceControlRepositoryVisibility; |
| 129 | readonly createRepository: (input: { | 134 | }) => Effect.Effect<SourceControlRepositoryCloneUrls, BitbucketApiError>; |
| 130 | readonly cwd: string; | 135 | readonly createPullRequest: (input: { |
| 131 | readonly repository: string; | 136 | readonly cwd: string; |
| 132 | readonly visibility: SourceControlRepositoryVisibility; | 137 | readonly context?: SourceControlProvider.SourceControlProviderContext; |
| 133 | }) => Effect.Effect<SourceControlRepositoryCloneUrls, BitbucketApiError>; | 138 | readonly baseBranch: string; |
| 134 | readonly createPullRequest: (input: { | 139 | readonly headSelector: string; |
| 135 | readonly cwd: string; | 140 | readonly source?: SourceControlProvider.SourceControlRefSelector; |
| 136 | readonly context?: SourceControlProvider.SourceControlProviderContext; | 141 | readonly target?: SourceControlProvider.SourceControlRefSelector; |
| 137 | readonly baseBranch: string; | 142 | readonly title: string; |
| 138 | readonly headSelector: string; | 143 | readonly bodyFile: string; |
| 139 | readonly source?: SourceControlProvider.SourceControlRefSelector; | 144 | }) => Effect.Effect<void, BitbucketApiError>; |
| 140 | readonly target?: SourceControlProvider.SourceControlRefSelector; | 145 | readonly getDefaultBranch: (input: { |
| 141 | readonly title: string; | 146 | readonly cwd: string; |
| 142 | readonly bodyFile: string; | 147 | readonly context?: SourceControlProvider.SourceControlProviderContext; |
| 143 | }) => Effect.Effect<void, BitbucketApiError>; | 148 | }) => Effect.Effect<string | null, BitbucketApiError>; |
| 144 | readonly getDefaultBranch: (input: { | 149 | readonly checkoutPullRequest: (input: { |
| 145 | readonly cwd: string; | 150 | readonly cwd: string; |
| 146 | readonly context?: SourceControlProvider.SourceControlProviderContext; | 151 | readonly context?: SourceControlProvider.SourceControlProviderContext; |
| 147 | }) => Effect.Effect<string | null, BitbucketApiError>; | 152 | readonly reference: string; |
| 148 | readonly checkoutPullRequest: (input: { | 153 | readonly force?: boolean; |
| 149 | readonly cwd: string; | 154 | }) => Effect.Effect<void, BitbucketApiError>; |
| 150 | readonly context?: SourceControlProvider.SourceControlProviderContext; | 155 | } |
| 151 | readonly reference: string; | 156 | >()("t3/sourceControl/BitbucketApi") {} |
| 152 | readonly force?: boolean; | ||
| 153 | }) => Effect.Effect<void, BitbucketApiError>; | ||
| 154 | } | ||
| 155 | |||
| 156 | export class BitbucketApi extends Context.Service<BitbucketApi, BitbucketApiShape>()( | ||
| 157 | "t3/sourceControl/BitbucketApi", | ||
| 158 | ) {} | ||
| 159 | 157 | ||
| 160 | function nonEmpty(value: string | undefined): Option.Option<string> { | 158 | function nonEmpty(value: string | undefined): Option.Option<string> { |
| 161 | const trimmed = value?.trim(); | 159 | const trimmed = value?.trim(); |
| ... | @@ -299,9 +297,7 @@ function checkoutBranchName(input: { | ... | @@ -299,9 +297,7 @@ function checkoutBranchName(input: { |
| 299 | } | 297 | } |
| 300 | 298 | ||
| 301 | function repositoryNameWithOwner( | 299 | function repositoryNameWithOwner( |
| 302 | repository: Schema.Schema.Type< | 300 | repository: Schema.Schema.Type<typeof BitbucketPullRequestSchema>["source"]["repository"], |
| 303 | typeof BitbucketPullRequests.BitbucketPullRequestSchema | ||
| 304 | >["source"]["repository"], | ||
| 305 | ): string | null { | 301 | ): string | null { |
| 306 | const fullName = repository?.full_name?.trim() ?? ""; | 302 | const fullName = repository?.full_name?.trim() ?? ""; |
| 307 | return fullName.length > 0 ? fullName : null; | 303 | return fullName.length > 0 ? fullName : null; |
| ... | @@ -350,10 +346,6 @@ function requestError(operation: string, cause: unknown): BitbucketApiError { | ... | @@ -350,10 +346,6 @@ function requestError(operation: string, cause: unknown): BitbucketApiError { |
| 350 | }); | 346 | }); |
| 351 | } | 347 | } |
| 352 | 348 | ||
| 353 | function isBitbucketApiError(cause: unknown): cause is BitbucketApiError { | ||
| 354 | return isBitbucketApiErrorValue(cause); | ||
| 355 | } | ||
| 356 | |||
| 357 | function responseError( | 349 | function responseError( |
| 358 | operation: string, | 350 | operation: string, |
| 359 | response: HttpClientResponse.HttpClientResponse, | 351 | response: HttpClientResponse.HttpClientResponse, |
| ... | @@ -375,7 +367,7 @@ function responseError( | ... | @@ -375,7 +367,7 @@ function responseError( |
| 375 | ); | 367 | ); |
| 376 | } | 368 | } |
| 377 | 369 | ||
| 378 | export const make = Effect.fn("makeBitbucketApi")(function* () { | 370 | export const make = Effect.gen(function* () { |
| 379 | const config = yield* BitbucketApiEnvConfig; | 371 | const config = yield* BitbucketApiEnvConfig; |
| 380 | const httpClient = yield* HttpClient.HttpClient; | 372 | const httpClient = yield* HttpClient.HttpClient; |
| 381 | const fileSystem = yield* FileSystem.FileSystem; | 373 | const fileSystem = yield* FileSystem.FileSystem; |
| ... | @@ -511,7 +503,7 @@ export const make = Effect.fn("makeBitbucketApi")(function* () { | ... | @@ -511,7 +503,7 @@ export const make = Effect.fn("makeBitbucketApi")(function* () { |
| 511 | `/repositories/${encodeURIComponent(repository.workspace)}/${encodeURIComponent(repository.repoSlug)}/pullrequests/${encodeURIComponent(normalizeChangeRequestId(reference))}`, | 503 | `/repositories/${encodeURIComponent(repository.workspace)}/${encodeURIComponent(repository.repoSlug)}/pullrequests/${encodeURIComponent(normalizeChangeRequestId(reference))}`, |
| 512 | ), | 504 | ), |
| 513 | ), | 505 | ), |
| 514 | BitbucketPullRequests.BitbucketPullRequestSchema, | 506 | BitbucketPullRequestSchema, |
| 515 | ); | 507 | ); |
| 516 | 508 | ||
| 517 | const getRawPullRequest = (input: { | 509 | const getRawPullRequest = (input: { |
| ... | @@ -599,17 +591,13 @@ export const make = Effect.fn("makeBitbucketApi")(function* () { | ... | @@ -599,17 +591,13 @@ export const make = Effect.fn("makeBitbucketApi")(function* () { |
| 599 | ), | 591 | ), |
| 600 | { urlParams: query }, | 592 | { urlParams: query }, |
| 601 | ), | 593 | ), |
| 602 | BitbucketPullRequests.BitbucketPullRequestListSchema, | 594 | BitbucketPullRequestListSchema, |
| 603 | ); | 595 | ); |
| 604 | }), | 596 | }), |
| 605 | Effect.map((list) => | 597 | Effect.map((list) => list.values.map(normalizeBitbucketPullRequestRecord)), |
| 606 | list.values.map(BitbucketPullRequests.normalizeBitbucketPullRequestRecord), | ||
| 607 | ), | ||
| 608 | ), | 598 | ), |
| 609 | getPullRequest: (input) => | 599 | getPullRequest: (input) => |
| 610 | getRawPullRequest(input).pipe( | 600 | getRawPullRequest(input).pipe(Effect.map(normalizeBitbucketPullRequestRecord)), |
| 611 | Effect.map(BitbucketPullRequests.normalizeBitbucketPullRequestRecord), | ||
| 612 | ), | ||
| 613 | getRepositoryCloneUrls: (input) => | 601 | getRepositoryCloneUrls: (input) => |
| 614 | getRepository(input).pipe(Effect.map(normalizeRepositoryCloneUrls)), | 602 | getRepository(input).pipe(Effect.map(normalizeRepositoryCloneUrls)), |
| 615 | createRepository: (input) => | 603 | createRepository: (input) => |
| ... | @@ -675,7 +663,7 @@ export const make = Effect.fn("makeBitbucketApi")(function* () { | ... | @@ -675,7 +663,7 @@ export const make = Effect.fn("makeBitbucketApi")(function* () { |
| 675 | `/repositories/${encodeURIComponent(repository.workspace)}/${encodeURIComponent(repository.repoSlug)}/pullrequests`, | 663 | `/repositories/${encodeURIComponent(repository.workspace)}/${encodeURIComponent(repository.repoSlug)}/pullrequests`, |
| 676 | ), | 664 | ), |
| 677 | ).pipe(HttpClientRequest.bodyJsonUnsafe(body)), | 665 | ).pipe(HttpClientRequest.bodyJsonUnsafe(body)), |
| 678 | BitbucketPullRequests.BitbucketPullRequestSchema, | 666 | BitbucketPullRequestSchema, |
| 679 | ); | 667 | ); |
| 680 | }), | 668 | }), |
| 681 | getDefaultBranch: (input) => | 669 | getDefaultBranch: (input) => |
| ... | @@ -766,4 +754,4 @@ export const make = Effect.fn("makeBitbucketApi")(function* () { | ... | @@ -766,4 +754,4 @@ export const make = Effect.fn("makeBitbucketApi")(function* () { |
| 766 | }); | 754 | }); |
| 767 | }); | 755 | }); |
| 768 | 756 | ||
| 769 | export const layer = Layer.effect(BitbucketApi, make()); | 757 | export const layer = Layer.effect(BitbucketApi, make); |
apps/server/src/sourceControl/BitbucketSourceControlProvider.test.ts+7-5| ... | @@ -6,8 +6,8 @@ import * as Option from "effect/Option"; | ... | @@ -6,8 +6,8 @@ import * as Option from "effect/Option"; |
| 6 | import * as BitbucketApi from "./BitbucketApi.ts"; | 6 | import * as BitbucketApi from "./BitbucketApi.ts"; |
| 7 | import * as BitbucketSourceControlProvider from "./BitbucketSourceControlProvider.ts"; | 7 | import * as BitbucketSourceControlProvider from "./BitbucketSourceControlProvider.ts"; |
| 8 | 8 | ||
| 9 | function makeProvider(bitbucket: Partial<BitbucketApi.BitbucketApiShape>) { | 9 | function makeProvider(bitbucket: Partial<BitbucketApi.BitbucketApi["Service"]>) { |
| 10 | return BitbucketSourceControlProvider.make().pipe( | 10 | return BitbucketSourceControlProvider.make.pipe( |
| 11 | Effect.provide(Layer.mock(BitbucketApi.BitbucketApi)(bitbucket)), | 11 | Effect.provide(Layer.mock(BitbucketApi.BitbucketApi)(bitbucket)), |
| 12 | ); | 12 | ); |
| 13 | } | 13 | } |
| ... | @@ -53,7 +53,8 @@ it.effect("maps Bitbucket PR summaries into provider-neutral change requests", ( | ... | @@ -53,7 +53,8 @@ it.effect("maps Bitbucket PR summaries into provider-neutral change requests", ( |
| 53 | 53 | ||
| 54 | it.effect("lists Bitbucket PRs through provider-neutral input names", () => | 54 | it.effect("lists Bitbucket PRs through provider-neutral input names", () => |
| 55 | Effect.gen(function* () { | 55 | Effect.gen(function* () { |
| 56 | let listInput: Parameters<BitbucketApi.BitbucketApiShape["listPullRequests"]>[0] | null = null; | 56 | let listInput: Parameters<BitbucketApi.BitbucketApi["Service"]["listPullRequests"]>[0] | null = |
| 57 | null; | ||
| 57 | const provider = yield* makeProvider({ | 58 | const provider = yield* makeProvider({ |
| 58 | listPullRequests: (input) => { | 59 | listPullRequests: (input) => { |
| 59 | listInput = input; | 60 | listInput = input; |
| ... | @@ -79,8 +80,9 @@ it.effect("lists Bitbucket PRs through provider-neutral input names", () => | ... | @@ -79,8 +80,9 @@ it.effect("lists Bitbucket PRs through provider-neutral input names", () => |
| 79 | 80 | ||
| 80 | it.effect("creates Bitbucket PRs through provider-neutral input names", () => | 81 | it.effect("creates Bitbucket PRs through provider-neutral input names", () => |
| 81 | Effect.gen(function* () { | 82 | Effect.gen(function* () { |
| 82 | let createInput: Parameters<BitbucketApi.BitbucketApiShape["createPullRequest"]>[0] | null = | 83 | let createInput: |
| 83 | null; | 84 | | Parameters<BitbucketApi.BitbucketApi["Service"]["createPullRequest"]>[0] |
| 85 | | null = null; | ||
| 84 | const provider = yield* makeProvider({ | 86 | const provider = yield* makeProvider({ |
| 85 | createPullRequest: (input) => { | 87 | createPullRequest: (input) => { |
| 86 | createInput = input; | 88 | createInput = input; |
apps/server/src/sourceControl/BitbucketSourceControlProvider.ts+7-9| ... | @@ -4,9 +4,9 @@ import * as Option from "effect/Option"; | ... | @@ -4,9 +4,9 @@ import * as Option from "effect/Option"; |
| 4 | import { SourceControlProviderError, type ChangeRequest } from "@t3tools/contracts"; | 4 | import { SourceControlProviderError, type ChangeRequest } from "@t3tools/contracts"; |
| 5 | 5 | ||
| 6 | import * as BitbucketApi from "./BitbucketApi.ts"; | 6 | import * as BitbucketApi from "./BitbucketApi.ts"; |
| 7 | import * as BitbucketPullRequests from "./bitbucketPullRequests.ts"; | 7 | import type { NormalizedBitbucketPullRequestRecord } from "./bitbucketPullRequests.ts"; |
| 8 | import * as SourceControlProvider from "./SourceControlProvider.ts"; | 8 | import * as SourceControlProvider from "./SourceControlProvider.ts"; |
| 9 | import type * as SourceControlProviderDiscovery from "./SourceControlProviderDiscovery.ts"; | 9 | import type { SourceControlApiDiscoverySpec } from "./SourceControlProviderDiscovery.ts"; |
| 10 | 10 | ||
| 11 | function providerError( | 11 | function providerError( |
| 12 | operation: string, | 12 | operation: string, |
| ... | @@ -20,9 +20,7 @@ function providerError( | ... | @@ -20,9 +20,7 @@ function providerError( |
| 20 | }); | 20 | }); |
| 21 | } | 21 | } |
| 22 | 22 | ||
| 23 | function toChangeRequest( | 23 | function toChangeRequest(summary: NormalizedBitbucketPullRequestRecord): ChangeRequest { |
| 24 | summary: BitbucketPullRequests.NormalizedBitbucketPullRequestRecord, | ||
| 25 | ): ChangeRequest { | ||
| 26 | return { | 24 | return { |
| 27 | provider: "bitbucket", | 25 | provider: "bitbucket", |
| 28 | number: summary.number, | 26 | number: summary.number, |
| ... | @@ -44,7 +42,7 @@ function toChangeRequest( | ... | @@ -44,7 +42,7 @@ function toChangeRequest( |
| 44 | }; | 42 | }; |
| 45 | } | 43 | } |
| 46 | 44 | ||
| 47 | export const make = Effect.fn("makeBitbucketSourceControlProvider")(function* () { | 45 | export const make = Effect.gen(function* () { |
| 48 | const bitbucket = yield* BitbucketApi.BitbucketApi; | 46 | const bitbucket = yield* BitbucketApi.BitbucketApi; |
| 49 | 47 | ||
| 50 | return SourceControlProvider.SourceControlProvider.of({ | 48 | return SourceControlProvider.SourceControlProvider.of({ |
| ... | @@ -112,9 +110,9 @@ export const make = Effect.fn("makeBitbucketSourceControlProvider")(function* () | ... | @@ -112,9 +110,9 @@ export const make = Effect.fn("makeBitbucketSourceControlProvider")(function* () |
| 112 | }); | 110 | }); |
| 113 | }); | 111 | }); |
| 114 | 112 | ||
| 115 | export const layer = Layer.effect(SourceControlProvider.SourceControlProvider, make()); | 113 | export const layer = Layer.effect(SourceControlProvider.SourceControlProvider, make); |
| 116 | 114 | ||
| 117 | export const makeDiscovery = Effect.fn("makeBitbucketSourceControlProviderDiscovery")(function* () { | 115 | export const makeDiscovery = Effect.gen(function* () { |
| 118 | const bitbucket = yield* BitbucketApi.BitbucketApi; | 116 | const bitbucket = yield* BitbucketApi.BitbucketApi; |
| 119 | 117 | ||
| 120 | return { | 118 | return { |
| ... | @@ -124,5 +122,5 @@ export const makeDiscovery = Effect.fn("makeBitbucketSourceControlProviderDiscov | ... | @@ -124,5 +122,5 @@ export const makeDiscovery = Effect.fn("makeBitbucketSourceControlProviderDiscov |
| 124 | installHint: | 122 | installHint: |
| 125 | "Set T3CODE_BITBUCKET_EMAIL and T3CODE_BITBUCKET_API_TOKEN on the server (use a Bitbucket API token with pull request and repository scopes).", | 123 | "Set T3CODE_BITBUCKET_EMAIL and T3CODE_BITBUCKET_API_TOKEN on the server (use a Bitbucket API token with pull request and repository scopes).", |
| 126 | probeAuth: bitbucket.probeAuth, | 124 | probeAuth: bitbucket.probeAuth, |
| 127 | } satisfies SourceControlProviderDiscovery.SourceControlApiDiscoverySpec; | 125 | } satisfies SourceControlApiDiscoverySpec; |
| 128 | }); | 126 | }); |
apps/server/src/sourceControl/GitHubCli.test.ts+1-1| ... | @@ -15,7 +15,7 @@ const processOutput = (stdout: string): VcsProcess.VcsProcessOutput => ({ | ... | @@ -15,7 +15,7 @@ const processOutput = (stdout: string): VcsProcess.VcsProcessOutput => ({ |
| 15 | stderrTruncated: false, | 15 | stderrTruncated: false, |
| 16 | }); | 16 | }); |
| 17 | 17 | ||
| 18 | const mockRun = vi.fn<VcsProcess.VcsProcessShape["run"]>(); | 18 | const mockRun = vi.fn<VcsProcess.VcsProcess["Service"]["run"]>(); |
| 19 | 19 | ||
| 20 | const layer = GitHubCli.layer.pipe( | 20 | const layer = GitHubCli.layer.pipe( |
| 21 | Layer.provide( | 21 | Layer.provide( |
apps/server/src/sourceControl/GitHubCli.ts+62-59| ... | @@ -12,7 +12,11 @@ import { | ... | @@ -12,7 +12,11 @@ import { |
| 12 | } from "@t3tools/contracts"; | 12 | } from "@t3tools/contracts"; |
| 13 | 13 | ||
| 14 | import * as VcsProcess from "../vcs/VcsProcess.ts"; | 14 | import * as VcsProcess from "../vcs/VcsProcess.ts"; |
| 15 | import * as GitHubPullRequests from "./gitHubPullRequests.ts"; | 15 | import { |
| 16 | decodeGitHubPullRequestJson, | ||
| 17 | decodeGitHubPullRequestListJson, | ||
| 18 | formatGitHubJsonDecodeError, | ||
| 19 | } from "./gitHubPullRequests.ts"; | ||
| 16 | 20 | ||
| 17 | const DEFAULT_TIMEOUT_MS = 30_000; | 21 | const DEFAULT_TIMEOUT_MS = 30_000; |
| 18 | 22 | ||
| ... | @@ -44,57 +48,56 @@ export interface GitHubRepositoryCloneUrls { | ... | @@ -44,57 +48,56 @@ export interface GitHubRepositoryCloneUrls { |
| 44 | readonly sshUrl: string; | 48 | readonly sshUrl: string; |
| 45 | } | 49 | } |
| 46 | 50 | ||
| 47 | export interface GitHubCliShape { | 51 | export class GitHubCli extends Context.Service< |
| 48 | readonly execute: (input: { | 52 | GitHubCli, |
| 49 | readonly cwd: string; | 53 | { |
| 50 | readonly args: ReadonlyArray<string>; | 54 | readonly execute: (input: { |
| 51 | readonly timeoutMs?: number; | 55 | readonly cwd: string; |
| 52 | }) => Effect.Effect<VcsProcess.VcsProcessOutput, GitHubCliError>; | 56 | readonly args: ReadonlyArray<string>; |
| 53 | 57 | readonly timeoutMs?: number; | |
| 54 | readonly listOpenPullRequests: (input: { | 58 | }) => Effect.Effect<VcsProcess.VcsProcessOutput, GitHubCliError>; |
| 55 | readonly cwd: string; | 59 | |
| 56 | readonly headSelector: string; | 60 | readonly listOpenPullRequests: (input: { |
| 57 | readonly limit?: number; | 61 | readonly cwd: string; |
| 58 | }) => Effect.Effect<ReadonlyArray<GitHubPullRequestSummary>, GitHubCliError>; | 62 | readonly headSelector: string; |
| 59 | 63 | readonly limit?: number; | |
| 60 | readonly getPullRequest: (input: { | 64 | }) => Effect.Effect<ReadonlyArray<GitHubPullRequestSummary>, GitHubCliError>; |
| 61 | readonly cwd: string; | 65 | |
| 62 | readonly reference: string; | 66 | readonly getPullRequest: (input: { |
| 63 | }) => Effect.Effect<GitHubPullRequestSummary, GitHubCliError>; | 67 | readonly cwd: string; |
| 64 | 68 | readonly reference: string; | |
| 65 | readonly getRepositoryCloneUrls: (input: { | 69 | }) => Effect.Effect<GitHubPullRequestSummary, GitHubCliError>; |
| 66 | readonly cwd: string; | 70 | |
| 67 | readonly repository: string; | 71 | readonly getRepositoryCloneUrls: (input: { |
| 68 | }) => Effect.Effect<GitHubRepositoryCloneUrls, GitHubCliError>; | 72 | readonly cwd: string; |
| 69 | 73 | readonly repository: string; | |
| 70 | readonly createRepository: (input: { | 74 | }) => Effect.Effect<GitHubRepositoryCloneUrls, GitHubCliError>; |
| 71 | readonly cwd: string; | 75 | |
| 72 | readonly repository: string; | 76 | readonly createRepository: (input: { |
| 73 | readonly visibility: SourceControlRepositoryVisibility; | 77 | readonly cwd: string; |
| 74 | }) => Effect.Effect<GitHubRepositoryCloneUrls, GitHubCliError>; | 78 | readonly repository: string; |
| 75 | 79 | readonly visibility: SourceControlRepositoryVisibility; | |
| 76 | readonly createPullRequest: (input: { | 80 | }) => Effect.Effect<GitHubRepositoryCloneUrls, GitHubCliError>; |
| 77 | readonly cwd: string; | 81 | |
| 78 | readonly baseBranch: string; | 82 | readonly createPullRequest: (input: { |
| 79 | readonly headSelector: string; | 83 | readonly cwd: string; |
| 80 | readonly title: string; | 84 | readonly baseBranch: string; |
| 81 | readonly bodyFile: string; | 85 | readonly headSelector: string; |
| 82 | }) => Effect.Effect<void, GitHubCliError>; | 86 | readonly title: string; |
| 83 | 87 | readonly bodyFile: string; | |
| 84 | readonly getDefaultBranch: (input: { | 88 | }) => Effect.Effect<void, GitHubCliError>; |
| 85 | readonly cwd: string; | 89 | |
| 86 | }) => Effect.Effect<string | null, GitHubCliError>; | 90 | readonly getDefaultBranch: (input: { |
| 87 | 91 | readonly cwd: string; | |
| 88 | readonly checkoutPullRequest: (input: { | 92 | }) => Effect.Effect<string | null, GitHubCliError>; |
| 89 | readonly cwd: string; | 93 | |
| 90 | readonly reference: string; | 94 | readonly checkoutPullRequest: (input: { |
| 91 | readonly force?: boolean; | 95 | readonly cwd: string; |
| 92 | }) => Effect.Effect<void, GitHubCliError>; | 96 | readonly reference: string; |
| 93 | } | 97 | readonly force?: boolean; |
| 94 | 98 | }) => Effect.Effect<void, GitHubCliError>; | |
| 95 | export class GitHubCli extends Context.Service<GitHubCli, GitHubCliShape>()( | 99 | } |
| 96 | "t3/sourceControl/GitHubCli", | 100 | >()("t3/sourceControl/GitHubCli") {} |
| 97 | ) {} | ||
| 98 | 101 | ||
| 99 | function errorText(error: VcsError | unknown): string { | 102 | function errorText(error: VcsError | unknown): string { |
| 100 | if (typeof error === "object" && error !== null) { | 103 | if (typeof error === "object" && error !== null) { |
| ... | @@ -226,10 +229,10 @@ function decodeGitHubJson<S extends Schema.Top>( | ... | @@ -226,10 +229,10 @@ function decodeGitHubJson<S extends Schema.Top>( |
| 226 | ); | 229 | ); |
| 227 | } | 230 | } |
| 228 | 231 | ||
| 229 | export const make = Effect.fn("makeGitHubCli")(function* () { | 232 | export const make = Effect.gen(function* () { |
| 230 | const process = yield* VcsProcess.VcsProcess; | 233 | const process = yield* VcsProcess.VcsProcess; |
| 231 | 234 | ||
| 232 | const execute: GitHubCliShape["execute"] = (input) => | 235 | const execute: GitHubCli["Service"]["execute"] = (input) => |
| 233 | process | 236 | process |
| 234 | .run({ | 237 | .run({ |
| 235 | operation: "GitHubCli.execute", | 238 | operation: "GitHubCli.execute", |
| ... | @@ -262,13 +265,13 @@ export const make = Effect.fn("makeGitHubCli")(function* () { | ... | @@ -262,13 +265,13 @@ export const make = Effect.fn("makeGitHubCli")(function* () { |
| 262 | Effect.flatMap((raw) => | 265 | Effect.flatMap((raw) => |
| 263 | raw.length === 0 | 266 | raw.length === 0 |
| 264 | ? Effect.succeed([]) | 267 | ? Effect.succeed([]) |
| 265 | : Effect.sync(() => GitHubPullRequests.decodeGitHubPullRequestListJson(raw)).pipe( | 268 | : Effect.sync(() => decodeGitHubPullRequestListJson(raw)).pipe( |
| 266 | Effect.flatMap((decoded) => { | 269 | Effect.flatMap((decoded) => { |
| 267 | if (!Result.isSuccess(decoded)) { | 270 | if (!Result.isSuccess(decoded)) { |
| 268 | return Effect.fail( | 271 | return Effect.fail( |
| 269 | new GitHubCliError({ | 272 | new GitHubCliError({ |
| 270 | operation: "listOpenPullRequests", | 273 | operation: "listOpenPullRequests", |
| 271 | detail: `GitHub CLI returned invalid PR list JSON: ${GitHubPullRequests.formatGitHubJsonDecodeError(decoded.failure)}`, | 274 | detail: `GitHub CLI returned invalid PR list JSON: ${formatGitHubJsonDecodeError(decoded.failure)}`, |
| 272 | cause: decoded.failure, | 275 | cause: decoded.failure, |
| 273 | }), | 276 | }), |
| 274 | ); | 277 | ); |
| ... | @@ -294,13 +297,13 @@ export const make = Effect.fn("makeGitHubCli")(function* () { | ... | @@ -294,13 +297,13 @@ export const make = Effect.fn("makeGitHubCli")(function* () { |
| 294 | }).pipe( | 297 | }).pipe( |
| 295 | Effect.map((result) => result.stdout.trim()), | 298 | Effect.map((result) => result.stdout.trim()), |
| 296 | Effect.flatMap((raw) => | 299 | Effect.flatMap((raw) => |
| 297 | Effect.sync(() => GitHubPullRequests.decodeGitHubPullRequestJson(raw)).pipe( | 300 | Effect.sync(() => decodeGitHubPullRequestJson(raw)).pipe( |
| 298 | Effect.flatMap((decoded) => { | 301 | Effect.flatMap((decoded) => { |
| 299 | if (!Result.isSuccess(decoded)) { | 302 | if (!Result.isSuccess(decoded)) { |
| 300 | return Effect.fail( | 303 | return Effect.fail( |
| 301 | new GitHubCliError({ | 304 | new GitHubCliError({ |
| 302 | operation: "getPullRequest", | 305 | operation: "getPullRequest", |
| 303 | detail: `GitHub CLI returned invalid pull request JSON: ${GitHubPullRequests.formatGitHubJsonDecodeError(decoded.failure)}`, | 306 | detail: `GitHub CLI returned invalid pull request JSON: ${formatGitHubJsonDecodeError(decoded.failure)}`, |
| 304 | cause: decoded.failure, | 307 | cause: decoded.failure, |
| 305 | }), | 308 | }), |
| 306 | ); | 309 | ); |
| ... | @@ -372,4 +375,4 @@ export const make = Effect.fn("makeGitHubCli")(function* () { | ... | @@ -372,4 +375,4 @@ export const make = Effect.fn("makeGitHubCli")(function* () { |
| 372 | }); | 375 | }); |
| 373 | }); | 376 | }); |
| 374 | 377 | ||
| 375 | export const layer = Layer.effect(GitHubCli, make()); | 378 | export const layer = Layer.effect(GitHubCli, make); |
apps/server/src/sourceControl/GitHubSourceControlProvider.test.ts+4-3| ... | @@ -24,8 +24,8 @@ const processResult = ( | ... | @@ -24,8 +24,8 @@ const processResult = ( |
| 24 | stderrTruncated: false, | 24 | stderrTruncated: false, |
| 25 | }); | 25 | }); |
| 26 | 26 | ||
| 27 | function makeProvider(github: Partial<GitHubCli.GitHubCliShape>) { | 27 | function makeProvider(github: Partial<GitHubCli.GitHubCli["Service"]>) { |
| 28 | return GitHubSourceControlProvider.make().pipe( | 28 | return GitHubSourceControlProvider.make.pipe( |
| 29 | Effect.provide(Layer.mock(GitHubCli.GitHubCli)(github)), | 29 | Effect.provide(Layer.mock(GitHubCli.GitHubCli)(github)), |
| 30 | ); | 30 | ); |
| 31 | } | 31 | } |
| ... | @@ -139,7 +139,8 @@ it.effect("treats empty non-open change request listing output as no results", ( | ... | @@ -139,7 +139,8 @@ it.effect("treats empty non-open change request listing output as no results", ( |
| 139 | 139 | ||
| 140 | it.effect("creates GitHub PRs through provider-neutral input names", () => | 140 | it.effect("creates GitHub PRs through provider-neutral input names", () => |
| 141 | Effect.gen(function* () { | 141 | Effect.gen(function* () { |
| 142 | let createInput: Parameters<GitHubCli.GitHubCliShape["createPullRequest"]>[0] | null = null; | 142 | let createInput: Parameters<GitHubCli.GitHubCli["Service"]["createPullRequest"]>[0] | null = |
| 143 | null; | ||
| 143 | const provider = yield* makeProvider({ | 144 | const provider = yield* makeProvider({ |
| 144 | createPullRequest: (input) => { | 145 | createPullRequest: (input) => { |
| 145 | createInput = input; | 146 | createInput = input; |
apps/server/src/sourceControl/GitHubSourceControlProvider.ts+21-19| ... | @@ -11,9 +11,15 @@ import { | ... | @@ -11,9 +11,15 @@ import { |
| 11 | 11 | ||
| 12 | import * as GitHubCli from "./GitHubCli.ts"; | 12 | import * as GitHubCli from "./GitHubCli.ts"; |
| 13 | import { findAuthenticatedGitHubAccount, parseGitHubAuthStatus } from "./gitHubAuthStatus.ts"; | 13 | import { findAuthenticatedGitHubAccount, parseGitHubAuthStatus } from "./gitHubAuthStatus.ts"; |
| 14 | import * as GitHubPullRequests from "./gitHubPullRequests.ts"; | 14 | import { decodeGitHubPullRequestListJson } from "./gitHubPullRequests.ts"; |
| 15 | import * as SourceControlProvider from "./SourceControlProvider.ts"; | 15 | import * as SourceControlProvider from "./SourceControlProvider.ts"; |
| 16 | import * as SourceControlProviderDiscovery from "./SourceControlProviderDiscovery.ts"; | 16 | import { |
| 17 | combinedAuthOutput, | ||
| 18 | firstSafeAuthLine, | ||
| 19 | providerAuth, | ||
| 20 | type SourceControlAuthProbeInput, | ||
| 21 | type SourceControlCliDiscoverySpec, | ||
| 22 | } from "./SourceControlProviderDiscovery.ts"; | ||
| 17 | const isSourceControlProviderError = Schema.is(SourceControlProviderError); | 23 | const isSourceControlProviderError = Schema.is(SourceControlProviderError); |
| 18 | 24 | ||
| 19 | function providerError( | 25 | function providerError( |
| ... | @@ -50,14 +56,14 @@ function toChangeRequest(summary: GitHubCli.GitHubPullRequestSummary): ChangeReq | ... | @@ -50,14 +56,14 @@ function toChangeRequest(summary: GitHubCli.GitHubPullRequestSummary): ChangeReq |
| 50 | }; | 56 | }; |
| 51 | } | 57 | } |
| 52 | 58 | ||
| 53 | function parseGitHubAuth(input: SourceControlProviderDiscovery.SourceControlAuthProbeInput) { | 59 | function parseGitHubAuth(input: SourceControlAuthProbeInput) { |
| 54 | const output = SourceControlProviderDiscovery.combinedAuthOutput(input); | 60 | const output = combinedAuthOutput(input); |
| 55 | const authStatus = parseGitHubAuthStatus(input.stdout); | 61 | const authStatus = parseGitHubAuthStatus(input.stdout); |
| 56 | const authenticatedAccount = findAuthenticatedGitHubAccount(authStatus.accounts); | 62 | const authenticatedAccount = findAuthenticatedGitHubAccount(authStatus.accounts); |
| 57 | const host = authenticatedAccount?.host; | 63 | const host = authenticatedAccount?.host; |
| 58 | 64 | ||
| 59 | if (authenticatedAccount) { | 65 | if (authenticatedAccount) { |
| 60 | return SourceControlProviderDiscovery.providerAuth({ | 66 | return providerAuth({ |
| 61 | status: "authenticated", | 67 | status: "authenticated", |
| 62 | account: authenticatedAccount.account, | 68 | account: authenticatedAccount.account, |
| 63 | host, | 69 | host, |
| ... | @@ -66,7 +72,7 @@ function parseGitHubAuth(input: SourceControlProviderDiscovery.SourceControlAuth | ... | @@ -66,7 +72,7 @@ function parseGitHubAuth(input: SourceControlProviderDiscovery.SourceControlAuth |
| 66 | 72 | ||
| 67 | const failedAccount = authStatus.accounts.find((entry) => entry.active) ?? authStatus.accounts[0]; | 73 | const failedAccount = authStatus.accounts.find((entry) => entry.active) ?? authStatus.accounts[0]; |
| 68 | if (authStatus.parsed) { | 74 | if (authStatus.parsed) { |
| 69 | return SourceControlProviderDiscovery.providerAuth({ | 75 | return providerAuth({ |
| 70 | status: "unauthenticated", | 76 | status: "unauthenticated", |
| 71 | host: failedAccount?.host, | 77 | host: failedAccount?.host, |
| 72 | detail: | 78 | detail: |
| ... | @@ -76,21 +82,17 @@ function parseGitHubAuth(input: SourceControlProviderDiscovery.SourceControlAuth | ... | @@ -76,21 +82,17 @@ function parseGitHubAuth(input: SourceControlProviderDiscovery.SourceControlAuth |
| 76 | } | 82 | } |
| 77 | 83 | ||
| 78 | if (input.exitCode !== 0) { | 84 | if (input.exitCode !== 0) { |
| 79 | return SourceControlProviderDiscovery.providerAuth({ | 85 | return providerAuth({ |
| 80 | status: "unauthenticated", | 86 | status: "unauthenticated", |
| 81 | host, | 87 | host, |
| 82 | detail: | 88 | detail: firstSafeAuthLine(output) ?? "Run `gh auth login` to authenticate GitHub CLI.", |
| 83 | SourceControlProviderDiscovery.firstSafeAuthLine(output) ?? | ||
| 84 | "Run `gh auth login` to authenticate GitHub CLI.", | ||
| 85 | }); | 89 | }); |
| 86 | } | 90 | } |
| 87 | 91 | ||
| 88 | return SourceControlProviderDiscovery.providerAuth({ | 92 | return providerAuth({ |
| 89 | status: "unknown", | 93 | status: "unknown", |
| 90 | host, | 94 | host, |
| 91 | detail: | 95 | detail: firstSafeAuthLine(output) ?? "GitHub CLI auth status could not be parsed.", |
| 92 | SourceControlProviderDiscovery.firstSafeAuthLine(output) ?? | ||
| 93 | "GitHub CLI auth status could not be parsed.", | ||
| 94 | }); | 96 | }); |
| 95 | } | 97 | } |
| 96 | 98 | ||
| ... | @@ -104,12 +106,12 @@ export const discovery = { | ... | @@ -104,12 +106,12 @@ export const discovery = { |
| 104 | parseAuth: parseGitHubAuth, | 106 | parseAuth: parseGitHubAuth, |
| 105 | installHint: | 107 | installHint: |
| 106 | "Install the GitHub command-line tool (`gh`) via https://cli.github.com/ or your package manager (for example `brew install gh`).", | 108 | "Install the GitHub command-line tool (`gh`) via https://cli.github.com/ or your package manager (for example `brew install gh`).", |
| 107 | } satisfies SourceControlProviderDiscovery.SourceControlCliDiscoverySpec; | 109 | } satisfies SourceControlCliDiscoverySpec; |
| 108 | 110 | ||
| 109 | export const make = Effect.fn("makeGitHubSourceControlProvider")(function* () { | 111 | export const make = Effect.gen(function* () { |
| 110 | const github = yield* GitHubCli.GitHubCli; | 112 | const github = yield* GitHubCli.GitHubCli; |
| 111 | 113 | ||
| 112 | const listChangeRequests: SourceControlProvider.SourceControlProviderShape["listChangeRequests"] = | 114 | const listChangeRequests: SourceControlProvider.SourceControlProvider["Service"]["listChangeRequests"] = |
| 113 | (input) => { | 115 | (input) => { |
| 114 | if (input.state === "open") { | 116 | if (input.state === "open") { |
| 115 | return github | 117 | return github |
| ... | @@ -147,7 +149,7 @@ export const make = Effect.fn("makeGitHubSourceControlProvider")(function* () { | ... | @@ -147,7 +149,7 @@ export const make = Effect.fn("makeGitHubSourceControlProvider")(function* () { |
| 147 | if (raw.length === 0) { | 149 | if (raw.length === 0) { |
| 148 | return Effect.succeed([]); | 150 | return Effect.succeed([]); |
| 149 | } | 151 | } |
| 150 | return Effect.sync(() => GitHubPullRequests.decodeGitHubPullRequestListJson(raw)).pipe( | 152 | return Effect.sync(() => decodeGitHubPullRequestListJson(raw)).pipe( |
| 151 | Effect.flatMap((decoded) => | 153 | Effect.flatMap((decoded) => |
| 152 | Result.isSuccess(decoded) | 154 | Result.isSuccess(decoded) |
| 153 | ? Effect.succeed( | 155 | ? Effect.succeed( |
| ... | @@ -212,4 +214,4 @@ export const make = Effect.fn("makeGitHubSourceControlProvider")(function* () { | ... | @@ -212,4 +214,4 @@ export const make = Effect.fn("makeGitHubSourceControlProvider")(function* () { |
| 212 | }); | 214 | }); |
| 213 | }); | 215 | }); |
| 214 | 216 | ||
| 215 | export const layer = Layer.effect(SourceControlProvider.SourceControlProvider, make()); | 217 | export const layer = Layer.effect(SourceControlProvider.SourceControlProvider, make); |
apps/server/src/sourceControl/GitLabCli.test.ts+1-1| ... | @@ -8,7 +8,7 @@ import { VcsProcessExitError } from "@t3tools/contracts"; | ... | @@ -8,7 +8,7 @@ import { VcsProcessExitError } from "@t3tools/contracts"; |
| 8 | import * as VcsProcess from "../vcs/VcsProcess.ts"; | 8 | import * as VcsProcess from "../vcs/VcsProcess.ts"; |
| 9 | import * as GitLabCli from "./GitLabCli.ts"; | 9 | import * as GitLabCli from "./GitLabCli.ts"; |
| 10 | 10 | ||
| 11 | const mockedRun = vi.fn<VcsProcess.VcsProcessShape["run"]>(); | 11 | const mockedRun = vi.fn<VcsProcess.VcsProcess["Service"]["run"]>(); |
| 12 | const layer = it.layer( | 12 | const layer = it.layer( |
| 13 | GitLabCli.layer.pipe( | 13 | GitLabCli.layer.pipe( |
| 14 | Layer.provide( | 14 | Layer.provide( |
apps/server/src/sourceControl/GitLabCli.ts+66-63| ... | @@ -10,7 +10,11 @@ import type * as DateTime from "effect/DateTime"; | ... | @@ -10,7 +10,11 @@ import type * as DateTime from "effect/DateTime"; |
| 10 | import { TrimmedNonEmptyString, type SourceControlRepositoryVisibility } from "@t3tools/contracts"; | 10 | import { TrimmedNonEmptyString, type SourceControlRepositoryVisibility } from "@t3tools/contracts"; |
| 11 | 11 | ||
| 12 | import * as VcsProcess from "../vcs/VcsProcess.ts"; | 12 | import * as VcsProcess from "../vcs/VcsProcess.ts"; |
| 13 | import * as GitLabMergeRequests from "./gitLabMergeRequests.ts"; | 13 | import { |
| 14 | decodeGitLabMergeRequestJson, | ||
| 15 | decodeGitLabMergeRequestListJson, | ||
| 16 | formatGitLabJsonDecodeError, | ||
| 17 | } from "./gitLabMergeRequests.ts"; | ||
| 14 | import type * as SourceControlProvider from "./SourceControlProvider.ts"; | 18 | import type * as SourceControlProvider from "./SourceControlProvider.ts"; |
| 15 | 19 | ||
| 16 | const DEFAULT_TIMEOUT_MS = 30_000; | 20 | const DEFAULT_TIMEOUT_MS = 30_000; |
| ... | @@ -44,61 +48,60 @@ export interface GitLabRepositoryCloneUrls { | ... | @@ -44,61 +48,60 @@ export interface GitLabRepositoryCloneUrls { |
| 44 | readonly sshUrl: string; | 48 | readonly sshUrl: string; |
| 45 | } | 49 | } |
| 46 | 50 | ||
| 47 | export interface GitLabCliShape { | 51 | export class GitLabCli extends Context.Service< |
| 48 | readonly execute: (input: { | 52 | GitLabCli, |
| 49 | readonly cwd: string; | 53 | { |
| 50 | readonly args: ReadonlyArray<string>; | 54 | readonly execute: (input: { |
| 51 | readonly timeoutMs?: number; | 55 | readonly cwd: string; |
| 52 | }) => Effect.Effect<VcsProcess.VcsProcessOutput, GitLabCliError>; | 56 | readonly args: ReadonlyArray<string>; |
| 53 | 57 | readonly timeoutMs?: number; | |
| 54 | readonly listMergeRequests: (input: { | 58 | }) => Effect.Effect<VcsProcess.VcsProcessOutput, GitLabCliError>; |
| 55 | readonly cwd: string; | 59 | |
| 56 | readonly headSelector: string; | 60 | readonly listMergeRequests: (input: { |
| 57 | readonly source?: SourceControlProvider.SourceControlRefSelector; | 61 | readonly cwd: string; |
| 58 | readonly state: "open" | "closed" | "merged" | "all"; | 62 | readonly headSelector: string; |
| 59 | readonly limit?: number; | 63 | readonly source?: SourceControlProvider.SourceControlRefSelector; |
| 60 | }) => Effect.Effect<ReadonlyArray<GitLabMergeRequestSummary>, GitLabCliError>; | 64 | readonly state: "open" | "closed" | "merged" | "all"; |
| 61 | 65 | readonly limit?: number; | |
| 62 | readonly getMergeRequest: (input: { | 66 | }) => Effect.Effect<ReadonlyArray<GitLabMergeRequestSummary>, GitLabCliError>; |
| 63 | readonly cwd: string; | 67 | |
| 64 | readonly reference: string; | 68 | readonly getMergeRequest: (input: { |
| 65 | }) => Effect.Effect<GitLabMergeRequestSummary, GitLabCliError>; | 69 | readonly cwd: string; |
| 66 | 70 | readonly reference: string; | |
| 67 | readonly getRepositoryCloneUrls: (input: { | 71 | }) => Effect.Effect<GitLabMergeRequestSummary, GitLabCliError>; |
| 68 | readonly cwd: string; | 72 | |
| 69 | readonly repository: string; | 73 | readonly getRepositoryCloneUrls: (input: { |
| 70 | }) => Effect.Effect<GitLabRepositoryCloneUrls, GitLabCliError>; | 74 | readonly cwd: string; |
| 71 | 75 | readonly repository: string; | |
| 72 | readonly createRepository: (input: { | 76 | }) => Effect.Effect<GitLabRepositoryCloneUrls, GitLabCliError>; |
| 73 | readonly cwd: string; | 77 | |
| 74 | readonly repository: string; | 78 | readonly createRepository: (input: { |
| 75 | readonly visibility: SourceControlRepositoryVisibility; | 79 | readonly cwd: string; |
| 76 | }) => Effect.Effect<GitLabRepositoryCloneUrls, GitLabCliError>; | 80 | readonly repository: string; |
| 77 | 81 | readonly visibility: SourceControlRepositoryVisibility; | |
| 78 | readonly createMergeRequest: (input: { | 82 | }) => Effect.Effect<GitLabRepositoryCloneUrls, GitLabCliError>; |
| 79 | readonly cwd: string; | 83 | |
| 80 | readonly baseBranch: string; | 84 | readonly createMergeRequest: (input: { |
| 81 | readonly headSelector: string; | 85 | readonly cwd: string; |
| 82 | readonly source?: SourceControlProvider.SourceControlRefSelector; | 86 | readonly baseBranch: string; |
| 83 | readonly target?: SourceControlProvider.SourceControlRefSelector; | 87 | readonly headSelector: string; |
| 84 | readonly title: string; | 88 | readonly source?: SourceControlProvider.SourceControlRefSelector; |
| 85 | readonly bodyFile: string; | 89 | readonly target?: SourceControlProvider.SourceControlRefSelector; |
| 86 | }) => Effect.Effect<void, GitLabCliError>; | 90 | readonly title: string; |
| 87 | 91 | readonly bodyFile: string; | |
| 88 | readonly getDefaultBranch: (input: { | 92 | }) => Effect.Effect<void, GitLabCliError>; |
| 89 | readonly cwd: string; | 93 | |
| 90 | }) => Effect.Effect<string | null, GitLabCliError>; | 94 | readonly getDefaultBranch: (input: { |
| 91 | 95 | readonly cwd: string; | |
| 92 | readonly checkoutMergeRequest: (input: { | 96 | }) => Effect.Effect<string | null, GitLabCliError>; |
| 93 | readonly cwd: string; | 97 | |
| 94 | readonly reference: string; | 98 | readonly checkoutMergeRequest: (input: { |
| 95 | readonly force?: boolean; | 99 | readonly cwd: string; |
| 96 | }) => Effect.Effect<void, GitLabCliError>; | 100 | readonly reference: string; |
| 97 | } | 101 | readonly force?: boolean; |
| 98 | 102 | }) => Effect.Effect<void, GitLabCliError>; | |
| 99 | export class GitLabCli extends Context.Service<GitLabCli, GitLabCliShape>()( | 103 | } |
| 100 | "t3/sourceControl/GitLabCli", | 104 | >()("t3/sourceControl/GitLabCli") {} |
| 101 | ) {} | ||
| 102 | 105 | ||
| 103 | function isVcsProcessSpawnError(error: unknown): boolean { | 106 | function isVcsProcessSpawnError(error: unknown): boolean { |
| 104 | return ( | 107 | return ( |
| ... | @@ -259,10 +262,10 @@ function parseRepositoryPath(repository: string): { | ... | @@ -259,10 +262,10 @@ function parseRepositoryPath(repository: string): { |
| 259 | return { namespacePath, projectPath }; | 262 | return { namespacePath, projectPath }; |
| 260 | } | 263 | } |
| 261 | 264 | ||
| 262 | export const make = Effect.fn("makeGitLabCli")(function* () { | 265 | export const make = Effect.gen(function* () { |
| 263 | const process = yield* VcsProcess.VcsProcess; | 266 | const process = yield* VcsProcess.VcsProcess; |
| 264 | 267 | ||
| 265 | const execute: GitLabCliShape["execute"] = (input) => | 268 | const execute: GitLabCli["Service"]["execute"] = (input) => |
| 266 | process | 269 | process |
| 267 | .run({ | 270 | .run({ |
| 268 | operation: "GitLabCli.execute", | 271 | operation: "GitLabCli.execute", |
| ... | @@ -294,13 +297,13 @@ export const make = Effect.fn("makeGitLabCli")(function* () { | ... | @@ -294,13 +297,13 @@ export const make = Effect.fn("makeGitLabCli")(function* () { |
| 294 | Effect.flatMap((raw) => | 297 | Effect.flatMap((raw) => |
| 295 | raw.length === 0 | 298 | raw.length === 0 |
| 296 | ? Effect.succeed([]) | 299 | ? Effect.succeed([]) |
| 297 | : Effect.sync(() => GitLabMergeRequests.decodeGitLabMergeRequestListJson(raw)).pipe( | 300 | : Effect.sync(() => decodeGitLabMergeRequestListJson(raw)).pipe( |
| 298 | Effect.flatMap((decoded) => { | 301 | Effect.flatMap((decoded) => { |
| 299 | if (!Result.isSuccess(decoded)) { | 302 | if (!Result.isSuccess(decoded)) { |
| 300 | return Effect.fail( | 303 | return Effect.fail( |
| 301 | new GitLabCliError({ | 304 | new GitLabCliError({ |
| 302 | operation: "listMergeRequests", | 305 | operation: "listMergeRequests", |
| 303 | detail: `GitLab CLI returned invalid MR list JSON: ${GitLabMergeRequests.formatGitLabJsonDecodeError(decoded.failure)}`, | 306 | detail: `GitLab CLI returned invalid MR list JSON: ${formatGitLabJsonDecodeError(decoded.failure)}`, |
| 304 | cause: decoded.failure, | 307 | cause: decoded.failure, |
| 305 | }), | 308 | }), |
| 306 | ); | 309 | ); |
| ... | @@ -318,13 +321,13 @@ export const make = Effect.fn("makeGitLabCli")(function* () { | ... | @@ -318,13 +321,13 @@ export const make = Effect.fn("makeGitLabCli")(function* () { |
| 318 | }).pipe( | 321 | }).pipe( |
| 319 | Effect.map((result) => result.stdout.trim()), | 322 | Effect.map((result) => result.stdout.trim()), |
| 320 | Effect.flatMap((raw) => | 323 | Effect.flatMap((raw) => |
| 321 | Effect.sync(() => GitLabMergeRequests.decodeGitLabMergeRequestJson(raw)).pipe( | 324 | Effect.sync(() => decodeGitLabMergeRequestJson(raw)).pipe( |
| 322 | Effect.flatMap((decoded) => { | 325 | Effect.flatMap((decoded) => { |
| 323 | if (!Result.isSuccess(decoded)) { | 326 | if (!Result.isSuccess(decoded)) { |
| 324 | return Effect.fail( | 327 | return Effect.fail( |
| 325 | new GitLabCliError({ | 328 | new GitLabCliError({ |
| 326 | operation: "getMergeRequest", | 329 | operation: "getMergeRequest", |
| 327 | detail: `GitLab CLI returned invalid merge request JSON: ${GitLabMergeRequests.formatGitLabJsonDecodeError(decoded.failure)}`, | 330 | detail: `GitLab CLI returned invalid merge request JSON: ${formatGitLabJsonDecodeError(decoded.failure)}`, |
| 328 | cause: decoded.failure, | 331 | cause: decoded.failure, |
| 329 | }), | 332 | }), |
| 330 | ); | 333 | ); |
| ... | @@ -449,4 +452,4 @@ export const make = Effect.fn("makeGitLabCli")(function* () { | ... | @@ -449,4 +452,4 @@ export const make = Effect.fn("makeGitLabCli")(function* () { |
| 449 | }); | 452 | }); |
| 450 | }); | 453 | }); |
| 451 | 454 | ||
| 452 | export const layer = Layer.effect(GitLabCli, make()); | 455 | export const layer = Layer.effect(GitLabCli, make); |
apps/server/src/sourceControl/GitLabSourceControlProvider.test.ts+5-4| ... | @@ -8,8 +8,8 @@ import * as GitLabCli from "./GitLabCli.ts"; | ... | @@ -8,8 +8,8 @@ import * as GitLabCli from "./GitLabCli.ts"; |
| 8 | import { parseGitLabAuthStatusHosts } from "./gitLabAuthStatus.ts"; | 8 | import { parseGitLabAuthStatusHosts } from "./gitLabAuthStatus.ts"; |
| 9 | import * as GitLabSourceControlProvider from "./GitLabSourceControlProvider.ts"; | 9 | import * as GitLabSourceControlProvider from "./GitLabSourceControlProvider.ts"; |
| 10 | 10 | ||
| 11 | function makeProvider(gitlab: Partial<GitLabCli.GitLabCliShape>) { | 11 | function makeProvider(gitlab: Partial<GitLabCli.GitLabCli["Service"]>) { |
| 12 | return GitLabSourceControlProvider.make().pipe( | 12 | return GitLabSourceControlProvider.make.pipe( |
| 13 | Effect.provide(Layer.mock(GitLabCli.GitLabCli)(gitlab)), | 13 | Effect.provide(Layer.mock(GitLabCli.GitLabCli)(gitlab)), |
| 14 | ); | 14 | ); |
| 15 | } | 15 | } |
| ... | @@ -54,7 +54,7 @@ it.effect("maps GitLab MR summaries into provider-neutral change requests", () = | ... | @@ -54,7 +54,7 @@ it.effect("maps GitLab MR summaries into provider-neutral change requests", () = |
| 54 | 54 | ||
| 55 | it.effect("lists GitLab MRs through provider-neutral input names", () => | 55 | it.effect("lists GitLab MRs through provider-neutral input names", () => |
| 56 | Effect.gen(function* () { | 56 | Effect.gen(function* () { |
| 57 | let listInput: Parameters<GitLabCli.GitLabCliShape["listMergeRequests"]>[0] | null = null; | 57 | let listInput: Parameters<GitLabCli.GitLabCli["Service"]["listMergeRequests"]>[0] | null = null; |
| 58 | const provider = yield* makeProvider({ | 58 | const provider = yield* makeProvider({ |
| 59 | listMergeRequests: (input) => { | 59 | listMergeRequests: (input) => { |
| 60 | listInput = input; | 60 | listInput = input; |
| ... | @@ -80,7 +80,8 @@ it.effect("lists GitLab MRs through provider-neutral input names", () => | ... | @@ -80,7 +80,8 @@ it.effect("lists GitLab MRs through provider-neutral input names", () => |
| 80 | 80 | ||
| 81 | it.effect("creates GitLab MRs through provider-neutral input names", () => | 81 | it.effect("creates GitLab MRs through provider-neutral input names", () => |
| 82 | Effect.gen(function* () { | 82 | Effect.gen(function* () { |
| 83 | let createInput: Parameters<GitLabCli.GitLabCliShape["createMergeRequest"]>[0] | null = null; | 83 | let createInput: Parameters<GitLabCli.GitLabCli["Service"]["createMergeRequest"]>[0] | null = |
| 84 | null; | ||
| 84 | const provider = yield* makeProvider({ | 85 | const provider = yield* makeProvider({ |
| 85 | createMergeRequest: (input) => { | 86 | createMergeRequest: (input) => { |
| 86 | createInput = input; | 87 | createInput = input; |
apps/server/src/sourceControl/GitLabSourceControlProvider.ts+26-23| ... | @@ -5,7 +5,16 @@ import { SourceControlProviderError, type ChangeRequest } from "@t3tools/contrac | ... | @@ -5,7 +5,16 @@ import { SourceControlProviderError, type ChangeRequest } from "@t3tools/contrac |
| 5 | 5 | ||
| 6 | import * as GitLabCli from "./GitLabCli.ts"; | 6 | import * as GitLabCli from "./GitLabCli.ts"; |
| 7 | import * as SourceControlProvider from "./SourceControlProvider.ts"; | 7 | import * as SourceControlProvider from "./SourceControlProvider.ts"; |
| 8 | import * as SourceControlProviderDiscovery from "./SourceControlProviderDiscovery.ts"; | 8 | import { |
| 9 | combinedAuthOutput, | ||
| 10 | firstSafeAuthLine, | ||
| 11 | matchFirst, | ||
| 12 | parseCliHost, | ||
| 13 | providerAuth, | ||
| 14 | type SourceControlAuthProbeInput, | ||
| 15 | type SourceControlCliDiscoverySpec, | ||
| 16 | type SourceControlUnknownRemoteRefinementInput, | ||
| 17 | } from "./SourceControlProviderDiscovery.ts"; | ||
| 9 | import { findAuthenticatedGitLabHost, parseGitLabAuthStatusHosts } from "./gitLabAuthStatus.ts"; | 18 | import { findAuthenticatedGitLabHost, parseGitLabAuthStatusHosts } from "./gitLabAuthStatus.ts"; |
| 10 | 19 | ||
| 11 | function providerError( | 20 | function providerError( |
| ... | @@ -42,48 +51,42 @@ function toChangeRequest(summary: GitLabCli.GitLabMergeRequestSummary): ChangeRe | ... | @@ -42,48 +51,42 @@ function toChangeRequest(summary: GitLabCli.GitLabMergeRequestSummary): ChangeRe |
| 42 | }; | 51 | }; |
| 43 | } | 52 | } |
| 44 | 53 | ||
| 45 | function parseGitLabAuth(input: SourceControlProviderDiscovery.SourceControlAuthProbeInput) { | 54 | function parseGitLabAuth(input: SourceControlAuthProbeInput) { |
| 46 | const output = SourceControlProviderDiscovery.combinedAuthOutput(input); | 55 | const output = combinedAuthOutput(input); |
| 47 | const authenticatedHost = findAuthenticatedGitLabHost(parseGitLabAuthStatusHosts(output)); | 56 | const authenticatedHost = findAuthenticatedGitLabHost(parseGitLabAuthStatusHosts(output)); |
| 48 | const account = | 57 | const account = |
| 49 | authenticatedHost?.account ?? | 58 | authenticatedHost?.account ?? |
| 50 | SourceControlProviderDiscovery.matchFirst(output, [ | 59 | matchFirst(output, [ |
| 51 | /Logged in to .* as\s+([^\s(]+)/iu, | 60 | /Logged in to .* as\s+([^\s(]+)/iu, |
| 52 | /Logged in to .* account\s+([^\s(]+)/iu, | 61 | /Logged in to .* account\s+([^\s(]+)/iu, |
| 53 | /account:\s*([^\s(]+)/iu, | 62 | /account:\s*([^\s(]+)/iu, |
| 54 | ]); | 63 | ]); |
| 55 | const host = authenticatedHost?.host ?? SourceControlProviderDiscovery.parseCliHost(output); | 64 | const host = authenticatedHost?.host ?? parseCliHost(output); |
| 56 | 65 | ||
| 57 | if (account) { | 66 | if (account) { |
| 58 | return SourceControlProviderDiscovery.providerAuth({ status: "authenticated", account, host }); | 67 | return providerAuth({ status: "authenticated", account, host }); |
| 59 | } | 68 | } |
| 60 | 69 | ||
| 61 | if (input.exitCode !== 0) { | 70 | if (input.exitCode !== 0) { |
| 62 | return SourceControlProviderDiscovery.providerAuth({ | 71 | return providerAuth({ |
| 63 | status: "unauthenticated", | 72 | status: "unauthenticated", |
| 64 | host, | 73 | host, |
| 65 | detail: | 74 | detail: firstSafeAuthLine(output) ?? "Run `glab auth login` to authenticate GitLab CLI.", |
| 66 | SourceControlProviderDiscovery.firstSafeAuthLine(output) ?? | ||
| 67 | "Run `glab auth login` to authenticate GitLab CLI.", | ||
| 68 | }); | 75 | }); |
| 69 | } | 76 | } |
| 70 | 77 | ||
| 71 | return SourceControlProviderDiscovery.providerAuth({ | 78 | return providerAuth({ |
| 72 | status: "unknown", | 79 | status: "unknown", |
| 73 | host, | 80 | host, |
| 74 | detail: | 81 | detail: firstSafeAuthLine(output) ?? "GitLab CLI auth status could not be parsed.", |
| 75 | SourceControlProviderDiscovery.firstSafeAuthLine(output) ?? | ||
| 76 | "GitLab CLI auth status could not be parsed.", | ||
| 77 | }); | 82 | }); |
| 78 | } | 83 | } |
| 79 | 84 | ||
| 80 | function refineUnknownGitLabRemote( | 85 | function refineUnknownGitLabRemote(input: SourceControlUnknownRemoteRefinementInput) { |
| 81 | input: SourceControlProviderDiscovery.SourceControlUnknownRemoteRefinementInput, | ||
| 82 | ) { | ||
| 83 | const host = input.context.provider.name.toLowerCase(); | 86 | const host = input.context.provider.name.toLowerCase(); |
| 84 | const authenticated = parseGitLabAuthStatusHosts( | 87 | const authenticated = parseGitLabAuthStatusHosts(combinedAuthOutput(input.auth)).some( |
| 85 | SourceControlProviderDiscovery.combinedAuthOutput(input.auth), | 88 | (entry) => entry.account !== null && entry.host === host, |
| 86 | ).some((entry) => entry.account !== null && entry.host === host); | 89 | ); |
| 87 | 90 | ||
| 88 | if (!authenticated) { | 91 | if (!authenticated) { |
| 89 | return null; | 92 | return null; |
| ... | @@ -107,9 +110,9 @@ export const discovery = { | ... | @@ -107,9 +110,9 @@ export const discovery = { |
| 107 | refineUnknownRemote: refineUnknownGitLabRemote, | 110 | refineUnknownRemote: refineUnknownGitLabRemote, |
| 108 | installHint: | 111 | installHint: |
| 109 | "Install the GitLab command-line tool (`glab`) from https://gitlab.com/gitlab-org/cli or your package manager (for example `brew install glab`).", | 112 | "Install the GitLab command-line tool (`glab`) from https://gitlab.com/gitlab-org/cli or your package manager (for example `brew install glab`).", |
| 110 | } satisfies SourceControlProviderDiscovery.SourceControlCliDiscoverySpec; | 113 | } satisfies SourceControlCliDiscoverySpec; |
| 111 | 114 | ||
| 112 | export const make = Effect.fn("makeGitLabSourceControlProvider")(function* () { | 115 | export const make = Effect.gen(function* () { |
| 113 | const gitlab = yield* GitLabCli.GitLabCli; | 116 | const gitlab = yield* GitLabCli.GitLabCli; |
| 114 | 117 | ||
| 115 | return SourceControlProvider.SourceControlProvider.of({ | 118 | return SourceControlProvider.SourceControlProvider.of({ |
| ... | @@ -167,4 +170,4 @@ export const make = Effect.fn("makeGitLabSourceControlProvider")(function* () { | ... | @@ -167,4 +170,4 @@ export const make = Effect.fn("makeGitLabSourceControlProvider")(function* () { |
| 167 | }); | 170 | }); |
| 168 | }); | 171 | }); |
| 169 | 172 | ||
| 170 | export const layer = Layer.effect(SourceControlProvider.SourceControlProvider, make()); | 173 | export const layer = Layer.effect(SourceControlProvider.SourceControlProvider, make); |
apps/server/src/sourceControl/SourceControlDiscovery.test.ts+14-10| ... | @@ -6,7 +6,7 @@ import * as Option from "effect/Option"; | ... | @@ -6,7 +6,7 @@ import * as Option from "effect/Option"; |
| 6 | import { ChildProcessSpawner } from "effect/unstable/process"; | 6 | import { ChildProcessSpawner } from "effect/unstable/process"; |
| 7 | import { VcsProcessSpawnError } from "@t3tools/contracts"; | 7 | import { VcsProcessSpawnError } from "@t3tools/contracts"; |
| 8 | 8 | ||
| 9 | import { ServerConfig } from "../config.ts"; | 9 | import * as ServerConfig from "../config.ts"; |
| 10 | import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts"; | 10 | import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts"; |
| 11 | import * as VcsProcess from "../vcs/VcsProcess.ts"; | 11 | import * as VcsProcess from "../vcs/VcsProcess.ts"; |
| 12 | import * as AzureDevOpsCli from "./AzureDevOpsCli.ts"; | 12 | import * as AzureDevOpsCli from "./AzureDevOpsCli.ts"; |
| ... | @@ -17,15 +17,15 @@ import * as SourceControlDiscovery from "./SourceControlDiscovery.ts"; | ... | @@ -17,15 +17,15 @@ import * as SourceControlDiscovery from "./SourceControlDiscovery.ts"; |
| 17 | import * as SourceControlProviderRegistry from "./SourceControlProviderRegistry.ts"; | 17 | import * as SourceControlProviderRegistry from "./SourceControlProviderRegistry.ts"; |
| 18 | 18 | ||
| 19 | const sourceControlProviderRegistryTestLayer = (input: { | 19 | const sourceControlProviderRegistryTestLayer = (input: { |
| 20 | readonly bitbucket: Partial<BitbucketApi.BitbucketApiShape>; | 20 | readonly bitbucket: Partial<BitbucketApi.BitbucketApi["Service"]>; |
| 21 | readonly process: Partial<VcsProcess.VcsProcessShape>; | 21 | readonly process: Partial<VcsProcess.VcsProcess["Service"]>; |
| 22 | }) => | 22 | }) => |
| 23 | SourceControlProviderRegistry.layer.pipe( | 23 | SourceControlProviderRegistry.layer.pipe( |
| 24 | Layer.provide( | 24 | Layer.provide( |
| 25 | Layer.mergeAll( | 25 | Layer.mergeAll( |
| 26 | ServerConfig.layerTest(process.cwd(), { prefix: "t3-source-control-registry-test-" }).pipe( | 26 | ServerConfig.layerTest(process.cwd(), { |
| 27 | Layer.provide(NodeServices.layer), | 27 | prefix: "t3-source-control-registry-test-", |
| 28 | ), | 28 | }).pipe(Layer.provide(NodeServices.layer)), |
| 29 | Layer.mock(AzureDevOpsCli.AzureDevOpsCli)({}), | 29 | Layer.mock(AzureDevOpsCli.AzureDevOpsCli)({}), |
| 30 | Layer.mock(BitbucketApi.BitbucketApi)(input.bitbucket), | 30 | Layer.mock(BitbucketApi.BitbucketApi)(input.bitbucket), |
| 31 | Layer.mock(GitHubCli.GitHubCli)({}), | 31 | Layer.mock(GitHubCli.GitHubCli)({}), |
| ... | @@ -88,10 +88,12 @@ it.effect("reports implemented tools separately from locally available executabl | ... | @@ -88,10 +88,12 @@ it.effect("reports implemented tools separately from locally available executabl |
| 88 | }), | 88 | }), |
| 89 | ); | 89 | ); |
| 90 | }, | 90 | }, |
| 91 | } satisfies Partial<VcsProcess.VcsProcessShape>; | 91 | } satisfies Partial<VcsProcess.VcsProcess["Service"]>; |
| 92 | const testLayer = SourceControlDiscovery.layer.pipe( | 92 | const testLayer = SourceControlDiscovery.layer.pipe( |
| 93 | Layer.provide( | 93 | Layer.provide( |
| 94 | ServerConfig.layerTest(process.cwd(), { prefix: "t3-source-control-discovery-" }), | 94 | ServerConfig.layerTest(process.cwd(), { |
| 95 | prefix: "t3-source-control-discovery-", | ||
| 96 | }), | ||
| 95 | ), | 97 | ), |
| 96 | Layer.provide(Layer.mock(VcsProcess.VcsProcess)(processMock)), | 98 | Layer.provide(Layer.mock(VcsProcess.VcsProcess)(processMock)), |
| 97 | Layer.provide( | 99 | Layer.provide( |
| ... | @@ -215,10 +217,12 @@ Logged in to gitlab.com as gitlab-user | ... | @@ -215,10 +217,12 @@ Logged in to gitlab.com as gitlab-user |
| 215 | }), | 217 | }), |
| 216 | ); | 218 | ); |
| 217 | }, | 219 | }, |
| 218 | } satisfies Partial<VcsProcess.VcsProcessShape>; | 220 | } satisfies Partial<VcsProcess.VcsProcess["Service"]>; |
| 219 | const testLayer = SourceControlDiscovery.layer.pipe( | 221 | const testLayer = SourceControlDiscovery.layer.pipe( |
| 220 | Layer.provide( | 222 | Layer.provide( |
| 221 | ServerConfig.layerTest(process.cwd(), { prefix: "t3-source-control-auth-discovery-" }), | 223 | ServerConfig.layerTest(process.cwd(), { |
| 224 | prefix: "t3-source-control-auth-discovery-", | ||
| 225 | }), | ||
| 222 | ), | 226 | ), |
| 223 | Layer.provide(Layer.mock(VcsProcess.VcsProcess)(processMock)), | 227 | Layer.provide(Layer.mock(VcsProcess.VcsProcess)(processMock)), |
| 224 | Layer.provide( | 228 | Layer.provide( |
apps/server/src/sourceControl/SourceControlDiscovery.ts+71-76| ... | @@ -10,7 +10,7 @@ import * as Option from "effect/Option"; | ... | @@ -10,7 +10,7 @@ import * as Option from "effect/Option"; |
| 10 | 10 | ||
| 11 | import { ServerConfig } from "../config.ts"; | 11 | import { ServerConfig } from "../config.ts"; |
| 12 | import * as VcsProcess from "../vcs/VcsProcess.ts"; | 12 | import * as VcsProcess from "../vcs/VcsProcess.ts"; |
| 13 | import * as SourceControlProviderDiscovery from "./SourceControlProviderDiscovery.ts"; | 13 | import { detailFromCause, firstNonEmptyLine } from "./SourceControlProviderDiscovery.ts"; |
| 14 | import * as SourceControlProviderRegistry from "./SourceControlProviderRegistry.ts"; | 14 | import * as SourceControlProviderRegistry from "./SourceControlProviderRegistry.ts"; |
| 15 | 15 | ||
| 16 | interface DiscoveryProbe { | 16 | interface DiscoveryProbe { |
| ... | @@ -57,91 +57,86 @@ const VCS_PROBES: ReadonlyArray<VcsProbe> = [ | ... | @@ -57,91 +57,86 @@ const VCS_PROBES: ReadonlyArray<VcsProbe> = [ |
| 57 | }, | 57 | }, |
| 58 | ]; | 58 | ]; |
| 59 | 59 | ||
| 60 | export interface SourceControlDiscoveryShape { | ||
| 61 | readonly discover: Effect.Effect<SourceControlDiscoveryResult>; | ||
| 62 | } | ||
| 63 | |||
| 64 | export class SourceControlDiscovery extends Context.Service< | 60 | export class SourceControlDiscovery extends Context.Service< |
| 65 | SourceControlDiscovery, | 61 | SourceControlDiscovery, |
| 66 | SourceControlDiscoveryShape | 62 | { |
| 63 | readonly discover: Effect.Effect<SourceControlDiscoveryResult>; | ||
| 64 | } | ||
| 67 | >()("t3/sourceControl/SourceControlDiscovery") {} | 65 | >()("t3/sourceControl/SourceControlDiscovery") {} |
| 68 | 66 | ||
| 69 | export const layer = Layer.effect( | 67 | export const make = Effect.gen(function* () { |
| 70 | SourceControlDiscovery, | 68 | const config = yield* ServerConfig; |
| 71 | Effect.gen(function* () { | 69 | const process = yield* VcsProcess.VcsProcess; |
| 72 | const config = yield* ServerConfig; | 70 | const sourceControlProviders = yield* SourceControlProviderRegistry.SourceControlProviderRegistry; |
| 73 | const process = yield* VcsProcess.VcsProcess; | ||
| 74 | const sourceControlProviders = | ||
| 75 | yield* SourceControlProviderRegistry.SourceControlProviderRegistry; | ||
| 76 | 71 | ||
| 77 | const probe = <Kind extends VcsDriverKind>( | 72 | const probe = <Kind extends VcsDriverKind>( |
| 78 | input: DiscoveryProbe & { readonly kind: Kind }, | 73 | input: DiscoveryProbe & { readonly kind: Kind }, |
| 79 | ): Effect.Effect<DiscoveryProbeResult<Kind>> => { | 74 | ): Effect.Effect<DiscoveryProbeResult<Kind>> => { |
| 80 | const executable = input.executable; | 75 | const executable = input.executable; |
| 81 | const versionArgs = input.versionArgs; | 76 | const versionArgs = input.versionArgs; |
| 82 | 77 | ||
| 83 | if (!executable || !versionArgs) { | 78 | if (!executable || !versionArgs) { |
| 84 | return Effect.succeed({ | 79 | return Effect.succeed({ |
| 85 | kind: input.kind, | 80 | kind: input.kind, |
| 86 | label: input.label, | 81 | label: input.label, |
| 87 | implemented: input.implemented, | 82 | implemented: input.implemented, |
| 88 | status: "missing" as const, | 83 | status: "missing" as const, |
| 89 | version: Option.none<string>(), | 84 | version: Option.none<string>(), |
| 90 | installHint: input.installHint, | 85 | installHint: input.installHint, |
| 91 | detail: Option.some(input.installHint), | 86 | detail: Option.some(input.installHint), |
| 92 | } satisfies DiscoveryProbeResult<Kind>); | 87 | } satisfies DiscoveryProbeResult<Kind>); |
| 93 | } | 88 | } |
| 94 | 89 | ||
| 95 | return process | 90 | return process |
| 96 | .run({ | 91 | .run({ |
| 97 | operation: "source-control.discovery.probe", | 92 | operation: "source-control.discovery.probe", |
| 98 | command: executable, | 93 | command: executable, |
| 99 | args: versionArgs, | 94 | args: versionArgs, |
| 100 | cwd: config.cwd, | 95 | cwd: config.cwd, |
| 101 | timeoutMs: 5_000, | 96 | timeoutMs: 5_000, |
| 102 | maxOutputBytes: 8_000, | 97 | maxOutputBytes: 8_000, |
| 103 | appendTruncationMarker: true, | 98 | appendTruncationMarker: true, |
| 104 | }) | 99 | }) |
| 105 | .pipe( | 100 | .pipe( |
| 106 | Effect.map( | 101 | Effect.map( |
| 107 | (result) => | 102 | (result) => |
| 108 | ({ | 103 | ({ |
| 109 | kind: input.kind, | ||
| 110 | label: input.label, | ||
| 111 | executable, | ||
| 112 | implemented: input.implemented, | ||
| 113 | status: "available" as const, | ||
| 114 | version: Option.orElse( | ||
| 115 | SourceControlProviderDiscovery.firstNonEmptyLine(result.stdout), | ||
| 116 | () => SourceControlProviderDiscovery.firstNonEmptyLine(result.stderr), | ||
| 117 | ), | ||
| 118 | installHint: input.installHint, | ||
| 119 | detail: Option.none<string>(), | ||
| 120 | }) satisfies DiscoveryProbeResult<Kind>, | ||
| 121 | ), | ||
| 122 | Effect.catch((cause) => | ||
| 123 | Effect.succeed({ | ||
| 124 | kind: input.kind, | 104 | kind: input.kind, |
| 125 | label: input.label, | 105 | label: input.label, |
| 126 | executable, | 106 | executable, |
| 127 | implemented: input.implemented, | 107 | implemented: input.implemented, |
| 128 | status: "missing" as const, | 108 | status: "available" as const, |
| 129 | version: Option.none<string>(), | 109 | version: Option.orElse(firstNonEmptyLine(result.stdout), () => |
| 110 | firstNonEmptyLine(result.stderr), | ||
| 111 | ), | ||
| 130 | installHint: input.installHint, | 112 | installHint: input.installHint, |
| 131 | detail: SourceControlProviderDiscovery.detailFromCause(cause), | 113 | detail: Option.none<string>(), |
| 132 | } satisfies DiscoveryProbeResult<Kind>), | 114 | }) satisfies DiscoveryProbeResult<Kind>, |
| 133 | ), | 115 | ), |
| 134 | ); | 116 | Effect.catch((cause) => |
| 135 | }; | 117 | Effect.succeed({ |
| 136 | 118 | kind: input.kind, | |
| 137 | return SourceControlDiscovery.of({ | 119 | label: input.label, |
| 138 | discover: Effect.all({ | 120 | executable, |
| 139 | versionControlSystems: Effect.all( | 121 | implemented: input.implemented, |
| 140 | VCS_PROBES.map((entry) => probe(entry)) as ReadonlyArray<Effect.Effect<VcsDiscoveryItem>>, | 122 | status: "missing" as const, |
| 141 | { concurrency: "unbounded" }, | 123 | version: Option.none<string>(), |
| 124 | installHint: input.installHint, | ||
| 125 | detail: detailFromCause(cause), | ||
| 126 | } satisfies DiscoveryProbeResult<Kind>), | ||
| 142 | ), | 127 | ), |
| 143 | sourceControlProviders: sourceControlProviders.discover, | 128 | ); |
| 144 | }), | 129 | }; |
| 145 | }); | 130 | |
| 146 | }), | 131 | return SourceControlDiscovery.of({ |
| 147 | ); | 132 | discover: Effect.all({ |
| 133 | versionControlSystems: Effect.all( | ||
| 134 | VCS_PROBES.map((entry) => probe(entry)) as ReadonlyArray<Effect.Effect<VcsDiscoveryItem>>, | ||
| 135 | { concurrency: "unbounded" }, | ||
| 136 | ), | ||
| 137 | sourceControlProviders: sourceControlProviders.discover, | ||
| 138 | }), | ||
| 139 | }); | ||
| 140 | }); | ||
| 141 | |||
| 142 | export const layer = Layer.effect(SourceControlDiscovery, make); |
apps/server/src/sourceControl/SourceControlProvider.ts+46-48| ... | @@ -49,54 +49,52 @@ export function sourceControlRefFromInput(input: { | ... | @@ -49,54 +49,52 @@ export function sourceControlRefFromInput(input: { |
| 49 | return input.source ?? parseSourceControlOwnerRef(input.headSelector); | 49 | return input.source ?? parseSourceControlOwnerRef(input.headSelector); |
| 50 | } | 50 | } |
| 51 | 51 | ||
| 52 | export interface SourceControlProviderShape { | ||
| 53 | readonly kind: SourceControlProviderKind; | ||
| 54 | readonly listChangeRequests: (input: { | ||
| 55 | readonly cwd: string; | ||
| 56 | readonly context?: SourceControlProviderContext; | ||
| 57 | readonly source?: SourceControlRefSelector; | ||
| 58 | readonly headSelector: string; | ||
| 59 | readonly state: ChangeRequestState | "all"; | ||
| 60 | readonly limit?: number; | ||
| 61 | }) => Effect.Effect<ReadonlyArray<ChangeRequest>, SourceControlProviderError>; | ||
| 62 | readonly getChangeRequest: (input: { | ||
| 63 | readonly cwd: string; | ||
| 64 | readonly context?: SourceControlProviderContext; | ||
| 65 | readonly reference: string; | ||
| 66 | }) => Effect.Effect<ChangeRequest, SourceControlProviderError>; | ||
| 67 | readonly createChangeRequest: (input: { | ||
| 68 | readonly cwd: string; | ||
| 69 | readonly context?: SourceControlProviderContext; | ||
| 70 | readonly source?: SourceControlRefSelector; | ||
| 71 | readonly target?: SourceControlRefSelector; | ||
| 72 | readonly baseRefName: string; | ||
| 73 | readonly headSelector: string; | ||
| 74 | readonly title: string; | ||
| 75 | readonly bodyFile: string; | ||
| 76 | }) => Effect.Effect<void, SourceControlProviderError>; | ||
| 77 | readonly getRepositoryCloneUrls: (input: { | ||
| 78 | readonly cwd: string; | ||
| 79 | readonly context?: SourceControlProviderContext; | ||
| 80 | readonly repository: string; | ||
| 81 | }) => Effect.Effect<SourceControlRepositoryCloneUrls, SourceControlProviderError>; | ||
| 82 | readonly createRepository: (input: { | ||
| 83 | readonly cwd: string; | ||
| 84 | readonly repository: string; | ||
| 85 | readonly visibility: SourceControlRepositoryVisibility; | ||
| 86 | }) => Effect.Effect<SourceControlRepositoryCloneUrls, SourceControlProviderError>; | ||
| 87 | readonly getDefaultBranch: (input: { | ||
| 88 | readonly cwd: string; | ||
| 89 | readonly context?: SourceControlProviderContext; | ||
| 90 | }) => Effect.Effect<string | null, SourceControlProviderError>; | ||
| 91 | readonly checkoutChangeRequest: (input: { | ||
| 92 | readonly cwd: string; | ||
| 93 | readonly context?: SourceControlProviderContext; | ||
| 94 | readonly reference: string; | ||
| 95 | readonly force?: boolean; | ||
| 96 | }) => Effect.Effect<void, SourceControlProviderError>; | ||
| 97 | } | ||
| 98 | |||
| 99 | export class SourceControlProvider extends Context.Service< | 52 | export class SourceControlProvider extends Context.Service< |
| 100 | SourceControlProvider, | 53 | SourceControlProvider, |
| 101 | SourceControlProviderShape | 54 | { |
| 55 | readonly kind: SourceControlProviderKind; | ||
| 56 | readonly listChangeRequests: (input: { | ||
| 57 | readonly cwd: string; | ||
| 58 | readonly context?: SourceControlProviderContext; | ||
| 59 | readonly source?: SourceControlRefSelector; | ||
| 60 | readonly headSelector: string; | ||
| 61 | readonly state: ChangeRequestState | "all"; | ||
| 62 | readonly limit?: number; | ||
| 63 | }) => Effect.Effect<ReadonlyArray<ChangeRequest>, SourceControlProviderError>; | ||
| 64 | readonly getChangeRequest: (input: { | ||
| 65 | readonly cwd: string; | ||
| 66 | readonly context?: SourceControlProviderContext; | ||
| 67 | readonly reference: string; | ||
| 68 | }) => Effect.Effect<ChangeRequest, SourceControlProviderError>; | ||
| 69 | readonly createChangeRequest: (input: { | ||
| 70 | readonly cwd: string; | ||
| 71 | readonly context?: SourceControlProviderContext; | ||
| 72 | readonly source?: SourceControlRefSelector; | ||
| 73 | readonly target?: SourceControlRefSelector; | ||
| 74 | readonly baseRefName: string; | ||
| 75 | readonly headSelector: string; | ||
| 76 | readonly title: string; | ||
| 77 | readonly bodyFile: string; | ||
| 78 | }) => Effect.Effect<void, SourceControlProviderError>; | ||
| 79 | readonly getRepositoryCloneUrls: (input: { | ||
| 80 | readonly cwd: string; | ||
| 81 | readonly context?: SourceControlProviderContext; | ||
| 82 | readonly repository: string; | ||
| 83 | }) => Effect.Effect<SourceControlRepositoryCloneUrls, SourceControlProviderError>; | ||
| 84 | readonly createRepository: (input: { | ||
| 85 | readonly cwd: string; | ||
| 86 | readonly repository: string; | ||
| 87 | readonly visibility: SourceControlRepositoryVisibility; | ||
| 88 | }) => Effect.Effect<SourceControlRepositoryCloneUrls, SourceControlProviderError>; | ||
| 89 | readonly getDefaultBranch: (input: { | ||
| 90 | readonly cwd: string; | ||
| 91 | readonly context?: SourceControlProviderContext; | ||
| 92 | }) => Effect.Effect<string | null, SourceControlProviderError>; | ||
| 93 | readonly checkoutChangeRequest: (input: { | ||
| 94 | readonly cwd: string; | ||
| 95 | readonly context?: SourceControlProviderContext; | ||
| 96 | readonly reference: string; | ||
| 97 | readonly force?: boolean; | ||
| 98 | }) => Effect.Effect<void, SourceControlProviderError>; | ||
| 99 | } | ||
| 102 | >()("t3/sourceControl/SourceControlProvider") {} | 100 | >()("t3/sourceControl/SourceControlProvider") {} |
apps/server/src/sourceControl/SourceControlProviderDiscovery.ts+3-3| ... | @@ -158,7 +158,7 @@ function isCliRemoteRefinementSpec( | ... | @@ -158,7 +158,7 @@ function isCliRemoteRefinementSpec( |
| 158 | 158 | ||
| 159 | function probeCli(input: { | 159 | function probeCli(input: { |
| 160 | readonly spec: SourceControlCliDiscoverySpec; | 160 | readonly spec: SourceControlCliDiscoverySpec; |
| 161 | readonly process: VcsProcess.VcsProcessShape; | 161 | readonly process: VcsProcess.VcsProcess["Service"]; |
| 162 | readonly cwd: string; | 162 | readonly cwd: string; |
| 163 | }): Effect.Effect<DiscoveryProbeResult> { | 163 | }): Effect.Effect<DiscoveryProbeResult> { |
| 164 | return input.process | 164 | return input.process |
| ... | @@ -202,7 +202,7 @@ function probeCli(input: { | ... | @@ -202,7 +202,7 @@ function probeCli(input: { |
| 202 | 202 | ||
| 203 | export function probeSourceControlProvider(input: { | 203 | export function probeSourceControlProvider(input: { |
| 204 | readonly spec: SourceControlProviderDiscoverySpec; | 204 | readonly spec: SourceControlProviderDiscoverySpec; |
| 205 | readonly process: VcsProcess.VcsProcessShape; | 205 | readonly process: VcsProcess.VcsProcess["Service"]; |
| 206 | readonly cwd: string; | 206 | readonly cwd: string; |
| 207 | }): Effect.Effect<SourceControlProviderDiscoveryItem> { | 207 | }): Effect.Effect<SourceControlProviderDiscoveryItem> { |
| 208 | if (input.spec.type === "api") { | 208 | if (input.spec.type === "api") { |
| ... | @@ -270,7 +270,7 @@ export function probeSourceControlProvider(input: { | ... | @@ -270,7 +270,7 @@ export function probeSourceControlProvider(input: { |
| 270 | export const refineUnknownRemoteProvider = Effect.fn("refineUnknownRemoteProvider")( | 270 | export const refineUnknownRemoteProvider = Effect.fn("refineUnknownRemoteProvider")( |
| 271 | function* (input: { | 271 | function* (input: { |
| 272 | readonly specs: ReadonlyArray<SourceControlProviderDiscoverySpec>; | 272 | readonly specs: ReadonlyArray<SourceControlProviderDiscoverySpec>; |
| 273 | readonly process: VcsProcess.VcsProcessShape; | 273 | readonly process: VcsProcess.VcsProcess["Service"]; |
| 274 | readonly cwd: string; | 274 | readonly cwd: string; |
| 275 | readonly context: SourceControlProvider.SourceControlProviderContext | null; | 275 | readonly context: SourceControlProvider.SourceControlProviderContext | null; |
| 276 | }): Effect.fn.Return<SourceControlProvider.SourceControlProviderContext | null> { | 276 | }): Effect.fn.Return<SourceControlProvider.SourceControlProviderContext | null> { |
apps/server/src/sourceControl/SourceControlProviderRegistry.test.ts+9-9| ... | @@ -6,7 +6,7 @@ import * as Layer from "effect/Layer"; | ... | @@ -6,7 +6,7 @@ import * as Layer from "effect/Layer"; |
| 6 | import * as Option from "effect/Option"; | 6 | import * as Option from "effect/Option"; |
| 7 | import { ChildProcessSpawner } from "effect/unstable/process"; | 7 | import { ChildProcessSpawner } from "effect/unstable/process"; |
| 8 | 8 | ||
| 9 | import { ServerConfig } from "../config.ts"; | 9 | import * as ServerConfig from "../config.ts"; |
| 10 | import type * as VcsDriver from "../vcs/VcsDriver.ts"; | 10 | import type * as VcsDriver from "../vcs/VcsDriver.ts"; |
| 11 | import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts"; | 11 | import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts"; |
| 12 | import * as VcsProcess from "../vcs/VcsProcess.ts"; | 12 | import * as VcsProcess from "../vcs/VcsProcess.ts"; |
| ... | @@ -37,7 +37,7 @@ function makeRegistry(input: { | ... | @@ -37,7 +37,7 @@ function makeRegistry(input: { |
| 37 | readonly name: string; | 37 | readonly name: string; |
| 38 | readonly url: string; | 38 | readonly url: string; |
| 39 | }>; | 39 | }>; |
| 40 | readonly process?: Partial<VcsProcess.VcsProcessShape>; | 40 | readonly process?: Partial<VcsProcess.VcsProcess["Service"]>; |
| 41 | }) { | 41 | }) { |
| 42 | const driver = { | 42 | const driver = { |
| 43 | listRemotes: () => | 43 | listRemotes: () => |
| ... | @@ -53,10 +53,10 @@ function makeRegistry(input: { | ... | @@ -53,10 +53,10 @@ function makeRegistry(input: { |
| 53 | expiresAt: Option.none(), | 53 | expiresAt: Option.none(), |
| 54 | }, | 54 | }, |
| 55 | }), | 55 | }), |
| 56 | } satisfies Partial<VcsDriver.VcsDriverShape>; | 56 | } satisfies Partial<VcsDriver.VcsDriver["Service"]>; |
| 57 | 57 | ||
| 58 | const registryLayer = Layer.mock(VcsDriverRegistry.VcsDriverRegistry)({ | 58 | const registryLayer = Layer.mock(VcsDriverRegistry.VcsDriverRegistry)({ |
| 59 | get: () => Effect.succeed(driver as unknown as VcsDriver.VcsDriverShape), | 59 | get: () => Effect.succeed(driver as unknown as VcsDriver.VcsDriver["Service"]), |
| 60 | resolve: () => | 60 | resolve: () => |
| 61 | Effect.succeed({ | 61 | Effect.succeed({ |
| 62 | kind: "git", | 62 | kind: "git", |
| ... | @@ -70,7 +70,7 @@ function makeRegistry(input: { | ... | @@ -70,7 +70,7 @@ function makeRegistry(input: { |
| 70 | expiresAt: Option.none(), | 70 | expiresAt: Option.none(), |
| 71 | }, | 71 | }, |
| 72 | }, | 72 | }, |
| 73 | driver: driver as unknown as VcsDriver.VcsDriverShape, | 73 | driver: driver as unknown as VcsDriver.VcsDriver["Service"], |
| 74 | }), | 74 | }), |
| 75 | }); | 75 | }); |
| 76 | 76 | ||
| ... | @@ -79,7 +79,7 @@ function makeRegistry(input: { | ... | @@ -79,7 +79,7 @@ function makeRegistry(input: { |
| 79 | ...input.process, | 79 | ...input.process, |
| 80 | }); | 80 | }); |
| 81 | 81 | ||
| 82 | return SourceControlProviderRegistry.make().pipe( | 82 | return SourceControlProviderRegistry.make.pipe( |
| 83 | Effect.provide( | 83 | Effect.provide( |
| 84 | Layer.mergeAll( | 84 | Layer.mergeAll( |
| 85 | registryLayer, | 85 | registryLayer, |
| ... | @@ -88,9 +88,9 @@ function makeRegistry(input: { | ... | @@ -88,9 +88,9 @@ function makeRegistry(input: { |
| 88 | Layer.mock(BitbucketApi.BitbucketApi)({}), | 88 | Layer.mock(BitbucketApi.BitbucketApi)({}), |
| 89 | Layer.mock(GitHubCli.GitHubCli)({}), | 89 | Layer.mock(GitHubCli.GitHubCli)({}), |
| 90 | Layer.mock(GitLabCli.GitLabCli)({}), | 90 | Layer.mock(GitLabCli.GitLabCli)({}), |
| 91 | ServerConfig.layerTest(process.cwd(), { prefix: "t3-source-control-registry-test-" }).pipe( | 91 | ServerConfig.layerTest(process.cwd(), { |
| 92 | Layer.provide(NodeServices.layer), | 92 | prefix: "t3-source-control-registry-test-", |
| 93 | ), | 93 | }).pipe(Layer.provide(NodeServices.layer)), |
| 94 | ), | 94 | ), |
| 95 | ), | 95 | ), |
| 96 | ); | 96 | ); |
apps/server/src/sourceControl/SourceControlProviderRegistry.ts+41-33| ... | @@ -16,7 +16,11 @@ import * as BitbucketSourceControlProvider from "./BitbucketSourceControlProvide | ... | @@ -16,7 +16,11 @@ import * as BitbucketSourceControlProvider from "./BitbucketSourceControlProvide |
| 16 | import * as GitHubSourceControlProvider from "./GitHubSourceControlProvider.ts"; | 16 | import * as GitHubSourceControlProvider from "./GitHubSourceControlProvider.ts"; |
| 17 | import * as GitLabSourceControlProvider from "./GitLabSourceControlProvider.ts"; | 17 | import * as GitLabSourceControlProvider from "./GitLabSourceControlProvider.ts"; |
| 18 | import * as SourceControlProvider from "./SourceControlProvider.ts"; | 18 | import * as SourceControlProvider from "./SourceControlProvider.ts"; |
| 19 | import * as SourceControlProviderDiscovery from "./SourceControlProviderDiscovery.ts"; | 19 | import { |
| 20 | probeSourceControlProvider, | ||
| 21 | refineUnknownRemoteProvider, | ||
| 22 | type SourceControlProviderDiscoverySpec, | ||
| 23 | } from "./SourceControlProviderDiscovery.ts"; | ||
| 20 | import { ServerConfig } from "../config.ts"; | 24 | import { ServerConfig } from "../config.ts"; |
| 21 | import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts"; | 25 | import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts"; |
| 22 | import * as VcsProcess from "../vcs/VcsProcess.ts"; | 26 | import * as VcsProcess from "../vcs/VcsProcess.ts"; |
| ... | @@ -26,36 +30,40 @@ const PROVIDER_DETECTION_CACHE_TTL = Duration.seconds(5); | ... | @@ -26,36 +30,40 @@ const PROVIDER_DETECTION_CACHE_TTL = Duration.seconds(5); |
| 26 | 30 | ||
| 27 | export interface SourceControlProviderRegistration { | 31 | export interface SourceControlProviderRegistration { |
| 28 | readonly kind: SourceControlProviderKind; | 32 | readonly kind: SourceControlProviderKind; |
| 29 | readonly provider: SourceControlProvider.SourceControlProviderShape; | 33 | readonly provider: SourceControlProvider.SourceControlProvider["Service"]; |
| 30 | readonly discovery: SourceControlProviderDiscovery.SourceControlProviderDiscoverySpec; | 34 | readonly discovery: SourceControlProviderDiscoverySpec; |
| 31 | } | 35 | } |
| 32 | 36 | ||
| 33 | export interface SourceControlProviderHandle { | 37 | export interface SourceControlProviderHandle { |
| 34 | readonly provider: SourceControlProvider.SourceControlProviderShape; | 38 | readonly provider: SourceControlProvider.SourceControlProvider["Service"]; |
| 35 | readonly context: SourceControlProvider.SourceControlProviderContext | null; | 39 | readonly context: SourceControlProvider.SourceControlProviderContext | null; |
| 36 | } | 40 | } |
| 37 | 41 | ||
| 38 | export interface SourceControlProviderRegistryShape { | ||
| 39 | readonly get: ( | ||
| 40 | kind: SourceControlProviderKind, | ||
| 41 | ) => Effect.Effect<SourceControlProvider.SourceControlProviderShape, SourceControlProviderError>; | ||
| 42 | readonly resolveHandle: (input: { | ||
| 43 | readonly cwd: string; | ||
| 44 | }) => Effect.Effect<SourceControlProviderHandle, SourceControlProviderError>; | ||
| 45 | readonly resolve: (input: { | ||
| 46 | readonly cwd: string; | ||
| 47 | }) => Effect.Effect<SourceControlProvider.SourceControlProviderShape, SourceControlProviderError>; | ||
| 48 | readonly discover: Effect.Effect<ReadonlyArray<SourceControlProviderDiscoveryItem>>; | ||
| 49 | } | ||
| 50 | |||
| 51 | export class SourceControlProviderRegistry extends Context.Service< | 42 | export class SourceControlProviderRegistry extends Context.Service< |
| 52 | SourceControlProviderRegistry, | 43 | SourceControlProviderRegistry, |
| 53 | SourceControlProviderRegistryShape | 44 | { |
| 45 | readonly get: ( | ||
| 46 | kind: SourceControlProviderKind, | ||
| 47 | ) => Effect.Effect< | ||
| 48 | SourceControlProvider.SourceControlProvider["Service"], | ||
| 49 | SourceControlProviderError | ||
| 50 | >; | ||
| 51 | readonly resolveHandle: (input: { | ||
| 52 | readonly cwd: string; | ||
| 53 | }) => Effect.Effect<SourceControlProviderHandle, SourceControlProviderError>; | ||
| 54 | readonly resolve: (input: { | ||
| 55 | readonly cwd: string; | ||
| 56 | }) => Effect.Effect< | ||
| 57 | SourceControlProvider.SourceControlProvider["Service"], | ||
| 58 | SourceControlProviderError | ||
| 59 | >; | ||
| 60 | readonly discover: Effect.Effect<ReadonlyArray<SourceControlProviderDiscoveryItem>>; | ||
| 61 | } | ||
| 54 | >()("t3/sourceControl/SourceControlProviderRegistry") {} | 62 | >()("t3/sourceControl/SourceControlProviderRegistry") {} |
| 55 | 63 | ||
| 56 | function unsupportedProvider( | 64 | function unsupportedProvider( |
| 57 | kind: SourceControlProviderKind, | 65 | kind: SourceControlProviderKind, |
| 58 | ): SourceControlProvider.SourceControlProviderShape { | 66 | ): SourceControlProvider.SourceControlProvider["Service"] { |
| 59 | const unsupported = (operation: string) => | 67 | const unsupported = (operation: string) => |
| 60 | Effect.fail( | 68 | Effect.fail( |
| 61 | new SourceControlProviderError({ | 69 | new SourceControlProviderError({ |
| ... | @@ -113,9 +121,9 @@ function selectProviderContext( | ... | @@ -113,9 +121,9 @@ function selectProviderContext( |
| 113 | } | 121 | } |
| 114 | 122 | ||
| 115 | function bindProviderContext( | 123 | function bindProviderContext( |
| 116 | provider: SourceControlProvider.SourceControlProviderShape, | 124 | provider: SourceControlProvider.SourceControlProvider["Service"], |
| 117 | context: SourceControlProvider.SourceControlProviderContext | null, | 125 | context: SourceControlProvider.SourceControlProviderContext | null, |
| 118 | ): SourceControlProvider.SourceControlProviderShape { | 126 | ): SourceControlProvider.SourceControlProvider["Service"] { |
| 119 | if (context === null) { | 127 | if (context === null) { |
| 120 | return provider; | 128 | return provider; |
| 121 | } | 129 | } |
| ... | @@ -163,11 +171,11 @@ export const makeWithProviders = Effect.fn("makeSourceControlProviderRegistryWit | ... | @@ -163,11 +171,11 @@ export const makeWithProviders = Effect.fn("makeSourceControlProviderRegistryWit |
| 163 | const vcsRegistry = yield* VcsDriverRegistry.VcsDriverRegistry; | 171 | const vcsRegistry = yield* VcsDriverRegistry.VcsDriverRegistry; |
| 164 | const providers = new Map< | 172 | const providers = new Map< |
| 165 | SourceControlProviderKind, | 173 | SourceControlProviderKind, |
| 166 | SourceControlProvider.SourceControlProviderShape | 174 | SourceControlProvider.SourceControlProvider["Service"] |
| 167 | >(registrations.map((registration) => [registration.kind, registration.provider])); | 175 | >(registrations.map((registration) => [registration.kind, registration.provider])); |
| 168 | const discoverySpecs = registrations.map((registration) => registration.discovery); | 176 | const discoverySpecs = registrations.map((registration) => registration.discovery); |
| 169 | 177 | ||
| 170 | const get: SourceControlProviderRegistryShape["get"] = (kind) => | 178 | const get: SourceControlProviderRegistry["Service"]["get"] = (kind) => |
| 171 | Effect.succeed(providers.get(kind) ?? unsupportedProvider(kind)); | 179 | Effect.succeed(providers.get(kind) ?? unsupportedProvider(kind)); |
| 172 | 180 | ||
| 173 | const detectProviderContext = Effect.fn("SourceControlProviderRegistry.detectProviderContext")( | 181 | const detectProviderContext = Effect.fn("SourceControlProviderRegistry.detectProviderContext")( |
| ... | @@ -180,7 +188,7 @@ export const makeWithProviders = Effect.fn("makeSourceControlProviderRegistryWit | ... | @@ -180,7 +188,7 @@ export const makeWithProviders = Effect.fn("makeSourceControlProviderRegistryWit |
| 180 | .pipe(Effect.mapError((error) => providerDetectionError("detectProvider", cwd, error))); | 188 | .pipe(Effect.mapError((error) => providerDetectionError("detectProvider", cwd, error))); |
| 181 | const context = selectProviderContext(remotes.remotes); | 189 | const context = selectProviderContext(remotes.remotes); |
| 182 | 190 | ||
| 183 | return yield* SourceControlProviderDiscovery.refineUnknownRemoteProvider({ | 191 | return yield* refineUnknownRemoteProvider({ |
| 184 | specs: discoverySpecs, | 192 | specs: discoverySpecs, |
| 185 | process, | 193 | process, |
| 186 | cwd, | 194 | cwd, |
| ... | @@ -198,7 +206,7 @@ export const makeWithProviders = Effect.fn("makeSourceControlProviderRegistryWit | ... | @@ -198,7 +206,7 @@ export const makeWithProviders = Effect.fn("makeSourceControlProviderRegistryWit |
| 198 | timeToLive: (exit) => (Exit.isSuccess(exit) ? PROVIDER_DETECTION_CACHE_TTL : Duration.zero), | 206 | timeToLive: (exit) => (Exit.isSuccess(exit) ? PROVIDER_DETECTION_CACHE_TTL : Duration.zero), |
| 199 | }); | 207 | }); |
| 200 | 208 | ||
| 201 | const resolveHandle: SourceControlProviderRegistryShape["resolveHandle"] = (input) => | 209 | const resolveHandle: SourceControlProviderRegistry["Service"]["resolveHandle"] = (input) => |
| 202 | Cache.get(providerContextCache, input.cwd).pipe( | 210 | Cache.get(providerContextCache, input.cwd).pipe( |
| 203 | Effect.map((context) => { | 211 | Effect.map((context) => { |
| 204 | const kind = context?.provider.kind ?? "unknown"; | 212 | const kind = context?.provider.kind ?? "unknown"; |
| ... | @@ -216,7 +224,7 @@ export const makeWithProviders = Effect.fn("makeSourceControlProviderRegistryWit | ... | @@ -216,7 +224,7 @@ export const makeWithProviders = Effect.fn("makeSourceControlProviderRegistryWit |
| 216 | resolve: (input) => resolveHandle(input).pipe(Effect.map((handle) => handle.provider)), | 224 | resolve: (input) => resolveHandle(input).pipe(Effect.map((handle) => handle.provider)), |
| 217 | discover: Effect.all( | 225 | discover: Effect.all( |
| 218 | discoverySpecs.map((spec) => | 226 | discoverySpecs.map((spec) => |
| 219 | SourceControlProviderDiscovery.probeSourceControlProvider({ | 227 | probeSourceControlProvider({ |
| 220 | spec, | 228 | spec, |
| 221 | process, | 229 | process, |
| 222 | cwd: config.cwd, | 230 | cwd: config.cwd, |
| ... | @@ -228,12 +236,12 @@ export const makeWithProviders = Effect.fn("makeSourceControlProviderRegistryWit | ... | @@ -228,12 +236,12 @@ export const makeWithProviders = Effect.fn("makeSourceControlProviderRegistryWit |
| 228 | }, | 236 | }, |
| 229 | ); | 237 | ); |
| 230 | 238 | ||
| 231 | export const make = Effect.fn("makeSourceControlProviderRegistry")(function* () { | 239 | export const make = Effect.gen(function* () { |
| 232 | const github = yield* GitHubSourceControlProvider.make(); | 240 | const github = yield* GitHubSourceControlProvider.make; |
| 233 | const gitlab = yield* GitLabSourceControlProvider.make(); | 241 | const gitlab = yield* GitLabSourceControlProvider.make; |
| 234 | const bitbucket = yield* BitbucketSourceControlProvider.make(); | 242 | const bitbucket = yield* BitbucketSourceControlProvider.make; |
| 235 | const bitbucketDiscovery = yield* BitbucketSourceControlProvider.makeDiscovery(); | 243 | const bitbucketDiscovery = yield* BitbucketSourceControlProvider.makeDiscovery; |
| 236 | const azureDevOps = yield* AzureDevOpsSourceControlProvider.make(); | 244 | const azureDevOps = yield* AzureDevOpsSourceControlProvider.make; |
| 237 | return yield* makeWithProviders([ | 245 | return yield* makeWithProviders([ |
| 238 | { | 246 | { |
| 239 | kind: "github", | 247 | kind: "github", |
| ... | @@ -258,4 +266,4 @@ export const make = Effect.fn("makeSourceControlProviderRegistry")(function* () | ... | @@ -258,4 +266,4 @@ export const make = Effect.fn("makeSourceControlProviderRegistry")(function* () |
| 258 | ]); | 266 | ]); |
| 259 | }); | 267 | }); |
| 260 | 268 | ||
| 261 | export const layer = Layer.effect(SourceControlProviderRegistry, make()); | 269 | export const layer = Layer.effect(SourceControlProviderRegistry, make); |
apps/server/src/sourceControl/SourceControlRepositoryService.test.ts+5-5| ... | @@ -7,7 +7,7 @@ import { ChildProcessSpawner } from "effect/unstable/process"; | ... | @@ -7,7 +7,7 @@ import { ChildProcessSpawner } from "effect/unstable/process"; |
| 7 | 7 | ||
| 8 | import { GitCommandError, type SourceControlProviderError } from "@t3tools/contracts"; | 8 | import { GitCommandError, type SourceControlProviderError } from "@t3tools/contracts"; |
| 9 | 9 | ||
| 10 | import { ServerConfig } from "../config.ts"; | 10 | import * as ServerConfig from "../config.ts"; |
| 11 | import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; | 11 | import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; |
| 12 | import type * as SourceControlProvider from "./SourceControlProvider.ts"; | 12 | import type * as SourceControlProvider from "./SourceControlProvider.ts"; |
| 13 | import * as SourceControlProviderRegistry from "./SourceControlProviderRegistry.ts"; | 13 | import * as SourceControlProviderRegistry from "./SourceControlProviderRegistry.ts"; |
| ... | @@ -20,8 +20,8 @@ const CLONE_URLS = { | ... | @@ -20,8 +20,8 @@ const CLONE_URLS = { |
| 20 | }; | 20 | }; |
| 21 | 21 | ||
| 22 | function makeProvider( | 22 | function makeProvider( |
| 23 | overrides: Partial<SourceControlProvider.SourceControlProviderShape> = {}, | 23 | overrides: Partial<SourceControlProvider.SourceControlProvider["Service"]> = {}, |
| 24 | ): SourceControlProvider.SourceControlProviderShape { | 24 | ): SourceControlProvider.SourceControlProvider["Service"] { |
| 25 | const unsupported = (operation: string) => | 25 | const unsupported = (operation: string) => |
| 26 | Effect.die(`unexpected provider operation ${operation}`) as Effect.Effect< | 26 | Effect.die(`unexpected provider operation ${operation}`) as Effect.Effect< |
| 27 | never, | 27 | never, |
| ... | @@ -52,8 +52,8 @@ function processOutput(): GitVcsDriver.ExecuteGitResult { | ... | @@ -52,8 +52,8 @@ function processOutput(): GitVcsDriver.ExecuteGitResult { |
| 52 | } | 52 | } |
| 53 | 53 | ||
| 54 | function makeLayer(input: { | 54 | function makeLayer(input: { |
| 55 | readonly provider?: SourceControlProvider.SourceControlProviderShape; | 55 | readonly provider?: SourceControlProvider.SourceControlProvider["Service"]; |
| 56 | readonly git?: Partial<GitVcsDriver.GitVcsDriverShape>; | 56 | readonly git?: Partial<GitVcsDriver.GitVcsDriver["Service"]>; |
| 57 | }) { | 57 | }) { |
| 58 | return SourceControlRepositoryService.layer.pipe( | 58 | return SourceControlRepositoryService.layer.pipe( |
| 59 | Layer.provide( | 59 | Layer.provide( |
apps/server/src/sourceControl/SourceControlRepositoryService.ts+13-15| ... | @@ -24,21 +24,19 @@ import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; | ... | @@ -24,21 +24,19 @@ import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; |
| 24 | import * as SourceControlProviderRegistry from "./SourceControlProviderRegistry.ts"; | 24 | import * as SourceControlProviderRegistry from "./SourceControlProviderRegistry.ts"; |
| 25 | const isSourceControlRepositoryError = Schema.is(SourceControlRepositoryError); | 25 | const isSourceControlRepositoryError = Schema.is(SourceControlRepositoryError); |
| 26 | 26 | ||
| 27 | export interface SourceControlRepositoryServiceShape { | ||
| 28 | readonly lookupRepository: ( | ||
| 29 | input: SourceControlRepositoryLookupInput, | ||
| 30 | ) => Effect.Effect<SourceControlRepositoryInfo, SourceControlRepositoryError>; | ||
| 31 | readonly cloneRepository: ( | ||
| 32 | input: SourceControlCloneRepositoryInput, | ||
| 33 | ) => Effect.Effect<SourceControlCloneRepositoryResult, SourceControlRepositoryError>; | ||
| 34 | readonly publishRepository: ( | ||
| 35 | input: SourceControlPublishRepositoryInput, | ||
| 36 | ) => Effect.Effect<SourceControlPublishRepositoryResult, SourceControlRepositoryError>; | ||
| 37 | } | ||
| 38 | |||
| 39 | export class SourceControlRepositoryService extends Context.Service< | 27 | export class SourceControlRepositoryService extends Context.Service< |
| 40 | SourceControlRepositoryService, | 28 | SourceControlRepositoryService, |
| 41 | SourceControlRepositoryServiceShape | 29 | { |
| 30 | readonly lookupRepository: ( | ||
| 31 | input: SourceControlRepositoryLookupInput, | ||
| 32 | ) => Effect.Effect<SourceControlRepositoryInfo, SourceControlRepositoryError>; | ||
| 33 | readonly cloneRepository: ( | ||
| 34 | input: SourceControlCloneRepositoryInput, | ||
| 35 | ) => Effect.Effect<SourceControlCloneRepositoryResult, SourceControlRepositoryError>; | ||
| 36 | readonly publishRepository: ( | ||
| 37 | input: SourceControlPublishRepositoryInput, | ||
| 38 | ) => Effect.Effect<SourceControlPublishRepositoryResult, SourceControlRepositoryError>; | ||
| 39 | } | ||
| 42 | >()("t3/sourceControl/SourceControlRepositoryService") {} | 40 | >()("t3/sourceControl/SourceControlRepositoryService") {} |
| 43 | 41 | ||
| 44 | function detailFromUnknown(cause: unknown): string { | 42 | function detailFromUnknown(cause: unknown): string { |
| ... | @@ -116,7 +114,7 @@ function expandHomePath(input: string, path: Path.Path): string { | ... | @@ -116,7 +114,7 @@ function expandHomePath(input: string, path: Path.Path): string { |
| 116 | return input; | 114 | return input; |
| 117 | } | 115 | } |
| 118 | 116 | ||
| 119 | export const make = Effect.fn("makeSourceControlRepositoryService")(function* () { | 117 | export const make = Effect.gen(function* () { |
| 120 | const config = yield* ServerConfig; | 118 | const config = yield* ServerConfig; |
| 121 | const fileSystem = yield* FileSystem.FileSystem; | 119 | const fileSystem = yield* FileSystem.FileSystem; |
| 122 | const git = yield* GitVcsDriver.GitVcsDriver; | 120 | const git = yield* GitVcsDriver.GitVcsDriver; |
| ... | @@ -315,4 +313,4 @@ export const make = Effect.fn("makeSourceControlRepositoryService")(function* () | ... | @@ -315,4 +313,4 @@ export const make = Effect.fn("makeSourceControlRepositoryService")(function* () |
| 315 | }); | 313 | }); |
| 316 | }); | 314 | }); |
| 317 | 315 | ||
| 318 | export const layer = Layer.effect(SourceControlRepositoryService, make()); | 316 | export const layer = Layer.effect(SourceControlRepositoryService, make); |
apps/server/src/vcs/GitVcsDriver.test.ts+1-1| ... | @@ -8,7 +8,7 @@ import { ChildProcessSpawner } from "effect/unstable/process"; | ... | @@ -8,7 +8,7 @@ import { ChildProcessSpawner } from "effect/unstable/process"; |
| 8 | import { assert, it } from "@effect/vitest"; | 8 | import { assert, it } from "@effect/vitest"; |
| 9 | 9 | ||
| 10 | import { GitCommandError } from "@t3tools/contracts"; | 10 | import { GitCommandError } from "@t3tools/contracts"; |
| 11 | import { ServerConfig } from "../config.ts"; | 11 | import * as ServerConfig from "../config.ts"; |
| 12 | import * as GitVcsDriver from "./GitVcsDriver.ts"; | 12 | import * as GitVcsDriver from "./GitVcsDriver.ts"; |
| 13 | import * as VcsProcess from "./VcsProcess.ts"; | 13 | import * as VcsProcess from "./VcsProcess.ts"; |
| 14 | import { runVcsDriverContractSuite } from "./testing/VcsDriverContractHarness.ts"; | 14 | import { runVcsDriverContractSuite } from "./testing/VcsDriverContractHarness.ts"; |
apps/server/src/vcs/GitVcsDriver.ts+94-91| ... | @@ -28,7 +28,7 @@ import { | ... | @@ -28,7 +28,7 @@ import { |
| 28 | type VcsStatusInput, | 28 | type VcsStatusInput, |
| 29 | type VcsStatusResult, | 29 | type VcsStatusResult, |
| 30 | } from "@t3tools/contracts"; | 30 | } from "@t3tools/contracts"; |
| 31 | import * as GitVcsDriverCore from "./GitVcsDriverCore.ts"; | 31 | import { makeGitVcsDriverCore } from "./GitVcsDriverCore.ts"; |
| 32 | import * as VcsDriver from "./VcsDriver.ts"; | 32 | import * as VcsDriver from "./VcsDriver.ts"; |
| 33 | import * as VcsProcess from "./VcsProcess.ts"; | 33 | import * as VcsProcess from "./VcsProcess.ts"; |
| 34 | 34 | ||
| ... | @@ -188,81 +188,84 @@ export interface GitRemoteStatusOptions { | ... | @@ -188,81 +188,84 @@ export interface GitRemoteStatusOptions { |
| 188 | readonly refreshUpstream?: boolean; | 188 | readonly refreshUpstream?: boolean; |
| 189 | } | 189 | } |
| 190 | 190 | ||
| 191 | export interface GitVcsDriverShape { | 191 | export class GitVcsDriver extends Context.Service< |
| 192 | readonly execute: (input: ExecuteGitInput) => Effect.Effect<ExecuteGitResult, GitCommandError>; | 192 | GitVcsDriver, |
| 193 | readonly status: (input: VcsStatusInput) => Effect.Effect<VcsStatusResult, GitCommandError>; | 193 | { |
| 194 | readonly statusDetails: (cwd: string) => Effect.Effect<GitStatusDetails, GitCommandError>; | 194 | readonly execute: (input: ExecuteGitInput) => Effect.Effect<ExecuteGitResult, GitCommandError>; |
| 195 | readonly statusDetailsLocal: (cwd: string) => Effect.Effect<GitStatusDetails, GitCommandError>; | 195 | readonly status: (input: VcsStatusInput) => Effect.Effect<VcsStatusResult, GitCommandError>; |
| 196 | readonly statusDetailsRemote: ( | 196 | readonly statusDetails: (cwd: string) => Effect.Effect<GitStatusDetails, GitCommandError>; |
| 197 | cwd: string, | 197 | readonly statusDetailsLocal: (cwd: string) => Effect.Effect<GitStatusDetails, GitCommandError>; |
| 198 | options?: GitRemoteStatusOptions, | 198 | readonly statusDetailsRemote: ( |
| 199 | ) => Effect.Effect<GitRemoteStatusDetails, GitCommandError>; | 199 | cwd: string, |
| 200 | readonly prepareCommitContext: ( | 200 | options?: GitRemoteStatusOptions, |
| 201 | cwd: string, | 201 | ) => Effect.Effect<GitRemoteStatusDetails, GitCommandError>; |
| 202 | filePaths?: readonly string[], | 202 | readonly prepareCommitContext: ( |
| 203 | ) => Effect.Effect<GitPreparedCommitContext | null, GitCommandError>; | 203 | cwd: string, |
| 204 | readonly commit: ( | 204 | filePaths?: readonly string[], |
| 205 | cwd: string, | 205 | ) => Effect.Effect<GitPreparedCommitContext | null, GitCommandError>; |
| 206 | subject: string, | 206 | readonly commit: ( |
| 207 | body: string, | 207 | cwd: string, |
| 208 | options?: GitCommitOptions, | 208 | subject: string, |
| 209 | ) => Effect.Effect<{ commitSha: string }, GitCommandError>; | 209 | body: string, |
| 210 | readonly pushCurrentBranch: ( | 210 | options?: GitCommitOptions, |
| 211 | cwd: string, | 211 | ) => Effect.Effect<{ commitSha: string }, GitCommandError>; |
| 212 | fallbackBranch: string | null, | 212 | readonly pushCurrentBranch: ( |
| 213 | options?: { readonly remoteName?: string | null }, | 213 | cwd: string, |
| 214 | ) => Effect.Effect<GitPushResult, GitCommandError>; | 214 | fallbackBranch: string | null, |
| 215 | readonly readRangeContext: ( | 215 | options?: { readonly remoteName?: string | null }, |
| 216 | cwd: string, | 216 | ) => Effect.Effect<GitPushResult, GitCommandError>; |
| 217 | baseRef: string, | 217 | readonly readRangeContext: ( |
| 218 | ) => Effect.Effect<GitRangeContext, GitCommandError>; | 218 | cwd: string, |
| 219 | readonly getReviewDiffPreview: ( | 219 | baseRef: string, |
| 220 | input: ReviewDiffPreviewInput, | 220 | ) => Effect.Effect<GitRangeContext, GitCommandError>; |
| 221 | ) => Effect.Effect<ReviewDiffPreviewResult, GitCommandError>; | 221 | readonly getReviewDiffPreview: ( |
| 222 | readonly readConfigValue: ( | 222 | input: ReviewDiffPreviewInput, |
| 223 | cwd: string, | 223 | ) => Effect.Effect<ReviewDiffPreviewResult, GitCommandError>; |
| 224 | key: string, | 224 | readonly readConfigValue: ( |
| 225 | ) => Effect.Effect<string | null, GitCommandError>; | 225 | cwd: string, |
| 226 | readonly listRefs: (input: VcsListRefsInput) => Effect.Effect<VcsListRefsResult, GitCommandError>; | 226 | key: string, |
| 227 | readonly pullCurrentBranch: (cwd: string) => Effect.Effect<VcsPullResult, GitCommandError>; | 227 | ) => Effect.Effect<string | null, GitCommandError>; |
| 228 | readonly createWorktree: ( | 228 | readonly listRefs: ( |
| 229 | input: VcsCreateWorktreeInput, | 229 | input: VcsListRefsInput, |
| 230 | ) => Effect.Effect<VcsCreateWorktreeResult, GitCommandError>; | 230 | ) => Effect.Effect<VcsListRefsResult, GitCommandError>; |
| 231 | readonly fetchPullRequestBranch: ( | 231 | readonly pullCurrentBranch: (cwd: string) => Effect.Effect<VcsPullResult, GitCommandError>; |
| 232 | input: GitFetchPullRequestBranchInput, | 232 | readonly createWorktree: ( |
| 233 | ) => Effect.Effect<void, GitCommandError>; | 233 | input: VcsCreateWorktreeInput, |
| 234 | readonly ensureRemote: (input: GitEnsureRemoteInput) => Effect.Effect<string, GitCommandError>; | 234 | ) => Effect.Effect<VcsCreateWorktreeResult, GitCommandError>; |
| 235 | readonly resolvePrimaryRemoteName: (cwd: string) => Effect.Effect<string, GitCommandError>; | 235 | readonly fetchPullRequestBranch: ( |
| 236 | readonly fetchRemote: (input: GitFetchRemoteInput) => Effect.Effect<void, GitCommandError>; | 236 | input: GitFetchPullRequestBranchInput, |
| 237 | readonly resolveRemoteTrackingCommit: ( | 237 | ) => Effect.Effect<void, GitCommandError>; |
| 238 | input: GitResolveRemoteTrackingCommitInput, | 238 | readonly ensureRemote: (input: GitEnsureRemoteInput) => Effect.Effect<string, GitCommandError>; |
| 239 | ) => Effect.Effect<GitResolveRemoteTrackingCommitResult, GitCommandError>; | 239 | readonly resolvePrimaryRemoteName: (cwd: string) => Effect.Effect<string, GitCommandError>; |
| 240 | readonly fetchRemoteBranch: ( | 240 | readonly fetchRemote: (input: GitFetchRemoteInput) => Effect.Effect<void, GitCommandError>; |
| 241 | input: GitFetchRemoteBranchInput, | 241 | readonly resolveRemoteTrackingCommit: ( |
| 242 | ) => Effect.Effect<void, GitCommandError>; | 242 | input: GitResolveRemoteTrackingCommitInput, |
| 243 | readonly fetchRemoteTrackingBranch: ( | 243 | ) => Effect.Effect<GitResolveRemoteTrackingCommitResult, GitCommandError>; |
| 244 | input: GitFetchRemoteTrackingBranchInput, | 244 | readonly fetchRemoteBranch: ( |
| 245 | ) => Effect.Effect<void, GitCommandError>; | 245 | input: GitFetchRemoteBranchInput, |
| 246 | readonly setBranchUpstream: ( | 246 | ) => Effect.Effect<void, GitCommandError>; |
| 247 | input: GitSetBranchUpstreamInput, | 247 | readonly fetchRemoteTrackingBranch: ( |
| 248 | ) => Effect.Effect<void, GitCommandError>; | 248 | input: GitFetchRemoteTrackingBranchInput, |
| 249 | readonly removeWorktree: (input: VcsRemoveWorktreeInput) => Effect.Effect<void, GitCommandError>; | 249 | ) => Effect.Effect<void, GitCommandError>; |
| 250 | readonly renameBranch: ( | 250 | readonly setBranchUpstream: ( |
| 251 | input: GitRenameBranchInput, | 251 | input: GitSetBranchUpstreamInput, |
| 252 | ) => Effect.Effect<GitRenameBranchResult, GitCommandError>; | 252 | ) => Effect.Effect<void, GitCommandError>; |
| 253 | readonly createRef: ( | 253 | readonly removeWorktree: ( |
| 254 | input: VcsCreateRefInput, | 254 | input: VcsRemoveWorktreeInput, |
| 255 | ) => Effect.Effect<VcsCreateRefResult, GitCommandError>; | 255 | ) => Effect.Effect<void, GitCommandError>; |
| 256 | readonly switchRef: ( | 256 | readonly renameBranch: ( |
| 257 | input: VcsSwitchRefInput, | 257 | input: GitRenameBranchInput, |
| 258 | ) => Effect.Effect<VcsSwitchRefResult, GitCommandError>; | 258 | ) => Effect.Effect<GitRenameBranchResult, GitCommandError>; |
| 259 | readonly initRepo: (input: VcsInitInput) => Effect.Effect<void, GitCommandError>; | 259 | readonly createRef: ( |
| 260 | readonly listLocalBranchNames: (cwd: string) => Effect.Effect<string[], GitCommandError>; | 260 | input: VcsCreateRefInput, |
| 261 | } | 261 | ) => Effect.Effect<VcsCreateRefResult, GitCommandError>; |
| 262 | 262 | readonly switchRef: ( | |
| 263 | export class GitVcsDriver extends Context.Service<GitVcsDriver, GitVcsDriverShape>()( | 263 | input: VcsSwitchRefInput, |
| 264 | "t3/vcs/GitVcsDriver", | 264 | ) => Effect.Effect<VcsSwitchRefResult, GitCommandError>; |
| 265 | ) {} | 265 | readonly initRepo: (input: VcsInitInput) => Effect.Effect<void, GitCommandError>; |
| 266 | readonly listLocalBranchNames: (cwd: string) => Effect.Effect<string[], GitCommandError>; | ||
| 267 | } | ||
| 268 | >()("t3/vcs/GitVcsDriver") {} | ||
| 266 | 269 | ||
| 267 | const WORKSPACE_FILES_MAX_OUTPUT_BYTES = 16 * 1024 * 1024; | 270 | const WORKSPACE_FILES_MAX_OUTPUT_BYTES = 16 * 1024 * 1024; |
| 268 | const GIT_CHECK_IGNORE_MAX_STDIN_BYTES = 256 * 1024; | 271 | const GIT_CHECK_IGNORE_MAX_STDIN_BYTES = 256 * 1024; |
| ... | @@ -357,7 +360,7 @@ function parseGitRemoteVerboseOutput( | ... | @@ -357,7 +360,7 @@ function parseGitRemoteVerboseOutput( |
| 357 | } | 360 | } |
| 358 | 361 | ||
| 359 | const gitCommand = ( | 362 | const gitCommand = ( |
| 360 | process: VcsProcess.VcsProcessShape, | 363 | process: VcsProcess.VcsProcess["Service"], |
| 361 | operation: string, | 364 | operation: string, |
| 362 | cwd: string, | 365 | cwd: string, |
| 363 | args: ReadonlyArray<string>, | 366 | args: ReadonlyArray<string>, |
| ... | @@ -401,7 +404,7 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( | ... | @@ -401,7 +404,7 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( |
| 401 | ignoreClassifier: "native" as const, | 404 | ignoreClassifier: "native" as const, |
| 402 | }; | 405 | }; |
| 403 | 406 | ||
| 404 | const isInsideWorkTree: VcsDriver.VcsDriverShape["isInsideWorkTree"] = (cwd) => | 407 | const isInsideWorkTree: VcsDriver.VcsDriver["Service"]["isInsideWorkTree"] = (cwd) => |
| 405 | gitCommand( | 408 | gitCommand( |
| 406 | vcsProcess, | 409 | vcsProcess, |
| 407 | "GitVcsDriver.isInsideWorkTree", | 410 | "GitVcsDriver.isInsideWorkTree", |
| ... | @@ -414,7 +417,7 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( | ... | @@ -414,7 +417,7 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( |
| 414 | }, | 417 | }, |
| 415 | ).pipe(Effect.map((result) => result.exitCode === 0 && result.stdout.trim() === "true")); | 418 | ).pipe(Effect.map((result) => result.exitCode === 0 && result.stdout.trim() === "true")); |
| 416 | 419 | ||
| 417 | const execute: VcsDriver.VcsDriverShape["execute"] = (input) => | 420 | const execute: VcsDriver.VcsDriver["Service"]["execute"] = (input) => |
| 418 | gitCommand(vcsProcess, input.operation, input.cwd, input.args, { | 421 | gitCommand(vcsProcess, input.operation, input.cwd, input.args, { |
| 419 | ...(input.stdin !== undefined ? { stdin: input.stdin } : {}), | 422 | ...(input.stdin !== undefined ? { stdin: input.stdin } : {}), |
| 420 | ...(input.env !== undefined ? { env: input.env } : {}), | 423 | ...(input.env !== undefined ? { env: input.env } : {}), |
| ... | @@ -426,7 +429,7 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( | ... | @@ -426,7 +429,7 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( |
| 426 | : {}), | 429 | : {}), |
| 427 | }); | 430 | }); |
| 428 | 431 | ||
| 429 | const detectRepository: VcsDriver.VcsDriverShape["detectRepository"] = Effect.fn( | 432 | const detectRepository: VcsDriver.VcsDriver["Service"]["detectRepository"] = Effect.fn( |
| 430 | "detectRepository", | 433 | "detectRepository", |
| 431 | )(function* (cwd) { | 434 | )(function* (cwd) { |
| 432 | if (!(yield* isInsideWorkTree(cwd))) { | 435 | if (!(yield* isInsideWorkTree(cwd))) { |
| ... | @@ -452,7 +455,7 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( | ... | @@ -452,7 +455,7 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( |
| 452 | }; | 455 | }; |
| 453 | }); | 456 | }); |
| 454 | 457 | ||
| 455 | const listWorkspaceFiles: VcsDriver.VcsDriverShape["listWorkspaceFiles"] = (cwd) => | 458 | const listWorkspaceFiles: VcsDriver.VcsDriver["Service"]["listWorkspaceFiles"] = (cwd) => |
| 456 | gitCommand( | 459 | gitCommand( |
| 457 | vcsProcess, | 460 | vcsProcess, |
| 458 | "GitVcsDriver.listWorkspaceFiles", | 461 | "GitVcsDriver.listWorkspaceFiles", |
| ... | @@ -494,7 +497,7 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( | ... | @@ -494,7 +497,7 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( |
| 494 | ), | 497 | ), |
| 495 | ); | 498 | ); |
| 496 | 499 | ||
| 497 | const listRemotes: VcsDriver.VcsDriverShape["listRemotes"] = Effect.fn("listRemotes")( | 500 | const listRemotes: VcsDriver.VcsDriver["Service"]["listRemotes"] = Effect.fn("listRemotes")( |
| 498 | function* (cwd) { | 501 | function* (cwd) { |
| 499 | const result = yield* gitCommand( | 502 | const result = yield* gitCommand( |
| 500 | vcsProcess, | 503 | vcsProcess, |
| ... | @@ -540,7 +543,7 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( | ... | @@ -540,7 +543,7 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( |
| 540 | }, | 543 | }, |
| 541 | ); | 544 | ); |
| 542 | 545 | ||
| 543 | const filterIgnoredPaths: VcsDriver.VcsDriverShape["filterIgnoredPaths"] = Effect.fn( | 546 | const filterIgnoredPaths: VcsDriver.VcsDriver["Service"]["filterIgnoredPaths"] = Effect.fn( |
| 544 | "filterIgnoredPaths", | 547 | "filterIgnoredPaths", |
| 545 | )(function* (cwd, relativePaths) { | 548 | )(function* (cwd, relativePaths) { |
| 546 | if (relativePaths.length === 0) { | 549 | if (relativePaths.length === 0) { |
| ... | @@ -587,7 +590,7 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( | ... | @@ -587,7 +590,7 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( |
| 587 | return relativePaths.filter((relativePath) => !ignoredPaths.has(relativePath)); | 590 | return relativePaths.filter((relativePath) => !ignoredPaths.has(relativePath)); |
| 588 | }); | 591 | }); |
| 589 | 592 | ||
| 590 | const initRepository: VcsDriver.VcsDriverShape["initRepository"] = (input) => | 593 | const initRepository: VcsDriver.VcsDriver["Service"]["initRepository"] = (input) => |
| 591 | gitCommand(vcsProcess, "GitVcsDriver.initRepository", input.cwd, ["init"], { | 594 | gitCommand(vcsProcess, "GitVcsDriver.initRepository", input.cwd, ["init"], { |
| 592 | timeoutMs: 10_000, | 595 | timeoutMs: 10_000, |
| 593 | maxOutputBytes: 64 * 1024, | 596 | maxOutputBytes: 64 * 1024, |
| ... | @@ -844,7 +847,7 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( | ... | @@ -844,7 +847,7 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( |
| 844 | ), | 847 | ), |
| 845 | }; | 848 | }; |
| 846 | 849 | ||
| 847 | return VcsDriver.VcsDriver.of({ | 850 | return { |
| 848 | capabilities, | 851 | capabilities, |
| 849 | execute, | 852 | execute, |
| 850 | checkpoints, | 853 | checkpoints, |
| ... | @@ -854,18 +857,18 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( | ... | @@ -854,18 +857,18 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( |
| 854 | listRemotes, | 857 | listRemotes, |
| 855 | filterIgnoredPaths, | 858 | filterIgnoredPaths, |
| 856 | initRepository, | 859 | initRepository, |
| 857 | }); | 860 | }; |
| 858 | }); | 861 | }); |
| 859 | 862 | ||
| 860 | export const makeVcsDriver = Effect.fn("makeGitVcsDriver")(function* () { | 863 | export const makeVcsDriver = Effect.gen(function* () { |
| 861 | const driver = yield* makeVcsDriverShape(); | 864 | const driver = yield* makeVcsDriverShape(); |
| 862 | return VcsDriver.VcsDriver.of(driver); | 865 | return VcsDriver.VcsDriver.of(driver); |
| 863 | }); | 866 | }); |
| 864 | 867 | ||
| 865 | export const make = Effect.fn("makeGitVcsDriverService")(function* () { | 868 | export const make = Effect.gen(function* () { |
| 866 | const git = yield* GitVcsDriverCore.makeGitVcsDriverCore(); | 869 | const git = yield* makeGitVcsDriverCore(); |
| 867 | return GitVcsDriver.of(git); | 870 | return GitVcsDriver.of(git); |
| 868 | }); | 871 | }); |
| 869 | 872 | ||
| 870 | export const vcsLayer = Layer.effect(VcsDriver.VcsDriver, makeVcsDriver()); | 873 | export const vcsLayer = Layer.effect(VcsDriver.VcsDriver, makeVcsDriver); |
| 871 | export const layer = Layer.effect(GitVcsDriver, make()); | 874 | export const layer = Layer.effect(GitVcsDriver, make); |
apps/server/src/vcs/GitVcsDriverCore.ts+126-126| ... | @@ -655,7 +655,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* | ... | @@ -655,7 +655,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* |
| 655 | const { worktreesDir } = yield* ServerConfig; | 655 | const { worktreesDir } = yield* ServerConfig; |
| 656 | const crypto = yield* Crypto.Crypto; | 656 | const crypto = yield* Crypto.Crypto; |
| 657 | 657 | ||
| 658 | const executeRaw: GitVcsDriver.GitVcsDriverShape["execute"] = Effect.fnUntraced( | 658 | const executeRaw: GitVcsDriver.GitVcsDriver["Service"]["execute"] = Effect.fnUntraced( |
| 659 | function* (input) { | 659 | function* (input) { |
| 660 | const commandInput = { | 660 | const commandInput = { |
| 661 | ...input, | 661 | ...input, |
| ... | @@ -756,7 +756,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* | ... | @@ -756,7 +756,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* |
| 756 | }, | 756 | }, |
| 757 | ); | 757 | ); |
| 758 | 758 | ||
| 759 | const execute: GitVcsDriver.GitVcsDriverShape["execute"] = (input) => | 759 | const execute: GitVcsDriver.GitVcsDriver["Service"]["execute"] = (input) => |
| 760 | executeRaw(input).pipe( | 760 | executeRaw(input).pipe( |
| 761 | withMetrics({ | 761 | withMetrics({ |
| 762 | counter: gitCommandsTotal, | 762 | counter: gitCommandsTotal, |
| ... | @@ -1059,38 +1059,38 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* | ... | @@ -1059,38 +1059,38 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* |
| 1059 | return yield* resolvePrimaryRemoteName(cwd).pipe(Effect.orElseSucceed(() => null)); | 1059 | return yield* resolvePrimaryRemoteName(cwd).pipe(Effect.orElseSucceed(() => null)); |
| 1060 | }); | 1060 | }); |
| 1061 | 1061 | ||
| 1062 | const ensureRemote: GitVcsDriver.GitVcsDriverShape["ensureRemote"] = Effect.fn("ensureRemote")( | 1062 | const ensureRemote: GitVcsDriver.GitVcsDriver["Service"]["ensureRemote"] = Effect.fn( |
| 1063 | function* (input) { | 1063 | "ensureRemote", |
| 1064 | const preferredName = sanitizeRemoteName(input.preferredName); | 1064 | )(function* (input) { |
| 1065 | const normalizedTargetUrl = normalizeRemoteUrl(input.url); | 1065 | const preferredName = sanitizeRemoteName(input.preferredName); |
| 1066 | const remoteFetchUrls = yield* runGitStdout( | 1066 | const normalizedTargetUrl = normalizeRemoteUrl(input.url); |
| 1067 | "GitVcsDriver.ensureRemote.listRemoteUrls", | 1067 | const remoteFetchUrls = yield* runGitStdout( |
| 1068 | input.cwd, | 1068 | "GitVcsDriver.ensureRemote.listRemoteUrls", |
| 1069 | ["remote", "-v"], | 1069 | input.cwd, |
| 1070 | ).pipe(Effect.map((stdout) => parseRemoteFetchUrls(stdout))); | 1070 | ["remote", "-v"], |
| 1071 | ).pipe(Effect.map((stdout) => parseRemoteFetchUrls(stdout))); | ||
| 1071 | 1072 | ||
| 1072 | for (const [remoteName, remoteUrl] of remoteFetchUrls.entries()) { | 1073 | for (const [remoteName, remoteUrl] of remoteFetchUrls.entries()) { |
| 1073 | if (normalizeRemoteUrl(remoteUrl) === normalizedTargetUrl) { | 1074 | if (normalizeRemoteUrl(remoteUrl) === normalizedTargetUrl) { |
| 1074 | return remoteName; | 1075 | return remoteName; |
| 1075 | } | ||
| 1076 | } | 1076 | } |
| 1077 | } | ||
| 1077 | 1078 | ||
| 1078 | let remoteName = preferredName; | 1079 | let remoteName = preferredName; |
| 1079 | let suffix = 1; | 1080 | let suffix = 1; |
| 1080 | while (remoteFetchUrls.has(remoteName)) { | 1081 | while (remoteFetchUrls.has(remoteName)) { |
| 1081 | remoteName = `${preferredName}-${suffix}`; | 1082 | remoteName = `${preferredName}-${suffix}`; |
| 1082 | suffix += 1; | 1083 | suffix += 1; |
| 1083 | } | 1084 | } |
| 1084 | 1085 | ||
| 1085 | yield* runGit("GitVcsDriver.ensureRemote.add", input.cwd, [ | 1086 | yield* runGit("GitVcsDriver.ensureRemote.add", input.cwd, [ |
| 1086 | "remote", | 1087 | "remote", |
| 1087 | "add", | 1088 | "add", |
| 1088 | remoteName, | 1089 | remoteName, |
| 1089 | input.url, | 1090 | input.url, |
| 1090 | ]); | 1091 | ]); |
| 1091 | return remoteName; | 1092 | return remoteName; |
| 1092 | }, | 1093 | }); |
| 1093 | ); | ||
| 1094 | 1094 | ||
| 1095 | const resolveBaseBranchForNoUpstream = Effect.fn("resolveBaseBranchForNoUpstream")(function* ( | 1095 | const resolveBaseBranchForNoUpstream = Effect.fn("resolveBaseBranchForNoUpstream")(function* ( |
| 1096 | cwd: string, | 1096 | cwd: string, |
| ... | @@ -1426,35 +1426,34 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* | ... | @@ -1426,35 +1426,34 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* |
| 1426 | }; | 1426 | }; |
| 1427 | }); | 1427 | }); |
| 1428 | 1428 | ||
| 1429 | const statusDetailsLocal: GitVcsDriver.GitVcsDriverShape["statusDetailsLocal"] = Effect.fn( | 1429 | const statusDetailsLocal: GitVcsDriver.GitVcsDriver["Service"]["statusDetailsLocal"] = Effect.fn( |
| 1430 | "statusDetailsLocal", | 1430 | "statusDetailsLocal", |
| 1431 | )(function* (cwd) { | 1431 | )(function* (cwd) { |
| 1432 | return yield* readStatusDetailsLocal(cwd); | 1432 | return yield* readStatusDetailsLocal(cwd); |
| 1433 | }); | 1433 | }); |
| 1434 | 1434 | ||
| 1435 | const statusDetails: GitVcsDriver.GitVcsDriverShape["statusDetails"] = Effect.fn("statusDetails")( | 1435 | const statusDetails: GitVcsDriver.GitVcsDriver["Service"]["statusDetails"] = Effect.fn( |
| 1436 | function* (cwd) { | 1436 | "statusDetails", |
| 1437 | yield* refreshStatusUpstreamIfStale(cwd).pipe( | 1437 | )(function* (cwd) { |
| 1438 | Effect.catchIf(isMissingGitCwdError, () => Effect.void), | 1438 | yield* refreshStatusUpstreamIfStale(cwd).pipe( |
| 1439 | Effect.ignoreCause({ log: true }), | 1439 | Effect.catchIf(isMissingGitCwdError, () => Effect.void), |
| 1440 | ); | 1440 | Effect.ignoreCause({ log: true }), |
| 1441 | return yield* readStatusDetailsLocal(cwd); | 1441 | ); |
| 1442 | }, | 1442 | return yield* readStatusDetailsLocal(cwd); |
| 1443 | ); | ||
| 1444 | |||
| 1445 | const statusDetailsRemote: GitVcsDriver.GitVcsDriverShape["statusDetailsRemote"] = Effect.fn( | ||
| 1446 | "statusDetailsRemote", | ||
| 1447 | )(function* (cwd, options) { | ||
| 1448 | if (options?.refreshUpstream !== false) { | ||
| 1449 | yield* refreshStatusUpstreamIfStale(cwd).pipe( | ||
| 1450 | Effect.catchIf(isMissingGitCwdError, () => Effect.void), | ||
| 1451 | Effect.ignoreCause({ log: true }), | ||
| 1452 | ); | ||
| 1453 | } | ||
| 1454 | return yield* readStatusDetailsRemote(cwd); | ||
| 1455 | }); | 1443 | }); |
| 1456 | 1444 | ||
| 1457 | const status: GitVcsDriver.GitVcsDriverShape["status"] = (input) => | 1445 | const statusDetailsRemote: GitVcsDriver.GitVcsDriver["Service"]["statusDetailsRemote"] = |
| 1446 | Effect.fn("statusDetailsRemote")(function* (cwd, options) { | ||
| 1447 | if (options?.refreshUpstream !== false) { | ||
| 1448 | yield* refreshStatusUpstreamIfStale(cwd).pipe( | ||
| 1449 | Effect.catchIf(isMissingGitCwdError, () => Effect.void), | ||
| 1450 | Effect.ignoreCause({ log: true }), | ||
| 1451 | ); | ||
| 1452 | } | ||
| 1453 | return yield* readStatusDetailsRemote(cwd); | ||
| 1454 | }); | ||
| 1455 | |||
| 1456 | const status: GitVcsDriver.GitVcsDriver["Service"]["status"] = (input) => | ||
| 1458 | statusDetails(input.cwd).pipe( | 1457 | statusDetails(input.cwd).pipe( |
| 1459 | Effect.map((details) => ({ | 1458 | Effect.map((details) => ({ |
| 1460 | isRepo: details.isRepo, | 1459 | isRepo: details.isRepo, |
| ... | @@ -1471,49 +1470,48 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* | ... | @@ -1471,49 +1470,48 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* |
| 1471 | })), | 1470 | })), |
| 1472 | ); | 1471 | ); |
| 1473 | 1472 | ||
| 1474 | const prepareCommitContext: GitVcsDriver.GitVcsDriverShape["prepareCommitContext"] = Effect.fn( | 1473 | const prepareCommitContext: GitVcsDriver.GitVcsDriver["Service"]["prepareCommitContext"] = |
| 1475 | "prepareCommitContext", | 1474 | Effect.fn("prepareCommitContext")(function* (cwd, filePaths) { |
| 1476 | )(function* (cwd, filePaths) { | 1475 | if (filePaths && filePaths.length > 0) { |
| 1477 | if (filePaths && filePaths.length > 0) { | 1476 | yield* runGit("GitVcsDriver.prepareCommitContext.reset", cwd, ["reset"]).pipe( |
| 1478 | yield* runGit("GitVcsDriver.prepareCommitContext.reset", cwd, ["reset"]).pipe( | 1477 | Effect.catch(() => Effect.void), |
| 1479 | Effect.catch(() => Effect.void), | 1478 | ); |
| 1480 | ); | 1479 | yield* runGit("GitVcsDriver.prepareCommitContext.addSelected", cwd, [ |
| 1481 | yield* runGit("GitVcsDriver.prepareCommitContext.addSelected", cwd, [ | 1480 | "add", |
| 1482 | "add", | 1481 | "-A", |
| 1483 | "-A", | 1482 | "--", |
| 1484 | "--", | 1483 | ...filePaths, |
| 1485 | ...filePaths, | 1484 | ]); |
| 1486 | ]); | 1485 | } else { |
| 1487 | } else { | 1486 | yield* runGit("GitVcsDriver.prepareCommitContext.addAll", cwd, ["add", "-A"]); |
| 1488 | yield* runGit("GitVcsDriver.prepareCommitContext.addAll", cwd, ["add", "-A"]); | 1487 | } |
| 1489 | } | ||
| 1490 | 1488 | ||
| 1491 | const stagedSummary = yield* runGitStdout( | 1489 | const stagedSummary = yield* runGitStdout( |
| 1492 | "GitVcsDriver.prepareCommitContext.stagedSummary", | 1490 | "GitVcsDriver.prepareCommitContext.stagedSummary", |
| 1493 | cwd, | 1491 | cwd, |
| 1494 | ["diff", "--cached", "--name-status"], | 1492 | ["diff", "--cached", "--name-status"], |
| 1495 | ).pipe(Effect.map((stdout) => stdout.trim())); | 1493 | ).pipe(Effect.map((stdout) => stdout.trim())); |
| 1496 | if (stagedSummary.length === 0) { | 1494 | if (stagedSummary.length === 0) { |
| 1497 | return null; | 1495 | return null; |
| 1498 | } | 1496 | } |
| 1499 | 1497 | ||
| 1500 | const stagedPatch = yield* runGitStdoutWithOptions( | 1498 | const stagedPatch = yield* runGitStdoutWithOptions( |
| 1501 | "GitVcsDriver.prepareCommitContext.stagedPatch", | 1499 | "GitVcsDriver.prepareCommitContext.stagedPatch", |
| 1502 | cwd, | 1500 | cwd, |
| 1503 | ["diff", "--no-ext-diff", "--cached", "--patch", "--minimal"], | 1501 | ["diff", "--no-ext-diff", "--cached", "--patch", "--minimal"], |
| 1504 | { | 1502 | { |
| 1505 | maxOutputBytes: PREPARED_COMMIT_PATCH_MAX_OUTPUT_BYTES, | 1503 | maxOutputBytes: PREPARED_COMMIT_PATCH_MAX_OUTPUT_BYTES, |
| 1506 | appendTruncationMarker: true, | 1504 | appendTruncationMarker: true, |
| 1507 | }, | 1505 | }, |
| 1508 | ); | 1506 | ); |
| 1509 | 1507 | ||
| 1510 | return { | 1508 | return { |
| 1511 | stagedSummary, | 1509 | stagedSummary, |
| 1512 | stagedPatch, | 1510 | stagedPatch, |
| 1513 | }; | 1511 | }; |
| 1514 | }); | 1512 | }); |
| 1515 | 1513 | ||
| 1516 | const commit: GitVcsDriver.GitVcsDriverShape["commit"] = Effect.fn("commit")(function* ( | 1514 | const commit: GitVcsDriver.GitVcsDriver["Service"]["commit"] = Effect.fn("commit")(function* ( |
| 1517 | cwd, | 1515 | cwd, |
| 1518 | subject, | 1516 | subject, |
| 1519 | body, | 1517 | body, |
| ... | @@ -1546,7 +1544,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* | ... | @@ -1546,7 +1544,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* |
| 1546 | return { commitSha }; | 1544 | return { commitSha }; |
| 1547 | }); | 1545 | }); |
| 1548 | 1546 | ||
| 1549 | const pushCurrentBranch: GitVcsDriver.GitVcsDriverShape["pushCurrentBranch"] = Effect.fn( | 1547 | const pushCurrentBranch: GitVcsDriver.GitVcsDriver["Service"]["pushCurrentBranch"] = Effect.fn( |
| 1550 | "pushCurrentBranch", | 1548 | "pushCurrentBranch", |
| 1551 | )(function* (cwd, fallbackBranch, options) { | 1549 | )(function* (cwd, fallbackBranch, options) { |
| 1552 | const details = yield* statusDetails(cwd); | 1550 | const details = yield* statusDetails(cwd); |
| ... | @@ -1664,7 +1662,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* | ... | @@ -1664,7 +1662,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* |
| 1664 | }; | 1662 | }; |
| 1665 | }); | 1663 | }); |
| 1666 | 1664 | ||
| 1667 | const pullCurrentBranch: GitVcsDriver.GitVcsDriverShape["pullCurrentBranch"] = Effect.fn( | 1665 | const pullCurrentBranch: GitVcsDriver.GitVcsDriver["Service"]["pullCurrentBranch"] = Effect.fn( |
| 1668 | "pullCurrentBranch", | 1666 | "pullCurrentBranch", |
| 1669 | )(function* (cwd) { | 1667 | )(function* (cwd) { |
| 1670 | const details = yield* statusDetails(cwd); | 1668 | const details = yield* statusDetails(cwd); |
| ... | @@ -1710,7 +1708,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* | ... | @@ -1710,7 +1708,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* |
| 1710 | }; | 1708 | }; |
| 1711 | }); | 1709 | }); |
| 1712 | 1710 | ||
| 1713 | const readRangeContext: GitVcsDriver.GitVcsDriverShape["readRangeContext"] = Effect.fn( | 1711 | const readRangeContext: GitVcsDriver.GitVcsDriver["Service"]["readRangeContext"] = Effect.fn( |
| 1714 | "readRangeContext", | 1712 | "readRangeContext", |
| 1715 | )(function* (cwd, baseRef) { | 1713 | )(function* (cwd, baseRef) { |
| 1716 | const range = `${baseRef}..HEAD`; | 1714 | const range = `${baseRef}..HEAD`; |
| ... | @@ -1921,13 +1919,13 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* | ... | @@ -1921,13 +1919,13 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* |
| 1921 | }; | 1919 | }; |
| 1922 | }); | 1920 | }); |
| 1923 | 1921 | ||
| 1924 | const readConfigValue: GitVcsDriver.GitVcsDriverShape["readConfigValue"] = (cwd, key) => | 1922 | const readConfigValue: GitVcsDriver.GitVcsDriver["Service"]["readConfigValue"] = (cwd, key) => |
| 1925 | runGitStdout("GitVcsDriver.readConfigValue", cwd, ["config", "--get", key], true).pipe( | 1923 | runGitStdout("GitVcsDriver.readConfigValue", cwd, ["config", "--get", key], true).pipe( |
| 1926 | Effect.map((stdout) => stdout.trim()), | 1924 | Effect.map((stdout) => stdout.trim()), |
| 1927 | Effect.map((trimmed) => (trimmed.length > 0 ? trimmed : null)), | 1925 | Effect.map((trimmed) => (trimmed.length > 0 ? trimmed : null)), |
| 1928 | ); | 1926 | ); |
| 1929 | 1927 | ||
| 1930 | const listRefs: GitVcsDriver.GitVcsDriverShape["listRefs"] = Effect.fn("listRefs")( | 1928 | const listRefs: GitVcsDriver.GitVcsDriver["Service"]["listRefs"] = Effect.fn("listRefs")( |
| 1931 | function* (input) { | 1929 | function* (input) { |
| 1932 | const branchRecencyPromise = readBranchRecency(input.cwd).pipe( | 1930 | const branchRecencyPromise = readBranchRecency(input.cwd).pipe( |
| 1933 | Effect.orElseSucceed(() => new Map<string, number>()), | 1931 | Effect.orElseSucceed(() => new Map<string, number>()), |
| ... | @@ -2165,7 +2163,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* | ... | @@ -2165,7 +2163,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* |
| 2165 | }, | 2163 | }, |
| 2166 | ); | 2164 | ); |
| 2167 | 2165 | ||
| 2168 | const createWorktree: GitVcsDriver.GitVcsDriverShape["createWorktree"] = Effect.fn( | 2166 | const createWorktree: GitVcsDriver.GitVcsDriver["Service"]["createWorktree"] = Effect.fn( |
| 2169 | "createWorktree", | 2167 | "createWorktree", |
| 2170 | )(function* (input) { | 2168 | )(function* (input) { |
| 2171 | const targetBranch = input.newRefName ?? input.refName; | 2169 | const targetBranch = input.newRefName ?? input.refName; |
| ... | @@ -2188,7 +2186,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* | ... | @@ -2188,7 +2186,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* |
| 2188 | }; | 2186 | }; |
| 2189 | }); | 2187 | }); |
| 2190 | 2188 | ||
| 2191 | const fetchPullRequestBranch: GitVcsDriver.GitVcsDriverShape["fetchPullRequestBranch"] = | 2189 | const fetchPullRequestBranch: GitVcsDriver.GitVcsDriver["Service"]["fetchPullRequestBranch"] = |
| 2192 | Effect.fn("fetchPullRequestBranch")(function* (input) { | 2190 | Effect.fn("fetchPullRequestBranch")(function* (input) { |
| 2193 | const remoteName = yield* resolvePrimaryRemoteName(input.cwd); | 2191 | const remoteName = yield* resolvePrimaryRemoteName(input.cwd); |
| 2194 | yield* executeGit( | 2192 | yield* executeGit( |
| ... | @@ -2207,7 +2205,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* | ... | @@ -2207,7 +2205,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* |
| 2207 | ); | 2205 | ); |
| 2208 | }); | 2206 | }); |
| 2209 | 2207 | ||
| 2210 | const fetchRemote: GitVcsDriver.GitVcsDriverShape["fetchRemote"] = Effect.fn("fetchRemote")( | 2208 | const fetchRemote: GitVcsDriver.GitVcsDriver["Service"]["fetchRemote"] = Effect.fn("fetchRemote")( |
| 2211 | function* (input) { | 2209 | function* (input) { |
| 2212 | yield* executeGit( | 2210 | yield* executeGit( |
| 2213 | "GitVcsDriver.fetchRemote", | 2211 | "GitVcsDriver.fetchRemote", |
| ... | @@ -2221,7 +2219,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* | ... | @@ -2221,7 +2219,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* |
| 2221 | }, | 2219 | }, |
| 2222 | ); | 2220 | ); |
| 2223 | 2221 | ||
| 2224 | const resolveRemoteTrackingCommit: GitVcsDriver.GitVcsDriverShape["resolveRemoteTrackingCommit"] = | 2222 | const resolveRemoteTrackingCommit: GitVcsDriver.GitVcsDriver["Service"]["resolveRemoteTrackingCommit"] = |
| 2225 | Effect.fn("resolveRemoteTrackingCommit")(function* (input) { | 2223 | Effect.fn("resolveRemoteTrackingCommit")(function* (input) { |
| 2226 | const remoteNames = yield* listRemoteNames(input.cwd); | 2224 | const remoteNames = yield* listRemoteNames(input.cwd); |
| 2227 | const parsedRemoteRef = parseRemoteRefWithRemoteNames( | 2225 | const parsedRemoteRef = parseRemoteRefWithRemoteNames( |
| ... | @@ -2239,7 +2237,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* | ... | @@ -2239,7 +2237,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* |
| 2239 | return { commitSha, remoteRefName }; | 2237 | return { commitSha, remoteRefName }; |
| 2240 | }); | 2238 | }); |
| 2241 | 2239 | ||
| 2242 | const fetchRemoteBranch: GitVcsDriver.GitVcsDriverShape["fetchRemoteBranch"] = Effect.fn( | 2240 | const fetchRemoteBranch: GitVcsDriver.GitVcsDriver["Service"]["fetchRemoteBranch"] = Effect.fn( |
| 2243 | "fetchRemoteBranch", | 2241 | "fetchRemoteBranch", |
| 2244 | )(function* (input) { | 2242 | )(function* (input) { |
| 2245 | yield* runGit("GitVcsDriver.fetchRemoteBranch.fetch", input.cwd, [ | 2243 | yield* runGit("GitVcsDriver.fetchRemoteBranch.fetch", input.cwd, [ |
| ... | @@ -2261,7 +2259,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* | ... | @@ -2261,7 +2259,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* |
| 2261 | ); | 2259 | ); |
| 2262 | }); | 2260 | }); |
| 2263 | 2261 | ||
| 2264 | const fetchRemoteTrackingBranch: GitVcsDriver.GitVcsDriverShape["fetchRemoteTrackingBranch"] = | 2262 | const fetchRemoteTrackingBranch: GitVcsDriver.GitVcsDriver["Service"]["fetchRemoteTrackingBranch"] = |
| 2265 | Effect.fn("fetchRemoteTrackingBranch")(function* (input) { | 2263 | Effect.fn("fetchRemoteTrackingBranch")(function* (input) { |
| 2266 | yield* runGit("GitVcsDriver.fetchRemoteTrackingBranch", input.cwd, [ | 2264 | yield* runGit("GitVcsDriver.fetchRemoteTrackingBranch", input.cwd, [ |
| 2267 | "fetch", | 2265 | "fetch", |
| ... | @@ -2272,7 +2270,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* | ... | @@ -2272,7 +2270,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* |
| 2272 | ]); | 2270 | ]); |
| 2273 | }); | 2271 | }); |
| 2274 | 2272 | ||
| 2275 | const setBranchUpstream: GitVcsDriver.GitVcsDriverShape["setBranchUpstream"] = (input) => | 2273 | const setBranchUpstream: GitVcsDriver.GitVcsDriver["Service"]["setBranchUpstream"] = (input) => |
| 2276 | runGit("GitVcsDriver.setBranchUpstream", input.cwd, [ | 2274 | runGit("GitVcsDriver.setBranchUpstream", input.cwd, [ |
| 2277 | "branch", | 2275 | "branch", |
| 2278 | "--set-upstream-to", | 2276 | "--set-upstream-to", |
| ... | @@ -2280,7 +2278,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* | ... | @@ -2280,7 +2278,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* |
| 2280 | input.branch, | 2278 | input.branch, |
| 2281 | ]); | 2279 | ]); |
| 2282 | 2280 | ||
| 2283 | const removeWorktree: GitVcsDriver.GitVcsDriverShape["removeWorktree"] = Effect.fn( | 2281 | const removeWorktree: GitVcsDriver.GitVcsDriver["Service"]["removeWorktree"] = Effect.fn( |
| 2284 | "removeWorktree", | 2282 | "removeWorktree", |
| 2285 | )(function* (input) { | 2283 | )(function* (input) { |
| 2286 | const args = ["worktree", "remove"]; | 2284 | const args = ["worktree", "remove"]; |
| ... | @@ -2304,28 +2302,28 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* | ... | @@ -2304,28 +2302,28 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* |
| 2304 | ); | 2302 | ); |
| 2305 | }); | 2303 | }); |
| 2306 | 2304 | ||
| 2307 | const renameBranch: GitVcsDriver.GitVcsDriverShape["renameBranch"] = Effect.fn("renameBranch")( | 2305 | const renameBranch: GitVcsDriver.GitVcsDriver["Service"]["renameBranch"] = Effect.fn( |
| 2308 | function* (input) { | 2306 | "renameBranch", |
| 2309 | if (input.oldBranch === input.newBranch) { | 2307 | )(function* (input) { |
| 2310 | return { branch: input.newBranch }; | 2308 | if (input.oldBranch === input.newBranch) { |
| 2311 | } | 2309 | return { branch: input.newBranch }; |
| 2312 | const targetBranch = yield* resolveAvailableBranchName(input.cwd, input.newBranch); | 2310 | } |
| 2311 | const targetBranch = yield* resolveAvailableBranchName(input.cwd, input.newBranch); | ||
| 2313 | 2312 | ||
| 2314 | yield* executeGit( | 2313 | yield* executeGit( |
| 2315 | "GitVcsDriver.renameBranch", | 2314 | "GitVcsDriver.renameBranch", |
| 2316 | input.cwd, | 2315 | input.cwd, |
| 2317 | ["branch", "-m", "--", input.oldBranch, targetBranch], | 2316 | ["branch", "-m", "--", input.oldBranch, targetBranch], |
| 2318 | { | 2317 | { |
| 2319 | timeoutMs: 10_000, | 2318 | timeoutMs: 10_000, |
| 2320 | fallbackErrorMessage: "git branch rename failed", | 2319 | fallbackErrorMessage: "git branch rename failed", |
| 2321 | }, | 2320 | }, |
| 2322 | ); | 2321 | ); |
| 2323 | 2322 | ||
| 2324 | return { branch: targetBranch }; | 2323 | return { branch: targetBranch }; |
| 2325 | }, | 2324 | }); |
| 2326 | ); | ||
| 2327 | 2325 | ||
| 2328 | const switchRef: GitVcsDriver.GitVcsDriverShape["switchRef"] = Effect.fn("switchRef")( | 2326 | const switchRef: GitVcsDriver.GitVcsDriver["Service"]["switchRef"] = Effect.fn("switchRef")( |
| 2329 | function* (input) { | 2327 | function* (input) { |
| 2330 | const [localInputExists, remoteExists] = yield* Effect.all( | 2328 | const [localInputExists, remoteExists] = yield* Effect.all( |
| 2331 | [ | 2329 | [ |
| ... | @@ -2407,7 +2405,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* | ... | @@ -2407,7 +2405,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* |
| 2407 | }, | 2405 | }, |
| 2408 | ); | 2406 | ); |
| 2409 | 2407 | ||
| 2410 | const createRef: GitVcsDriver.GitVcsDriverShape["createRef"] = Effect.fn("createRef")( | 2408 | const createRef: GitVcsDriver.GitVcsDriver["Service"]["createRef"] = Effect.fn("createRef")( |
| 2411 | function* (input) { | 2409 | function* (input) { |
| 2412 | yield* executeGit("GitVcsDriver.createRef", input.cwd, ["branch", input.refName], { | 2410 | yield* executeGit("GitVcsDriver.createRef", input.cwd, ["branch", input.refName], { |
| 2413 | timeoutMs: 10_000, | 2411 | timeoutMs: 10_000, |
| ... | @@ -2421,13 +2419,15 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* | ... | @@ -2421,13 +2419,15 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* |
| 2421 | }, | 2419 | }, |
| 2422 | ); | 2420 | ); |
| 2423 | 2421 | ||
| 2424 | const initRepo: GitVcsDriver.GitVcsDriverShape["initRepo"] = (input) => | 2422 | const initRepo: GitVcsDriver.GitVcsDriver["Service"]["initRepo"] = (input) => |
| 2425 | executeGit("GitVcsDriver.initRepo", input.cwd, ["init"], { | 2423 | executeGit("GitVcsDriver.initRepo", input.cwd, ["init"], { |
| 2426 | timeoutMs: 10_000, | 2424 | timeoutMs: 10_000, |
| 2427 | fallbackErrorMessage: "git init failed", | 2425 | fallbackErrorMessage: "git init failed", |
| 2428 | }).pipe(Effect.asVoid); | 2426 | }).pipe(Effect.asVoid); |
| 2429 | 2427 | ||
| 2430 | const listLocalBranchNames: GitVcsDriver.GitVcsDriverShape["listLocalBranchNames"] = (cwd) => | 2428 | const listLocalBranchNames: GitVcsDriver.GitVcsDriver["Service"]["listLocalBranchNames"] = ( |
| 2429 | cwd, | ||
| 2430 | ) => | ||
| 2431 | runGitStdout("GitVcsDriver.listLocalBranchNames", cwd, [ | 2431 | runGitStdout("GitVcsDriver.listLocalBranchNames", cwd, [ |
| 2432 | "branch", | 2432 | "branch", |
| 2433 | "--list", | 2433 | "--list", |
apps/server/src/vcs/VcsDriver.ts+26-23| ... | @@ -52,26 +52,29 @@ export interface VcsCheckpointOps { | ... | @@ -52,26 +52,29 @@ export interface VcsCheckpointOps { |
| 52 | ) => Effect.Effect<void, VcsError>; | 52 | ) => Effect.Effect<void, VcsError>; |
| 53 | } | 53 | } |
| 54 | 54 | ||
| 55 | export interface VcsDriverShape { | 55 | export class VcsDriver extends Context.Service< |
| 56 | readonly capabilities: VcsDriverCapabilities; | 56 | VcsDriver, |
| 57 | readonly execute: ( | 57 | { |
| 58 | input: Omit<VcsProcess.VcsProcessInput, "command">, | 58 | readonly capabilities: VcsDriverCapabilities; |
| 59 | ) => Effect.Effect<VcsProcess.VcsProcessOutput, VcsError>; | 59 | readonly execute: ( |
| 60 | readonly checkpoints?: VcsCheckpointOps; | 60 | input: Omit<VcsProcess.VcsProcessInput, "command">, |
| 61 | readonly detectRepository: (cwd: string) => Effect.Effect<VcsRepositoryIdentity | null, VcsError>; | 61 | ) => Effect.Effect<VcsProcess.VcsProcessOutput, VcsError>; |
| 62 | readonly isInsideWorkTree: (cwd: string) => Effect.Effect<boolean, VcsError>; | 62 | readonly checkpoints?: VcsCheckpointOps; |
| 63 | readonly listWorkspaceFiles: ( | 63 | readonly detectRepository: ( |
| 64 | cwd: string, | 64 | cwd: string, |
| 65 | ) => Effect.Effect<VcsListWorkspaceFilesResult, VcsError>; | 65 | ) => Effect.Effect<VcsRepositoryIdentity | null, VcsError>; |
| 66 | readonly listRemotes: (cwd: string) => Effect.Effect<VcsListRemotesResult, VcsError>; | 66 | readonly isInsideWorkTree: (cwd: string) => Effect.Effect<boolean, VcsError>; |
| 67 | readonly filterIgnoredPaths: ( | 67 | readonly listWorkspaceFiles: ( |
| 68 | cwd: string, | 68 | cwd: string, |
| 69 | relativePaths: ReadonlyArray<string>, | 69 | ) => Effect.Effect<VcsListWorkspaceFilesResult, VcsError>; |
| 70 | ) => Effect.Effect<ReadonlyArray<string>, VcsError>; | 70 | readonly listRemotes: (cwd: string) => Effect.Effect<VcsListRemotesResult, VcsError>; |
| 71 | readonly initRepository: (input: VcsInitInput) => Effect.Effect<void, VcsError>; | 71 | readonly filterIgnoredPaths: ( |
| 72 | readonly getDiffPreview?: ( | 72 | cwd: string, |
| 73 | input: ReviewDiffPreviewInput, | 73 | relativePaths: ReadonlyArray<string>, |
| 74 | ) => Effect.Effect<ReviewDiffPreviewResult, VcsError>; | 74 | ) => Effect.Effect<ReadonlyArray<string>, VcsError>; |
| 75 | } | 75 | readonly initRepository: (input: VcsInitInput) => Effect.Effect<void, VcsError>; |
| 76 | 76 | readonly getDiffPreview?: ( | |
| 77 | export class VcsDriver extends Context.Service<VcsDriver, VcsDriverShape>()("t3/vcs/VcsDriver") {} | 77 | input: ReviewDiffPreviewInput, |
| 78 | ) => Effect.Effect<ReviewDiffPreviewResult, VcsError>; | ||
| 79 | } | ||
| 80 | >()("t3/vcs/VcsDriver") {} |
apps/server/src/vcs/VcsDriverRegistry.test.ts+2-2| ... | @@ -21,7 +21,7 @@ const normalizeGitArgs = (args: ReadonlyArray<string>): ReadonlyArray<string> => | ... | @@ -21,7 +21,7 @@ const normalizeGitArgs = (args: ReadonlyArray<string>): ReadonlyArray<string> => |
| 21 | 21 | ||
| 22 | describe("VcsDriverRegistry", () => { | 22 | describe("VcsDriverRegistry", () => { |
| 23 | it.effect("routes directly by VCS driver kind for non-repository workflows", () => { | 23 | it.effect("routes directly by VCS driver kind for non-repository workflows", () => { |
| 24 | const layer = Layer.effect(VcsDriverRegistry.VcsDriverRegistry, VcsDriverRegistry.make()).pipe( | 24 | const layer = Layer.effect(VcsDriverRegistry.VcsDriverRegistry, VcsDriverRegistry.make).pipe( |
| 25 | Layer.provide(NodeServices.layer), | 25 | Layer.provide(NodeServices.layer), |
| 26 | Layer.provide( | 26 | Layer.provide( |
| 27 | Layer.mock(VcsProjectConfig.VcsProjectConfig)({ | 27 | Layer.mock(VcsProjectConfig.VcsProjectConfig)({ |
| ... | @@ -45,7 +45,7 @@ describe("VcsDriverRegistry", () => { | ... | @@ -45,7 +45,7 @@ describe("VcsDriverRegistry", () => { |
| 45 | 45 | ||
| 46 | it.effect("caches repository detection for repeated resolves in the same cwd and kind", () => { | 46 | it.effect("caches repository detection for repeated resolves in the same cwd and kind", () => { |
| 47 | const calls: VcsProcess.VcsProcessInput[] = []; | 47 | const calls: VcsProcess.VcsProcessInput[] = []; |
| 48 | const layer = Layer.effect(VcsDriverRegistry.VcsDriverRegistry, VcsDriverRegistry.make()).pipe( | 48 | const layer = Layer.effect(VcsDriverRegistry.VcsDriverRegistry, VcsDriverRegistry.make).pipe( |
| 49 | Layer.provide(NodeServices.layer), | 49 | Layer.provide(NodeServices.layer), |
| 50 | Layer.provide( | 50 | Layer.provide( |
| 51 | Layer.mock(VcsProjectConfig.VcsProjectConfig)({ | 51 | Layer.mock(VcsProjectConfig.VcsProjectConfig)({ |
apps/server/src/vcs/VcsDriverRegistry.ts+19-20| ... | @@ -22,20 +22,19 @@ export interface VcsDriverResolveInput { | ... | @@ -22,20 +22,19 @@ export interface VcsDriverResolveInput { |
| 22 | export interface VcsDriverHandle { | 22 | export interface VcsDriverHandle { |
| 23 | readonly kind: VcsDriverKind; | 23 | readonly kind: VcsDriverKind; |
| 24 | readonly repository: VcsRepositoryIdentity; | 24 | readonly repository: VcsRepositoryIdentity; |
| 25 | readonly driver: VcsDriver.VcsDriverShape; | 25 | readonly driver: VcsDriver.VcsDriver["Service"]; |
| 26 | } | 26 | } |
| 27 | 27 | ||
| 28 | export interface VcsDriverRegistryShape { | 28 | export class VcsDriverRegistry extends Context.Service< |
| 29 | readonly get: (kind: VcsDriverKind) => Effect.Effect<VcsDriver.VcsDriverShape, VcsError>; | 29 | VcsDriverRegistry, |
| 30 | readonly detect: ( | 30 | { |
| 31 | input: VcsDriverResolveInput, | 31 | readonly get: (kind: VcsDriverKind) => Effect.Effect<VcsDriver.VcsDriver["Service"], VcsError>; |
| 32 | ) => Effect.Effect<VcsDriverHandle | null, VcsError>; | 32 | readonly detect: ( |
| 33 | readonly resolve: (input: VcsDriverResolveInput) => Effect.Effect<VcsDriverHandle, VcsError>; | 33 | input: VcsDriverResolveInput, |
| 34 | } | 34 | ) => Effect.Effect<VcsDriverHandle | null, VcsError>; |
| 35 | 35 | readonly resolve: (input: VcsDriverResolveInput) => Effect.Effect<VcsDriverHandle, VcsError>; | |
| 36 | export class VcsDriverRegistry extends Context.Service<VcsDriverRegistry, VcsDriverRegistryShape>()( | 36 | } |
| 37 | "t3/vcs/VcsDriverRegistry", | 37 | >()("t3/vcs/VcsDriverRegistry") {} |
| 38 | ) {} | ||
| 39 | 38 | ||
| 40 | const unsupported = (operation: string, kind: VcsDriverKind, detail: string) => | 39 | const unsupported = (operation: string, kind: VcsDriverKind, detail: string) => |
| 41 | new VcsUnsupportedOperationError({ | 40 | new VcsUnsupportedOperationError({ |
| ... | @@ -68,14 +67,14 @@ function parseDetectionCacheKey(key: string): { | ... | @@ -68,14 +67,14 @@ function parseDetectionCacheKey(key: string): { |
| 68 | }; | 67 | }; |
| 69 | } | 68 | } |
| 70 | 69 | ||
| 71 | export const make = Effect.fn("makeVcsDriverRegistry")(function* () { | 70 | export const make = Effect.gen(function* () { |
| 72 | const projectConfig = yield* VcsProjectConfig.VcsProjectConfig; | 71 | const projectConfig = yield* VcsProjectConfig.VcsProjectConfig; |
| 73 | const git = yield* GitVcsDriver.makeVcsDriverShape(); | 72 | const git = yield* GitVcsDriver.makeVcsDriver; |
| 74 | const drivers: Partial<Record<VcsDriverKind, VcsDriver.VcsDriverShape>> = { | 73 | const drivers: Partial<Record<VcsDriverKind, VcsDriver.VcsDriver["Service"]>> = { |
| 75 | git, | 74 | git, |
| 76 | }; | 75 | }; |
| 77 | 76 | ||
| 78 | const get: VcsDriverRegistryShape["get"] = (kind) => { | 77 | const get: VcsDriverRegistry["Service"]["get"] = (kind) => { |
| 79 | const driver = drivers[kind]; | 78 | const driver = drivers[kind]; |
| 80 | if (!driver) { | 79 | if (!driver) { |
| 81 | return Effect.fail( | 80 | return Effect.fail( |
| ... | @@ -87,7 +86,7 @@ export const make = Effect.fn("makeVcsDriverRegistry")(function* () { | ... | @@ -87,7 +86,7 @@ export const make = Effect.fn("makeVcsDriverRegistry")(function* () { |
| 87 | 86 | ||
| 88 | const detectWithDriver = Effect.fn("VcsDriverRegistry.detectWithDriver")(function* ( | 87 | const detectWithDriver = Effect.fn("VcsDriverRegistry.detectWithDriver")(function* ( |
| 89 | kind: VcsDriverKind, | 88 | kind: VcsDriverKind, |
| 90 | driver: VcsDriver.VcsDriverShape, | 89 | driver: VcsDriver.VcsDriver["Service"], |
| 91 | cwd: string, | 90 | cwd: string, |
| 92 | ) { | 91 | ) { |
| 93 | const repository = yield* driver.detectRepository(cwd); | 92 | const repository = yield* driver.detectRepository(cwd); |
| ... | @@ -123,14 +122,14 @@ export const make = Effect.fn("makeVcsDriverRegistry")(function* () { | ... | @@ -123,14 +122,14 @@ export const make = Effect.fn("makeVcsDriverRegistry")(function* () { |
| 123 | }, | 122 | }, |
| 124 | ); | 123 | ); |
| 125 | 124 | ||
| 126 | const detect: VcsDriverRegistryShape["detect"] = Effect.fn("VcsDriverRegistry.detect")( | 125 | const detect: VcsDriverRegistry["Service"]["detect"] = Effect.fn("VcsDriverRegistry.detect")( |
| 127 | function* (input) { | 126 | function* (input) { |
| 128 | const requestedKind = yield* projectConfig.resolveKind(input); | 127 | const requestedKind = yield* projectConfig.resolveKind(input); |
| 129 | return yield* Cache.get(detectionCache, detectionCacheKey({ cwd: input.cwd, requestedKind })); | 128 | return yield* Cache.get(detectionCache, detectionCacheKey({ cwd: input.cwd, requestedKind })); |
| 130 | }, | 129 | }, |
| 131 | ); | 130 | ); |
| 132 | 131 | ||
| 133 | const resolve: VcsDriverRegistryShape["resolve"] = Effect.fn("VcsDriverRegistry.resolve")( | 132 | const resolve: VcsDriverRegistry["Service"]["resolve"] = Effect.fn("VcsDriverRegistry.resolve")( |
| 134 | function* (input) { | 133 | function* (input) { |
| 135 | const detected = yield* detect(input); | 134 | const detected = yield* detect(input); |
| 136 | if (detected) { | 135 | if (detected) { |
| ... | @@ -155,6 +154,6 @@ export const make = Effect.fn("makeVcsDriverRegistry")(function* () { | ... | @@ -155,6 +154,6 @@ export const make = Effect.fn("makeVcsDriverRegistry")(function* () { |
| 155 | }); | 154 | }); |
| 156 | }); | 155 | }); |
| 157 | 156 | ||
| 158 | export const layer = Layer.effect(VcsDriverRegistry, make()).pipe( | 157 | export const layer = Layer.effect(VcsDriverRegistry, make).pipe( |
| 159 | Layer.provide(VcsProjectConfig.layer), | 158 | Layer.provide(VcsProjectConfig.layer), |
| 160 | ); | 159 | ); |
apps/server/src/vcs/VcsProcess.ts+11-12| ... | @@ -1,6 +1,7 @@ | ... | @@ -1,6 +1,7 @@ |
| 1 | import * as Context from "effect/Context"; | 1 | import * as Context from "effect/Context"; |
| 2 | import * as Effect from "effect/Effect"; | 2 | import * as Effect from "effect/Effect"; |
| 3 | import * as Layer from "effect/Layer"; | 3 | import * as Layer from "effect/Layer"; |
| 4 | import * as Match from "effect/Match"; | ||
| 4 | import { ChildProcessSpawner } from "effect/unstable/process"; | 5 | import { ChildProcessSpawner } from "effect/unstable/process"; |
| 5 | 6 | ||
| 6 | import { | 7 | import { |
| ... | @@ -10,8 +11,7 @@ import { | ... | @@ -10,8 +11,7 @@ import { |
| 10 | VcsProcessSpawnError, | 11 | VcsProcessSpawnError, |
| 11 | VcsProcessTimeoutError, | 12 | VcsProcessTimeoutError, |
| 12 | } from "@t3tools/contracts"; | 13 | } from "@t3tools/contracts"; |
| 13 | import { ProcessRunner, layer as ProcessRunnerLive } from "../processRunner.ts"; | 14 | import * as ProcessRunner from "../processRunner.ts"; |
| 14 | import * as Match from "effect/Match"; | ||
| 15 | 15 | ||
| 16 | export interface VcsProcessInput { | 16 | export interface VcsProcessInput { |
| 17 | readonly operation: string; | 17 | readonly operation: string; |
| ... | @@ -35,13 +35,12 @@ export interface VcsProcessOutput { | ... | @@ -35,13 +35,12 @@ export interface VcsProcessOutput { |
| 35 | readonly stderrTruncated: boolean; | 35 | readonly stderrTruncated: boolean; |
| 36 | } | 36 | } |
| 37 | 37 | ||
| 38 | export interface VcsProcessShape { | 38 | export class VcsProcess extends Context.Service< |
| 39 | readonly run: (input: VcsProcessInput) => Effect.Effect<VcsProcessOutput, VcsError>; | 39 | VcsProcess, |
| 40 | } | 40 | { |
| 41 | 41 | readonly run: (input: VcsProcessInput) => Effect.Effect<VcsProcessOutput, VcsError>; | |
| 42 | export class VcsProcess extends Context.Service<VcsProcess, VcsProcessShape>()( | 42 | } |
| 43 | "t3/vcs/VcsProcess", | 43 | >()("t3/vcs/VcsProcess") {} |
| 44 | ) {} | ||
| 45 | 44 | ||
| 46 | const DEFAULT_TIMEOUT_MS = 30_000; | 45 | const DEFAULT_TIMEOUT_MS = 30_000; |
| 47 | const DEFAULT_MAX_OUTPUT_BYTES = 1_000_000; | 46 | const DEFAULT_MAX_OUTPUT_BYTES = 1_000_000; |
| ... | @@ -51,8 +50,8 @@ function commandLabel(command: string, args: ReadonlyArray<string>): string { | ... | @@ -51,8 +50,8 @@ function commandLabel(command: string, args: ReadonlyArray<string>): string { |
| 51 | return [command, ...args].join(" "); | 50 | return [command, ...args].join(" "); |
| 52 | } | 51 | } |
| 53 | 52 | ||
| 54 | export const make = Effect.fn("makeVcsProcess")(function* () { | 53 | export const make = Effect.gen(function* () { |
| 55 | const processRunner = yield* ProcessRunner; | 54 | const processRunner = yield* ProcessRunner.ProcessRunner; |
| 56 | 55 | ||
| 57 | const run = Effect.fn("VcsProcess.run")(function* (input: VcsProcessInput) { | 56 | const run = Effect.fn("VcsProcess.run")(function* (input: VcsProcessInput) { |
| 58 | const label = commandLabel(input.command, input.args); | 57 | const label = commandLabel(input.command, input.args); |
| ... | @@ -119,4 +118,4 @@ export const make = Effect.fn("makeVcsProcess")(function* () { | ... | @@ -119,4 +118,4 @@ export const make = Effect.fn("makeVcsProcess")(function* () { |
| 119 | return VcsProcess.of({ run }); | 118 | return VcsProcess.of({ run }); |
| 120 | }); | 119 | }); |
| 121 | 120 | ||
| 122 | export const layer = Layer.effect(VcsProcess, make()).pipe(Layer.provide(ProcessRunnerLive)); | 121 | export const layer = Layer.effect(VcsProcess, make).pipe(Layer.provide(ProcessRunner.layer)); |
apps/server/src/vcs/VcsProjectConfig.ts+11-12| ... | @@ -27,15 +27,14 @@ export interface VcsProjectConfigResolveInput { | ... | @@ -27,15 +27,14 @@ export interface VcsProjectConfigResolveInput { |
| 27 | readonly requestedKind?: VcsDriverKindType | "auto"; | 27 | readonly requestedKind?: VcsDriverKindType | "auto"; |
| 28 | } | 28 | } |
| 29 | 29 | ||
| 30 | export interface VcsProjectConfigShape { | 30 | export class VcsProjectConfig extends Context.Service< |
| 31 | readonly resolveKind: ( | 31 | VcsProjectConfig, |
| 32 | input: VcsProjectConfigResolveInput, | 32 | { |
| 33 | ) => Effect.Effect<VcsDriverKindType | "auto">; | 33 | readonly resolveKind: ( |
| 34 | } | 34 | input: VcsProjectConfigResolveInput, |
| 35 | 35 | ) => Effect.Effect<VcsDriverKindType | "auto">; | |
| 36 | export class VcsProjectConfig extends Context.Service<VcsProjectConfig, VcsProjectConfigShape>()( | 36 | } |
| 37 | "t3/vcs/VcsProjectConfig", | 37 | >()("t3/vcs/VcsProjectConfig") {} |
| 38 | ) {} | ||
| 39 | 38 | ||
| 40 | function configuredKind(config: ProjectVcsConfigFile): VcsDriverKindType | "auto" { | 39 | function configuredKind(config: ProjectVcsConfigFile): VcsDriverKindType | "auto" { |
| 41 | return config.vcs?.kind ?? config.vcsKind ?? "auto"; | 40 | return config.vcs?.kind ?? config.vcsKind ?? "auto"; |
| ... | @@ -44,7 +43,7 @@ function configuredKind(config: ProjectVcsConfigFile): VcsDriverKindType | "auto | ... | @@ -44,7 +43,7 @@ function configuredKind(config: ProjectVcsConfigFile): VcsDriverKindType | "auto |
| 44 | const parseConfig = (raw: string): Option.Option<ProjectVcsConfigFile> => | 43 | const parseConfig = (raw: string): Option.Option<ProjectVcsConfigFile> => |
| 45 | decodeProjectVcsConfigJson(raw); | 44 | decodeProjectVcsConfigJson(raw); |
| 46 | 45 | ||
| 47 | export const make = Effect.fn("makeVcsProjectConfig")(function* () { | 46 | export const make = Effect.gen(function* () { |
| 48 | const fileSystem = yield* FileSystem.FileSystem; | 47 | const fileSystem = yield* FileSystem.FileSystem; |
| 49 | const path = yield* Path.Path; | 48 | const path = yield* Path.Path; |
| 50 | 49 | ||
| ... | @@ -91,7 +90,7 @@ export const make = Effect.fn("makeVcsProjectConfig")(function* () { | ... | @@ -91,7 +90,7 @@ export const make = Effect.fn("makeVcsProjectConfig")(function* () { |
| 91 | return configuredKind(parsed.value); | 90 | return configuredKind(parsed.value); |
| 92 | }); | 91 | }); |
| 93 | 92 | ||
| 94 | const resolveKind: VcsProjectConfigShape["resolveKind"] = Effect.fn( | 93 | const resolveKind: VcsProjectConfig["Service"]["resolveKind"] = Effect.fn( |
| 95 | "VcsProjectConfig.resolveKind", | 94 | "VcsProjectConfig.resolveKind", |
| 96 | )(function* (input) { | 95 | )(function* (input) { |
| 97 | if (input.requestedKind !== undefined && input.requestedKind !== "auto") { | 96 | if (input.requestedKind !== undefined && input.requestedKind !== "auto") { |
| ... | @@ -110,4 +109,4 @@ export const make = Effect.fn("makeVcsProjectConfig")(function* () { | ... | @@ -110,4 +109,4 @@ export const make = Effect.fn("makeVcsProjectConfig")(function* () { |
| 110 | }); | 109 | }); |
| 111 | }); | 110 | }); |
| 112 | 111 | ||
| 113 | export const layer = Layer.effect(VcsProjectConfig, make()); | 112 | export const layer = Layer.effect(VcsProjectConfig, make); |
apps/server/src/vcs/VcsProvisioningService.test.ts+1-1| ... | @@ -11,7 +11,7 @@ import * as VcsProvisioningService from "./VcsProvisioningService.ts"; | ... | @@ -11,7 +11,7 @@ import * as VcsProvisioningService from "./VcsProvisioningService.ts"; |
| 11 | 11 | ||
| 12 | const TEST_EPOCH = DateTime.makeUnsafe("1970-01-01T00:00:00.000Z"); | 12 | const TEST_EPOCH = DateTime.makeUnsafe("1970-01-01T00:00:00.000Z"); |
| 13 | 13 | ||
| 14 | function makeDriver(calls: string[]): VcsDriver.VcsDriverShape { | 14 | function makeDriver(calls: string[]): VcsDriver.VcsDriver["Service"] { |
| 15 | return { | 15 | return { |
| 16 | capabilities: { | 16 | capabilities: { |
| 17 | kind: "git", | 17 | kind: "git", |
apps/server/src/vcs/VcsProvisioningService.ts+6-8| ... | @@ -10,13 +10,11 @@ import { | ... | @@ -10,13 +10,11 @@ import { |
| 10 | } from "@t3tools/contracts"; | 10 | } from "@t3tools/contracts"; |
| 11 | import * as VcsDriverRegistry from "./VcsDriverRegistry.ts"; | 11 | import * as VcsDriverRegistry from "./VcsDriverRegistry.ts"; |
| 12 | 12 | ||
| 13 | export interface VcsProvisioningServiceShape { | ||
| 14 | readonly initRepository: (input: VcsInitInput) => Effect.Effect<void, VcsError>; | ||
| 15 | } | ||
| 16 | |||
| 17 | export class VcsProvisioningService extends Context.Service< | 13 | export class VcsProvisioningService extends Context.Service< |
| 18 | VcsProvisioningService, | 14 | VcsProvisioningService, |
| 19 | VcsProvisioningServiceShape | 15 | { |
| 16 | readonly initRepository: (input: VcsInitInput) => Effect.Effect<void, VcsError>; | ||
| 17 | } | ||
| 20 | >()("t3/vcs/VcsProvisioningService") {} | 18 | >()("t3/vcs/VcsProvisioningService") {} |
| 21 | 19 | ||
| 22 | function resolveRequestedKind( | 20 | function resolveRequestedKind( |
| ... | @@ -37,10 +35,10 @@ function resolveRequestedKind( | ... | @@ -37,10 +35,10 @@ function resolveRequestedKind( |
| 37 | return Effect.succeed(kind); | 35 | return Effect.succeed(kind); |
| 38 | } | 36 | } |
| 39 | 37 | ||
| 40 | export const make = Effect.fn("makeVcsProvisioningService")(function* () { | 38 | export const make = Effect.gen(function* () { |
| 41 | const registry = yield* VcsDriverRegistry.VcsDriverRegistry; | 39 | const registry = yield* VcsDriverRegistry.VcsDriverRegistry; |
| 42 | 40 | ||
| 43 | const initRepository: VcsProvisioningServiceShape["initRepository"] = Effect.fn( | 41 | const initRepository: VcsProvisioningService["Service"]["initRepository"] = Effect.fn( |
| 44 | "VcsProvisioningService.initRepository", | 42 | "VcsProvisioningService.initRepository", |
| 45 | )(function* (input) { | 43 | )(function* (input) { |
| 46 | const kind = yield* resolveRequestedKind(input.kind); | 44 | const kind = yield* resolveRequestedKind(input.kind); |
| ... | @@ -53,4 +51,4 @@ export const make = Effect.fn("makeVcsProvisioningService")(function* () { | ... | @@ -53,4 +51,4 @@ export const make = Effect.fn("makeVcsProvisioningService")(function* () { |
| 53 | }); | 51 | }); |
| 54 | }); | 52 | }); |
| 55 | 53 | ||
| 56 | export const layer = Layer.effect(VcsProvisioningService, make()); | 54 | export const layer = Layer.effect(VcsProvisioningService, make); |
apps/server/src/vcs/VcsStatusBroadcaster.test.ts+2-2| ... | @@ -299,7 +299,7 @@ describe("VcsStatusBroadcaster", () => { | ... | @@ -299,7 +299,7 @@ describe("VcsStatusBroadcaster", () => { |
| 299 | Effect.sync(() => { | 299 | Effect.sync(() => { |
| 300 | state.remoteInvalidationCalls += 1; | 300 | state.remoteInvalidationCalls += 1; |
| 301 | }), | 301 | }), |
| 302 | } satisfies Partial<GitWorkflowService.GitWorkflowServiceShape>), | 302 | } satisfies Partial<GitWorkflowService.GitWorkflowService["Service"]>), |
| 303 | ), | 303 | ), |
| 304 | ); | 304 | ); |
| 305 | 305 | ||
| ... | @@ -617,7 +617,7 @@ describe("VcsStatusBroadcaster", () => { | ... | @@ -617,7 +617,7 @@ describe("VcsStatusBroadcaster", () => { |
| 617 | Effect.sync(() => { | 617 | Effect.sync(() => { |
| 618 | state.remoteInvalidationCalls += 1; | 618 | state.remoteInvalidationCalls += 1; |
| 619 | }), | 619 | }), |
| 620 | } satisfies Partial<GitWorkflowService.GitWorkflowServiceShape>), | 620 | } satisfies Partial<GitWorkflowService.GitWorkflowService["Service"]>), |
| 621 | ), | 621 | ), |
| 622 | ); | 622 | ); |
| 623 | 623 |
apps/server/src/vcs/VcsStatusBroadcaster.ts+312-319| ... | @@ -65,23 +65,21 @@ export function remoteRefreshFailureDelay( | ... | @@ -65,23 +65,21 @@ export function remoteRefreshFailureDelay( |
| 65 | return Duration.max(configuredInterval, cappedBackoff); | 65 | return Duration.max(configuredInterval, cappedBackoff); |
| 66 | } | 66 | } |
| 67 | 67 | ||
| 68 | export interface VcsStatusBroadcasterShape { | ||
| 69 | readonly getStatus: ( | ||
| 70 | input: VcsStatusInput, | ||
| 71 | ) => Effect.Effect<VcsStatusResult, GitManagerServiceError>; | ||
| 72 | readonly refreshLocalStatus: ( | ||
| 73 | cwd: string, | ||
| 74 | ) => Effect.Effect<VcsStatusLocalResult, GitManagerServiceError>; | ||
| 75 | readonly refreshStatus: (cwd: string) => Effect.Effect<VcsStatusResult, GitManagerServiceError>; | ||
| 76 | readonly streamStatus: ( | ||
| 77 | input: VcsStatusInput, | ||
| 78 | options?: StreamStatusOptions, | ||
| 79 | ) => Stream.Stream<VcsStatusStreamEvent, GitManagerServiceError>; | ||
| 80 | } | ||
| 81 | |||
| 82 | export class VcsStatusBroadcaster extends Context.Service< | 68 | export class VcsStatusBroadcaster extends Context.Service< |
| 83 | VcsStatusBroadcaster, | 69 | VcsStatusBroadcaster, |
| 84 | VcsStatusBroadcasterShape | 70 | { |
| 71 | readonly getStatus: ( | ||
| 72 | input: VcsStatusInput, | ||
| 73 | ) => Effect.Effect<VcsStatusResult, GitManagerServiceError>; | ||
| 74 | readonly refreshLocalStatus: ( | ||
| 75 | cwd: string, | ||
| 76 | ) => Effect.Effect<VcsStatusLocalResult, GitManagerServiceError>; | ||
| 77 | readonly refreshStatus: (cwd: string) => Effect.Effect<VcsStatusResult, GitManagerServiceError>; | ||
| 78 | readonly streamStatus: ( | ||
| 79 | input: VcsStatusInput, | ||
| 80 | options?: StreamStatusOptions, | ||
| 81 | ) => Stream.Stream<VcsStatusStreamEvent, GitManagerServiceError>; | ||
| 82 | } | ||
| 85 | >()("t3/vcs/VcsStatusBroadcaster") {} | 83 | >()("t3/vcs/VcsStatusBroadcaster") {} |
| 86 | 84 | ||
| 87 | function fingerprintStatusPart(status: unknown): string { | 85 | function fingerprintStatusPart(status: unknown): string { |
| ... | @@ -94,101 +92,57 @@ const normalizeCwd = (cwd: string) => | ... | @@ -94,101 +92,57 @@ const normalizeCwd = (cwd: string) => |
| 94 | Effect.orElseSucceed(() => cwd), | 92 | Effect.orElseSucceed(() => cwd), |
| 95 | ); | 93 | ); |
| 96 | 94 | ||
| 97 | export const layer = Layer.effect( | 95 | export const make = Effect.gen(function* () { |
| 98 | VcsStatusBroadcaster, | 96 | const workflow = yield* GitWorkflowService.GitWorkflowService; |
| 99 | Effect.gen(function* () { | 97 | const fs = yield* FileSystem.FileSystem; |
| 100 | const workflow = yield* GitWorkflowService.GitWorkflowService; | 98 | const changesPubSub = yield* Effect.acquireRelease( |
| 101 | const fs = yield* FileSystem.FileSystem; | 99 | PubSub.unbounded<VcsStatusChange>(), |
| 102 | const changesPubSub = yield* Effect.acquireRelease( | 100 | (pubsub) => PubSub.shutdown(pubsub), |
| 103 | PubSub.unbounded<VcsStatusChange>(), | 101 | ); |
| 104 | (pubsub) => PubSub.shutdown(pubsub), | 102 | const broadcasterScope = yield* Effect.acquireRelease(Scope.make(), (scope) => |
| 105 | ); | 103 | Scope.close(scope, Exit.void), |
| 106 | const broadcasterScope = yield* Effect.acquireRelease(Scope.make(), (scope) => | 104 | ); |
| 107 | Scope.close(scope, Exit.void), | 105 | const cacheRef = yield* Ref.make(new Map<string, CachedVcsStatus>()); |
| 108 | ); | 106 | const pollersRef = yield* SynchronizedRef.make(new Map<string, ActiveRemotePoller>()); |
| 109 | const cacheRef = yield* Ref.make(new Map<string, CachedVcsStatus>()); | ||
| 110 | const pollersRef = yield* SynchronizedRef.make(new Map<string, ActiveRemotePoller>()); | ||
| 111 | 107 | ||
| 112 | const getCachedStatus = Effect.fn("VcsStatusBroadcaster.getCachedStatus")(function* ( | 108 | const getCachedStatus = Effect.fn("VcsStatusBroadcaster.getCachedStatus")(function* ( |
| 113 | cwd: string, | 109 | cwd: string, |
| 114 | ) { | 110 | ) { |
| 115 | return yield* Ref.get(cacheRef).pipe(Effect.map((cache) => cache.get(cwd) ?? null)); | 111 | return yield* Ref.get(cacheRef).pipe(Effect.map((cache) => cache.get(cwd) ?? null)); |
| 116 | }); | 112 | }); |
| 117 | 113 | ||
| 118 | const updateCachedLocalStatus = Effect.fn("VcsStatusBroadcaster.updateCachedLocalStatus")( | 114 | const updateCachedLocalStatus = Effect.fn("VcsStatusBroadcaster.updateCachedLocalStatus")( |
| 119 | function* (cwd: string, local: VcsStatusLocalResult, options?: { publish?: boolean }) { | 115 | function* (cwd: string, local: VcsStatusLocalResult, options?: { publish?: boolean }) { |
| 120 | const nextLocal = { | 116 | const nextLocal = { |
| 121 | fingerprint: fingerprintStatusPart(local), | 117 | fingerprint: fingerprintStatusPart(local), |
| 122 | value: local, | 118 | value: local, |
| 123 | } satisfies CachedValue<VcsStatusLocalResult>; | 119 | } satisfies CachedValue<VcsStatusLocalResult>; |
| 124 | const shouldPublish = yield* Ref.modify(cacheRef, (cache) => { | 120 | const shouldPublish = yield* Ref.modify(cacheRef, (cache) => { |
| 125 | const previous = cache.get(cwd) ?? { local: null, remote: null }; | 121 | const previous = cache.get(cwd) ?? { local: null, remote: null }; |
| 126 | const nextCache = new Map(cache); | 122 | const nextCache = new Map(cache); |
| 127 | nextCache.set(cwd, { | 123 | nextCache.set(cwd, { |
| 128 | ...previous, | 124 | ...previous, |
| 129 | local: nextLocal, | 125 | local: nextLocal, |
| 130 | }); | ||
| 131 | return [previous.local?.fingerprint !== nextLocal.fingerprint, nextCache] as const; | ||
| 132 | }); | 126 | }); |
| 127 | return [previous.local?.fingerprint !== nextLocal.fingerprint, nextCache] as const; | ||
| 128 | }); | ||
| 133 | 129 | ||
| 134 | if (options?.publish && shouldPublish) { | 130 | if (options?.publish && shouldPublish) { |
| 135 | yield* PubSub.publish(changesPubSub, { | 131 | yield* PubSub.publish(changesPubSub, { |
| 136 | cwd, | 132 | cwd, |
| 137 | event: { | 133 | event: { |
| 138 | _tag: "localUpdated", | 134 | _tag: "localUpdated", |
| 139 | local, | 135 | local, |
| 140 | }, | 136 | }, |
| 141 | }); | ||
| 142 | } | ||
| 143 | |||
| 144 | return local; | ||
| 145 | }, | ||
| 146 | ); | ||
| 147 | |||
| 148 | const updateCachedRemoteStatus = Effect.fn("VcsStatusBroadcaster.updateCachedRemoteStatus")( | ||
| 149 | function* ( | ||
| 150 | cwd: string, | ||
| 151 | remote: VcsStatusRemoteResult | null, | ||
| 152 | options?: { publish?: boolean }, | ||
| 153 | ) { | ||
| 154 | const nextRemote = { | ||
| 155 | fingerprint: fingerprintStatusPart(remote), | ||
| 156 | value: remote, | ||
| 157 | } satisfies CachedValue<VcsStatusRemoteResult | null>; | ||
| 158 | const shouldPublish = yield* Ref.modify(cacheRef, (cache) => { | ||
| 159 | const previous = cache.get(cwd) ?? { local: null, remote: null }; | ||
| 160 | const nextCache = new Map(cache); | ||
| 161 | nextCache.set(cwd, { | ||
| 162 | ...previous, | ||
| 163 | remote: nextRemote, | ||
| 164 | }); | ||
| 165 | return [previous.remote?.fingerprint !== nextRemote.fingerprint, nextCache] as const; | ||
| 166 | }); | 137 | }); |
| 138 | } | ||
| 167 | 139 | ||
| 168 | if (options?.publish && shouldPublish) { | 140 | return local; |
| 169 | yield* PubSub.publish(changesPubSub, { | 141 | }, |
| 170 | cwd, | 142 | ); |
| 171 | event: { | ||
| 172 | _tag: "remoteUpdated", | ||
| 173 | remote, | ||
| 174 | }, | ||
| 175 | }); | ||
| 176 | } | ||
| 177 | |||
| 178 | return remote; | ||
| 179 | }, | ||
| 180 | ); | ||
| 181 | 143 | ||
| 182 | const updateCachedStatus = Effect.fn("VcsStatusBroadcaster.updateCachedStatus")(function* ( | 144 | const updateCachedRemoteStatus = Effect.fn("VcsStatusBroadcaster.updateCachedRemoteStatus")( |
| 183 | cwd: string, | 145 | function* (cwd: string, remote: VcsStatusRemoteResult | null, options?: { publish?: boolean }) { |
| 184 | local: VcsStatusLocalResult, | ||
| 185 | remote: VcsStatusRemoteResult | null, | ||
| 186 | options?: { publish?: boolean }, | ||
| 187 | ) { | ||
| 188 | const nextLocal = { | ||
| 189 | fingerprint: fingerprintStatusPart(local), | ||
| 190 | value: local, | ||
| 191 | } satisfies CachedValue<VcsStatusLocalResult>; | ||
| 192 | const nextRemote = { | 146 | const nextRemote = { |
| 193 | fingerprint: fingerprintStatusPart(remote), | 147 | fingerprint: fingerprintStatusPart(remote), |
| 194 | value: remote, | 148 | value: remote, |
| ... | @@ -197,263 +151,302 @@ export const layer = Layer.effect( | ... | @@ -197,263 +151,302 @@ export const layer = Layer.effect( |
| 197 | const previous = cache.get(cwd) ?? { local: null, remote: null }; | 151 | const previous = cache.get(cwd) ?? { local: null, remote: null }; |
| 198 | const nextCache = new Map(cache); | 152 | const nextCache = new Map(cache); |
| 199 | nextCache.set(cwd, { | 153 | nextCache.set(cwd, { |
| 200 | local: nextLocal, | 154 | ...previous, |
| 201 | remote: nextRemote, | 155 | remote: nextRemote, |
| 202 | }); | 156 | }); |
| 203 | return [ | 157 | return [previous.remote?.fingerprint !== nextRemote.fingerprint, nextCache] as const; |
| 204 | previous.local?.fingerprint !== nextLocal.fingerprint || | ||
| 205 | previous.remote?.fingerprint !== nextRemote.fingerprint, | ||
| 206 | nextCache, | ||
| 207 | ] as const; | ||
| 208 | }); | 158 | }); |
| 209 | 159 | ||
| 210 | if (options?.publish && shouldPublish) { | 160 | if (options?.publish && shouldPublish) { |
| 211 | yield* PubSub.publish(changesPubSub, { | 161 | yield* PubSub.publish(changesPubSub, { |
| 212 | cwd, | 162 | cwd, |
| 213 | event: { | 163 | event: { |
| 214 | _tag: "snapshot", | 164 | _tag: "remoteUpdated", |
| 215 | local, | ||
| 216 | remote, | 165 | remote, |
| 217 | }, | 166 | }, |
| 218 | }); | 167 | }); |
| 219 | } | 168 | } |
| 220 | 169 | ||
| 221 | return mergeGitStatusParts(local, remote); | 170 | return remote; |
| 222 | }); | 171 | }, |
| 172 | ); | ||
| 223 | 173 | ||
| 224 | const loadLocalStatus = Effect.fn("VcsStatusBroadcaster.loadLocalStatus")(function* ( | 174 | const updateCachedStatus = Effect.fn("VcsStatusBroadcaster.updateCachedStatus")(function* ( |
| 225 | cwd: string, | 175 | cwd: string, |
| 226 | ) { | 176 | local: VcsStatusLocalResult, |
| 227 | const local = yield* workflow.localStatus({ cwd }); | 177 | remote: VcsStatusRemoteResult | null, |
| 228 | return yield* updateCachedLocalStatus(cwd, local); | 178 | options?: { publish?: boolean }, |
| 179 | ) { | ||
| 180 | const nextLocal = { | ||
| 181 | fingerprint: fingerprintStatusPart(local), | ||
| 182 | value: local, | ||
| 183 | } satisfies CachedValue<VcsStatusLocalResult>; | ||
| 184 | const nextRemote = { | ||
| 185 | fingerprint: fingerprintStatusPart(remote), | ||
| 186 | value: remote, | ||
| 187 | } satisfies CachedValue<VcsStatusRemoteResult | null>; | ||
| 188 | const shouldPublish = yield* Ref.modify(cacheRef, (cache) => { | ||
| 189 | const previous = cache.get(cwd) ?? { local: null, remote: null }; | ||
| 190 | const nextCache = new Map(cache); | ||
| 191 | nextCache.set(cwd, { | ||
| 192 | local: nextLocal, | ||
| 193 | remote: nextRemote, | ||
| 194 | }); | ||
| 195 | return [ | ||
| 196 | previous.local?.fingerprint !== nextLocal.fingerprint || | ||
| 197 | previous.remote?.fingerprint !== nextRemote.fingerprint, | ||
| 198 | nextCache, | ||
| 199 | ] as const; | ||
| 229 | }); | 200 | }); |
| 230 | 201 | ||
| 231 | const getOrLoadLocalStatus = Effect.fn("VcsStatusBroadcaster.getOrLoadLocalStatus")(function* ( | 202 | if (options?.publish && shouldPublish) { |
| 232 | cwd: string, | 203 | yield* PubSub.publish(changesPubSub, { |
| 233 | ) { | 204 | cwd, |
| 234 | const cached = yield* getCachedStatus(cwd); | 205 | event: { |
| 235 | if (cached?.local) { | 206 | _tag: "snapshot", |
| 236 | return cached.local.value; | 207 | local, |
| 237 | } | 208 | remote, |
| 238 | return yield* loadLocalStatus(cwd); | 209 | }, |
| 239 | }); | 210 | }); |
| 211 | } | ||
| 240 | 212 | ||
| 241 | const withFileSystem = Effect.provideService(FileSystem.FileSystem, fs); | 213 | return mergeGitStatusParts(local, remote); |
| 214 | }); | ||
| 242 | 215 | ||
| 243 | const getStatus: VcsStatusBroadcasterShape["getStatus"] = Effect.fn( | 216 | const loadLocalStatus = Effect.fn("VcsStatusBroadcaster.loadLocalStatus")(function* ( |
| 244 | "VcsStatusBroadcaster.getStatus", | 217 | cwd: string, |
| 245 | )(function* (input) { | 218 | ) { |
| 246 | const cwd = yield* withFileSystem(normalizeCwd(input.cwd)); | 219 | const local = yield* workflow.localStatus({ cwd }); |
| 247 | const cached = yield* getCachedStatus(cwd); | 220 | return yield* updateCachedLocalStatus(cwd, local); |
| 248 | if (cached?.local && cached.remote) { | 221 | }); |
| 249 | return mergeGitStatusParts(cached.local.value, cached.remote.value); | ||
| 250 | } | ||
| 251 | const [local, remote] = yield* Effect.all( | ||
| 252 | [ | ||
| 253 | cached?.local ? Effect.succeed(cached.local.value) : workflow.localStatus({ cwd }), | ||
| 254 | cached?.remote ? Effect.succeed(cached.remote.value) : workflow.remoteStatus({ cwd }), | ||
| 255 | ], | ||
| 256 | { concurrency: "unbounded" }, | ||
| 257 | ); | ||
| 258 | return yield* updateCachedStatus(cwd, local, remote); | ||
| 259 | }); | ||
| 260 | 222 | ||
| 261 | const refreshLocalStatusCore = Effect.fn("VcsStatusBroadcaster.refreshLocalStatusCore")( | 223 | const getOrLoadLocalStatus = Effect.fn("VcsStatusBroadcaster.getOrLoadLocalStatus")(function* ( |
| 262 | function* (cwd: string) { | 224 | cwd: string, |
| 263 | yield* workflow.invalidateLocalStatus(cwd); | 225 | ) { |
| 264 | const local = yield* workflow.localStatus({ cwd }); | 226 | const cached = yield* getCachedStatus(cwd); |
| 265 | return yield* updateCachedLocalStatus(cwd, local, { publish: true }); | 227 | if (cached?.local) { |
| 266 | }, | 228 | return cached.local.value; |
| 229 | } | ||
| 230 | return yield* loadLocalStatus(cwd); | ||
| 231 | }); | ||
| 232 | |||
| 233 | const withFileSystem = Effect.provideService(FileSystem.FileSystem, fs); | ||
| 234 | |||
| 235 | const getStatus: VcsStatusBroadcaster["Service"]["getStatus"] = Effect.fn( | ||
| 236 | "VcsStatusBroadcaster.getStatus", | ||
| 237 | )(function* (input) { | ||
| 238 | const cwd = yield* withFileSystem(normalizeCwd(input.cwd)); | ||
| 239 | const cached = yield* getCachedStatus(cwd); | ||
| 240 | if (cached?.local && cached.remote) { | ||
| 241 | return mergeGitStatusParts(cached.local.value, cached.remote.value); | ||
| 242 | } | ||
| 243 | const [local, remote] = yield* Effect.all( | ||
| 244 | [ | ||
| 245 | cached?.local ? Effect.succeed(cached.local.value) : workflow.localStatus({ cwd }), | ||
| 246 | cached?.remote ? Effect.succeed(cached.remote.value) : workflow.remoteStatus({ cwd }), | ||
| 247 | ], | ||
| 248 | { concurrency: "unbounded" }, | ||
| 267 | ); | 249 | ); |
| 250 | return yield* updateCachedStatus(cwd, local, remote); | ||
| 251 | }); | ||
| 268 | 252 | ||
| 269 | const refreshLocalStatus: VcsStatusBroadcasterShape["refreshLocalStatus"] = Effect.fn( | 253 | const refreshLocalStatusCore = Effect.fn("VcsStatusBroadcaster.refreshLocalStatusCore")( |
| 270 | "VcsStatusBroadcaster.refreshLocalStatus", | 254 | function* (cwd: string) { |
| 271 | )(function* (rawCwd) { | 255 | yield* workflow.invalidateLocalStatus(cwd); |
| 272 | const cwd = yield* withFileSystem(normalizeCwd(rawCwd)); | 256 | const local = yield* workflow.localStatus({ cwd }); |
| 273 | return yield* refreshLocalStatusCore(cwd); | 257 | return yield* updateCachedLocalStatus(cwd, local, { publish: true }); |
| 274 | }); | 258 | }, |
| 259 | ); | ||
| 275 | 260 | ||
| 276 | const refreshRemoteStatus = Effect.fn("VcsStatusBroadcaster.refreshRemoteStatus")(function* ( | 261 | const refreshLocalStatus: VcsStatusBroadcaster["Service"]["refreshLocalStatus"] = Effect.fn( |
| 277 | cwd: string, | 262 | "VcsStatusBroadcaster.refreshLocalStatus", |
| 278 | options?: { readonly refreshUpstream?: boolean }, | 263 | )(function* (rawCwd) { |
| 279 | ) { | 264 | const cwd = yield* withFileSystem(normalizeCwd(rawCwd)); |
| 280 | if (options?.refreshUpstream !== false) { | 265 | return yield* refreshLocalStatusCore(cwd); |
| 281 | yield* workflow.invalidateRemoteStatus(cwd); | 266 | }); |
| 282 | } | ||
| 283 | const remote = yield* workflow.remoteStatus({ cwd }, options); | ||
| 284 | return yield* updateCachedRemoteStatus(cwd, remote, { publish: true }); | ||
| 285 | }); | ||
| 286 | 267 | ||
| 287 | const refreshStatus: VcsStatusBroadcasterShape["refreshStatus"] = Effect.fn( | 268 | const refreshRemoteStatus = Effect.fn("VcsStatusBroadcaster.refreshRemoteStatus")(function* ( |
| 288 | "VcsStatusBroadcaster.refreshStatus", | 269 | cwd: string, |
| 289 | )(function* (rawCwd) { | 270 | options?: { readonly refreshUpstream?: boolean }, |
| 290 | const cwd = yield* withFileSystem(normalizeCwd(rawCwd)); | 271 | ) { |
| 291 | yield* Effect.all( | 272 | if (options?.refreshUpstream !== false) { |
| 292 | [workflow.invalidateLocalStatus(cwd), workflow.invalidateRemoteStatus(cwd)], | 273 | yield* workflow.invalidateRemoteStatus(cwd); |
| 293 | { concurrency: "unbounded", discard: true }, | 274 | } |
| 294 | ); | 275 | const remote = yield* workflow.remoteStatus({ cwd }, options); |
| 295 | const [local, remote] = yield* Effect.all( | 276 | return yield* updateCachedRemoteStatus(cwd, remote, { publish: true }); |
| 296 | [workflow.localStatus({ cwd }), workflow.remoteStatus({ cwd })], | 277 | }); |
| 297 | { concurrency: "unbounded" }, | 278 | |
| 298 | ); | 279 | const refreshStatus: VcsStatusBroadcaster["Service"]["refreshStatus"] = Effect.fn( |
| 299 | return yield* updateCachedStatus(cwd, local, remote, { publish: true }); | 280 | "VcsStatusBroadcaster.refreshStatus", |
| 281 | )(function* (rawCwd) { | ||
| 282 | const cwd = yield* withFileSystem(normalizeCwd(rawCwd)); | ||
| 283 | yield* Effect.all([workflow.invalidateLocalStatus(cwd), workflow.invalidateRemoteStatus(cwd)], { | ||
| 284 | concurrency: "unbounded", | ||
| 285 | discard: true, | ||
| 300 | }); | 286 | }); |
| 287 | const [local, remote] = yield* Effect.all( | ||
| 288 | [workflow.localStatus({ cwd }), workflow.remoteStatus({ cwd })], | ||
| 289 | { concurrency: "unbounded" }, | ||
| 290 | ); | ||
| 291 | return yield* updateCachedStatus(cwd, local, remote, { publish: true }); | ||
| 292 | }); | ||
| 301 | 293 | ||
| 302 | const makeRemoteRefreshLoop = ( | 294 | const makeRemoteRefreshLoop = ( |
| 303 | cwd: string, | 295 | cwd: string, |
| 304 | automaticRemoteRefreshInterval: Effect.Effect<Duration.Duration, never>, | 296 | automaticRemoteRefreshInterval: Effect.Effect<Duration.Duration, never>, |
| 305 | refreshImmediately: boolean, | 297 | refreshImmediately: boolean, |
| 306 | ) => { | 298 | ) => { |
| 307 | return Effect.gen(function* () { | 299 | return Effect.gen(function* () { |
| 308 | const consecutiveFailuresRef = yield* Ref.make(0); | 300 | const consecutiveFailuresRef = yield* Ref.make(0); |
| 309 | const needsInitialRefreshRef = yield* Ref.make(refreshImmediately); | 301 | const needsInitialRefreshRef = yield* Ref.make(refreshImmediately); |
| 310 | const refreshRemoteStatusIfEnabled = Effect.gen(function* () { | 302 | const refreshRemoteStatusIfEnabled = Effect.gen(function* () { |
| 311 | const configuredInterval = yield* automaticRemoteRefreshInterval; | 303 | const configuredInterval = yield* automaticRemoteRefreshInterval; |
| 312 | const activeInterval = Duration.isZero(configuredInterval) | 304 | const activeInterval = Duration.isZero(configuredInterval) |
| 313 | ? DEFAULT_VCS_STATUS_REFRESH_INTERVAL | 305 | ? DEFAULT_VCS_STATUS_REFRESH_INTERVAL |
| 314 | : configuredInterval; | 306 | : configuredInterval; |
| 315 | const needsInitialRefresh = yield* Ref.get(needsInitialRefreshRef); | 307 | const needsInitialRefresh = yield* Ref.get(needsInitialRefreshRef); |
| 316 | if (Duration.isZero(configuredInterval) && !needsInitialRefresh) { | 308 | if (Duration.isZero(configuredInterval) && !needsInitialRefresh) { |
| 317 | return activeInterval; | 309 | return activeInterval; |
| 318 | } | 310 | } |
| 319 | |||
| 320 | const exit = yield* refreshRemoteStatus(cwd, { | ||
| 321 | refreshUpstream: !Duration.isZero(configuredInterval), | ||
| 322 | }).pipe(Effect.exit); | ||
| 323 | if (Exit.isSuccess(exit)) { | ||
| 324 | yield* Ref.set(needsInitialRefreshRef, false); | ||
| 325 | yield* Ref.set(consecutiveFailuresRef, 0); | ||
| 326 | return activeInterval; | ||
| 327 | } | ||
| 328 | |||
| 329 | const consecutiveFailures = yield* Ref.updateAndGet( | ||
| 330 | consecutiveFailuresRef, | ||
| 331 | (count) => count + 1, | ||
| 332 | ); | ||
| 333 | const nextDelay = remoteRefreshFailureDelay(consecutiveFailures, activeInterval); | ||
| 334 | yield* Effect.logWarning("VCS remote status refresh failed", { | ||
| 335 | cwd, | ||
| 336 | detail: exit.cause.toString(), | ||
| 337 | consecutiveFailures, | ||
| 338 | nextDelayMs: Duration.toMillis(nextDelay), | ||
| 339 | }); | ||
| 340 | return nextDelay; | ||
| 341 | }); | ||
| 342 | 311 | ||
| 343 | if (!refreshImmediately) { | 312 | const exit = yield* refreshRemoteStatus(cwd, { |
| 344 | const configuredInterval = yield* automaticRemoteRefreshInterval; | 313 | refreshUpstream: !Duration.isZero(configuredInterval), |
| 345 | yield* Effect.sleep( | 314 | }).pipe(Effect.exit); |
| 346 | Duration.isZero(configuredInterval) | 315 | if (Exit.isSuccess(exit)) { |
| 347 | ? DEFAULT_VCS_STATUS_REFRESH_INTERVAL | 316 | yield* Ref.set(needsInitialRefreshRef, false); |
| 348 | : configuredInterval, | 317 | yield* Ref.set(consecutiveFailuresRef, 0); |
| 349 | ); | 318 | return activeInterval; |
| 350 | } | 319 | } |
| 351 | 320 | ||
| 352 | return yield* refreshRemoteStatusIfEnabled.pipe( | 321 | const consecutiveFailures = yield* Ref.updateAndGet( |
| 353 | Effect.repeat( | 322 | consecutiveFailuresRef, |
| 354 | Schedule.identity<Duration.Duration>().pipe( | 323 | (count) => count + 1, |
| 355 | Schedule.addDelay((delay) => Effect.succeed(delay)), | ||
| 356 | ), | ||
| 357 | ), | ||
| 358 | Effect.asVoid, | ||
| 359 | ); | 324 | ); |
| 325 | const nextDelay = remoteRefreshFailureDelay(consecutiveFailures, activeInterval); | ||
| 326 | yield* Effect.logWarning("VCS remote status refresh failed", { | ||
| 327 | cwd, | ||
| 328 | detail: exit.cause.toString(), | ||
| 329 | consecutiveFailures, | ||
| 330 | nextDelayMs: Duration.toMillis(nextDelay), | ||
| 331 | }); | ||
| 332 | return nextDelay; | ||
| 360 | }); | 333 | }); |
| 361 | }; | ||
| 362 | 334 | ||
| 363 | const retainRemotePoller = Effect.fn("VcsStatusBroadcaster.retainRemotePoller")(function* ( | 335 | if (!refreshImmediately) { |
| 364 | cwd: string, | 336 | const configuredInterval = yield* automaticRemoteRefreshInterval; |
| 365 | automaticRemoteRefreshInterval: Effect.Effect<Duration.Duration, never>, | 337 | yield* Effect.sleep( |
| 366 | refreshImmediately: boolean, | 338 | Duration.isZero(configuredInterval) |
| 367 | ) { | 339 | ? DEFAULT_VCS_STATUS_REFRESH_INTERVAL |
| 368 | yield* SynchronizedRef.modifyEffect(pollersRef, (activePollers) => { | 340 | : configuredInterval, |
| 369 | const existing = activePollers.get(cwd); | ||
| 370 | if (existing) { | ||
| 371 | const nextPollers = new Map(activePollers); | ||
| 372 | nextPollers.set(cwd, { | ||
| 373 | ...existing, | ||
| 374 | subscriberCount: existing.subscriberCount + 1, | ||
| 375 | }); | ||
| 376 | return Effect.succeed([undefined, nextPollers] as const); | ||
| 377 | } | ||
| 378 | |||
| 379 | return makeRemoteRefreshLoop(cwd, automaticRemoteRefreshInterval, refreshImmediately).pipe( | ||
| 380 | Effect.forkIn(broadcasterScope), | ||
| 381 | Effect.map((fiber) => { | ||
| 382 | const nextPollers = new Map(activePollers); | ||
| 383 | nextPollers.set(cwd, { | ||
| 384 | fiber, | ||
| 385 | subscriberCount: 1, | ||
| 386 | }); | ||
| 387 | return [undefined, nextPollers] as const; | ||
| 388 | }), | ||
| 389 | ); | 341 | ); |
| 390 | }); | 342 | } |
| 343 | |||
| 344 | return yield* refreshRemoteStatusIfEnabled.pipe( | ||
| 345 | Effect.repeat( | ||
| 346 | Schedule.identity<Duration.Duration>().pipe( | ||
| 347 | Schedule.addDelay((delay) => Effect.succeed(delay)), | ||
| 348 | ), | ||
| 349 | ), | ||
| 350 | Effect.asVoid, | ||
| 351 | ); | ||
| 391 | }); | 352 | }); |
| 353 | }; | ||
| 392 | 354 | ||
| 393 | const releaseRemotePoller = Effect.fn("VcsStatusBroadcaster.releaseRemotePoller")(function* ( | 355 | const retainRemotePoller = Effect.fn("VcsStatusBroadcaster.retainRemotePoller")(function* ( |
| 394 | cwd: string, | 356 | cwd: string, |
| 395 | ) { | 357 | automaticRemoteRefreshInterval: Effect.Effect<Duration.Duration, never>, |
| 396 | const pollerToInterrupt = yield* SynchronizedRef.modify(pollersRef, (activePollers) => { | 358 | refreshImmediately: boolean, |
| 397 | const existing = activePollers.get(cwd); | 359 | ) { |
| 398 | if (!existing) { | 360 | yield* SynchronizedRef.modifyEffect(pollersRef, (activePollers) => { |
| 399 | return [null, activePollers] as const; | 361 | const existing = activePollers.get(cwd); |
| 400 | } | 362 | if (existing) { |
| 363 | const nextPollers = new Map(activePollers); | ||
| 364 | nextPollers.set(cwd, { | ||
| 365 | ...existing, | ||
| 366 | subscriberCount: existing.subscriberCount + 1, | ||
| 367 | }); | ||
| 368 | return Effect.succeed([undefined, nextPollers] as const); | ||
| 369 | } | ||
| 401 | 370 | ||
| 402 | if (existing.subscriberCount > 1) { | 371 | return makeRemoteRefreshLoop(cwd, automaticRemoteRefreshInterval, refreshImmediately).pipe( |
| 372 | Effect.forkIn(broadcasterScope), | ||
| 373 | Effect.map((fiber) => { | ||
| 403 | const nextPollers = new Map(activePollers); | 374 | const nextPollers = new Map(activePollers); |
| 404 | nextPollers.set(cwd, { | 375 | nextPollers.set(cwd, { |
| 405 | ...existing, | 376 | fiber, |
| 406 | subscriberCount: existing.subscriberCount - 1, | 377 | subscriberCount: 1, |
| 407 | }); | 378 | }); |
| 408 | return [null, nextPollers] as const; | 379 | return [undefined, nextPollers] as const; |
| 409 | } | 380 | }), |
| 381 | ); | ||
| 382 | }); | ||
| 383 | }); | ||
| 410 | 384 | ||
| 411 | const nextPollers = new Map(activePollers); | 385 | const releaseRemotePoller = Effect.fn("VcsStatusBroadcaster.releaseRemotePoller")(function* ( |
| 412 | nextPollers.delete(cwd); | 386 | cwd: string, |
| 413 | return [existing.fiber, nextPollers] as const; | 387 | ) { |
| 414 | }); | 388 | const pollerToInterrupt = yield* SynchronizedRef.modify(pollersRef, (activePollers) => { |
| 389 | const existing = activePollers.get(cwd); | ||
| 390 | if (!existing) { | ||
| 391 | return [null, activePollers] as const; | ||
| 392 | } | ||
| 415 | 393 | ||
| 416 | if (pollerToInterrupt) { | 394 | if (existing.subscriberCount > 1) { |
| 417 | yield* Fiber.interrupt(pollerToInterrupt).pipe(Effect.ignore); | 395 | const nextPollers = new Map(activePollers); |
| 396 | nextPollers.set(cwd, { | ||
| 397 | ...existing, | ||
| 398 | subscriberCount: existing.subscriberCount - 1, | ||
| 399 | }); | ||
| 400 | return [null, nextPollers] as const; | ||
| 418 | } | 401 | } |
| 402 | |||
| 403 | const nextPollers = new Map(activePollers); | ||
| 404 | nextPollers.delete(cwd); | ||
| 405 | return [existing.fiber, nextPollers] as const; | ||
| 419 | }); | 406 | }); |
| 420 | 407 | ||
| 421 | const streamStatus: VcsStatusBroadcasterShape["streamStatus"] = (input, options) => | 408 | if (pollerToInterrupt) { |
| 422 | Stream.unwrap( | 409 | yield* Fiber.interrupt(pollerToInterrupt).pipe(Effect.ignore); |
| 423 | Effect.gen(function* () { | 410 | } |
| 424 | const cwd = yield* withFileSystem(normalizeCwd(input.cwd)); | 411 | }); |
| 425 | const subscription = yield* PubSub.subscribe(changesPubSub); | 412 | |
| 426 | const initialLocal = yield* getOrLoadLocalStatus(cwd); | 413 | const streamStatus: VcsStatusBroadcaster["Service"]["streamStatus"] = (input, options) => |
| 427 | const cachedStatus = yield* getCachedStatus(cwd); | 414 | Stream.unwrap( |
| 428 | const initialRemote = cachedStatus?.remote?.value ?? null; | 415 | Effect.gen(function* () { |
| 429 | yield* retainRemotePoller( | 416 | const cwd = yield* withFileSystem(normalizeCwd(input.cwd)); |
| 430 | cwd, | 417 | const subscription = yield* PubSub.subscribe(changesPubSub); |
| 431 | options?.automaticRemoteRefreshInterval ?? | 418 | const initialLocal = yield* getOrLoadLocalStatus(cwd); |
| 432 | Effect.succeed(DEFAULT_VCS_STATUS_REFRESH_INTERVAL), | 419 | const cachedStatus = yield* getCachedStatus(cwd); |
| 433 | cachedStatus?.remote === null || cachedStatus?.remote === undefined, | 420 | const initialRemote = cachedStatus?.remote?.value ?? null; |
| 434 | ); | 421 | yield* retainRemotePoller( |
| 435 | 422 | cwd, | |
| 436 | const release = releaseRemotePoller(cwd).pipe(Effect.ignore, Effect.asVoid); | 423 | options?.automaticRemoteRefreshInterval ?? |
| 437 | 424 | Effect.succeed(DEFAULT_VCS_STATUS_REFRESH_INTERVAL), | |
| 438 | return Stream.concat( | 425 | cachedStatus?.remote === null || cachedStatus?.remote === undefined, |
| 439 | Stream.make({ | 426 | ); |
| 440 | _tag: "snapshot" as const, | ||
| 441 | local: initialLocal, | ||
| 442 | remote: initialRemote, | ||
| 443 | }), | ||
| 444 | Stream.fromSubscription(subscription).pipe( | ||
| 445 | Stream.filter((event) => event.cwd === cwd), | ||
| 446 | Stream.map((event) => event.event), | ||
| 447 | ), | ||
| 448 | ).pipe(Stream.ensuring(release)); | ||
| 449 | }), | ||
| 450 | ); | ||
| 451 | 427 | ||
| 452 | return VcsStatusBroadcaster.of({ | 428 | const release = releaseRemotePoller(cwd).pipe(Effect.ignore, Effect.asVoid); |
| 453 | getStatus, | 429 | |
| 454 | refreshLocalStatus, | 430 | return Stream.concat( |
| 455 | refreshStatus, | 431 | Stream.make({ |
| 456 | streamStatus, | 432 | _tag: "snapshot" as const, |
| 457 | }); | 433 | local: initialLocal, |
| 458 | }), | 434 | remote: initialRemote, |
| 459 | ); | 435 | }), |
| 436 | Stream.fromSubscription(subscription).pipe( | ||
| 437 | Stream.filter((event) => event.cwd === cwd), | ||
| 438 | Stream.map((event) => event.event), | ||
| 439 | ), | ||
| 440 | ).pipe(Stream.ensuring(release)); | ||
| 441 | }), | ||
| 442 | ); | ||
| 443 | |||
| 444 | return VcsStatusBroadcaster.of({ | ||
| 445 | getStatus, | ||
| 446 | refreshLocalStatus, | ||
| 447 | refreshStatus, | ||
| 448 | streamStatus, | ||
| 449 | }); | ||
| 450 | }); | ||
| 451 | |||
| 452 | export const layer = Layer.effect(VcsStatusBroadcaster, make); |
apps/server/src/ws.ts+3-3| ... | @@ -92,7 +92,7 @@ import * as EnvironmentAuth from "./auth/EnvironmentAuth.ts"; | ... | @@ -92,7 +92,7 @@ import * as EnvironmentAuth from "./auth/EnvironmentAuth.ts"; |
| 92 | import * as ProcessDiagnostics from "./diagnostics/ProcessDiagnostics.ts"; | 92 | import * as ProcessDiagnostics from "./diagnostics/ProcessDiagnostics.ts"; |
| 93 | import * as ProcessResourceMonitor from "./diagnostics/ProcessResourceMonitor.ts"; | 93 | import * as ProcessResourceMonitor from "./diagnostics/ProcessResourceMonitor.ts"; |
| 94 | import * as TraceDiagnostics from "./diagnostics/TraceDiagnostics.ts"; | 94 | import * as TraceDiagnostics from "./diagnostics/TraceDiagnostics.ts"; |
| 95 | import * as SourceControlDiscoveryLayer from "./sourceControl/SourceControlDiscovery.ts"; | 95 | import * as SourceControlDiscovery from "./sourceControl/SourceControlDiscovery.ts"; |
| 96 | import * as SourceControlRepositoryService from "./sourceControl/SourceControlRepositoryService.ts"; | 96 | import * as SourceControlRepositoryService from "./sourceControl/SourceControlRepositoryService.ts"; |
| 97 | import * as AzureDevOpsCli from "./sourceControl/AzureDevOpsCli.ts"; | 97 | import * as AzureDevOpsCli from "./sourceControl/AzureDevOpsCli.ts"; |
| 98 | import * as BitbucketApi from "./sourceControl/BitbucketApi.ts"; | 98 | import * as BitbucketApi from "./sourceControl/BitbucketApi.ts"; |
| ... | @@ -278,7 +278,7 @@ const makeWsRpcLayer = (currentSession: EnvironmentAuth.AuthenticatedSession) => | ... | @@ -278,7 +278,7 @@ const makeWsRpcLayer = (currentSession: EnvironmentAuth.AuthenticatedSession) => |
| 278 | yield* RepositoryIdentityResolver.RepositoryIdentityResolver; | 278 | yield* RepositoryIdentityResolver.RepositoryIdentityResolver; |
| 279 | const serverEnvironment = yield* ServerEnvironment.ServerEnvironment; | 279 | const serverEnvironment = yield* ServerEnvironment.ServerEnvironment; |
| 280 | const serverAuth = yield* EnvironmentAuth.EnvironmentAuth; | 280 | const serverAuth = yield* EnvironmentAuth.EnvironmentAuth; |
| 281 | const sourceControlDiscovery = yield* SourceControlDiscoveryLayer.SourceControlDiscovery; | 281 | const sourceControlDiscovery = yield* SourceControlDiscovery.SourceControlDiscovery; |
| 282 | const automaticGitFetchInterval = serverSettings.getSettings.pipe( | 282 | const automaticGitFetchInterval = serverSettings.getSettings.pipe( |
| 283 | Effect.map((settings) => settings.automaticGitFetchInterval), | 283 | Effect.map((settings) => settings.automaticGitFetchInterval), |
| 284 | Effect.catch((cause) => | 284 | Effect.catch((cause) => |
| ... | @@ -1669,7 +1669,7 @@ export const websocketRpcRouteLayer = Layer.unwrap( | ... | @@ -1669,7 +1669,7 @@ export const websocketRpcRouteLayer = Layer.unwrap( |
| 1669 | Layer.provide(PreviewAutomationBroker.layer), | 1669 | Layer.provide(PreviewAutomationBroker.layer), |
| 1670 | Layer.provide(ProviderMaintenanceRunner.layer), | 1670 | Layer.provide(ProviderMaintenanceRunner.layer), |
| 1671 | Layer.provide( | 1671 | Layer.provide( |
| 1672 | SourceControlDiscoveryLayer.layer.pipe( | 1672 | SourceControlDiscovery.layer.pipe( |
| 1673 | Layer.provide( | 1673 | Layer.provide( |
| 1674 | SourceControlProviderRegistry.layer.pipe( | 1674 | SourceControlProviderRegistry.layer.pipe( |
| 1675 | Layer.provide( | 1675 | Layer.provide( |