diff --git a/apps/server/package.json b/apps/server/package.json index a246302618a3..ab9ad49d4e62 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -46,6 +46,7 @@ "@t3tools/provider-opencode": "workspace:*", "@t3tools/provider-pi": "workspace:*", "@t3tools/source-control-azure-devops": "workspace:*", + "@t3tools/source-control-bitbucket": "workspace:*", "@t3tools/source-control-core": "workspace:*", "@t3tools/source-control-forgejo": "workspace:*", "@t3tools/source-control-gitlab": "workspace:*", diff --git a/apps/server/scripts/evaluate-thread-titles.ts b/apps/server/scripts/evaluate-thread-titles.ts index 0b3fb1393239..598f6cc2f930 100644 --- a/apps/server/scripts/evaluate-thread-titles.ts +++ b/apps/server/scripts/evaluate-thread-titles.ts @@ -25,7 +25,6 @@ import * as ThreadTitleLinks from "../src/textGeneration/ThreadTitleLinks.ts"; import * as SourceControlProviderRegistry from "../src/sourceControl/SourceControlProviderRegistry.ts"; import * as GitHubApi from "../src/sourceControl/GitHubApi.ts"; import * as SourceControlBuiltInDrivers from "../src/sourceControl/builtInDrivers.ts"; -import * as BitbucketApi from "../src/sourceControl/BitbucketApi.ts"; import * as ServerSettings from "../src/serverSettings.ts"; import * as VcsProcess from "../src/vcs/VcsProcess.ts"; import * as VcsDriverRegistry from "../src/vcs/VcsDriverRegistry.ts"; @@ -149,12 +148,7 @@ await Effect.runPromise( ProcessRunner.layer, SourceControlProviderRegistry.layer.pipe( Layer.provide( - Layer.mergeAll( - GitHubApi.layerWithDependencies, - SourceControlBuiltInDrivers.layer, - // No saved credentials here; Bitbucket falls back to T3CODE_BITBUCKET_* variables. - BitbucketApi.layer, - ), + Layer.mergeAll(GitHubApi.layerWithDependencies, SourceControlBuiltInDrivers.layer), ), // Default settings: no saved Bitbucket token, gh's own GitHub account choice. Layer.provide(ServerSettings.layerTest()), diff --git a/apps/server/src/pullRequest/PullRequestProviderRateLimit.test.ts b/apps/server/src/pullRequest/PullRequestProviderRateLimit.test.ts index b26bbe8ae9ba..6d35adc07246 100644 --- a/apps/server/src/pullRequest/PullRequestProviderRateLimit.test.ts +++ b/apps/server/src/pullRequest/PullRequestProviderRateLimit.test.ts @@ -1,12 +1,12 @@ import { assert, it } from "@effect/vitest"; import * as AzureDevOpsCli from "@t3tools/source-control-azure-devops/server/AzureDevOpsCli"; -import * as BitbucketApi from "../sourceControl/BitbucketApi.ts"; +import * as BitbucketApi from "@t3tools/source-control-bitbucket/server/BitbucketApi"; import * as GitHubApi from "../sourceControl/GitHubApi.ts"; import * as SourceControlRateLimit from "@t3tools/source-control-core/server/SourceControlRateLimit"; import * as GitLabCli from "@t3tools/source-control-gitlab/server/GitLabCli"; import { azureDevOpsProviderFailure } from "@t3tools/source-control-azure-devops/server/AzureDevOpsPullRequestProvider"; -import { bitbucketProviderFailure } from "./BitbucketPullRequestProvider.ts"; +import { bitbucketProviderFailure } from "@t3tools/source-control-bitbucket/server/BitbucketPullRequestProvider"; import { gitHubProviderFailure } from "./GitHubPullRequestProvider.ts"; import { gitLabProviderFailure } from "@t3tools/source-control-gitlab/server/GitLabPullRequestProvider"; diff --git a/apps/server/src/pullRequest/PullRequestProviderRegistry.ts b/apps/server/src/pullRequest/PullRequestProviderRegistry.ts index 74d7dc471748..5702957a0ff4 100644 --- a/apps/server/src/pullRequest/PullRequestProviderRegistry.ts +++ b/apps/server/src/pullRequest/PullRequestProviderRegistry.ts @@ -3,10 +3,7 @@ import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import type { SourceControlProviderKind } from "@t3tools/contracts"; -import * as BitbucketApi from "../sourceControl/BitbucketApi.ts"; import * as GitHubApi from "../sourceControl/GitHubApi.ts"; -import * as BitbucketPullRequestApi from "./BitbucketPullRequestApi.ts"; -import * as BitbucketPullRequestProvider from "./BitbucketPullRequestProvider.ts"; import * as GitHubPullRequestApi from "./GitHubPullRequestApi.ts"; import * as GitHubPullRequestProvider from "./GitHubPullRequestProvider.ts"; import * as BuiltInDrivers from "../sourceControl/builtInDrivers.ts"; @@ -39,10 +36,7 @@ export function fromProviders( * @public Service construction is part of the canonical Effect module API. */ export const make = Effect.gen(function* () { - const providers = yield* Effect.all([ - GitHubPullRequestProvider.make, - BitbucketPullRequestProvider.make, - ]); + const providers = yield* Effect.all([GitHubPullRequestProvider.make]); const drivers = yield* Effect.forEach(BuiltInDrivers.BUILT_IN_SOURCE_CONTROL_DRIVERS, (driver) => driver.make.pipe(Effect.map((instance) => instance.pullRequests)), ); @@ -62,5 +56,4 @@ export const layer = Layer.effect(PullRequestProviderRegistry, make).pipe( ), ), Layer.provide(BuiltInDrivers.layer), - Layer.provide(BitbucketPullRequestApi.layer.pipe(Layer.provide(BitbucketApi.layer))), ); diff --git a/apps/server/src/server.ts b/apps/server/src/server.ts index 8ea3b5cda198..ff4c4ef6c214 100644 --- a/apps/server/src/server.ts +++ b/apps/server/src/server.ts @@ -52,7 +52,6 @@ import * as ProviderHostLive from "./provider/ProviderHostLive.ts"; import * as AcpRegistrySupport from "@t3tools/provider-acp-registry/server/AcpRegistrySupport"; import * as CheckpointDiffQuery from "./checkpointing/CheckpointDiffQuery.ts"; import * as CheckpointStore from "./checkpointing/CheckpointStore.ts"; -import * as BitbucketApi from "./sourceControl/BitbucketApi.ts"; import * as GitHubApi from "./sourceControl/GitHubApi.ts"; import * as ForgejoCli from "@t3tools/source-control-forgejo/server/ForgejoCli"; import * as SourceControlBuiltInDrivers from "./sourceControl/builtInDrivers.ts"; @@ -297,13 +296,7 @@ const layerPersistence = Layer.empty.pipe(Layer.provideMerge(SqlitePersistence.l const layerVcsDriverRegistry = VcsDriverRegistry.layer.pipe(Layer.provide(VcsProjectConfig.layer)); const layerSourceControlProviderRegistry = SourceControlProviderRegistry.layer.pipe( - Layer.provide( - Layer.mergeAll( - BitbucketApi.layer, - GitHubApi.layerWithDependencies, - SourceControlBuiltInDrivers.layer, - ), - ), + Layer.provide(Layer.mergeAll(GitHubApi.layerWithDependencies, SourceControlBuiltInDrivers.layer)), Layer.provideMerge(GitVcsDriver.layer), Layer.provideMerge(layerVcsDriverRegistry), ); diff --git a/apps/server/src/sourceControl/BitbucketApi.test.ts b/apps/server/src/sourceControl/BitbucketApi.test.ts index 352bfc60d970..85e14a61bc62 100644 --- a/apps/server/src/sourceControl/BitbucketApi.test.ts +++ b/apps/server/src/sourceControl/BitbucketApi.test.ts @@ -11,11 +11,13 @@ import * as TestClock from "effect/testing/TestClock"; import { HttpClient, HttpClientError, HttpClientRequest, HttpClientResponse } from "effect/http"; import { GitCommandError } from "@t3tools/contracts"; -import * as BitbucketApi from "./BitbucketApi.ts"; +import * as BitbucketApi from "@t3tools/source-control-bitbucket/server/BitbucketApi"; import * as ServerSettings from "../serverSettings.ts"; import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts"; import type * as VcsDriver from "../vcs/VcsDriver.ts"; +import * as ServerSourceControlHost from "./ServerSourceControlHost.ts"; +import * as VcsProcess from "../vcs/VcsProcess.ts"; const isBitbucketRepositoryLocatorError = Schema.is(BitbucketApi.BitbucketRepositoryLocatorError); @@ -139,6 +141,8 @@ function makeLayer(input: { ); const layer = BitbucketApi.layer.pipe( + Layer.provide(ServerSourceControlHost.layer), + Layer.provide(Layer.mock(VcsProcess.VcsProcess)({})), Layer.provide( Layer.succeed( HttpClient.HttpClient, diff --git a/apps/server/src/sourceControl/ServerSourceControlHost.ts b/apps/server/src/sourceControl/ServerSourceControlHost.ts index f458795ab76e..a21e7d5fe089 100644 --- a/apps/server/src/sourceControl/ServerSourceControlHost.ts +++ b/apps/server/src/sourceControl/ServerSourceControlHost.ts @@ -9,6 +9,8 @@ import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as ServerSettings from "../serverSettings.ts"; +import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; +import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts"; import * as VcsProcess from "../vcs/VcsProcess.ts"; export const layer = Layer.effect( @@ -16,9 +18,23 @@ export const layer = Layer.effect( Effect.gen(function* () { const serverSettings = yield* ServerSettings.ServerSettingsService; const process = yield* VcsProcess.VcsProcess; + const git = yield* GitVcsDriver.GitVcsDriver; + const vcsRegistry = yield* VcsDriverRegistry.VcsDriverRegistry; return SourceControlHost.SourceControlHost.of({ settings: { get: serverSettings.getSettings }, process: { run: process.run }, + git: { + remotes: (cwd) => + vcsRegistry.resolve({ cwd }).pipe(Effect.map((handle) => handle.driver.listRemotes(cwd))), + readConfigValue: git.readConfigValue, + resolvePrimaryRemoteName: git.resolvePrimaryRemoteName, + ensureRemote: git.ensureRemote, + listLocalBranchNames: git.listLocalBranchNames, + fetchRemoteBranch: git.fetchRemoteBranch, + fetchRemoteTrackingBranch: git.fetchRemoteTrackingBranch, + setBranchUpstream: git.setBranchUpstream, + switchRef: git.switchRef, + }, }); }), ); diff --git a/apps/server/src/sourceControl/SourceControlDiscovery.test.ts b/apps/server/src/sourceControl/SourceControlDiscovery.test.ts index c87d731953da..3cda7d31d9fb 100644 --- a/apps/server/src/sourceControl/SourceControlDiscovery.test.ts +++ b/apps/server/src/sourceControl/SourceControlDiscovery.test.ts @@ -18,7 +18,8 @@ import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts"; import * as VcsProcess from "../vcs/VcsProcess.ts"; import * as AzureDevOpsCli from "@t3tools/source-control-azure-devops/server/AzureDevOpsCli"; import * as AzureDevOpsPullRequestCli from "@t3tools/source-control-azure-devops/server/AzureDevOpsPullRequestCli"; -import * as BitbucketApi from "./BitbucketApi.ts"; +import * as BitbucketApi from "@t3tools/source-control-bitbucket/server/BitbucketApi"; +import * as BitbucketPullRequestApi from "@t3tools/source-control-bitbucket/server/BitbucketPullRequestApi"; import * as GitHubApi from "./GitHubApi.ts"; import * as GitLabCli from "@t3tools/source-control-gitlab/server/GitLabCli"; import * as GitLabPullRequestCli from "@t3tools/source-control-gitlab/server/GitLabPullRequestCli"; @@ -43,6 +44,7 @@ const layerSourceControlProviderRegistryTest = (input: { Layer.mock(AzureDevOpsCli.AzureDevOpsCli)({}), Layer.mock(AzureDevOpsPullRequestCli.AzureDevOpsPullRequestCli)({}), Layer.mock(BitbucketApi.BitbucketApi)(input.bitbucket), + Layer.mock(BitbucketPullRequestApi.BitbucketPullRequestApi)({}), ServerSettings.ServerSettingsService.layerTest(), Layer.mock(GitHubApi.GitHubApi)({}), Layer.mock(GitVcsDriver.GitVcsDriver)({}), @@ -53,9 +55,11 @@ const layerSourceControlProviderRegistryTest = (input: { Layer.mock(VcsProcess.VcsProcess)(input.process), ServerSourceControlHost.layer.pipe( Layer.provide( - Layer.merge( + Layer.mergeAll( Layer.mock(VcsProcess.VcsProcess)(input.process), ServerSettings.ServerSettingsService.layerTest(), + Layer.mock(GitVcsDriver.GitVcsDriver)({}), + Layer.mock(VcsDriverRegistry.VcsDriverRegistry)({}), ), ), ), @@ -395,6 +399,7 @@ it.effect( SourceControlHost.SourceControlHost, SourceControlHost.SourceControlHost.of({ settings: host.settings, + git: host.git, process: { run: (input) => { if (input.args[0] !== "fetch") return git.run(input); @@ -529,6 +534,8 @@ it.effect( ServerSourceControlHost.layer.pipe( Layer.provideMerge(VcsProcess.layer), Layer.provideMerge(ServerSettings.ServerSettingsService.layerTest()), + Layer.provideMerge(Layer.mock(GitVcsDriver.GitVcsDriver)({})), + Layer.provideMerge(Layer.mock(VcsDriverRegistry.VcsDriverRegistry)({})), Layer.provideMerge(NodeServices.layer), ), ), diff --git a/apps/server/src/sourceControl/SourceControlProviderRegistry.test.ts b/apps/server/src/sourceControl/SourceControlProviderRegistry.test.ts index 582848b38a4b..b16ccfa44e73 100644 --- a/apps/server/src/sourceControl/SourceControlProviderRegistry.test.ts +++ b/apps/server/src/sourceControl/SourceControlProviderRegistry.test.ts @@ -15,7 +15,8 @@ import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts"; import * as VcsProcess from "../vcs/VcsProcess.ts"; import * as AzureDevOpsCli from "@t3tools/source-control-azure-devops/server/AzureDevOpsCli"; import * as AzureDevOpsPullRequestCli from "@t3tools/source-control-azure-devops/server/AzureDevOpsPullRequestCli"; -import * as BitbucketApi from "./BitbucketApi.ts"; +import * as BitbucketApi from "@t3tools/source-control-bitbucket/server/BitbucketApi"; +import * as BitbucketPullRequestApi from "@t3tools/source-control-bitbucket/server/BitbucketPullRequestApi"; import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; import * as GitHubApi from "./GitHubApi.ts"; import * as GitHubCredentials from "./GitHubCredentials.ts"; @@ -101,12 +102,18 @@ function makeRegistry(input: { layerProcess, ServerSourceControlHost.layer.pipe( Layer.provide( - Layer.merge(layerProcess, ServerSettings.ServerSettingsService.layerTest()), + Layer.mergeAll( + layerProcess, + layerRegistry, + ServerSettings.ServerSettingsService.layerTest(), + Layer.mock(GitVcsDriver.GitVcsDriver)({}), + ), ), ), Layer.mock(AzureDevOpsCli.AzureDevOpsCli)({}), Layer.mock(AzureDevOpsPullRequestCli.AzureDevOpsPullRequestCli)({}), Layer.mock(BitbucketApi.BitbucketApi)({}), + Layer.mock(BitbucketPullRequestApi.BitbucketPullRequestApi)({}), ServerSettings.ServerSettingsService.layerTest(), Layer.mock(GitHubApi.GitHubApi)({ // No GitHub credential unless a test supplies one, so custom hosts stay unclaimed. diff --git a/apps/server/src/sourceControl/SourceControlProviderRegistry.ts b/apps/server/src/sourceControl/SourceControlProviderRegistry.ts index 9eeae3d935f7..607e1b957b9b 100644 --- a/apps/server/src/sourceControl/SourceControlProviderRegistry.ts +++ b/apps/server/src/sourceControl/SourceControlProviderRegistry.ts @@ -11,7 +11,6 @@ import { import type { SourceControlProviderKind } from "@t3tools/contracts"; import { detectSourceControlProviderFromRemoteUrl } from "@t3tools/shared/sourceControl"; -import * as BitbucketSourceControlProvider from "./BitbucketSourceControlProvider.ts"; import * as GitHubSourceControlProvider from "./GitHubSourceControlProvider.ts"; import * as BuiltInDrivers from "./builtInDrivers.ts"; import * as SourceControlProvider from "@t3tools/source-control-core/server/SourceControlProvider"; @@ -305,8 +304,6 @@ export const makeWithProviders = Effect.fn("makeSourceControlProviderRegistryWit export const make = Effect.gen(function* () { const github = yield* GitHubSourceControlProvider.make; const githubDiscovery = yield* GitHubSourceControlProvider.makeDiscovery; - const bitbucket = yield* BitbucketSourceControlProvider.make; - const bitbucketDiscovery = yield* BitbucketSourceControlProvider.makeDiscovery; const drivers = yield* Effect.forEach(BuiltInDrivers.BUILT_IN_SOURCE_CONTROL_DRIVERS, (driver) => driver.make.pipe( Effect.map((instance): SourceControlProviderRegistration => ({ @@ -324,11 +321,6 @@ export const make = Effect.gen(function* () { provider: github, discovery: githubDiscovery, }, - { - kind: "bitbucket", - provider: bitbucket, - discovery: bitbucketDiscovery, - }, ...drivers, ]; return yield* makeWithProviders( diff --git a/apps/server/src/sourceControl/SourceControlRepositoryService.test.ts b/apps/server/src/sourceControl/SourceControlRepositoryService.test.ts index 8aba02284403..c051ab47f42f 100644 --- a/apps/server/src/sourceControl/SourceControlRepositoryService.test.ts +++ b/apps/server/src/sourceControl/SourceControlRepositoryService.test.ts @@ -13,8 +13,8 @@ import { GitCommandError, SourceControlProviderError } from "@t3tools/contracts" import * as ServerConfig from "../config.ts"; import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; -import * as BitbucketApi from "./BitbucketApi.ts"; -import * as BitbucketSourceControlProvider from "./BitbucketSourceControlProvider.ts"; +import * as BitbucketApi from "@t3tools/source-control-bitbucket/server/BitbucketApi"; +import * as BitbucketSourceControlProvider from "@t3tools/source-control-bitbucket/server/BitbucketSourceControlProvider"; import type * as SourceControlProvider from "@t3tools/source-control-core/server/SourceControlProvider"; import * as SourceControlProviderRegistry from "./SourceControlProviderRegistry.ts"; import * as SourceControlRepositoryService from "./SourceControlRepositoryService.ts"; diff --git a/apps/server/src/sourceControl/SourceControlRepositoryService.ts b/apps/server/src/sourceControl/SourceControlRepositoryService.ts index 4a4d1fec6434..1bbca1e2b3d5 100644 --- a/apps/server/src/sourceControl/SourceControlRepositoryService.ts +++ b/apps/server/src/sourceControl/SourceControlRepositoryService.ts @@ -27,7 +27,7 @@ import { type GitCloneProgressLine, } from "../project/gitCloneProgress.ts"; import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; -import * as BitbucketApi from "./BitbucketApi.ts"; +import * as BitbucketApi from "@t3tools/source-control-bitbucket/server/BitbucketApi"; import * as SourceControlProviderRegistry from "./SourceControlProviderRegistry.ts"; const isSourceControlRepositoryError = Schema.is(SourceControlRepositoryError); const isSourceControlProviderError = Schema.is(SourceControlProviderError); diff --git a/apps/server/src/sourceControl/builtInDrivers.ts b/apps/server/src/sourceControl/builtInDrivers.ts index 83ee1d35e8f5..9a972e9e5f00 100644 --- a/apps/server/src/sourceControl/builtInDrivers.ts +++ b/apps/server/src/sourceControl/builtInDrivers.ts @@ -10,6 +10,9 @@ import * as AzureDevOpsCli from "@t3tools/source-control-azure-devops/server/AzureDevOpsCli"; import * as AzureDevOpsPullRequestCli from "@t3tools/source-control-azure-devops/server/AzureDevOpsPullRequestCli"; import * as AzureDevOpsDriver from "@t3tools/source-control-azure-devops/server/driver"; +import * as BitbucketApi from "@t3tools/source-control-bitbucket/server/BitbucketApi"; +import * as BitbucketPullRequestApi from "@t3tools/source-control-bitbucket/server/BitbucketPullRequestApi"; +import * as BitbucketDriver from "@t3tools/source-control-bitbucket/server/driver"; import * as ForgejoCli from "@t3tools/source-control-forgejo/server/ForgejoCli"; import * as ForgejoDriver from "@t3tools/source-control-forgejo/server/driver"; import * as GitLabCli from "@t3tools/source-control-gitlab/server/GitLabCli"; @@ -20,7 +23,12 @@ import * as Layer from "effect/Layer"; import * as ServerSourceControlHost from "./ServerSourceControlHost.ts"; -const drivers = [GitLabDriver.driver, AzureDevOpsDriver.driver, ForgejoDriver.driver]; +const drivers = [ + GitLabDriver.driver, + AzureDevOpsDriver.driver, + BitbucketDriver.driver, + ForgejoDriver.driver, +]; /** Every service a built-in driver's `make` needs; the server's layers must provide them all. */ export type BuiltInSourceControlDriversEnv = @@ -33,6 +41,7 @@ export const BUILT_IN_SOURCE_CONTROL_DRIVERS: ReadonlyArray< /** The services the built-in drivers' packages own, plus the host port they all run against. */ export const layer = Layer.mergeAll( AzureDevOpsPullRequestCli.layer.pipe(Layer.provideMerge(AzureDevOpsCli.layer)), + BitbucketPullRequestApi.layer.pipe(Layer.provideMerge(BitbucketApi.layer)), ForgejoCli.layer, GitLabPullRequestCli.layer.pipe(Layer.provideMerge(GitLabCli.layer)), ).pipe(Layer.provideMerge(ServerSourceControlHost.layer)); diff --git a/apps/server/src/ws.ts b/apps/server/src/ws.ts index 19ed2600ba5e..a3b239ffaa62 100644 --- a/apps/server/src/ws.ts +++ b/apps/server/src/ws.ts @@ -229,7 +229,6 @@ import * as SqlClient from "effect/sql/SqlClient"; import * as PullRequestSyncReactor from "./orchestration-v2/PullRequestSyncReactor.ts"; import * as SourceControlDiscovery from "./sourceControl/SourceControlDiscovery.ts"; import * as SourceControlRepositoryService from "./sourceControl/SourceControlRepositoryService.ts"; -import * as BitbucketApi from "./sourceControl/BitbucketApi.ts"; import * as GitHubApi from "./sourceControl/GitHubApi.ts"; import * as SourceControlBuiltInDrivers from "./sourceControl/builtInDrivers.ts"; import * as SourceControlProviderRegistry from "./sourceControl/SourceControlProviderRegistry.ts"; @@ -3202,7 +3201,6 @@ export const layer = Layer.unwrap( SourceControlProviderRegistry.layer.pipe( Layer.provide( Layer.mergeAll( - BitbucketApi.layer, GitHubApi.layerWithDependencies, SourceControlBuiltInDrivers.layer, ), diff --git a/docs/internals/pull-request-file-revisions.md b/docs/internals/pull-request-file-revisions.md index 8fd0dfdb69db..dfc03a436aef 100644 --- a/docs/internals/pull-request-file-revisions.md +++ b/docs/internals/pull-request-file-revisions.md @@ -27,7 +27,7 @@ What "never got to look at" is belongs to the host, and the contract cannot know - [Azure DevOps](../../packages/source-control-azure-devops/src/server/AzureDevOpsPullRequestProvider.ts) reads every version off one iteration listing, so a change too long to follow to its end leaves the paths past that point out. -- [Bitbucket](../../apps/server/src/pullRequest/BitbucketPullRequestApi.ts) reads them off the +- [Bitbucket](../../packages/source-control-bitbucket/src/server/BitbucketPullRequestApi.ts) reads them off the pull request's own patch, the only place it states a file's version, so a patch cut short at the byte ceiling leaves the paths past the cut out. - [GitLab](../../packages/source-control-gitlab/src/server/GitLabPullRequestCli.ts) asks in batches, so a batch diff --git a/package.json b/package.json index 4e0ba35ecc7e..764f45f27c97 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "tc": "vp run -r --concurrency-limit 2 typecheck", "lint": "vp lint --report-unused-disable-directives", "knip": "knip --preprocessor ./scripts/knip-schemas.ts", - "knip:check": "knip --include files,dependencies --no-config-hints && knip --workspace apps/server --workspace apps/desktop --workspace apps/web --workspace packages/client-runtime --workspace packages/contracts --workspace packages/effect-acp --workspace packages/effect-codex-app-server --workspace packages/shared --workspace packages/ssh --workspace packages/tailscale --workspace packages/provider-core --workspace packages/provider-acp --workspace packages/provider-acp-registry --workspace packages/provider-testing --workspace packages/provider-pi --workspace packages/provider-muse --workspace packages/provider-opencode --workspace packages/provider-cursor --workspace packages/provider-grok --workspace packages/source-control-core --workspace packages/source-control-azure-devops --workspace packages/source-control-forgejo --workspace packages/source-control-gitlab --workspace packages/source-control-testing --exports --preprocessor ./scripts/knip-schemas.ts --no-config-hints", + "knip:check": "knip --include files,dependencies --no-config-hints && knip --workspace apps/server --workspace apps/desktop --workspace apps/web --workspace packages/client-runtime --workspace packages/contracts --workspace packages/effect-acp --workspace packages/effect-codex-app-server --workspace packages/shared --workspace packages/ssh --workspace packages/tailscale --workspace packages/provider-core --workspace packages/provider-acp --workspace packages/provider-acp-registry --workspace packages/provider-testing --workspace packages/provider-pi --workspace packages/provider-muse --workspace packages/provider-opencode --workspace packages/provider-cursor --workspace packages/provider-grok --workspace packages/source-control-core --workspace packages/source-control-azure-devops --workspace packages/source-control-bitbucket --workspace packages/source-control-forgejo --workspace packages/source-control-gitlab --workspace packages/source-control-testing --exports --preprocessor ./scripts/knip-schemas.ts --no-config-hints", "knip:production": "knip --production --preprocessor ./scripts/knip-schemas.ts", "lint:mobile": "node scripts/mobile-native-static-check.ts", "test": "vp run -r test", diff --git a/packages/source-control-bitbucket/package.json b/packages/source-control-bitbucket/package.json new file mode 100644 index 000000000000..c878093ef54e --- /dev/null +++ b/packages/source-control-bitbucket/package.json @@ -0,0 +1,51 @@ +{ + "name": "@t3tools/source-control-bitbucket", + "private": true, + "type": "module", + "exports": { + "./server/BitbucketApi": { + "types": "./src/server/BitbucketApi.ts", + "import": "./src/server/BitbucketApi.ts" + }, + "./server/BitbucketPullRequestApi": { + "types": "./src/server/BitbucketPullRequestApi.ts", + "import": "./src/server/BitbucketPullRequestApi.ts" + }, + "./server/bitbucketPullRequestJson": { + "types": "./src/server/bitbucketPullRequestJson.ts", + "import": "./src/server/bitbucketPullRequestJson.ts" + }, + "./server/BitbucketPullRequestProvider": { + "types": "./src/server/BitbucketPullRequestProvider.ts", + "import": "./src/server/BitbucketPullRequestProvider.ts" + }, + "./server/bitbucketPullRequests": { + "types": "./src/server/bitbucketPullRequests.ts", + "import": "./src/server/bitbucketPullRequests.ts" + }, + "./server/BitbucketSourceControlProvider": { + "types": "./src/server/BitbucketSourceControlProvider.ts", + "import": "./src/server/BitbucketSourceControlProvider.ts" + }, + "./server/driver": { + "types": "./src/server/driver.ts", + "import": "./src/server/driver.ts" + } + }, + "scripts": { + "typecheck": "tsc --noEmit", + "test": "vp test run --config ../../vite.config.ts --dir ." + }, + "dependencies": { + "@t3tools/contracts": "workspace:*", + "@t3tools/provider-core": "workspace:*", + "@t3tools/shared": "workspace:*", + "@t3tools/source-control-core": "workspace:*", + "effect": "catalog:" + }, + "devDependencies": { + "@effect/vitest": "catalog:", + "@types/node": "catalog:", + "vite-plus": "catalog:" + } +} diff --git a/apps/server/src/sourceControl/BitbucketApi.ts b/packages/source-control-bitbucket/src/server/BitbucketApi.ts similarity index 98% rename from apps/server/src/sourceControl/BitbucketApi.ts rename to packages/source-control-bitbucket/src/server/BitbucketApi.ts index f7fa5cc4568c..3df66527baee 100644 --- a/apps/server/src/sourceControl/BitbucketApi.ts +++ b/packages/source-control-bitbucket/src/server/BitbucketApi.ts @@ -30,9 +30,7 @@ import { } from "./bitbucketPullRequests.ts"; import { collectUint8StreamText } from "@t3tools/provider-core/server/collectStreamText"; import * as SourceControlProvider from "@t3tools/source-control-core/server/SourceControlProvider"; -import * as ServerSettings from "../serverSettings.ts"; -import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; -import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts"; +import * as SourceControlHost from "@t3tools/source-control-core/server/SourceControlHost"; import { retryAtFromHeader } from "@t3tools/source-control-core/server/SourceControlRateLimit"; const DEFAULT_API_BASE_URL = "https://api.bitbucket.org/2.0"; @@ -390,7 +388,7 @@ export class BitbucketApi extends Context.Service< readonly force?: boolean; }) => Effect.Effect; } ->()("t3/sourceControl/BitbucketApi") {} +>()("@t3tools/source-control-bitbucket/server/BitbucketApi") {} function nonEmpty(value: string | undefined): Option.Option { const trimmed = value?.trim(); @@ -657,16 +655,15 @@ function responseError( /** @public Service construction is part of the canonical Effect module API. */ export const make = Effect.gen(function* () { const config = yield* BitbucketApiEnvConfig; - const serverSettings = yield* ServerSettings.ServerSettingsService; + const host = yield* SourceControlHost.SourceControlHost; + const git = host.git; const httpClient = yield* HttpClient.HttpClient; const fileSystem = yield* FileSystem.FileSystem; - const git = yield* GitVcsDriver.GitVcsDriver; - const vcsRegistry = yield* VcsDriverRegistry.VcsDriverRegistry; const apiUrl = (path: string) => `${config.baseUrl.replace(/\/+$/u, "")}${path}`; // Read on every request so credentials saved in settings apply without a restart. - const currentCredential = serverSettings.getSettings.pipe( + const currentCredential = host.settings.get.pipe( Effect.map((settings) => resolveCredential(settings.bitbucket, config)), Effect.catch((error) => // No cause: a settings decode error can quote a hand-edited token. @@ -739,7 +736,7 @@ export const make = Effect.gen(function* () { : null; if (fromContext) return fromContext; - const handle = yield* vcsRegistry.resolve({ cwd: input.cwd }).pipe( + const listRemotes = yield* git.remotes(input.cwd).pipe( Effect.mapError( (cause) => new BitbucketRepositoryVcsResolveError({ @@ -748,7 +745,7 @@ export const make = Effect.gen(function* () { }), ), ); - const remotes = yield* handle.driver.listRemotes(input.cwd).pipe( + const remotes = yield* listRemotes.pipe( Effect.mapError( (cause) => new BitbucketRepositoryRemotesListError({ diff --git a/apps/server/src/pullRequest/BitbucketPullRequestApi.test.ts b/packages/source-control-bitbucket/src/server/BitbucketPullRequestApi.test.ts similarity index 99% rename from apps/server/src/pullRequest/BitbucketPullRequestApi.test.ts rename to packages/source-control-bitbucket/src/server/BitbucketPullRequestApi.test.ts index d614dd90a7e7..8d8feaba8150 100644 --- a/apps/server/src/pullRequest/BitbucketPullRequestApi.test.ts +++ b/packages/source-control-bitbucket/src/server/BitbucketPullRequestApi.test.ts @@ -4,7 +4,7 @@ import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as TestClock from "effect/testing/TestClock"; -import * as BitbucketApi from "../sourceControl/BitbucketApi.ts"; +import * as BitbucketApi from "./BitbucketApi.ts"; import * as BitbucketPullRequestApi from "./BitbucketPullRequestApi.ts"; const mockedRequest = vi.fn(); diff --git a/apps/server/src/pullRequest/BitbucketPullRequestApi.ts b/packages/source-control-bitbucket/src/server/BitbucketPullRequestApi.ts similarity index 99% rename from apps/server/src/pullRequest/BitbucketPullRequestApi.ts rename to packages/source-control-bitbucket/src/server/BitbucketPullRequestApi.ts index b62b86ffe48f..6dd04a4a0773 100644 --- a/apps/server/src/pullRequest/BitbucketPullRequestApi.ts +++ b/packages/source-control-bitbucket/src/server/BitbucketPullRequestApi.ts @@ -21,7 +21,7 @@ import type { PullRequestReviewerCandidateList, } from "@t3tools/contracts"; -import * as BitbucketApi from "../sourceControl/BitbucketApi.ts"; +import * as BitbucketApi from "./BitbucketApi.ts"; import { parseDiffFileRevisions } from "@t3tools/source-control-core/server/diffFileRevisions"; import { buildReviewThreads, @@ -304,7 +304,7 @@ export class BitbucketPullRequestApi extends Context.Service< readonly resolved: boolean; }) => Effect.Effect; } ->()("t3/pullRequest/BitbucketPullRequestApi") {} +>()("@t3tools/source-control-bitbucket/server/BitbucketPullRequestApi") {} /** `workspace/slug`; Bitbucket has no deeper nesting to address. */ function repositorySegments( diff --git a/apps/server/src/pullRequest/BitbucketPullRequestProvider.test.ts b/packages/source-control-bitbucket/src/server/BitbucketPullRequestProvider.test.ts similarity index 99% rename from apps/server/src/pullRequest/BitbucketPullRequestProvider.test.ts rename to packages/source-control-bitbucket/src/server/BitbucketPullRequestProvider.test.ts index 7c2d58af950e..b5470628be0a 100644 --- a/apps/server/src/pullRequest/BitbucketPullRequestProvider.test.ts +++ b/packages/source-control-bitbucket/src/server/BitbucketPullRequestProvider.test.ts @@ -3,7 +3,7 @@ import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as Result from "effect/Result"; -import * as BitbucketApi from "../sourceControl/BitbucketApi.ts"; +import * as BitbucketApi from "./BitbucketApi.ts"; import * as BitbucketPullRequestApi from "./BitbucketPullRequestApi.ts"; import { decodePullRequestJson } from "./bitbucketPullRequestJson.ts"; import { diff --git a/apps/server/src/pullRequest/BitbucketPullRequestProvider.ts b/packages/source-control-bitbucket/src/server/BitbucketPullRequestProvider.ts similarity index 100% rename from apps/server/src/pullRequest/BitbucketPullRequestProvider.ts rename to packages/source-control-bitbucket/src/server/BitbucketPullRequestProvider.ts diff --git a/apps/server/src/sourceControl/BitbucketSourceControlProvider.test.ts b/packages/source-control-bitbucket/src/server/BitbucketSourceControlProvider.test.ts similarity index 100% rename from apps/server/src/sourceControl/BitbucketSourceControlProvider.test.ts rename to packages/source-control-bitbucket/src/server/BitbucketSourceControlProvider.test.ts diff --git a/apps/server/src/sourceControl/BitbucketSourceControlProvider.ts b/packages/source-control-bitbucket/src/server/BitbucketSourceControlProvider.ts similarity index 100% rename from apps/server/src/sourceControl/BitbucketSourceControlProvider.ts rename to packages/source-control-bitbucket/src/server/BitbucketSourceControlProvider.ts diff --git a/apps/server/src/pullRequest/bitbucketPullRequestJson.test.ts b/packages/source-control-bitbucket/src/server/bitbucketPullRequestJson.test.ts similarity index 100% rename from apps/server/src/pullRequest/bitbucketPullRequestJson.test.ts rename to packages/source-control-bitbucket/src/server/bitbucketPullRequestJson.test.ts diff --git a/apps/server/src/pullRequest/bitbucketPullRequestJson.ts b/packages/source-control-bitbucket/src/server/bitbucketPullRequestJson.ts similarity index 100% rename from apps/server/src/pullRequest/bitbucketPullRequestJson.ts rename to packages/source-control-bitbucket/src/server/bitbucketPullRequestJson.ts diff --git a/apps/server/src/sourceControl/bitbucketPullRequests.ts b/packages/source-control-bitbucket/src/server/bitbucketPullRequests.ts similarity index 100% rename from apps/server/src/sourceControl/bitbucketPullRequests.ts rename to packages/source-control-bitbucket/src/server/bitbucketPullRequests.ts diff --git a/packages/source-control-bitbucket/src/server/driver.ts b/packages/source-control-bitbucket/src/server/driver.ts new file mode 100644 index 000000000000..06cd225ab4e4 --- /dev/null +++ b/packages/source-control-bitbucket/src/server/driver.ts @@ -0,0 +1,20 @@ +/** + * The Bitbucket Cloud driver: repository operations, discovery, and pull requests through the + * Bitbucket REST API. + * + * @module source-control-bitbucket/server/driver + */ +import { defineSourceControlDriver } from "@t3tools/source-control-core/server/driver"; +import * as Effect from "effect/Effect"; + +import * as BitbucketPullRequestProvider from "./BitbucketPullRequestProvider.ts"; +import * as BitbucketSourceControlProvider from "./BitbucketSourceControlProvider.ts"; + +export const driver = defineSourceControlDriver({ + kind: "bitbucket", + make: Effect.all({ + sourceControl: BitbucketSourceControlProvider.make, + discovery: BitbucketSourceControlProvider.makeDiscovery, + pullRequests: BitbucketPullRequestProvider.make, + }), +}); diff --git a/packages/source-control-bitbucket/tsconfig.json b/packages/source-control-bitbucket/tsconfig.json new file mode 100644 index 000000000000..374bac55202d --- /dev/null +++ b/packages/source-control-bitbucket/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "types": ["node"] + }, + "include": ["src"] +} diff --git a/packages/source-control-core/src/server/SourceControlHost.ts b/packages/source-control-core/src/server/SourceControlHost.ts index d779f86fe0e2..564b318d85b4 100644 --- a/packages/source-control-core/src/server/SourceControlHost.ts +++ b/packages/source-control-core/src/server/SourceControlHost.ts @@ -7,9 +7,20 @@ * its own API and CLI code. HTTP, the filesystem, and paths come from Effect's platform * services directly. * + * `git` is the subset of the server's git driver that checking out a change request needs; it + * grows only when a provider needs another operation. + * * @module source-control-core/server/SourceControlHost */ -import type { ServerSettings, ServerSettingsError, VcsError } from "@t3tools/contracts"; +import type { + GitCommandError, + ServerSettings, + ServerSettingsError, + VcsError, + VcsListRemotesResult, + VcsSwitchRefInput, + VcsSwitchRefResult, +} from "@t3tools/contracts"; import * as Context from "effect/Context"; import type * as Effect from "effect/Effect"; import type * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"; @@ -43,6 +54,12 @@ export interface SourceControlProcessOutput { readonly stderrInvalidUtf8?: boolean; } +export interface SourceControlGitRemoteBranch { + readonly cwd: string; + readonly remoteName: string; + readonly remoteBranch: string; +} + export class SourceControlHost extends Context.Service< SourceControlHost, { @@ -55,5 +72,38 @@ export class SourceControlHost extends Context.Service< input: SourceControlProcessInput, ) => Effect.Effect; }; + readonly git: { + /** + * Lists the remotes of the repository at `cwd`. The outer effect resolves the repository + * and fails when the server cannot drive one there; the inner one lists its remotes. + */ + readonly remotes: ( + cwd: string, + ) => Effect.Effect, VcsError>; + readonly readConfigValue: ( + cwd: string, + key: string, + ) => Effect.Effect; + readonly resolvePrimaryRemoteName: (cwd: string) => Effect.Effect; + /** Adds a remote for `url` unless one exists, and returns the name it is under. */ + readonly ensureRemote: (input: { + readonly cwd: string; + readonly preferredName: string; + readonly url: string; + }) => Effect.Effect; + readonly listLocalBranchNames: (cwd: string) => Effect.Effect; + readonly fetchRemoteBranch: ( + input: SourceControlGitRemoteBranch & { readonly localBranch: string }, + ) => Effect.Effect; + readonly fetchRemoteTrackingBranch: ( + input: SourceControlGitRemoteBranch, + ) => Effect.Effect; + readonly setBranchUpstream: ( + input: SourceControlGitRemoteBranch & { readonly branch: string }, + ) => Effect.Effect; + readonly switchRef: ( + input: VcsSwitchRefInput, + ) => Effect.Effect; + }; } >()("@t3tools/source-control-core/server/SourceControlHost") {} diff --git a/packages/source-control-testing/src/TestSourceControlHost.ts b/packages/source-control-testing/src/TestSourceControlHost.ts index 293339d0867e..eee1a96f13a9 100644 --- a/packages/source-control-testing/src/TestSourceControlHost.ts +++ b/packages/source-control-testing/src/TestSourceControlHost.ts @@ -1,7 +1,7 @@ /** * A `SourceControlHost.SourceControlHost` for provider tests. Settings are fixed unless the test - * supplies its own, and every process run goes to the test's `run`, which fails by default so an - * unexpected CLI call is visible. + * supplies its own. Process runs and git operations go to what the test supplies; anything else + * dies, so an unexpected CLI or git call is visible. * * @module source-control-testing/TestSourceControlHost */ @@ -14,8 +14,26 @@ import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"; export interface TestSourceControlHostOptions { readonly settings?: ServerSettings; readonly process?: Partial; + readonly git?: Partial; } +type Git = SourceControlHost.SourceControlHost["Service"]["git"]; + +const unexpectedGit = (operation: string) => () => + Effect.die(`Unexpected git ${operation} in a test that supplied none.`); + +const failingGit: Git = { + remotes: unexpectedGit("remotes"), + readConfigValue: unexpectedGit("readConfigValue"), + resolvePrimaryRemoteName: unexpectedGit("resolvePrimaryRemoteName"), + ensureRemote: unexpectedGit("ensureRemote"), + listLocalBranchNames: unexpectedGit("listLocalBranchNames"), + fetchRemoteBranch: unexpectedGit("fetchRemoteBranch"), + fetchRemoteTrackingBranch: unexpectedGit("fetchRemoteTrackingBranch"), + setBranchUpstream: unexpectedGit("setBranchUpstream"), + switchRef: unexpectedGit("switchRef"), +}; + export const layer = ( options: TestSourceControlHostOptions = {}, ): Layer.Layer => @@ -28,6 +46,7 @@ export const layer = ( options.process?.run ?? ((input) => Effect.die(`Unexpected ${input.command} run in ${input.operation}.`)), }, + git: { ...failingGit, ...options.git }, }), ); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 13c583be13f0..5ad28290d985 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -651,6 +651,9 @@ importers: '@t3tools/source-control-azure-devops': specifier: workspace:* version: link:../../packages/source-control-azure-devops + '@t3tools/source-control-bitbucket': + specifier: workspace:* + version: link:../../packages/source-control-bitbucket '@t3tools/source-control-core': specifier: workspace:* version: link:../../packages/source-control-core @@ -1556,6 +1559,34 @@ importers: specifier: 1.0.0 version: 1.0.0(@types/node@24.12.4)(@vitest/ui@5.0.1(vitest@5.0.1))(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + packages/source-control-bitbucket: + dependencies: + '@t3tools/contracts': + specifier: workspace:* + version: link:../contracts + '@t3tools/provider-core': + specifier: workspace:* + version: link:../provider-core + '@t3tools/shared': + specifier: workspace:* + version: link:../shared + '@t3tools/source-control-core': + specifier: workspace:* + version: link:../source-control-core + effect: + specifier: 4.0.2 + version: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688) + devDependencies: + '@effect/vitest': + specifier: 4.0.2 + version: 4.0.2(patch_hash=595b7d26fb635caba9c3d39fcb189bb78a000192f8d3b60ead7174b310d1ade9)(@types/node@24.12.4)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688))(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + '@types/node': + specifier: 24.12.4 + version: 24.12.4 + vite-plus: + specifier: 1.0.0 + version: 1.0.0(@types/node@24.12.4)(@vitest/ui@5.0.1(vitest@5.0.1))(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + packages/source-control-core: dependencies: '@t3tools/contracts': diff --git a/scripts/release-smoke.ts b/scripts/release-smoke.ts index c19a1ee9cac4..96b1df41e1ea 100644 --- a/scripts/release-smoke.ts +++ b/scripts/release-smoke.ts @@ -38,6 +38,7 @@ const workspaceFiles = [ "packages/provider-muse/package.json", "packages/provider-opencode/package.json", "packages/source-control-azure-devops/package.json", + "packages/source-control-bitbucket/package.json", "packages/source-control-core/package.json", "packages/source-control-forgejo/package.json", "packages/source-control-gitlab/package.json",