Files
orca/src/shared/git-configured-branch-target.ts
T
Jinjing 692e0b0ab9 Push existing fork review branches without republishing (#5531)
* 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.
2026-06-16 18:10:01 -07:00

122 lines
3.9 KiB
TypeScript

import { gitRefTargetsBranchOnRemote } from './git-remote-branch-name'
type GitCommandRunner = (args: string[]) => Promise<{ stdout: string }>
type RemoteTrackingRefExists = (remoteName: string, branchName: string) => Promise<boolean>
export type ConfiguredBranchRemoteUpstream = {
upstreamName: string
remoteName: string
branchName: string
isConfiguredUpstream: false
}
async function getGitConfigValue(runGit: GitCommandRunner, key: string): Promise<string | null> {
try {
const { stdout } = await runGit(['config', '--get', key])
const value = stdout.trim()
return value || null
} catch {
return null
}
}
function isUrlValuedRemote(remote: string): boolean {
return /^[A-Za-z][A-Za-z0-9+.-]*:\/\//.test(remote) || /^[^@/:]+@[^:]+:.+/.test(remote)
}
async function findRemoteNameForUrl(
runGit: GitCommandRunner,
remoteUrl: string
): Promise<string | null> {
try {
const { stdout } = await runGit(['remote'])
const remotes = stdout
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean)
for (const remoteName of remotes) {
try {
const { stdout: urlStdout } = await runGit(['remote', 'get-url', remoteName])
if (urlStdout.trim() === remoteUrl) {
return remoteName
}
} catch {
// Ignore a remote that disappeared or has no fetch URL.
}
}
} catch {
return null
}
return null
}
export async function getConfiguredBranchRemoteUpstream(
runGit: GitCommandRunner,
currentBranchName: string,
remoteTrackingRefExists: RemoteTrackingRefExists
): Promise<ConfiguredBranchRemoteUpstream | null> {
const [remote, mergeRef, baseRef] = await Promise.all([
getGitConfigValue(runGit, `branch.${currentBranchName}.remote`),
getGitConfigValue(runGit, `branch.${currentBranchName}.merge`),
getGitConfigValue(runGit, `branch.${currentBranchName}.base`)
])
const branchName = mergeRef?.replace(/^refs\/heads\//, '') ?? ''
if (!remote || !branchName || branchName === mergeRef || remote === '.') {
return null
}
const remoteName = isUrlValuedRemote(remote) ? await findRemoteNameForUrl(runGit, remote) : remote
if (
!remoteName ||
gitRefTargetsBranchOnRemote(baseRef, remoteName, branchName) ||
!(await remoteTrackingRefExists(remoteName, branchName))
) {
return null
}
return {
upstreamName: `${remoteName}/${branchName}`,
remoteName,
branchName,
isConfiguredUpstream: false
}
}
export async function hasConfiguredBranchPushTarget(
runGit: GitCommandRunner,
currentBranchName: string
): Promise<boolean> {
const [pushRemote, pushDefault, branchRemote, mergeRef, baseRef] = await Promise.all([
getGitConfigValue(runGit, `branch.${currentBranchName}.pushRemote`),
getGitConfigValue(runGit, 'remote.pushDefault'),
getGitConfigValue(runGit, `branch.${currentBranchName}.remote`),
getGitConfigValue(runGit, `branch.${currentBranchName}.merge`),
getGitConfigValue(runGit, `branch.${currentBranchName}.base`)
])
const remote = pushRemote ?? pushDefault ?? branchRemote
const branchName = mergeRef?.replace(/^refs\/heads\//, '') ?? ''
if (!remote || remote === '.' || !branchName || branchName === mergeRef) {
return false
}
const pushRemoteName = isUrlValuedRemote(remote)
? ((await findRemoteNameForUrl(runGit, remote)) ?? remote)
: remote
const branchRemoteName = branchRemote
? isUrlValuedRemote(branchRemote)
? ((await findRemoteNameForUrl(runGit, branchRemote)) ?? branchRemote)
: branchRemote
: null
if (gitRefTargetsBranchOnRemote(baseRef, pushRemoteName, branchName)) {
return false
}
// Why: branch.merge belongs to branch.remote. Do not combine a user's
// pushDefault fork with an origin/main merge target and call it pushable.
if (
branchName !== currentBranchName &&
(pushRemoteName === 'origin' || branchRemoteName !== pushRemoteName)
) {
return false
}
return true
}