mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
* Enable pushing to configured push targets for existing fork reviews * Resolve push targets using branch.pushRemote, remote.pushDefault, and URL-valued remotes normalized to matching named remotes. * Support this push target resolution on both local and relay/SSH git handlers to prevent drift in SSH worktrees. * Offer "Push" instead of "Publish Branch" in the Source Control UI when a linked review exists and a valid push target is available. * Prevent pushing a feature branch's base branch (e.g. main) directly to a fork via remote.pushDefault. * Keep fork push target when contributor branch matches base branch name Ensure that a fork push target is not incorrectly discarded when its branch name matches the base branch name on another remote (for example, targeting fork/main while the base is origin/main). Previously, the matching logic only compared the branch leaf name, which treated different remotes as identical and disabled the push target. We now qualify the ref comparison with the remote name to differentiate them.
169 lines
5.3 KiB
TypeScript
169 lines
5.3 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest'
|
|
import { resolveRelayPushTarget } from './git-handler-push-target'
|
|
|
|
type GitArgs = string[]
|
|
|
|
function gitForConfig(config: {
|
|
branch?: string
|
|
pushRemote?: string | Error
|
|
pushDefault?: string | Error
|
|
branchRemote?: string | Error
|
|
merge?: string
|
|
base?: string | Error
|
|
remotes?: string[]
|
|
remoteUrls?: Record<string, string>
|
|
}) {
|
|
const branch = config.branch ?? 'feature/fix'
|
|
const merge = config.merge ?? `refs/heads/${branch}`
|
|
return vi.fn(async (args: GitArgs) => {
|
|
if (args[0] === 'symbolic-ref') {
|
|
return { stdout: `${branch}\n`, stderr: '' }
|
|
}
|
|
if (args[0] === 'config' && args[2] === `branch.${branch}.pushRemote`) {
|
|
if (config.pushRemote instanceof Error) {
|
|
throw config.pushRemote
|
|
}
|
|
return { stdout: `${config.pushRemote ?? ''}\n`, stderr: '' }
|
|
}
|
|
if (args[0] === 'config' && args[2] === 'remote.pushDefault') {
|
|
if (config.pushDefault instanceof Error) {
|
|
throw config.pushDefault
|
|
}
|
|
return { stdout: `${config.pushDefault ?? ''}\n`, stderr: '' }
|
|
}
|
|
if (args[0] === 'config' && args[2] === `branch.${branch}.remote`) {
|
|
if (config.branchRemote instanceof Error) {
|
|
throw config.branchRemote
|
|
}
|
|
return { stdout: `${config.branchRemote ?? ''}\n`, stderr: '' }
|
|
}
|
|
if (args[0] === 'config' && args[2] === `branch.${branch}.merge`) {
|
|
return { stdout: `${merge}\n`, stderr: '' }
|
|
}
|
|
if (args[0] === 'config' && args[2] === `branch.${branch}.base`) {
|
|
if (config.base instanceof Error) {
|
|
throw config.base
|
|
}
|
|
return { stdout: `${config.base ?? ''}\n`, stderr: '' }
|
|
}
|
|
if (args[0] === 'remote' && args.length === 1) {
|
|
return { stdout: `${config.remotes?.join('\n') ?? ''}\n`, stderr: '' }
|
|
}
|
|
if (args[0] === 'remote' && args[1] === 'get-url') {
|
|
const remoteUrl = config.remoteUrls?.[args[2] ?? '']
|
|
if (!remoteUrl) {
|
|
throw new Error('missing remote URL')
|
|
}
|
|
return { stdout: `${remoteUrl}\n`, stderr: '' }
|
|
}
|
|
throw new Error(`unexpected git args: ${args.join(' ')}`)
|
|
})
|
|
}
|
|
|
|
describe('resolveRelayPushTarget', () => {
|
|
it('uses branch pushRemote for a configured review head branch', async () => {
|
|
const git = gitForConfig({
|
|
pushRemote: 'fork',
|
|
branchRemote: 'fork',
|
|
merge: 'refs/heads/contributor/fix'
|
|
})
|
|
|
|
await expect(resolveRelayPushTarget(git, '/repo', undefined)).resolves.toEqual({
|
|
remote: 'fork',
|
|
refspec: 'HEAD:contributor/fix'
|
|
})
|
|
})
|
|
|
|
it('does not combine remote.pushDefault with a base-branch merge target', async () => {
|
|
const git = gitForConfig({
|
|
branch: 'feature/fix',
|
|
pushRemote: new Error('missing pushRemote'),
|
|
pushDefault: 'fork',
|
|
branchRemote: 'origin',
|
|
merge: 'refs/heads/main',
|
|
base: 'refs/remotes/origin/main'
|
|
})
|
|
|
|
await expect(resolveRelayPushTarget(git, '/repo', undefined)).resolves.toBeNull()
|
|
})
|
|
|
|
it('keeps a fork head target when the contributor branch matches the base branch name', async () => {
|
|
const git = gitForConfig({
|
|
branch: 'review/pr-1',
|
|
pushRemote: 'fork',
|
|
branchRemote: 'fork',
|
|
merge: 'refs/heads/main',
|
|
base: 'refs/remotes/origin/main'
|
|
})
|
|
|
|
await expect(resolveRelayPushTarget(git, '/repo', undefined)).resolves.toEqual({
|
|
remote: 'fork',
|
|
refspec: 'HEAD:main'
|
|
})
|
|
})
|
|
|
|
it('uses remote.pushDefault when branch pushRemote is missing', async () => {
|
|
const git = gitForConfig({
|
|
pushRemote: new Error('missing pushRemote'),
|
|
pushDefault: 'fork',
|
|
branchRemote: 'origin'
|
|
})
|
|
|
|
await expect(resolveRelayPushTarget(git, '/repo', undefined)).resolves.toEqual({
|
|
remote: 'fork',
|
|
refspec: 'HEAD:feature/fix'
|
|
})
|
|
})
|
|
|
|
it('normalizes a URL-valued branch remote to a matching named remote', async () => {
|
|
const forkUrl = 'https://github.com/contributor/orca.git'
|
|
const git = gitForConfig({
|
|
pushRemote: new Error('missing pushRemote'),
|
|
pushDefault: new Error('missing pushDefault'),
|
|
branchRemote: forkUrl,
|
|
remotes: ['origin', 'pr-contributor-orca'],
|
|
remoteUrls: {
|
|
origin: 'https://github.com/stablyai/orca.git',
|
|
'pr-contributor-orca': forkUrl
|
|
}
|
|
})
|
|
|
|
await expect(resolveRelayPushTarget(git, '/repo', undefined)).resolves.toEqual({
|
|
remote: 'pr-contributor-orca',
|
|
refspec: 'HEAD:feature/fix'
|
|
})
|
|
})
|
|
|
|
it('keeps a URL-valued pushRemote when no named remote matches it', async () => {
|
|
const forkUrl = 'git@github.com:contributor/orca.git'
|
|
const git = gitForConfig({
|
|
pushRemote: forkUrl,
|
|
branchRemote: forkUrl,
|
|
remotes: ['origin'],
|
|
remoteUrls: {
|
|
origin: 'git@github.com:stablyai/orca.git'
|
|
}
|
|
})
|
|
|
|
await expect(resolveRelayPushTarget(git, '/repo', undefined)).resolves.toEqual({
|
|
remote: forkUrl,
|
|
refspec: 'HEAD:feature/fix'
|
|
})
|
|
})
|
|
|
|
it('uses an explicit push target without reading branch config', async () => {
|
|
const git = vi.fn(async () => ({ stdout: '', stderr: '' }))
|
|
|
|
await expect(
|
|
resolveRelayPushTarget(git, '/repo', {
|
|
remoteName: 'fork',
|
|
branchName: 'feature/head'
|
|
})
|
|
).resolves.toEqual({
|
|
remote: 'fork',
|
|
refspec: 'HEAD:feature/head'
|
|
})
|
|
expect(git).toHaveBeenCalledWith(['check-ref-format', '--branch', 'feature/head'], '/repo')
|
|
})
|
|
})
|