mirror of
https://github.com/stablyai/orca.git
synced 2026-10-15 08:03:08 +00:00
Extract hosted review forge provider contract
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const {
|
||||
createGitHubPullRequestMock,
|
||||
createGitLabMergeRequestMock,
|
||||
getAzureDevOpsRepoSlugMock,
|
||||
getBitbucketRepoSlugMock,
|
||||
getGiteaRepoSlugMock,
|
||||
getMergeRequestForBranchMock,
|
||||
getProjectSlugMock,
|
||||
getPRForBranchMock,
|
||||
getRepoSlugMock
|
||||
} = vi.hoisted(() => ({
|
||||
createGitHubPullRequestMock: vi.fn(),
|
||||
createGitLabMergeRequestMock: vi.fn(),
|
||||
getAzureDevOpsRepoSlugMock: vi.fn(),
|
||||
getBitbucketRepoSlugMock: vi.fn(),
|
||||
getGiteaRepoSlugMock: vi.fn(),
|
||||
getMergeRequestForBranchMock: vi.fn(),
|
||||
getProjectSlugMock: vi.fn(),
|
||||
getPRForBranchMock: vi.fn(),
|
||||
getRepoSlugMock: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('../gitlab/client', () => ({
|
||||
getProjectSlug: getProjectSlugMock,
|
||||
getMergeRequestForBranch: getMergeRequestForBranchMock,
|
||||
getMergeRequest: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('../gitlab/merge-request-creation', () => ({
|
||||
createGitLabMergeRequest: createGitLabMergeRequestMock
|
||||
}))
|
||||
|
||||
vi.mock('../github/client', () => ({
|
||||
createGitHubPullRequest: createGitHubPullRequestMock,
|
||||
getRepoSlug: getRepoSlugMock,
|
||||
getPRForBranch: getPRForBranchMock
|
||||
}))
|
||||
|
||||
vi.mock('../bitbucket/client', () => ({
|
||||
getBitbucketRepoSlug: getBitbucketRepoSlugMock,
|
||||
getBitbucketPullRequestForBranch: vi.fn(),
|
||||
getBitbucketPullRequest: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('../azure-devops/client', () => ({
|
||||
getAzureDevOpsRepoSlug: getAzureDevOpsRepoSlugMock,
|
||||
getAzureDevOpsPullRequestForBranch: vi.fn(),
|
||||
getAzureDevOpsPullRequest: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('../gitea/client', () => ({
|
||||
getGiteaRepoSlug: getGiteaRepoSlugMock,
|
||||
getGiteaPullRequestForBranch: vi.fn(),
|
||||
getGiteaPullRequest: vi.fn()
|
||||
}))
|
||||
|
||||
import {
|
||||
FORGE_PROVIDERS,
|
||||
detectHostedReviewProvider,
|
||||
getForgeProviderById,
|
||||
getForgeProviderForRepository
|
||||
} from './forge-provider'
|
||||
|
||||
describe('forge provider interface', () => {
|
||||
beforeEach(() => {
|
||||
createGitHubPullRequestMock.mockReset()
|
||||
createGitLabMergeRequestMock.mockReset()
|
||||
getAzureDevOpsRepoSlugMock.mockReset()
|
||||
getBitbucketRepoSlugMock.mockReset()
|
||||
getGiteaRepoSlugMock.mockReset()
|
||||
getMergeRequestForBranchMock.mockReset()
|
||||
getProjectSlugMock.mockReset()
|
||||
getPRForBranchMock.mockReset()
|
||||
getRepoSlugMock.mockReset()
|
||||
})
|
||||
|
||||
it('preserves the existing hosted provider detection order', async () => {
|
||||
getProjectSlugMock.mockResolvedValue({ host: 'gitlab.com', path: 'team/orca' })
|
||||
getRepoSlugMock.mockResolvedValue({ owner: 'team', repo: 'orca' })
|
||||
|
||||
await expect(detectHostedReviewProvider({ repoPath: '/repo' })).resolves.toBe('gitlab')
|
||||
await expect(getForgeProviderForRepository({ repoPath: '/repo' })).resolves.toMatchObject({
|
||||
id: 'gitlab'
|
||||
})
|
||||
expect(getRepoSlugMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('keeps review creation capability scoped to providers with creation support', async () => {
|
||||
expect(
|
||||
FORGE_PROVIDERS.map((provider) => [provider.id, provider.supportsReviewCreation])
|
||||
).toEqual([
|
||||
['gitlab', true],
|
||||
['github', true],
|
||||
['bitbucket', false],
|
||||
['azure-devops', false],
|
||||
['gitea', false]
|
||||
])
|
||||
createGitHubPullRequestMock.mockResolvedValue({
|
||||
ok: true,
|
||||
number: 12,
|
||||
url: 'https://github.com/team/orca/pull/12'
|
||||
})
|
||||
|
||||
const provider = getForgeProviderById('github')
|
||||
await expect(
|
||||
provider.createReview?.('/repo', {
|
||||
provider: 'github',
|
||||
base: 'main',
|
||||
head: 'feature/provider-interface',
|
||||
title: 'Add provider interface'
|
||||
})
|
||||
).resolves.toEqual({
|
||||
ok: true,
|
||||
number: 12,
|
||||
url: 'https://github.com/team/orca/pull/12'
|
||||
})
|
||||
expect(createGitHubPullRequestMock).toHaveBeenCalledWith('/repo', {
|
||||
provider: 'github',
|
||||
base: 'main',
|
||||
head: 'feature/provider-interface',
|
||||
title: 'Add provider interface'
|
||||
})
|
||||
})
|
||||
|
||||
it('routes GitLab review creation through the shared provider contract', async () => {
|
||||
createGitLabMergeRequestMock.mockResolvedValue({
|
||||
ok: true,
|
||||
number: 44,
|
||||
url: 'https://gitlab.com/team/orca/-/merge_requests/44'
|
||||
})
|
||||
|
||||
const provider = getForgeProviderById('gitlab')
|
||||
await expect(
|
||||
provider.createReview?.(
|
||||
'/repo',
|
||||
{
|
||||
provider: 'gitlab',
|
||||
base: 'main',
|
||||
head: 'feature/provider-interface',
|
||||
title: 'Add provider interface'
|
||||
},
|
||||
'ssh-1'
|
||||
)
|
||||
).resolves.toEqual({
|
||||
ok: true,
|
||||
number: 44,
|
||||
url: 'https://gitlab.com/team/orca/-/merge_requests/44'
|
||||
})
|
||||
expect(createGitLabMergeRequestMock).toHaveBeenCalledWith(
|
||||
'/repo',
|
||||
{
|
||||
provider: 'gitlab',
|
||||
base: 'main',
|
||||
head: 'feature/provider-interface',
|
||||
title: 'Add provider interface'
|
||||
},
|
||||
'ssh-1'
|
||||
)
|
||||
})
|
||||
|
||||
it('adapts GitHub branch lookup through the shared provider contract', async () => {
|
||||
getPRForBranchMock.mockResolvedValue({
|
||||
number: 7,
|
||||
title: 'Provider branch',
|
||||
state: 'open',
|
||||
url: 'https://github.com/team/orca/pull/7',
|
||||
checksStatus: 'success',
|
||||
updatedAt: '2026-05-29T00:00:00.000Z',
|
||||
mergeable: 'MERGEABLE'
|
||||
})
|
||||
|
||||
await expect(
|
||||
getForgeProviderById('github').getReviewForBranch({
|
||||
repoPath: '/repo',
|
||||
connectionId: 'ssh-1',
|
||||
branch: '',
|
||||
fallbackReviewNumber: 7
|
||||
})
|
||||
).resolves.toMatchObject({
|
||||
provider: 'github',
|
||||
number: 7,
|
||||
status: 'success'
|
||||
})
|
||||
expect(getPRForBranchMock).toHaveBeenCalledWith('/repo', '', null, 'ssh-1', 7)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,266 @@
|
||||
import type {
|
||||
CreateHostedReviewInput,
|
||||
CreateHostedReviewResult,
|
||||
HostedReviewInfo,
|
||||
HostedReviewProvider
|
||||
} from '../../shared/hosted-review'
|
||||
import { hostedReviewInfoFromGitHubPRInfo } from '../../shared/hosted-review-github'
|
||||
import type { MRInfo, PRInfo } from '../../shared/types'
|
||||
import {
|
||||
getAzureDevOpsPullRequest,
|
||||
getAzureDevOpsPullRequestForBranch,
|
||||
getAzureDevOpsRepoSlug
|
||||
} from '../azure-devops/client'
|
||||
import type { AzureDevOpsPullRequestInfo } from '../azure-devops/pull-request-mappers'
|
||||
import {
|
||||
getBitbucketPullRequest,
|
||||
getBitbucketPullRequestForBranch,
|
||||
getBitbucketRepoSlug
|
||||
} from '../bitbucket/client'
|
||||
import type { BitbucketPullRequestInfo } from '../bitbucket/pull-request-mappers'
|
||||
import {
|
||||
getGiteaPullRequest,
|
||||
getGiteaPullRequestForBranch,
|
||||
getGiteaRepoSlug
|
||||
} from '../gitea/client'
|
||||
import type { GiteaPullRequestInfo } from '../gitea/pull-request-mappers'
|
||||
import { createGitHubPullRequest, getPRForBranch, getRepoSlug } from '../github/client'
|
||||
import { getMergeRequest, getMergeRequestForBranch, getProjectSlug } from '../gitlab/client'
|
||||
import { createGitLabMergeRequest } from '../gitlab/merge-request-creation'
|
||||
|
||||
export type ForgeProviderId = Exclude<HostedReviewProvider, 'unsupported'>
|
||||
|
||||
export type ForgeProviderRepositoryContext = {
|
||||
repoPath: string
|
||||
connectionId?: string | null
|
||||
}
|
||||
|
||||
export type ForgeReviewForBranchInput = ForgeProviderRepositoryContext & {
|
||||
branch: string
|
||||
linkedReviewNumber?: number | null
|
||||
fallbackReviewNumber?: number | null
|
||||
}
|
||||
|
||||
export type ForgeReviewByNumberInput = ForgeProviderRepositoryContext & {
|
||||
number: number
|
||||
}
|
||||
|
||||
export type ForgeProvider = {
|
||||
id: ForgeProviderId
|
||||
supportsReviewCreation: boolean
|
||||
resolveRepository(context: ForgeProviderRepositoryContext): Promise<unknown | null>
|
||||
getReviewForBranch(input: ForgeReviewForBranchInput): Promise<HostedReviewInfo | null>
|
||||
getReviewByNumber(input: ForgeReviewByNumberInput): Promise<HostedReviewInfo | null>
|
||||
createReview?(
|
||||
repoPath: string,
|
||||
input: CreateHostedReviewInput,
|
||||
connectionId?: string | null
|
||||
): Promise<CreateHostedReviewResult>
|
||||
}
|
||||
|
||||
function mapGitHubReview(pr: PRInfo): HostedReviewInfo {
|
||||
return hostedReviewInfoFromGitHubPRInfo(pr)
|
||||
}
|
||||
|
||||
function mapGitLabReviewState(state: MRInfo['state']): HostedReviewInfo['state'] {
|
||||
if (state === 'opened' || state === 'locked') {
|
||||
return 'open'
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
function mapGitLabReview(mr: MRInfo): HostedReviewInfo {
|
||||
return {
|
||||
provider: 'gitlab',
|
||||
number: mr.number,
|
||||
title: mr.title,
|
||||
state: mapGitLabReviewState(mr.state),
|
||||
url: mr.url,
|
||||
status: mr.pipelineStatus,
|
||||
updatedAt: mr.updatedAt,
|
||||
mergeable: mr.mergeable,
|
||||
...(mr.headSha ? { headSha: mr.headSha } : {}),
|
||||
...(mr.conflictSummary ? { conflictSummary: mr.conflictSummary } : {})
|
||||
}
|
||||
}
|
||||
|
||||
function mapBitbucketReview(pr: BitbucketPullRequestInfo): HostedReviewInfo {
|
||||
return {
|
||||
provider: 'bitbucket',
|
||||
number: pr.number,
|
||||
title: pr.title,
|
||||
state: pr.state,
|
||||
url: pr.url,
|
||||
status: pr.status,
|
||||
updatedAt: pr.updatedAt,
|
||||
mergeable: pr.mergeable,
|
||||
...(pr.headSha ? { headSha: pr.headSha } : {})
|
||||
}
|
||||
}
|
||||
|
||||
function mapAzureDevOpsReview(pr: AzureDevOpsPullRequestInfo): HostedReviewInfo {
|
||||
return {
|
||||
provider: 'azure-devops',
|
||||
number: pr.number,
|
||||
title: pr.title,
|
||||
state: pr.state,
|
||||
url: pr.url,
|
||||
status: pr.status,
|
||||
updatedAt: pr.updatedAt,
|
||||
mergeable: pr.mergeable,
|
||||
...(pr.headSha ? { headSha: pr.headSha } : {})
|
||||
}
|
||||
}
|
||||
|
||||
function mapGiteaReview(pr: GiteaPullRequestInfo): HostedReviewInfo {
|
||||
return {
|
||||
provider: 'gitea',
|
||||
number: pr.number,
|
||||
title: pr.title,
|
||||
state: pr.state,
|
||||
url: pr.url,
|
||||
status: pr.status,
|
||||
updatedAt: pr.updatedAt,
|
||||
mergeable: pr.mergeable,
|
||||
...(pr.headSha ? { headSha: pr.headSha } : {})
|
||||
}
|
||||
}
|
||||
|
||||
const gitLabForgeProvider = {
|
||||
id: 'gitlab',
|
||||
supportsReviewCreation: true,
|
||||
resolveRepository: ({ repoPath, connectionId }) => getProjectSlug(repoPath, connectionId),
|
||||
async getReviewForBranch(input) {
|
||||
const mr = await getMergeRequestForBranch(
|
||||
input.repoPath,
|
||||
input.branch,
|
||||
input.linkedReviewNumber ?? null,
|
||||
input.connectionId
|
||||
)
|
||||
return mr ? mapGitLabReview(mr) : null
|
||||
},
|
||||
async getReviewByNumber(input) {
|
||||
const mr = await getMergeRequest(input.repoPath, input.number, input.connectionId)
|
||||
return mr ? mapGitLabReview(mr) : null
|
||||
},
|
||||
createReview: createGitLabMergeRequest
|
||||
} satisfies ForgeProvider
|
||||
|
||||
const gitHubForgeProvider = {
|
||||
id: 'github',
|
||||
supportsReviewCreation: true,
|
||||
resolveRepository: ({ repoPath, connectionId }) => getRepoSlug(repoPath, connectionId),
|
||||
async getReviewForBranch(input) {
|
||||
const fallbackReviewNumber =
|
||||
input.linkedReviewNumber == null ? (input.fallbackReviewNumber ?? null) : null
|
||||
const pr =
|
||||
fallbackReviewNumber !== null
|
||||
? await getPRForBranch(
|
||||
input.repoPath,
|
||||
input.branch,
|
||||
input.linkedReviewNumber ?? null,
|
||||
input.connectionId,
|
||||
fallbackReviewNumber
|
||||
)
|
||||
: await getPRForBranch(
|
||||
input.repoPath,
|
||||
input.branch,
|
||||
input.linkedReviewNumber ?? null,
|
||||
input.connectionId
|
||||
)
|
||||
return pr ? mapGitHubReview(pr) : null
|
||||
},
|
||||
async getReviewByNumber(input) {
|
||||
const pr = await getPRForBranch(input.repoPath, '', input.number, input.connectionId)
|
||||
return pr ? mapGitHubReview(pr) : null
|
||||
},
|
||||
createReview: createGitHubPullRequest
|
||||
} satisfies ForgeProvider
|
||||
|
||||
const bitbucketForgeProvider = {
|
||||
id: 'bitbucket',
|
||||
supportsReviewCreation: false,
|
||||
resolveRepository: ({ repoPath, connectionId }) => getBitbucketRepoSlug(repoPath, connectionId),
|
||||
async getReviewForBranch(input) {
|
||||
const pr = await getBitbucketPullRequestForBranch(
|
||||
input.repoPath,
|
||||
input.branch,
|
||||
input.linkedReviewNumber ?? null,
|
||||
input.connectionId
|
||||
)
|
||||
return pr ? mapBitbucketReview(pr) : null
|
||||
},
|
||||
async getReviewByNumber(input) {
|
||||
const pr = await getBitbucketPullRequest(input.repoPath, input.number, input.connectionId)
|
||||
return pr ? mapBitbucketReview(pr) : null
|
||||
}
|
||||
} satisfies ForgeProvider
|
||||
|
||||
const azureDevOpsForgeProvider = {
|
||||
id: 'azure-devops',
|
||||
supportsReviewCreation: false,
|
||||
resolveRepository: ({ repoPath, connectionId }) => getAzureDevOpsRepoSlug(repoPath, connectionId),
|
||||
async getReviewForBranch(input) {
|
||||
const pr = await getAzureDevOpsPullRequestForBranch(
|
||||
input.repoPath,
|
||||
input.branch,
|
||||
input.linkedReviewNumber ?? null,
|
||||
input.connectionId
|
||||
)
|
||||
return pr ? mapAzureDevOpsReview(pr) : null
|
||||
},
|
||||
async getReviewByNumber(input) {
|
||||
const pr = await getAzureDevOpsPullRequest(input.repoPath, input.number, input.connectionId)
|
||||
return pr ? mapAzureDevOpsReview(pr) : null
|
||||
}
|
||||
} satisfies ForgeProvider
|
||||
|
||||
const giteaForgeProvider = {
|
||||
id: 'gitea',
|
||||
supportsReviewCreation: false,
|
||||
resolveRepository: ({ repoPath, connectionId }) => getGiteaRepoSlug(repoPath, connectionId),
|
||||
async getReviewForBranch(input) {
|
||||
const pr = await getGiteaPullRequestForBranch(
|
||||
input.repoPath,
|
||||
input.branch,
|
||||
input.linkedReviewNumber ?? null,
|
||||
input.connectionId
|
||||
)
|
||||
return pr ? mapGiteaReview(pr) : null
|
||||
},
|
||||
async getReviewByNumber(input) {
|
||||
const pr = await getGiteaPullRequest(input.repoPath, input.number, input.connectionId)
|
||||
return pr ? mapGiteaReview(pr) : null
|
||||
}
|
||||
} satisfies ForgeProvider
|
||||
|
||||
// Why: provider order preserves existing branch-status behavior when remotes
|
||||
// could be interpreted by more than one hosting integration.
|
||||
export const FORGE_PROVIDERS = [
|
||||
gitLabForgeProvider,
|
||||
gitHubForgeProvider,
|
||||
bitbucketForgeProvider,
|
||||
azureDevOpsForgeProvider,
|
||||
giteaForgeProvider
|
||||
] as const satisfies readonly ForgeProvider[]
|
||||
|
||||
export function getForgeProviderById(id: ForgeProviderId): ForgeProvider {
|
||||
return FORGE_PROVIDERS.find((provider) => provider.id === id) ?? gitHubForgeProvider
|
||||
}
|
||||
|
||||
export async function getForgeProviderForRepository(
|
||||
context: ForgeProviderRepositoryContext
|
||||
): Promise<ForgeProvider | null> {
|
||||
for (const provider of FORGE_PROVIDERS) {
|
||||
if (await provider.resolveRepository(context)) {
|
||||
return provider
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export async function detectHostedReviewProvider(
|
||||
context: ForgeProviderRepositoryContext
|
||||
): Promise<HostedReviewProvider> {
|
||||
return (await getForgeProviderForRepository(context))?.id ?? 'unsupported'
|
||||
}
|
||||
@@ -33,11 +33,14 @@ const {
|
||||
|
||||
vi.mock('../github/client', () => ({
|
||||
createGitHubPullRequest: createGitHubPullRequestMock,
|
||||
getRepoSlug: getRepoSlugMock
|
||||
getRepoSlug: getRepoSlugMock,
|
||||
getPRForBranch: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('../gitlab/client', () => ({
|
||||
getProjectSlug: getProjectSlugMock
|
||||
getProjectSlug: getProjectSlugMock,
|
||||
getMergeRequestForBranch: vi.fn(),
|
||||
getMergeRequest: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('../gitlab/merge-request-creation', () => ({
|
||||
@@ -45,15 +48,21 @@ vi.mock('../gitlab/merge-request-creation', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('../bitbucket/client', () => ({
|
||||
getBitbucketRepoSlug: getBitbucketRepoSlugMock
|
||||
getBitbucketRepoSlug: getBitbucketRepoSlugMock,
|
||||
getBitbucketPullRequestForBranch: vi.fn(),
|
||||
getBitbucketPullRequest: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('../azure-devops/client', () => ({
|
||||
getAzureDevOpsRepoSlug: getAzureDevOpsRepoSlugMock
|
||||
getAzureDevOpsRepoSlug: getAzureDevOpsRepoSlugMock,
|
||||
getAzureDevOpsPullRequestForBranch: vi.fn(),
|
||||
getAzureDevOpsPullRequest: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('../gitea/client', () => ({
|
||||
getGiteaRepoSlug: getGiteaRepoSlugMock
|
||||
getGiteaRepoSlug: getGiteaRepoSlugMock,
|
||||
getGiteaPullRequestForBranch: vi.fn(),
|
||||
getGiteaPullRequest: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('../github/gh-utils', () => ({
|
||||
|
||||
@@ -12,10 +12,6 @@ import {
|
||||
normalizeHostedReviewBaseRef,
|
||||
normalizeHostedReviewHeadRef
|
||||
} from '../../shared/hosted-review-refs'
|
||||
import { getAzureDevOpsRepoSlug } from '../azure-devops/client'
|
||||
import { getBitbucketRepoSlug } from '../bitbucket/client'
|
||||
import { getGiteaRepoSlug } from '../gitea/client'
|
||||
import { createGitHubPullRequest, getRepoSlug } from '../github/client'
|
||||
import { acquire, ghExecFileAsync, gitExecFileAsync, release } from '../github/gh-utils'
|
||||
import { isNoUpstreamError, normalizeGitErrorMessage } from '../../shared/git-remote-error'
|
||||
import type { GitUpstreamStatus } from '../../shared/types'
|
||||
@@ -23,7 +19,6 @@ import { gitOptionalLocksDisabledEnv } from '../git/runner'
|
||||
import { resolveDefaultBaseRefViaExec } from '../git/repo'
|
||||
import { getUpstreamStatus } from '../git/upstream'
|
||||
import { getProjectSlug } from '../gitlab/client'
|
||||
import { createGitLabMergeRequest } from '../gitlab/merge-request-creation'
|
||||
import {
|
||||
acquire as acquireGlab,
|
||||
glabExecFileAsync,
|
||||
@@ -31,6 +26,7 @@ import {
|
||||
release as releaseGlab
|
||||
} from '../gitlab/gl-utils'
|
||||
import { getSshGitProvider } from '../providers/ssh-git-dispatch'
|
||||
import { detectHostedReviewProvider, getForgeProviderForRepository } from './forge-provider'
|
||||
import { getHostedReviewForBranch } from './hosted-review'
|
||||
|
||||
type HostedReviewCreationEligibilityInput = HostedReviewCreationEligibilityArgs & {
|
||||
@@ -41,28 +37,6 @@ function stripRefPrefix(ref: string): string {
|
||||
return normalizeHostedReviewHeadRef(ref)
|
||||
}
|
||||
|
||||
async function detectHostedReviewProvider(
|
||||
repoPath: string,
|
||||
connectionId?: string | null
|
||||
): Promise<HostedReviewProvider> {
|
||||
if (await getProjectSlug(repoPath, connectionId)) {
|
||||
return 'gitlab'
|
||||
}
|
||||
if (await getRepoSlug(repoPath, connectionId)) {
|
||||
return 'github'
|
||||
}
|
||||
if (await getBitbucketRepoSlug(repoPath, connectionId)) {
|
||||
return 'bitbucket'
|
||||
}
|
||||
if (await getAzureDevOpsRepoSlug(repoPath, connectionId)) {
|
||||
return 'azure-devops'
|
||||
}
|
||||
if (await getGiteaRepoSlug(repoPath, connectionId)) {
|
||||
return 'gitea'
|
||||
}
|
||||
return 'unsupported'
|
||||
}
|
||||
|
||||
async function isGitHubAuthenticated(
|
||||
repoPath: string,
|
||||
connectionId?: string | null
|
||||
@@ -335,7 +309,10 @@ export async function getHostedReviewCreationEligibility(
|
||||
args: HostedReviewCreationEligibilityInput
|
||||
): Promise<HostedReviewCreationEligibility> {
|
||||
const branch = stripRefPrefix(args.branch).trim()
|
||||
const provider = await detectHostedReviewProvider(args.repoPath, args.connectionId)
|
||||
const provider = await detectHostedReviewProvider({
|
||||
repoPath: args.repoPath,
|
||||
connectionId: args.connectionId
|
||||
})
|
||||
const defaultBaseRef =
|
||||
args.base?.trim() || (await getDefaultBaseRef(args.repoPath, args.connectionId))
|
||||
const baseBranch = defaultBaseRef ? normalizeHostedReviewBaseRef(defaultBaseRef) : null
|
||||
@@ -422,8 +399,8 @@ export async function createHostedReview(
|
||||
error: 'Creating reviews for this provider is not supported yet.'
|
||||
}
|
||||
}
|
||||
const provider = await detectHostedReviewProvider(repoPath, connectionId)
|
||||
if (provider !== input.provider) {
|
||||
const provider = await getForgeProviderForRepository({ repoPath, connectionId })
|
||||
if (provider?.id !== input.provider || !provider.createReview) {
|
||||
const copy = reviewCopy(input.provider)
|
||||
return {
|
||||
ok: false,
|
||||
@@ -435,8 +412,5 @@ export async function createHostedReview(
|
||||
if (blocked) {
|
||||
return blocked
|
||||
}
|
||||
if (input.provider === 'gitlab') {
|
||||
return createGitLabMergeRequest(repoPath, input, connectionId)
|
||||
}
|
||||
return createGitHubPullRequest(repoPath, input, connectionId)
|
||||
return provider.createReview(repoPath, input, connectionId)
|
||||
}
|
||||
|
||||
@@ -32,7 +32,8 @@ vi.mock('../gitlab/client', () => ({
|
||||
|
||||
vi.mock('../github/client', () => ({
|
||||
getRepoSlug: getRepoSlugMock,
|
||||
getPRForBranch: getPRForBranchMock
|
||||
getPRForBranch: getPRForBranchMock,
|
||||
createGitHubPullRequest: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('../bitbucket/client', () => ({
|
||||
|
||||
@@ -1,92 +1,28 @@
|
||||
import type { HostedReviewInfo } from '../../shared/hosted-review'
|
||||
import type { MRInfo, PRInfo } from '../../shared/types'
|
||||
import { hostedReviewInfoFromGitHubPRInfo } from '../../shared/hosted-review-github'
|
||||
import {
|
||||
getAzureDevOpsPullRequest,
|
||||
getAzureDevOpsPullRequestForBranch,
|
||||
getAzureDevOpsRepoSlug
|
||||
} from '../azure-devops/client'
|
||||
import type { AzureDevOpsPullRequestInfo } from '../azure-devops/pull-request-mappers'
|
||||
import {
|
||||
getBitbucketPullRequest,
|
||||
getBitbucketPullRequestForBranch,
|
||||
getBitbucketRepoSlug
|
||||
} from '../bitbucket/client'
|
||||
import type { BitbucketPullRequestInfo } from '../bitbucket/pull-request-mappers'
|
||||
import {
|
||||
getGiteaPullRequest,
|
||||
getGiteaPullRequestForBranch,
|
||||
getGiteaRepoSlug
|
||||
} from '../gitea/client'
|
||||
import type { GiteaPullRequestInfo } from '../gitea/pull-request-mappers'
|
||||
import { getPRForBranch, getRepoSlug } from '../github/client'
|
||||
import { getMergeRequest, getMergeRequestForBranch, getProjectSlug } from '../gitlab/client'
|
||||
getForgeProviderById,
|
||||
getForgeProviderForRepository,
|
||||
type ForgeProviderId
|
||||
} from './forge-provider'
|
||||
|
||||
function mapGitHubReview(pr: PRInfo): HostedReviewInfo {
|
||||
return hostedReviewInfoFromGitHubPRInfo(pr)
|
||||
}
|
||||
|
||||
function mapGitLabReviewState(state: MRInfo['state']): HostedReviewInfo['state'] {
|
||||
if (state === 'opened' || state === 'locked') {
|
||||
return 'open'
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
function mapGitLabReview(mr: MRInfo): HostedReviewInfo {
|
||||
return {
|
||||
provider: 'gitlab',
|
||||
number: mr.number,
|
||||
title: mr.title,
|
||||
state: mapGitLabReviewState(mr.state),
|
||||
url: mr.url,
|
||||
status: mr.pipelineStatus,
|
||||
updatedAt: mr.updatedAt,
|
||||
mergeable: mr.mergeable,
|
||||
...(mr.headSha ? { headSha: mr.headSha } : {}),
|
||||
...(mr.conflictSummary ? { conflictSummary: mr.conflictSummary } : {})
|
||||
}
|
||||
}
|
||||
|
||||
function mapBitbucketReview(pr: BitbucketPullRequestInfo): HostedReviewInfo {
|
||||
return {
|
||||
provider: 'bitbucket',
|
||||
number: pr.number,
|
||||
title: pr.title,
|
||||
state: pr.state,
|
||||
url: pr.url,
|
||||
status: pr.status,
|
||||
updatedAt: pr.updatedAt,
|
||||
mergeable: pr.mergeable,
|
||||
...(pr.headSha ? { headSha: pr.headSha } : {})
|
||||
}
|
||||
}
|
||||
|
||||
function mapAzureDevOpsReview(pr: AzureDevOpsPullRequestInfo): HostedReviewInfo {
|
||||
return {
|
||||
provider: 'azure-devops',
|
||||
number: pr.number,
|
||||
title: pr.title,
|
||||
state: pr.state,
|
||||
url: pr.url,
|
||||
status: pr.status,
|
||||
updatedAt: pr.updatedAt,
|
||||
mergeable: pr.mergeable,
|
||||
...(pr.headSha ? { headSha: pr.headSha } : {})
|
||||
}
|
||||
}
|
||||
|
||||
function mapGiteaReview(pr: GiteaPullRequestInfo): HostedReviewInfo {
|
||||
return {
|
||||
provider: 'gitea',
|
||||
number: pr.number,
|
||||
title: pr.title,
|
||||
state: pr.state,
|
||||
url: pr.url,
|
||||
status: pr.status,
|
||||
updatedAt: pr.updatedAt,
|
||||
mergeable: pr.mergeable,
|
||||
...(pr.headSha ? { headSha: pr.headSha } : {})
|
||||
function reviewLinkForProvider(
|
||||
input: Parameters<typeof getHostedReviewForBranch>[0],
|
||||
provider: ForgeProviderId
|
||||
): { linkedReviewNumber?: number | null; fallbackReviewNumber?: number | null } {
|
||||
switch (provider) {
|
||||
case 'github':
|
||||
return {
|
||||
linkedReviewNumber: input.linkedGitHubPR ?? null,
|
||||
fallbackReviewNumber: input.linkedGitHubPR == null ? (input.fallbackGitHubPR ?? null) : null
|
||||
}
|
||||
case 'gitlab':
|
||||
return { linkedReviewNumber: input.linkedGitLabMR ?? null }
|
||||
case 'bitbucket':
|
||||
return { linkedReviewNumber: input.linkedBitbucketPR ?? null }
|
||||
case 'azure-devops':
|
||||
return { linkedReviewNumber: input.linkedAzureDevOpsPR ?? null }
|
||||
case 'gitea':
|
||||
return { linkedReviewNumber: input.linkedGiteaPR ?? null }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,99 +52,25 @@ export async function getHostedReviewForBranch(input: {
|
||||
return null
|
||||
}
|
||||
|
||||
// Why: branch review status is tied to the branch publishing remote.
|
||||
// GitHub and GitLab task/project surfaces may use richer per-provider
|
||||
// source preferences, but this core status should follow origin.
|
||||
const gitlabProject = await getProjectSlug(input.repoPath, input.connectionId)
|
||||
if (gitlabProject) {
|
||||
const mr =
|
||||
(await getMergeRequestForBranch(
|
||||
input.repoPath,
|
||||
branchName,
|
||||
input.linkedGitLabMR ?? null,
|
||||
input.connectionId
|
||||
)) ?? null
|
||||
return mr ? mapGitLabReview(mr) : null
|
||||
const provider = await getForgeProviderForRepository({
|
||||
repoPath: input.repoPath,
|
||||
connectionId: input.connectionId
|
||||
})
|
||||
if (!provider) {
|
||||
return null
|
||||
}
|
||||
|
||||
const githubRepo = await getRepoSlug(input.repoPath, input.connectionId)
|
||||
if (githubRepo) {
|
||||
const fallbackGitHubPR = input.linkedGitHubPR == null ? (input.fallbackGitHubPR ?? null) : null
|
||||
const pr =
|
||||
fallbackGitHubPR !== null
|
||||
? await getPRForBranch(
|
||||
input.repoPath,
|
||||
branchName,
|
||||
input.linkedGitHubPR ?? null,
|
||||
input.connectionId,
|
||||
fallbackGitHubPR
|
||||
)
|
||||
: await getPRForBranch(
|
||||
input.repoPath,
|
||||
branchName,
|
||||
input.linkedGitHubPR ?? null,
|
||||
input.connectionId
|
||||
)
|
||||
return pr ? mapGitHubReview(pr) : null
|
||||
}
|
||||
|
||||
const bitbucketRepo = await getBitbucketRepoSlug(input.repoPath, input.connectionId)
|
||||
if (bitbucketRepo) {
|
||||
const pr = await getBitbucketPullRequestForBranch(
|
||||
input.repoPath,
|
||||
branchName,
|
||||
input.linkedBitbucketPR ?? null,
|
||||
input.connectionId
|
||||
)
|
||||
return pr ? mapBitbucketReview(pr) : null
|
||||
}
|
||||
|
||||
const azureDevOpsRepo = await getAzureDevOpsRepoSlug(input.repoPath, input.connectionId)
|
||||
if (azureDevOpsRepo) {
|
||||
const pr = await getAzureDevOpsPullRequestForBranch(
|
||||
input.repoPath,
|
||||
branchName,
|
||||
input.linkedAzureDevOpsPR ?? null,
|
||||
input.connectionId
|
||||
)
|
||||
return pr ? mapAzureDevOpsReview(pr) : null
|
||||
}
|
||||
|
||||
const giteaRepo = await getGiteaRepoSlug(input.repoPath, input.connectionId)
|
||||
if (giteaRepo) {
|
||||
const pr = await getGiteaPullRequestForBranch(
|
||||
input.repoPath,
|
||||
branchName,
|
||||
input.linkedGiteaPR ?? null,
|
||||
input.connectionId
|
||||
)
|
||||
return pr ? mapGiteaReview(pr) : null
|
||||
}
|
||||
|
||||
return null
|
||||
return provider.getReviewForBranch({
|
||||
repoPath: input.repoPath,
|
||||
connectionId: input.connectionId,
|
||||
branch: branchName,
|
||||
...reviewLinkForProvider(input, provider.id)
|
||||
})
|
||||
}
|
||||
|
||||
export async function getHostedReviewByNumber(input: {
|
||||
repoPath: string
|
||||
provider: 'github' | 'gitlab' | 'bitbucket' | 'azure-devops' | 'gitea'
|
||||
provider: ForgeProviderId
|
||||
number: number
|
||||
}): Promise<HostedReviewInfo | null> {
|
||||
if (input.provider === 'gitlab') {
|
||||
const mr = await getMergeRequest(input.repoPath, input.number)
|
||||
return mr ? mapGitLabReview(mr) : null
|
||||
}
|
||||
if (input.provider === 'bitbucket') {
|
||||
const pr = await getBitbucketPullRequest(input.repoPath, input.number)
|
||||
return pr ? mapBitbucketReview(pr) : null
|
||||
}
|
||||
if (input.provider === 'azure-devops') {
|
||||
const pr = await getAzureDevOpsPullRequest(input.repoPath, input.number)
|
||||
return pr ? mapAzureDevOpsReview(pr) : null
|
||||
}
|
||||
if (input.provider === 'gitea') {
|
||||
const pr = await getGiteaPullRequest(input.repoPath, input.number)
|
||||
return pr ? mapGiteaReview(pr) : null
|
||||
}
|
||||
const pr = await getPRForBranch(input.repoPath, '', input.number)
|
||||
return pr ? mapGitHubReview(pr) : null
|
||||
return getForgeProviderById(input.provider).getReviewByNumber(input)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user