diff --git a/src/main/azure-devops/client.ts b/src/main/azure-devops/client.ts index ca692563e..956f389f2 100644 --- a/src/main/azure-devops/client.ts +++ b/src/main/azure-devops/client.ts @@ -254,14 +254,12 @@ export async function getAzureDevOpsAuthStatus(): Promise export async function getAzureDevOpsPullRequest( repoPath: string, - prNumber: number + prNumber: number, + connectionId?: string | null ): Promise { - const repo = await getAzureDevOpsRepoRef(repoPath) - if (!repo) { - return null - } - const repository = await getRepository(repo) - if (!repository) { + const repo = await getAzureDevOpsRepoRef(repoPath, connectionId) + const repository = repo ? await getRepository(repo) : null + if (!repo || !repository) { return null } const raw = await requestJson( @@ -276,19 +274,17 @@ export async function getAzureDevOpsPullRequest( export async function getAzureDevOpsPullRequestForBranch( repoPath: string, branch: string, - linkedPRNumber?: number | null + linkedPRNumber?: number | null, + connectionId?: string | null ): Promise { const branchName = branch.replace(/^refs\/heads\//, '') if (!branchName && linkedPRNumber == null) { return null } - const repo = await getAzureDevOpsRepoRef(repoPath) - if (!repo) { - return null - } - const repository = await getRepository(repo) - if (!repository) { + const repo = await getAzureDevOpsRepoRef(repoPath, connectionId) + const repository = repo ? await getRepository(repo) : null + if (!repo || !repository) { return null } @@ -322,6 +318,9 @@ export async function getAzureDevOpsPullRequestForBranch( return raw ? normalizePullRequest(repo, repository.idOrName, repository.webBaseUrl, raw) : null } -export async function getAzureDevOpsRepoSlug(repoPath: string): Promise { - return getAzureDevOpsRepoRef(repoPath) +export async function getAzureDevOpsRepoSlug( + repoPath: string, + connectionId?: string | null +): Promise { + return getAzureDevOpsRepoRef(repoPath, connectionId) } diff --git a/src/main/azure-devops/repository-ref.test.ts b/src/main/azure-devops/repository-ref.test.ts index 4d07f58c8..1c9b7e008 100644 --- a/src/main/azure-devops/repository-ref.test.ts +++ b/src/main/azure-devops/repository-ref.test.ts @@ -1,7 +1,28 @@ -import { describe, expect, it } from 'vitest' -import { parseAzureDevOpsRepoRef } from './repository-ref' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +const { sshExecMock } = vi.hoisted(() => ({ + sshExecMock: vi.fn() +})) + +import { + _resetAzureDevOpsRepoRefCache, + getAzureDevOpsRepoRefForRemote, + parseAzureDevOpsRepoRef +} from './repository-ref' +import { registerSshGitProvider, unregisterSshGitProvider } from '../providers/ssh-git-dispatch' describe('parseAzureDevOpsRepoRef', () => { + beforeEach(() => { + sshExecMock.mockReset() + unregisterSshGitProvider('conn-1') + _resetAzureDevOpsRepoRefCache() + }) + + afterEach(() => { + unregisterSshGitProvider('conn-1') + _resetAzureDevOpsRepoRefCache() + }) + it('parses dev.azure.com HTTPS remotes', () => { expect( parseAzureDevOpsRepoRef('https://dev.azure.com/acme/Project%20One/_git/repo-name') @@ -53,4 +74,43 @@ describe('parseAzureDevOpsRepoRef', () => { it('ignores non-Azure remotes', () => { expect(parseAzureDevOpsRepoRef('git@github.com:stablyai/orca.git')).toBeNull() }) + + it('resolves repository refs through the SSH git provider for connected repos', async () => { + sshExecMock.mockResolvedValueOnce({ + stdout: 'git@ssh.dev.azure.com:v3/acme/Project/repo\n', + stderr: '' + }) + registerSshGitProvider('conn-1', { exec: sshExecMock } as never) + + await expect(getAzureDevOpsRepoRefForRemote('/repo', 'origin', 'conn-1')).resolves.toEqual({ + host: 'dev.azure.com', + organization: 'acme', + project: 'Project', + repository: 'repo', + apiBaseUrl: 'https://dev.azure.com/acme/Project', + webBaseUrl: 'https://dev.azure.com/acme/Project/_git/repo' + }) + + expect(sshExecMock).toHaveBeenCalledWith(['remote', 'get-url', 'origin'], '/repo') + }) + + it('does not cache transient SSH provider failures as unsupported repos', async () => { + sshExecMock.mockRejectedValueOnce(new Error('connection closed')).mockResolvedValueOnce({ + stdout: 'git@ssh.dev.azure.com:v3/acme/Project/repo\n', + stderr: '' + }) + registerSshGitProvider('conn-1', { exec: sshExecMock } as never) + + await expect(getAzureDevOpsRepoRefForRemote('/repo', 'origin', 'conn-1')).resolves.toBeNull() + await expect(getAzureDevOpsRepoRefForRemote('/repo', 'origin', 'conn-1')).resolves.toEqual({ + host: 'dev.azure.com', + organization: 'acme', + project: 'Project', + repository: 'repo', + apiBaseUrl: 'https://dev.azure.com/acme/Project', + webBaseUrl: 'https://dev.azure.com/acme/Project/_git/repo' + }) + + expect(sshExecMock).toHaveBeenCalledTimes(2) + }) }) diff --git a/src/main/azure-devops/repository-ref.ts b/src/main/azure-devops/repository-ref.ts index 1da1d60fd..d694a365d 100644 --- a/src/main/azure-devops/repository-ref.ts +++ b/src/main/azure-devops/repository-ref.ts @@ -1,4 +1,5 @@ import { gitExecFileAsync } from '../git/runner' +import { getSshGitProvider } from '../providers/ssh-git-dispatch' export type AzureDevOpsRepoRef = { host: string @@ -184,25 +185,40 @@ export function parseAzureDevOpsRepoRef(remoteUrl: string): AzureDevOpsRepoRef | export async function getAzureDevOpsRepoRefForRemote( repoPath: string, - remoteName: string + remoteName: string, + connectionId?: string | null ): Promise { - const cacheKey = `${repoPath}\0${remoteName}` + const cacheKey = `${connectionId ?? 'local'}\0${repoPath}\0${remoteName}` if (repoRefCache.has(cacheKey)) { return repoRefCache.get(cacheKey)! } try { - const { stdout } = await gitExecFileAsync(['remote', 'get-url', remoteName], { - cwd: repoPath - }) + const sshGitProvider = connectionId ? getSshGitProvider(connectionId) : null + if (connectionId && !sshGitProvider) { + return null + } + const { stdout } = sshGitProvider + ? await sshGitProvider.exec(['remote', 'get-url', remoteName], repoPath) + : await gitExecFileAsync(['remote', 'get-url', remoteName], { + cwd: repoPath + }) const result = parseAzureDevOpsRepoRef(stdout) repoRefCache.set(cacheKey, result) return result } catch { + if (connectionId) { + // Why: SSH provider failures are often transient reconnect/tunnel states; + // caching them as "not Azure DevOps" would poison the repo for the session. + return null + } repoRefCache.set(cacheKey, null) return null } } -export async function getAzureDevOpsRepoRef(repoPath: string): Promise { - return getAzureDevOpsRepoRefForRemote(repoPath, 'origin') +export async function getAzureDevOpsRepoRef( + repoPath: string, + connectionId?: string | null +): Promise { + return getAzureDevOpsRepoRefForRemote(repoPath, 'origin', connectionId) } diff --git a/src/main/bitbucket/client.ts b/src/main/bitbucket/client.ts index d5c2c1637..d07c43b62 100644 --- a/src/main/bitbucket/client.ts +++ b/src/main/bitbucket/client.ts @@ -159,9 +159,10 @@ export async function getBitbucketAuthStatus(): Promise { export async function getBitbucketPullRequest( repoPath: string, - prNumber: number + prNumber: number, + connectionId?: string | null ): Promise { - const repo = await getBitbucketRepoRef(repoPath) + const repo = await getBitbucketRepoRef(repoPath, connectionId) if (!repo) { return null } @@ -174,14 +175,15 @@ export async function getBitbucketPullRequest( export async function getBitbucketPullRequestForBranch( repoPath: string, branch: string, - linkedPRNumber?: number | null + linkedPRNumber?: number | null, + connectionId?: string | null ): Promise { const branchName = branch.replace(/^refs\/heads\//, '') if (!branchName && linkedPRNumber == null) { return null } - const repo = await getBitbucketRepoRef(repoPath) + const repo = await getBitbucketRepoRef(repoPath, connectionId) if (!repo) { return null } @@ -217,6 +219,9 @@ export async function getBitbucketPullRequestForBranch( return raw ? normalizePullRequest(repo, raw) : null } -export async function getBitbucketRepoSlug(repoPath: string): Promise { - return getBitbucketRepoRef(repoPath) +export async function getBitbucketRepoSlug( + repoPath: string, + connectionId?: string | null +): Promise { + return getBitbucketRepoRef(repoPath, connectionId) } diff --git a/src/main/bitbucket/repository-ref.test.ts b/src/main/bitbucket/repository-ref.test.ts index a95bf8e85..33b36977c 100644 --- a/src/main/bitbucket/repository-ref.test.ts +++ b/src/main/bitbucket/repository-ref.test.ts @@ -1,7 +1,8 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -const { gitExecFileAsyncMock } = vi.hoisted(() => ({ - gitExecFileAsyncMock: vi.fn() +const { gitExecFileAsyncMock, sshExecMock } = vi.hoisted(() => ({ + gitExecFileAsyncMock: vi.fn(), + sshExecMock: vi.fn() })) vi.mock('../git/runner', () => ({ @@ -10,16 +11,24 @@ vi.mock('../git/runner', () => ({ import { _resetBitbucketRepoRefCache, + getBitbucketRepoRefForRemote, getBitbucketRepoRef, parseBitbucketRepoRef } from './repository-ref' +import { registerSshGitProvider, unregisterSshGitProvider } from '../providers/ssh-git-dispatch' describe('Bitbucket repository refs', () => { beforeEach(() => { gitExecFileAsyncMock.mockReset() + sshExecMock.mockReset() + unregisterSshGitProvider('conn-1') _resetBitbucketRepoRefCache() }) + afterEach(() => { + unregisterSshGitProvider('conn-1') + }) + it('parses HTTPS, SSH, and ssh:// Bitbucket remotes', () => { expect(parseBitbucketRepoRef('https://bitbucket.org/team/project.git')).toEqual({ workspace: 'team', @@ -72,4 +81,37 @@ describe('Bitbucket repository refs', () => { cwd: '/repo' }) }) + + it('resolves project refs through the SSH git provider for connected repos', async () => { + sshExecMock.mockResolvedValueOnce({ + stdout: 'git@bitbucket.org:remote/project.git\n', + stderr: '' + }) + registerSshGitProvider('conn-1', { exec: sshExecMock } as never) + + await expect(getBitbucketRepoRefForRemote('/repo', 'origin', 'conn-1')).resolves.toEqual({ + workspace: 'remote', + repoSlug: 'project' + }) + + expect(sshExecMock).toHaveBeenCalledWith(['remote', 'get-url', 'origin'], '/repo') + expect(gitExecFileAsyncMock).not.toHaveBeenCalled() + }) + + it('does not cache transient SSH provider failures as unsupported repos', async () => { + sshExecMock.mockRejectedValueOnce(new Error('connection closed')).mockResolvedValueOnce({ + stdout: 'git@bitbucket.org:remote/project.git\n', + stderr: '' + }) + registerSshGitProvider('conn-1', { exec: sshExecMock } as never) + + await expect(getBitbucketRepoRefForRemote('/repo', 'origin', 'conn-1')).resolves.toBeNull() + await expect(getBitbucketRepoRefForRemote('/repo', 'origin', 'conn-1')).resolves.toEqual({ + workspace: 'remote', + repoSlug: 'project' + }) + + expect(sshExecMock).toHaveBeenCalledTimes(2) + expect(gitExecFileAsyncMock).not.toHaveBeenCalled() + }) }) diff --git a/src/main/bitbucket/repository-ref.ts b/src/main/bitbucket/repository-ref.ts index 4d17e0b63..afb4de524 100644 --- a/src/main/bitbucket/repository-ref.ts +++ b/src/main/bitbucket/repository-ref.ts @@ -1,4 +1,5 @@ import { gitExecFileAsync } from '../git/runner' +import { getSshGitProvider } from '../providers/ssh-git-dispatch' export type BitbucketRepoRef = { workspace: string @@ -60,25 +61,40 @@ export function parseBitbucketRepoRef(remoteUrl: string): BitbucketRepoRef | nul export async function getBitbucketRepoRefForRemote( repoPath: string, - remoteName: string + remoteName: string, + connectionId?: string | null ): Promise { - const cacheKey = `${repoPath}\0${remoteName}` + const cacheKey = `${connectionId ?? 'local'}\0${repoPath}\0${remoteName}` if (repoRefCache.has(cacheKey)) { return repoRefCache.get(cacheKey)! } try { - const { stdout } = await gitExecFileAsync(['remote', 'get-url', remoteName], { - cwd: repoPath - }) + const sshGitProvider = connectionId ? getSshGitProvider(connectionId) : null + if (connectionId && !sshGitProvider) { + return null + } + const { stdout } = sshGitProvider + ? await sshGitProvider.exec(['remote', 'get-url', remoteName], repoPath) + : await gitExecFileAsync(['remote', 'get-url', remoteName], { + cwd: repoPath + }) const result = parseBitbucketRepoRef(stdout) repoRefCache.set(cacheKey, result) return result } catch { + if (connectionId) { + // Why: SSH provider failures are often transient reconnect/tunnel states; + // caching them as "not Bitbucket" would poison the repo for the session. + return null + } repoRefCache.set(cacheKey, null) return null } } -export async function getBitbucketRepoRef(repoPath: string): Promise { - return getBitbucketRepoRefForRemote(repoPath, 'origin') +export async function getBitbucketRepoRef( + repoPath: string, + connectionId?: string | null +): Promise { + return getBitbucketRepoRefForRemote(repoPath, 'origin', connectionId) } diff --git a/src/main/gitea/client.ts b/src/main/gitea/client.ts index c86dd89a9..5aadd803b 100644 --- a/src/main/gitea/client.ts +++ b/src/main/gitea/client.ts @@ -186,9 +186,10 @@ export async function getGiteaAuthStatus(): Promise { export async function getGiteaPullRequest( repoPath: string, - prNumber: number + prNumber: number, + connectionId?: string | null ): Promise { - const repo = await getGiteaRepoRef(repoPath) + const repo = await getGiteaRepoRef(repoPath, connectionId) if (!repo) { return null } @@ -202,14 +203,15 @@ export async function getGiteaPullRequest( export async function getGiteaPullRequestForBranch( repoPath: string, branch: string, - linkedPRNumber?: number | null + linkedPRNumber?: number | null, + connectionId?: string | null ): Promise { const branchName = branch.replace(/^refs\/heads\//, '') if (!branchName && linkedPRNumber == null) { return null } - const repo = await getGiteaRepoRef(repoPath) + const repo = await getGiteaRepoRef(repoPath, connectionId) if (!repo) { return null } @@ -248,6 +250,9 @@ export async function getGiteaPullRequestForBranch( return raw ? normalizePullRequest(repo, raw) : null } -export async function getGiteaRepoSlug(repoPath: string): Promise { - return getGiteaRepoRef(repoPath) +export async function getGiteaRepoSlug( + repoPath: string, + connectionId?: string | null +): Promise { + return getGiteaRepoRef(repoPath, connectionId) } diff --git a/src/main/gitea/repository-ref.test.ts b/src/main/gitea/repository-ref.test.ts index 0095fb9c9..399ce2930 100644 --- a/src/main/gitea/repository-ref.test.ts +++ b/src/main/gitea/repository-ref.test.ts @@ -1,21 +1,34 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -const { gitExecFileAsyncMock } = vi.hoisted(() => ({ - gitExecFileAsyncMock: vi.fn() +const { gitExecFileAsyncMock, sshExecMock } = vi.hoisted(() => ({ + gitExecFileAsyncMock: vi.fn(), + sshExecMock: vi.fn() })) vi.mock('../git/runner', () => ({ gitExecFileAsync: gitExecFileAsyncMock })) -import { _resetGiteaRepoRefCache, getGiteaRepoRef, parseGiteaRepoRef } from './repository-ref' +import { + _resetGiteaRepoRefCache, + getGiteaRepoRef, + getGiteaRepoRefForRemote, + parseGiteaRepoRef +} from './repository-ref' +import { registerSshGitProvider, unregisterSshGitProvider } from '../providers/ssh-git-dispatch' describe('Gitea repository ref parsing', () => { beforeEach(() => { gitExecFileAsyncMock.mockReset() + sshExecMock.mockReset() + unregisterSshGitProvider('conn-1') _resetGiteaRepoRefCache() }) + afterEach(() => { + unregisterSshGitProvider('conn-1') + }) + it('parses HTTPS remotes and derives the API base URL', () => { expect(parseGiteaRepoRef('https://git.example.com/team/project.git')).toEqual({ host: 'git.example.com', @@ -95,4 +108,39 @@ describe('Gitea repository ref parsing', () => { cwd: '/repo' }) }) + + it('resolves repository refs through the SSH git provider for connected repos', async () => { + sshExecMock.mockResolvedValueOnce({ + stdout: 'git@gitea.example.test:remote/project.git\n', + stderr: '' + }) + registerSshGitProvider('conn-1', { exec: sshExecMock } as never) + + await expect(getGiteaRepoRefForRemote('/repo', 'origin', 'conn-1')).resolves.toMatchObject({ + host: 'gitea.example.test', + owner: 'remote', + repo: 'project' + }) + + expect(sshExecMock).toHaveBeenCalledWith(['remote', 'get-url', 'origin'], '/repo') + expect(gitExecFileAsyncMock).not.toHaveBeenCalled() + }) + + it('does not cache transient SSH provider failures as unsupported repos', async () => { + sshExecMock.mockRejectedValueOnce(new Error('connection closed')).mockResolvedValueOnce({ + stdout: 'git@gitea.example.test:remote/project.git\n', + stderr: '' + }) + registerSshGitProvider('conn-1', { exec: sshExecMock } as never) + + await expect(getGiteaRepoRefForRemote('/repo', 'origin', 'conn-1')).resolves.toBeNull() + await expect(getGiteaRepoRefForRemote('/repo', 'origin', 'conn-1')).resolves.toMatchObject({ + host: 'gitea.example.test', + owner: 'remote', + repo: 'project' + }) + + expect(sshExecMock).toHaveBeenCalledTimes(2) + expect(gitExecFileAsyncMock).not.toHaveBeenCalled() + }) }) diff --git a/src/main/gitea/repository-ref.ts b/src/main/gitea/repository-ref.ts index 6687525a4..f7c5e7b8e 100644 --- a/src/main/gitea/repository-ref.ts +++ b/src/main/gitea/repository-ref.ts @@ -1,4 +1,5 @@ import { gitExecFileAsync } from '../git/runner' +import { getSshGitProvider } from '../providers/ssh-git-dispatch' export type GiteaRepoRef = { host: string @@ -121,25 +122,40 @@ export function parseGiteaRepoRef(remoteUrl: string): GiteaRepoRef | null { export async function getGiteaRepoRefForRemote( repoPath: string, - remoteName: string + remoteName: string, + connectionId?: string | null ): Promise { - const cacheKey = `${repoPath}\0${remoteName}` + const cacheKey = `${connectionId ?? 'local'}\0${repoPath}\0${remoteName}` if (repoRefCache.has(cacheKey)) { return repoRefCache.get(cacheKey)! } try { - const { stdout } = await gitExecFileAsync(['remote', 'get-url', remoteName], { - cwd: repoPath - }) + const sshGitProvider = connectionId ? getSshGitProvider(connectionId) : null + if (connectionId && !sshGitProvider) { + return null + } + const { stdout } = sshGitProvider + ? await sshGitProvider.exec(['remote', 'get-url', remoteName], repoPath) + : await gitExecFileAsync(['remote', 'get-url', remoteName], { + cwd: repoPath + }) const result = parseGiteaRepoRef(stdout) repoRefCache.set(cacheKey, result) return result } catch { + if (connectionId) { + // Why: SSH provider failures are often transient reconnect/tunnel states; + // caching them as "not Gitea" would poison the repo for the session. + return null + } repoRefCache.set(cacheKey, null) return null } } -export async function getGiteaRepoRef(repoPath: string): Promise { - return getGiteaRepoRefForRemote(repoPath, 'origin') +export async function getGiteaRepoRef( + repoPath: string, + connectionId?: string | null +): Promise { + return getGiteaRepoRefForRemote(repoPath, 'origin', connectionId) } diff --git a/src/main/source-control/hosted-review-creation.ts b/src/main/source-control/hosted-review-creation.ts index e03fb0e3b..1cecc2210 100644 --- a/src/main/source-control/hosted-review-creation.ts +++ b/src/main/source-control/hosted-review-creation.ts @@ -44,13 +44,13 @@ async function detectHostedReviewProvider( if (await getRepoSlug(repoPath, connectionId)) { return 'github' } - if (await getBitbucketRepoSlug(repoPath)) { + if (await getBitbucketRepoSlug(repoPath, connectionId)) { return 'bitbucket' } - if (await getAzureDevOpsRepoSlug(repoPath)) { + if (await getAzureDevOpsRepoSlug(repoPath, connectionId)) { return 'azure-devops' } - if (await getGiteaRepoSlug(repoPath)) { + if (await getGiteaRepoSlug(repoPath, connectionId)) { return 'gitea' } return 'unsupported' diff --git a/src/main/source-control/hosted-review.test.ts b/src/main/source-control/hosted-review.test.ts index 2c148e3b2..4573144a8 100644 --- a/src/main/source-control/hosted-review.test.ts +++ b/src/main/source-control/hosted-review.test.ts @@ -174,6 +174,7 @@ describe('getHostedReviewForBranch', () => { await expect( getHostedReviewForBranch({ repoPath: '/repo', + connectionId: 'ssh-1', branch: 'feature/bitbucket', linkedBitbucketPR: 11 }) @@ -188,10 +189,12 @@ describe('getHostedReviewForBranch', () => { mergeable: 'UNKNOWN', headSha: 'abc123' }) + expect(getBitbucketRepoSlugMock).toHaveBeenCalledWith('/repo', 'ssh-1') expect(getBitbucketPullRequestForBranchMock).toHaveBeenCalledWith( '/repo', 'feature/bitbucket', - 11 + 11, + 'ssh-1' ) }) @@ -219,6 +222,7 @@ describe('getHostedReviewForBranch', () => { await expect( getHostedReviewForBranch({ repoPath: '/repo', + connectionId: 'ssh-1', branch: 'feature/gitea', linkedGiteaPR: 14 }) @@ -233,7 +237,13 @@ describe('getHostedReviewForBranch', () => { mergeable: 'MERGEABLE', headSha: 'def456' }) - expect(getGiteaPullRequestForBranchMock).toHaveBeenCalledWith('/repo', 'feature/gitea', 14) + expect(getGiteaRepoSlugMock).toHaveBeenCalledWith('/repo', 'ssh-1') + expect(getGiteaPullRequestForBranchMock).toHaveBeenCalledWith( + '/repo', + 'feature/gitea', + 14, + 'ssh-1' + ) }) it('falls through to Azure DevOps before Gitea when origin is an Azure Repos remote', async () => { @@ -260,6 +270,7 @@ describe('getHostedReviewForBranch', () => { await expect( getHostedReviewForBranch({ repoPath: '/repo', + connectionId: 'ssh-1', branch: 'feature/azure', linkedAzureDevOpsPR: 21 }) @@ -274,10 +285,12 @@ describe('getHostedReviewForBranch', () => { mergeable: 'MERGEABLE', headSha: 'abc123' }) + expect(getAzureDevOpsRepoSlugMock).toHaveBeenCalledWith('/repo', 'ssh-1') expect(getAzureDevOpsPullRequestForBranchMock).toHaveBeenCalledWith( '/repo', 'feature/azure', - 21 + 21, + 'ssh-1' ) expect(getGiteaRepoSlugMock).not.toHaveBeenCalled() }) diff --git a/src/main/source-control/hosted-review.ts b/src/main/source-control/hosted-review.ts index 01d37a02b..05b308cf4 100644 --- a/src/main/source-control/hosted-review.ts +++ b/src/main/source-control/hosted-review.ts @@ -152,32 +152,35 @@ export async function getHostedReviewForBranch(input: { return pr ? mapGitHubReview(pr) : null } - const bitbucketRepo = await getBitbucketRepoSlug(input.repoPath) + const bitbucketRepo = await getBitbucketRepoSlug(input.repoPath, input.connectionId) if (bitbucketRepo) { const pr = await getBitbucketPullRequestForBranch( input.repoPath, branchName, - input.linkedBitbucketPR ?? null + input.linkedBitbucketPR ?? null, + input.connectionId ) return pr ? mapBitbucketReview(pr) : null } - const azureDevOpsRepo = await getAzureDevOpsRepoSlug(input.repoPath) + const azureDevOpsRepo = await getAzureDevOpsRepoSlug(input.repoPath, input.connectionId) if (azureDevOpsRepo) { const pr = await getAzureDevOpsPullRequestForBranch( input.repoPath, branchName, - input.linkedAzureDevOpsPR ?? null + input.linkedAzureDevOpsPR ?? null, + input.connectionId ) return pr ? mapAzureDevOpsReview(pr) : null } - const giteaRepo = await getGiteaRepoSlug(input.repoPath) + const giteaRepo = await getGiteaRepoSlug(input.repoPath, input.connectionId) if (giteaRepo) { const pr = await getGiteaPullRequestForBranch( input.repoPath, branchName, - input.linkedGiteaPR ?? null + input.linkedGiteaPR ?? null, + input.connectionId ) return pr ? mapGiteaReview(pr) : null }