Fix SSH hosted review provider detection (#3618)
* Fix SSH hosted review provider detection * test: cover ssh hosted review cache retries Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com> Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
parent
bc11db4e9f
commit
d67d8defa5
|
|
@ -254,14 +254,12 @@ export async function getAzureDevOpsAuthStatus(): Promise<AzureDevOpsAuthStatus>
|
|||
|
||||
export async function getAzureDevOpsPullRequest(
|
||||
repoPath: string,
|
||||
prNumber: number
|
||||
prNumber: number,
|
||||
connectionId?: string | null
|
||||
): Promise<AzureDevOpsPullRequestInfo | 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
|
||||
}
|
||||
const raw = await requestJson<RawAzureDevOpsPullRequest>(
|
||||
|
|
@ -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<AzureDevOpsPullRequestInfo | null> {
|
||||
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<AzureDevOpsRepoRef | null> {
|
||||
return getAzureDevOpsRepoRef(repoPath)
|
||||
export async function getAzureDevOpsRepoSlug(
|
||||
repoPath: string,
|
||||
connectionId?: string | null
|
||||
): Promise<AzureDevOpsRepoRef | null> {
|
||||
return getAzureDevOpsRepoRef(repoPath, connectionId)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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<AzureDevOpsRepoRef | null> {
|
||||
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<AzureDevOpsRepoRef | null> {
|
||||
return getAzureDevOpsRepoRefForRemote(repoPath, 'origin')
|
||||
export async function getAzureDevOpsRepoRef(
|
||||
repoPath: string,
|
||||
connectionId?: string | null
|
||||
): Promise<AzureDevOpsRepoRef | null> {
|
||||
return getAzureDevOpsRepoRefForRemote(repoPath, 'origin', connectionId)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -159,9 +159,10 @@ export async function getBitbucketAuthStatus(): Promise<BitbucketAuthStatus> {
|
|||
|
||||
export async function getBitbucketPullRequest(
|
||||
repoPath: string,
|
||||
prNumber: number
|
||||
prNumber: number,
|
||||
connectionId?: string | null
|
||||
): Promise<BitbucketPullRequestInfo | null> {
|
||||
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<BitbucketPullRequestInfo | null> {
|
||||
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<BitbucketRepoRef | null> {
|
||||
return getBitbucketRepoRef(repoPath)
|
||||
export async function getBitbucketRepoSlug(
|
||||
repoPath: string,
|
||||
connectionId?: string | null
|
||||
): Promise<BitbucketRepoRef | null> {
|
||||
return getBitbucketRepoRef(repoPath, connectionId)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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<BitbucketRepoRef | null> {
|
||||
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<BitbucketRepoRef | null> {
|
||||
return getBitbucketRepoRefForRemote(repoPath, 'origin')
|
||||
export async function getBitbucketRepoRef(
|
||||
repoPath: string,
|
||||
connectionId?: string | null
|
||||
): Promise<BitbucketRepoRef | null> {
|
||||
return getBitbucketRepoRefForRemote(repoPath, 'origin', connectionId)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -186,9 +186,10 @@ export async function getGiteaAuthStatus(): Promise<GiteaAuthStatus> {
|
|||
|
||||
export async function getGiteaPullRequest(
|
||||
repoPath: string,
|
||||
prNumber: number
|
||||
prNumber: number,
|
||||
connectionId?: string | null
|
||||
): Promise<GiteaPullRequestInfo | null> {
|
||||
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<GiteaPullRequestInfo | null> {
|
||||
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<GiteaRepoRef | null> {
|
||||
return getGiteaRepoRef(repoPath)
|
||||
export async function getGiteaRepoSlug(
|
||||
repoPath: string,
|
||||
connectionId?: string | null
|
||||
): Promise<GiteaRepoRef | null> {
|
||||
return getGiteaRepoRef(repoPath, connectionId)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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<GiteaRepoRef | null> {
|
||||
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<GiteaRepoRef | null> {
|
||||
return getGiteaRepoRefForRemote(repoPath, 'origin')
|
||||
export async function getGiteaRepoRef(
|
||||
repoPath: string,
|
||||
connectionId?: string | null
|
||||
): Promise<GiteaRepoRef | null> {
|
||||
return getGiteaRepoRefForRemote(repoPath, 'origin', connectionId)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue