# Files changed · PR \#22029 · pingdotgg/t3code

[View on GitCafe](https://git.cafe/pingdotgg/t3code/pull/22029/diffs)

Repository: [pingdotgg/t3code](https://git.cafe/pingdotgg/t3code)

Visibility: public

[fix\(bitbucket\): skip Cloud API reads for unsupported Server remotes](https://git.cafe/pingdotgg/t3code/pull/22029.md)

Version: 9

Head commit: 1ab72f7f53417096d1945be04c78d0b3f962025f

Base commit: cc0b86ddf12a34bd65dce88d6f2ae1f069ef841e

Comparison base: cc0b86ddf12a34bd65dce88d6f2ae1f069ef841e

Showing up to 20 entries from this revision. If the pull request changes, reopen the first page before continuing.

## apps/server/src/pullRequest/PullRequestService.test.ts

Status: modified; +46; −0

```diff
@@ -416,6 +416,52 @@
   }),
 );
 
+it.effect("reads nothing from a host its provider does not serve, but reports it", () =>
+  Effect.gen(function* () {
+    const reached = new Set<string>();
+    const bitbucket = (id: string, host: string) =>
+      project({
+        id,
+        title: host,
+        workspaceRoot: `/${id}`,
+        repository: "proj/repo",
+        provider: SourceControlProviderKind.make("bitbucket"),
+        host,
+      });
+    const service = yield* makeService({
+      projects: [bitbucket("p1", "bitbucket.org"), bitbucket("p2", "bitbucket.example.com")],
+      providers: [
+        fakePullRequestProvider(SourceControlProviderKind.make("bitbucket"), {
+          servesHost: (host) => host === "bitbucket.org",
+          getViewer: ({ host }) => {
+            reached.add(host ?? "");
+            return Effect.succeed("bilal");
+          },
+          listChangeRequests: ({ host }) => {
+            reached.add(host);
+            return Effect.succeed({ items: [], truncated: false, continues: false });
+          },
+        }),
+      ],
+    });
+
+    const result = yield* service.list({ state: "open" });
+
+    assert.deepStrictEqual([...reached], ["bitbucket.org"]);
+    assert.deepStrictEqual(
+      result.providers.map(({ host, configured, detail }) => ({ host, configured, detail })),
+      [
+        { host: "bitbucket.org", configured: true, detail: null },
+        {
+          host: "bitbucket.example.com",
+          configured: false,
+          detail: "This host cannot be browsed here yet.",
+        },
+      ],
+    );
+  }),
+);
+
 it.effect("says where each repository carries on, and from nothing it has run out of", () =>
   Effect.gen(function* () {
     const service = yield* makeService({
```

## apps/server/src/pullRequest/PullRequestService.ts

Status: modified; +3; −1

```diff
@@ -505,6 +505,7 @@
       ? {}
       : { unavailableRequirement: api.unavailableRequirement }),
     ...(api.repositoryKey === undefined ? {} : { repositoryKey: api.repositoryKey }),
+    ...(api.servesHost === undefined ? {} : { servesHost: api.servesHost }),
     ...(api.readsPublicHostWithoutCheckout === undefined
       ? {}
       : { readsPublicHostWithoutCheckout: api.readsPublicHostWithoutCheckout }),
@@ -884,7 +885,8 @@
           if (filter.host !== undefined && host !== filter.host.toLowerCase()) {
             continue;
           }
-          const api = registry.get(kind);
+          const registered = registry.get(kind);
+          const api = registered?.servesHost?.(host) === false ? null : registered;
           if (api !== null) {
             const roots = viewerRoots.get(host);
             if (roots === undefined) viewerRoots.set(host, [project.workspaceRoot]);
```

## packages/source-control-bitbucket/src/server/BitbucketApi.test.ts

Status: modified; +75; −1

```diff
@@ -10,6 +10,7 @@
 import * as Layer from "effect/Layer";
 import * as Option from "effect/Option";
 import * as Queue from "effect/Queue";
+import * as Result from "effect/Result";
 import * as Schema from "effect/Schema";
 import * as TestClock from "effect/testing/TestClock";
 import { HttpClient, HttpClientError, HttpClientRequest, HttpClientResponse } from "effect/http";
@@ -72,6 +73,7 @@
   readonly stall?: boolean;
   readonly git?: Partial<HostGit>;
   readonly env?: Record<string, string>;
+  readonly remoteUrl?: string;
 }) {
   const execute = vi.fn((request: HttpClientRequest.HttpClientRequest) =>
     input.stall
@@ -108,7 +110,7 @@
       remotes: [
         {
           name: "origin",
+          url: input.remoteUrl ?? "git@bitbucket.org:pingdotgg/t3code.git",
-          url: "git@bitbucket.org:pingdotgg/t3code.git",
           pushUrl: Option.none(),
           isPrimary: true,
         },
@@ -337,6 +339,78 @@
   }).pipe(Effect.provide(layer));
 });
 
+it.effect("sends a self-hosted Bitbucket remote nowhere unless an API base url is configured", () =>
+  Effect.gen(function* () {
+    const remoteUrl = "https://bitbucket.example.com/scm/proj/repo.git";
+    const context = {
+      provider: {
+        kind: SourceControlProviderKind.make("bitbucket"),
+        name: "Bitbucket Self-Hosted",
+        baseUrl: "https://bitbucket.example.com",
+      },
+      remoteName: "origin",
+      remoteUrl,
+    };
+    const cloudEnv = {
+      T3CODE_BITBUCKET_EMAIL: "user@example.com",
+      T3CODE_BITBUCKET_API_TOKEN: "t",
+    };
+    const response = (request: HttpClientRequest.HttpClientRequest) =>
+      Response.json(request.url.endsWith("/branching-model") ? {} : repositoryJson);
+    const defaultBranch = (setup: ReturnType<typeof makeLayer>, withContext: boolean) =>
+      BitbucketApi.BitbucketApi.pipe(
+        Effect.flatMap((bitbucket) =>
+          bitbucket.getDefaultBranch({ cwd: "/repo", ...(withContext ? { context } : {}) }),
+        ),
+        Effect.provide(setup.layer),
+      );
+    const requestedUrls = (setup: ReturnType<typeof makeLayer>) =>
+      setup.execute.mock.calls.map(([request]) => request.url).toSorted();
+
+    // A self-hosted remote in the checkout, then a self-hosted context beside a Cloud remote.
+    for (const [setup, withContext] of [
+      [makeLayer({ response, env: cloudEnv, remoteUrl }), false],
+      [makeLayer({ response, env: cloudEnv }), true],
+    ] as const) {
+      const result = yield* defaultBranch(setup, withContext).pipe(Effect.result);
+      assert.deepStrictEqual(requestedUrls(setup), []);
+      assert.strictEqual(
+        Result.isFailure(result) && result.failure._tag,
+        "BitbucketRepositoryRemoteNotFoundError",
+      );
+    }
+
+    // An explicit repository does not let a self-hosted context reach the Cloud API.
+    const explicit = makeLayer({ response, env: cloudEnv });
+    const cloneUrls = yield* BitbucketApi.BitbucketApi.pipe(
+      Effect.flatMap((bitbucket) =>
+        bitbucket.getRepositoryCloneUrls({ cwd: "/repo", context, repository: "proj/repo" }),
+      ),
+      Effect.provide(explicit.layer),
+      Effect.result,
+    );
+    assert.deepStrictEqual(requestedUrls(explicit), []);
+    assert.strictEqual(
+      Result.isFailure(cloneUrls) && cloneUrls.failure._tag,
+      "BitbucketRepositoryRemoteNotFoundError",
+    );
+
+    const cloud = makeLayer({ response, env: cloudEnv });
+    yield* defaultBranch(cloud, false);
+    assert.deepStrictEqual(requestedUrls(cloud), [
+      "https://api.bitbucket.org/2.0/repositories/pingdotgg/t3code",
+      "https://api.bitbucket.org/2.0/repositories/pingdotgg/t3code/branching-model",
+    ]);
+
+    const configured = makeLayer({ response, remoteUrl });
+    yield* defaultBranch(configured, true);
+    assert.deepStrictEqual(requestedUrls(configured), [
+      "https://api.test.local/2.0/repositories/proj/repo",
+      "https://api.test.local/2.0/repositories/proj/repo/branching-model",
+    ]);
+  }),
+);
+
 it.effect(
   "prefers the Bitbucket branching model development branch as the default PR target",
   () => {
```

## packages/source-control-bitbucket/src/server/BitbucketApi.ts

Status: modified; +20; −1

```diff
@@ -12,6 +12,7 @@
   SourceControlProviderKind,
   TrimmedNonEmptyString,
   type SourceControlProviderAuth,
+  type SourceControlProviderInfo,
   type SourceControlRepositoryCloneUrls,
   type SourceControlRepositoryVisibility,
 } from "@t3tools/contracts";
@@ -51,6 +52,15 @@
   apiToken: Config.String("T3CODE_BITBUCKET_API_TOKEN").pipe(Config.option),
 });
 
+// Server and Data Center use a different API; a custom Cloud endpoint must be explicit.
+export const BitbucketServesHost = Config.String("T3CODE_BITBUCKET_API_BASE_URL").pipe(
+  Config.option,
+  Config.map(
+    (baseUrl) => (host: string) =>
+      Option.isSome(baseUrl) || host === "bitbucket.org" || host.endsWith(".bitbucket.org"),
+  ),
+);
+
 const BitbucketApiOperation = Schema.Literals([
   "resolveRepository",
   "getRepository",
@@ -646,6 +656,11 @@
     host: "bitbucket.org",
   };
 
+  const servesHost = yield* BitbucketServesHost;
+
+  const servesProvider = (provider: SourceControlProviderInfo | null) =>
+    provider?.kind === "bitbucket" && servesHost(URL.parse(provider.baseUrl)?.hostname ?? "");
+
   const apiUrl = (path: string) => `${config.baseUrl.replace(/\/+$/u, "")}${path}`;
 
   const currentCredential = host.settings.get.pipe(
@@ -757,6 +772,10 @@
     readonly context?: SourceControlProvider.SourceControlProviderContext;
     readonly repository?: string;
   }) {
+    if (input.context?.provider.kind === "bitbucket" && !servesProvider(input.context.provider)) {
+      // Falling back to another remote could act on a different repository.
+      return yield* new BitbucketRepositoryRemoteNotFoundError({ cwd: input.cwd });
+    }
     if (input.repository !== undefined) {
       return yield* requireRepositoryLocator(input.repository);
     }
@@ -787,7 +806,7 @@
     );
 
     for (const remote of remotes.remotes) {
-      if (recognizeRemoteWith(definition, remote.url) === null) continue;
+      if (!servesProvider(recognizeRemoteWith(definition, remote.url))) continue;
       const parsed = parseBitbucketRemoteUrl(remote.url);
       if (parsed) return parsed;
     }
```

## packages/source-control-bitbucket/src/server/BitbucketPullRequestProvider.test.ts

Status: modified; +25; −0

```diff
@@ -1,5 +1,6 @@
 import { SourceControlProviderKind } from "@t3tools/contracts";
 import { describe, expect, it } from "@effect/vitest";
+import * as ConfigProvider from "effect/ConfigProvider";
 import * as Effect from "effect/Effect";
 import * as Layer from "effect/Layer";
 import * as Result from "effect/Result";
@@ -178,6 +179,30 @@
   }),
 );
 
+it.effect("serves only Bitbucket Cloud hosts unless an API base url is configured", () =>
+  Effect.gen(function* () {
+    const servedHosts = (env: Record<string, string>) =>
+      make.pipe(
+        Effect.map(({ servesHost }) =>
+          ["bitbucket.org", "altssh.bitbucket.org", "bitbucket.example.com"].filter(
+            (host) => servesHost?.(host) === true,
+          ),
+        ),
+        Effect.provide(
+          Layer.merge(
+            Layer.mock(BitbucketPullRequestApi.BitbucketPullRequestApi)({}),
+            ConfigProvider.layer(ConfigProvider.fromEnv({ env })),
+          ),
+        ),
+      );
+
+    expect(yield* servedHosts({})).toEqual(["bitbucket.org", "altssh.bitbucket.org"]);
+    expect(
+      yield* servedHosts({ T3CODE_BITBUCKET_API_BASE_URL: "https://bitbucket.example.com/api" }),
+    ).toEqual(["bitbucket.org", "altssh.bitbucket.org", "bitbucket.example.com"]);
+  }),
+);
+
 describe("bitbucketProviderFailure", () => {
   it("treats only an HTTP 401 as unusable credentials", () => {
     const responseError = (status: number) =>
```

## packages/source-control-bitbucket/src/server/BitbucketPullRequestProvider.ts

Status: modified; +3; −0

```diff
@@ -2,6 +2,7 @@
 import * as Effect from "effect/Effect";
 import type { PullRequestCapabilities, PullRequestViewerPermissions } from "@t3tools/contracts";
 
+import * as BitbucketApi from "./BitbucketApi.ts";
 import * as BitbucketPullRequestApi from "./BitbucketPullRequestApi.ts";
 import {
   PullRequestProviderError,
@@ -109,6 +110,7 @@
 
 export const make = Effect.gen(function* () {
   const api = yield* BitbucketPullRequestApi.BitbucketPullRequestApi;
+  const servesHost = yield* BitbucketApi.BitbucketServesHost.pipe(Effect.orDie);
 
   const fail =
     (operation: string) => (error: BitbucketPullRequestApi.BitbucketPullRequestApiError) =>
@@ -148,6 +150,7 @@
   const provider: PullRequestProviderApi = {
     kind: SourceControlProviderKind.make("bitbucket"),
     capabilities: CAPABILITIES,
+    servesHost,
     unavailableRequirement: UNAVAILABLE_REQUIREMENT,
     readsPublicHostWithoutCheckout: true,
 
```

## packages/source-control-core/src/server/PullRequestProvider.ts

Status: modified; +5; −0

```diff
@@ -300,6 +300,11 @@
    * definition's `publicHost` can be read from a project with no checkout of its repository.
    */
   readonly readsPublicHostWithoutCheckout?: boolean;
+  /**
+   * Absent means every host detected as this kind is read. A host it refuses is listed as one that
+   * cannot be browsed here, and nothing is sent for it.
+   */
+  readonly servesHost?: (host: string) => boolean;
 
   readonly getViewer: (input: {
     readonly cwd: string;
```
