perf(git): answer remote-URL questions from one subprocess, not one per remote (#18158)

Four copies of the same loop ran `git remote` and then a serial
`git remote get-url <name>` per remote to answer "which remote has this
URL". On a repo with 58 remotes that is 59 subprocesses -- measured at
1083 ms -- for one question, and worktree create asks it several times.
`git remote -v` answers for every remote from one child, reporting the
same insteadOf-expanded first fetch URL `get-url` prints.

The batched `cat-file --batch-check` branch-conflict probe decides from
stdout, but its WSL route was unfenced, so a login-shell fallback printed
the distro banner onto the stream it parses. That broke the
one-line-per-ref contract, made every batch undecided, and fell straight
back to one `show-ref` per remote -- the cost the batch exists to remove.

Measured at 58 remotes / 4346 branches, spawns and wall time:
  push-target remote scan      59 -> 1  (1083 ms -> 8 ms)
  branch-conflict probe        60 -> 3  (984 ms -> 43 ms)
  configured push target      123 -> 6  (2707 ms -> 157 ms)
This commit is contained in:
Neil
2026-09-02 12:53:48 -07:00
committed by GitHub
parent 1d94ebee3f
commit 104f9655e4
18 changed files with 841 additions and 128 deletions
+5 -1
View File
@@ -157,7 +157,11 @@ export async function probeAnyExactRefBatched(
} catch {
return { found: false, unknown: true }
}
const lines = stdout.split('\n').filter((line) => line.trim().length > 0)
// Trim per line so a CRLF-translating host's `\r` does not become part of the type.
const lines = stdout
.split('\n')
.map((line) => line.trim())
.filter((line) => line.length > 0)
// One line per input, in order; a short read means the batch never answered for the rest.
if (lines.length !== safeRefs.length) {
return { found: false, unknown: true }
+59
View File
@@ -193,6 +193,17 @@ describe('git remote operations', () => {
if (args[0] === 'remote' && args[1] === 'get-url' && args[2] === 'pr-pynickle-orca') {
return { stdout: 'https://github.com/pynickle/orca.git\n', stderr: '' }
}
if (args[0] === 'remote' && args[1] === '-v') {
return {
stdout: [
'origin\thttps://github.com/stablyai/orca.git (fetch)',
'origin\thttps://github.com/stablyai/orca.git (push)',
'pr-pynickle-orca\thttps://github.com/pynickle/orca.git (fetch)',
'pr-pynickle-orca\thttps://github.com/pynickle/orca.git (push)'
].join('\n'),
stderr: ''
}
}
if (args[0] === 'remote') {
return { stdout: 'origin\npr-pynickle-orca\n', stderr: '' }
}
@@ -207,6 +218,54 @@ describe('git remote operations', () => {
)
})
// Regression: normalizing a URL-valued push remote used to run `git remote` and then a
// serial `git remote get-url` per remote -- 59 subprocesses on a 58-remote repo.
it('normalizes a URL-valued push remote from one remote table read at 58 remotes', async () => {
const remotes = [
{ name: 'origin', url: 'https://github.com/stablyai/orca.git' },
...Array.from({ length: 56 }, (_, index) => ({
name: `pr-user${index}-orca`,
url: `https://github.com/user${index}/orca.git`
})),
{ name: 'pr-pynickle-orca', url: 'https://github.com/pynickle/orca.git' }
]
gitExecFileAsyncMock.mockImplementation(async (args: string[]) => {
if (args[0] === 'symbolic-ref') {
return { stdout: 'imp/chinese-translation\n', stderr: '' }
}
if (args[0] === 'config' && args.includes('branch.imp/chinese-translation.remote')) {
return { stdout: 'https://github.com/pynickle/orca.git\n', stderr: '' }
}
if (args[0] === 'config' && args.includes('branch.imp/chinese-translation.merge')) {
return { stdout: 'refs/heads/imp/chinese-translation\n', stderr: '' }
}
if (args[0] === 'config') {
throw new Error(`config key is not set: ${args.join(' ')}`)
}
if (args[0] === 'remote' && args[1] === '-v') {
return {
stdout: remotes
.flatMap(({ name, url }) => [`${name}\t${url} (fetch)`, `${name}\t${url} (push)`])
.join('\n'),
stderr: ''
}
}
if (args[0] === 'remote') {
throw new Error(`unexpected remote scan: ${args.join(' ')}`)
}
return { stdout: '', stderr: '' }
})
await gitPush('/repo', false)
const remoteReads = gitExecFileAsyncMock.mock.calls.filter(([args]) => args[0] === 'remote')
expect(remoteReads.map(([args]) => args)).toEqual([['remote', '-v']])
expect(gitExecFileAsyncMock).toHaveBeenLastCalledWith(
['push', '--set-upstream', 'pr-pynickle-orca', 'HEAD:imp/chinese-translation'],
{ cwd: '/repo' }
)
})
it('uses an explicit push target even when it differs from the local branch name', async () => {
gitExecFileAsyncMock
.mockResolvedValueOnce({ stdout: '', stderr: '' })
+15 -23
View File
@@ -4,6 +4,7 @@ import {
} from '../../shared/git-remote-error'
import { resolveEffectiveGitUpstream } from '../../shared/git-effective-upstream'
import { gitRefTargetsBranchOnRemote } from '../../shared/git-remote-branch-name'
import { findGitRemoteNameByFetchUrl } from '../../shared/git-remote-url-index'
import type { GitPushTarget } from '../../shared/worktree/types'
import type { GitRuntimeOptions } from './git-runtime-options'
import { gitOptionsForWorktree } from './git-runtime-options'
@@ -84,6 +85,8 @@ type ConfiguredPushRemote = {
branchRemote: string | null
}
// One `git remote -v` instead of `git remote` plus a serial `git remote get-url`
// per remote; both print the same insteadOf-expanded fetch URL.
async function findRemoteNameForUrl(
worktreePath: string,
remoteUrl: string,
@@ -91,30 +94,13 @@ async function findRemoteNameForUrl(
): Promise<string | null> {
try {
const { stdout } = await gitExecFileAsync(
['remote'],
['remote', '-v'],
gitOptionsForWorktree(worktreePath, options)
)
const remotes = stdout
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean)
for (const remoteName of remotes) {
try {
const { stdout: urlStdout } = await gitExecFileAsync(
['remote', 'get-url', remoteName],
gitOptionsForWorktree(worktreePath, options)
)
if (urlStdout.trim() === remoteUrl) {
return remoteName
}
} catch {
// Ignore a remote that disappeared or has no fetch URL.
}
}
return findGitRemoteNameByFetchUrl(stdout, (candidateUrl) => candidateUrl === remoteUrl)
} catch {
return null
}
return null
}
async function normalizePushRemote(
@@ -141,11 +127,17 @@ async function getConfiguredPushRemote(
if (!remote) {
return null
}
const normalizedRemote = await normalizePushRemote(worktreePath, remote, options)
// The two usually name the same URL; resolving it twice reads the remote table twice.
if (!branchRemote) {
return { remote: normalizedRemote, branchRemote: null }
}
return {
remote: await normalizePushRemote(worktreePath, remote, options),
branchRemote: branchRemote
? await normalizePushRemote(worktreePath, branchRemote, options)
: null
remote: normalizedRemote,
branchRemote:
branchRemote === remote
? normalizedRemote
: await normalizePushRemote(worktreePath, branchRemote, options)
}
}
@@ -0,0 +1,102 @@
// Why: the batched `cat-file --batch-check` conflict probe decides from stdout, so a
// WSL login-shell fallback that prints the distro banner onto that stream desynchronizes
// the one-line-per-ref contract. Every batch then came back undecided and fell through to
// one `show-ref` subprocess per remote -- the cost the batch exists to remove. These tests
// pin the fence request and the resulting subprocess count at 58 remotes.
import { beforeEach, describe, expect, it, vi } from 'vitest'
const { gitExecFileAsyncMock } = vi.hoisted(() => ({ gitExecFileAsyncMock: vi.fn() }))
vi.mock('./runner', () => ({ gitExecFileAsync: gitExecFileAsyncMock }))
import { getBranchConflictKind } from './repo-branch-conflict'
const REMOTES = Array.from({ length: 58 }, (_, index) => `r${index}`)
const BRANCH = 'user/feature'
const WSL_BANNER =
'Welcome to Ubuntu 24.04.1 LTS (GNU/Linux 5.15.167.4-microsoft-standard-WSL2 x86_64)\n' +
'To run a command as administrator (user "root"), use "sudo <command>".\n'
type GitExecOptions = { stdin?: string; captureWslLoginShellOutput?: boolean }
/**
* Stand-in for a WSL-routed runner: the login shell prepends its banner to stdout unless
* the caller asked for the fenced form, which slices the payload back out.
*/
function installLoginShellRunner(): { argv: string[][] } {
const argv: string[][] = []
gitExecFileAsyncMock.mockImplementation(async (args: string[], options: GitExecOptions = {}) => {
argv.push(args)
if (args[0] === 'rev-parse') {
throw new Error('local branch is absent')
}
if (args[0] === 'remote') {
return { stdout: `${WSL_BANNER}${REMOTES.join('\n')}\n`, stderr: '' }
}
if (args[0] === 'show-ref') {
throw Object.assign(new Error('missing ref'), { code: 1, stderr: '' })
}
if (args[0] === 'cat-file') {
const payload = `${(options.stdin ?? '')
.split('\n')
.filter(Boolean)
.map((ref) => `${ref} missing`)
.join('\n')}\n`
return {
stdout: options.captureWslLoginShellOutput ? payload : `${WSL_BANNER}${payload}`,
stderr: ''
}
}
throw new Error(`unexpected git command: ${args.join(' ')}`)
})
return { argv }
}
function countSubcommand(argv: readonly string[][], subcommand: string): number {
return argv.filter((args) => args[0] === subcommand).length
}
describe('getBranchConflictKind batched remote probe', () => {
beforeEach(() => {
gitExecFileAsyncMock.mockReset()
})
it('asks the WSL login shell to fence the batch payload it parses', async () => {
installLoginShellRunner()
await getBranchConflictKind('/repo', BRANCH)
const batchCall = gitExecFileAsyncMock.mock.calls.find(([args]) => args[0] === 'cat-file')
expect(batchCall?.[1]).toMatchObject({ captureWslLoginShellOutput: true })
})
it('answers from one batched subprocess instead of one show-ref per remote', async () => {
const { argv } = installLoginShellRunner()
await expect(getBranchConflictKind('/repo', BRANCH)).resolves.toBeNull()
expect(countSubcommand(argv, 'cat-file')).toBe(1)
expect(countSubcommand(argv, 'show-ref')).toBe(0)
})
it('still falls back to per-ref probes when the batch itself fails', async () => {
gitExecFileAsyncMock.mockImplementation(async (args: string[]) => {
if (args[0] === 'rev-parse') {
throw new Error('local branch is absent')
}
if (args[0] === 'remote') {
return { stdout: `${REMOTES.join('\n')}\n`, stderr: '' }
}
if (args[0] === 'cat-file') {
throw new Error('cat-file is unavailable on this host')
}
if (args[0] === 'show-ref') {
return { stdout: '', stderr: '' }
}
throw new Error(`unexpected git command: ${args.join(' ')}`)
})
await expect(getBranchConflictKind('/repo', BRANCH)).resolves.toBe('remote')
})
})
+10 -2
View File
@@ -147,10 +147,12 @@ export function getBranchConflictKind(
const execOptions = gitExecOptions(path, options)
const runLocalGit = (
argv: string[],
commandOptions?: ExactRefProbeExecOptions & { stdin?: string }
commandOptions?: ExactRefProbeExecOptions & { stdin?: string },
captureWslLoginShellOutput = false
): Promise<{ stdout: string }> =>
gitExecFileAsync(argv, {
...execOptions,
...(captureWslLoginShellOutput ? { captureWslLoginShellOutput: true } : {}),
...(commandOptions?.maxBuffer === undefined ? {} : { maxBuffer: commandOptions.maxBuffer }),
...(commandOptions?.timeoutMs === undefined ? {} : { timeout: commandOptions.timeoutMs }),
...(commandOptions?.stdin === undefined ? {} : { stdin: commandOptions.stdin })
@@ -160,7 +162,13 @@ export function getBranchConflictKind(
branchName,
allowedBaseRef,
{},
(argv, commandOptions) => runLocalGit(argv, commandOptions)
// Why fenced: the batch decides from stdout, and a WSL login-shell fallback writes
// the distro's rc/motd banner to that same stream. The extra lines break the
// one-line-per-ref contract, so every batch came back undecided and fell through to
// one `show-ref` subprocess per remote -- the exact cost the batch exists to remove.
// `show-ref --verify --quiet` prints nothing and is read by exit code, so it needs
// no fence; the capture wrapper preserves the payload's exit status either way.
(argv, commandOptions) => runLocalGit(argv, commandOptions, true)
)
}
+10
View File
@@ -363,6 +363,16 @@ describe('getUpstreamStatus', () => {
if (args[0] === 'remote' && args[1] === 'get-url' && args[2] === 'pr-pynickle-orca') {
return Promise.resolve({ stdout: 'https://github.com/pynickle/orca.git\n' })
}
if (args[0] === 'remote' && args[1] === '-v') {
return Promise.resolve({
stdout: [
'origin\thttps://github.com/stablyai/orca.git (fetch)',
'origin\thttps://github.com/stablyai/orca.git (push)',
'pr-pynickle-orca\thttps://github.com/pynickle/orca.git (fetch)',
'pr-pynickle-orca\thttps://github.com/pynickle/orca.git (push)'
].join('\n')
})
}
if (args[0] === 'remote') {
return Promise.resolve({ stdout: 'origin\npr-pynickle-orca\n' })
}
@@ -13,7 +13,7 @@ import { listWorktrees } from '../git/worktree'
import type { SshGitProvider } from '../providers/ssh-git-provider'
import type { GitPushTarget } from '../../shared/worktree/types'
import { WORKTREE_ID_SEPARATOR, worktreeIdComparisonKey } from '../../shared/worktree/id'
import { iterateProcessOutputLines } from '../../shared/process-output-field-scanner'
import { parseGitRemoteFetchUrls } from '../../shared/git-remote-url-index'
import {
findWorktreeMetaReferencingRemote,
hasBranchConfigUsingRemote,
@@ -44,26 +44,9 @@ async function listPrRemoteCandidates(
} catch {
return []
}
const candidates = new Map<string, string>()
for (const line of iterateProcessOutputLines(stdout)) {
const parsed = parseRemoteVerboseLine(line)
if (parsed?.direction === 'fetch' && isOrcaGeneratedPrRemoteName(parsed.name)) {
candidates.set(parsed.name, parsed.url)
}
}
return [...candidates.entries()].map(([name, url]) => ({ name, url }))
}
function parseRemoteVerboseLine(
line: string
): { name: string; url: string; direction: 'fetch' | 'push' } | null {
const tabIndex = line.indexOf('\t')
if (tabIndex === -1) {
return null
}
const name = line.slice(0, tabIndex)
const match = /^(.*) \((fetch|push)\)$/.exec(line.slice(tabIndex + 1).trim())
return match ? { name, url: match[1], direction: match[2] as 'fetch' | 'push' } : null
return [...parseGitRemoteFetchUrls(stdout)]
.filter(([name]) => isOrcaGeneratedPrRemoteName(name))
.map(([name, url]) => ({ name, url }))
}
async function shouldReclaimPrRemote(
@@ -0,0 +1,178 @@
// Why: `findRemoteForUrl` used to run `git remote` and then one serial
// `git remote get-url` per remote. These tests pin both halves of the fix: the
// subprocess count at 58 remotes, and result-for-result parity with the old scan
// across the remote shapes a real repo produces.
import { describe, expect, it } from 'vitest'
import { parseGitHubOwnerRepo } from '../github/gh-utils'
import { findRemoteForUrl } from './worktree-push-target-setup'
import type { GitRemoteExec } from './worktree-push-target-cleanup'
const SSH_FORK = 'git@github.com:contributor/orca.git'
const HTTPS_FORK = 'https://github.com/contributor/orca.git'
const GITLAB_FORK = 'https://gitlab.com/contributor/orca.git'
const UPSTREAM = 'https://github.com/stablyai/orca.git'
type RemoteRow = { name: string; fetchUrl: string; pushUrl?: string }
type CountingExec = GitRemoteExec & { spawns: string[][] }
function makeExec(remotes: readonly RemoteRow[]): CountingExec {
const spawns: string[][] = []
const exec: GitRemoteExec = async (args: string[]) => {
spawns.push(args)
if (args[0] === 'remote' && args.length === 1) {
return { stdout: `${remotes.map((remote) => remote.name).join('\n')}\n` }
}
if (args[0] === 'remote' && args[1] === '-v') {
return {
stdout: remotes
.flatMap((remote) => [
`${remote.name}\t${remote.fetchUrl} (fetch)`,
`${remote.name}\t${remote.pushUrl ?? remote.fetchUrl} (push)`
])
.join('\n')
}
}
if (args[0] === 'remote' && args[1] === 'get-url') {
const match = remotes.find((remote) => remote.name === args[2])
if (!match) {
throw new Error(`No such remote ${args[2]}`)
}
return { stdout: `${match.fetchUrl}\n` }
}
throw new Error(`unexpected git command: ${args.join(' ')}`)
}
return Object.assign(exec, { spawns })
}
/** The pre-fix scan, kept as the oracle the batched form must reproduce exactly. */
async function findRemoteForUrlPerRemote(
execGit: GitRemoteExec,
repoPath: string,
remoteUrl: string
): Promise<string | null> {
const target = parseGitHubOwnerRepo(remoteUrl)
try {
const { stdout } = await execGit(['remote'], repoPath)
for (const remote of stdout
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean)) {
try {
const { stdout: urlStdout } = await execGit(['remote', 'get-url', remote], repoPath)
const candidateUrl = urlStdout.trim()
const candidate = parseGitHubOwnerRepo(candidateUrl)
if (
target &&
candidate &&
target.owner.toLowerCase() === candidate.owner.toLowerCase() &&
target.repo.toLowerCase() === candidate.repo.toLowerCase()
) {
return remote
}
if (candidateUrl === remoteUrl) {
return remote
}
} catch {
// Ignore a remote that disappeared or has no fetch URL.
}
}
} catch {
return null
}
return null
}
const fiftyEightRemotes: RemoteRow[] = [
{ name: 'origin', fetchUrl: UPSTREAM },
...Array.from({ length: 56 }, (_, index) => ({
name: `pr-user${index}-orca`,
fetchUrl: `https://github.com/user${index}/orca.git`
})),
{ name: 'pr-contributor-orca', fetchUrl: SSH_FORK }
]
const matrix: { name: string; remotes: RemoteRow[]; lookupUrl: string }[] = [
{ name: 'no remotes', remotes: [], lookupUrl: SSH_FORK },
{
name: 'one matching remote',
remotes: [{ name: 'origin', fetchUrl: SSH_FORK }],
lookupUrl: SSH_FORK
},
{
name: 'one non-matching remote',
remotes: [{ name: 'origin', fetchUrl: UPSTREAM }],
lookupUrl: SSH_FORK
},
{ name: '58 remotes, match last', remotes: fiftyEightRemotes, lookupUrl: SSH_FORK },
{
name: '58 remotes, no match',
remotes: fiftyEightRemotes,
lookupUrl: 'https://github.com/nobody/other.git'
},
{
name: 'duplicate URLs on two remotes',
remotes: [
{ name: 'origin', fetchUrl: UPSTREAM },
{ name: 'fork-a', fetchUrl: SSH_FORK },
{ name: 'fork-b', fetchUrl: SSH_FORK }
],
lookupUrl: SSH_FORK
},
{
name: 'fetch and push URLs differ',
remotes: [{ name: 'split', fetchUrl: SSH_FORK, pushUrl: HTTPS_FORK }],
lookupUrl: SSH_FORK
},
{
name: 'SSH-form lookup against an HTTPS-form remote',
remotes: [
{ name: 'origin', fetchUrl: UPSTREAM },
{ name: 'fork', fetchUrl: HTTPS_FORK }
],
lookupUrl: SSH_FORK
},
{
name: 'HTTPS-form lookup against an SSH-form remote',
remotes: [
{ name: 'origin', fetchUrl: UPSTREAM },
{ name: 'fork', fetchUrl: SSH_FORK }
],
lookupUrl: HTTPS_FORK
},
{
name: 'non-GitHub provider matches only on the exact URL',
remotes: [{ name: 'gitlab-fork', fetchUrl: GITLAB_FORK }],
lookupUrl: GITLAB_FORK
},
{
name: 'non-GitHub provider with a different host does not match',
remotes: [{ name: 'gitlab-fork', fetchUrl: GITLAB_FORK }],
lookupUrl: 'https://bitbucket.org/contributor/orca.git'
}
]
describe('findRemoteForUrl', () => {
it.each(matrix)('matches the per-remote scan for $name', async ({ remotes, lookupUrl }) => {
const expected = await findRemoteForUrlPerRemote(makeExec(remotes), '/repo', lookupUrl)
await expect(findRemoteForUrl(makeExec(remotes), '/repo', lookupUrl)).resolves.toBe(expected)
})
it('answers from one subprocess at 58 remotes instead of one per remote', async () => {
const legacyExec = makeExec(fiftyEightRemotes)
await findRemoteForUrlPerRemote(legacyExec, '/repo', 'https://github.com/nobody/other.git')
expect(legacyExec.spawns).toHaveLength(fiftyEightRemotes.length + 1)
const exec = makeExec(fiftyEightRemotes)
await findRemoteForUrl(exec, '/repo', 'https://github.com/nobody/other.git')
expect(exec.spawns).toEqual([['remote', '-v']])
})
it('returns null when the remote table cannot be read', async () => {
const failing: GitRemoteExec = async () => {
throw new Error('not a git repository')
}
await expect(findRemoteForUrl(failing, '/repo', SSH_FORK)).resolves.toBeNull()
})
})
@@ -16,6 +16,13 @@ const REPO = '/repo-root'
const FORK_SSH = 'git@github.com:contributor/orca.git'
const FORK_HTTPS = 'https://github.com/contributor/orca.git'
/** Real `git remote -v` shape: a fetch row and a push row per remote, tab-separated. */
export function renderRemoteVerbose(remotes: Record<string, string>): string {
return Object.entries(remotes)
.flatMap(([name, url]) => [`${name}\t${url} (fetch)`, `${name}\t${url} (push)`])
.join('\n')
}
// A stateful fake git: `remotes` maps name -> url. `remote add` mutates it so
// later lookups see the new remote, matching real git behavior. Defaults
// `symbolic-ref --short HEAD` to a real branch name, since a worktree's HEAD
@@ -31,6 +38,9 @@ function makeRepoExec(
if (args[0] === 'remote' && args.length === 1) {
return { stdout: Object.keys(remotes).join('\n'), stderr: '' }
}
if (args[0] === 'remote' && args[1] === '-v' && args.length === 2) {
return { stdout: renderRemoteVerbose(remotes), stderr: '' }
}
if (args[0] === 'remote' && args[1] === 'get-url') {
const url = remotes[args[2]!]
if (!url) {
+11 -42
View File
@@ -5,53 +5,33 @@
// repo. The store-aware ownership decision stays with the caller via a predicate.
import type { GitPushTarget } from '../../shared/worktree/types'
import { parseGitHubOwnerRepo } from '../github/gh-utils'
import type { GitRemoteExec } from './worktree-push-target-cleanup'
import { findGitRemoteNameByFetchUrl } from '../../shared/git-remote-url-index'
import { sameGitHubRemoteUrl, type GitRemoteExec } from './worktree-push-target-cleanup'
import {
buildNarrowForkFetchRefspec,
ensureRemoteTracksBranchNarrowly
} from '../git/fork-remote-refspec'
// One `git remote -v` replaces `git remote` plus a serial `git remote get-url` per
// remote -- 59 subprocesses at 58 remotes, on every push-target resolution (#17914).
export async function findRemoteForUrl(
execGit: GitRemoteExec,
repoPath: string,
remoteUrl: string
): Promise<string | null> {
const target = parseGitHubOwnerRepo(remoteUrl)
try {
const { stdout } = await execGit(['remote'], repoPath)
for (const remote of stdout
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean)) {
try {
const { stdout: urlStdout } = await execGit(['remote', 'get-url', remote], repoPath)
const candidateUrl = urlStdout.trim()
const candidate = parseGitHubOwnerRepo(candidateUrl)
if (
target &&
candidate &&
target.owner.toLowerCase() === candidate.owner.toLowerCase() &&
target.repo.toLowerCase() === candidate.repo.toLowerCase()
) {
return remote
}
if (candidateUrl === remoteUrl) {
return remote
}
} catch {
// Ignore a remote that disappeared or has no fetch URL.
}
}
const { stdout } = await execGit(['remote', '-v'], repoPath)
return findGitRemoteNameByFetchUrl(stdout, (candidateUrl) =>
sameGitHubRemoteUrl(candidateUrl, remoteUrl)
)
} catch {
return null
}
return null
}
// O(1) probe used before materializing on demand (push/pull/fetch/fast-forward):
// a single `remote get-url <name>` avoids the O(remotes) `findRemoteForUrl` scan
// once a fork remote already exists under its expected name (#17828).
// a single `remote get-url <name>` skips the whole-remote-table read once a fork
// remote already exists under its expected name (#17828).
export async function remoteAlreadyMatchesUrl(
execGit: GitRemoteExec,
repoPath: string,
@@ -60,18 +40,7 @@ export async function remoteAlreadyMatchesUrl(
): Promise<boolean> {
try {
const { stdout } = await execGit(['remote', 'get-url', remoteName], repoPath)
const candidateUrl = stdout.trim()
if (candidateUrl === remoteUrl) {
return true
}
const target = parseGitHubOwnerRepo(remoteUrl)
const candidate = parseGitHubOwnerRepo(candidateUrl)
return Boolean(
target &&
candidate &&
target.owner.toLowerCase() === candidate.owner.toLowerCase() &&
target.repo.toLowerCase() === candidate.repo.toLowerCase()
)
return sameGitHubRemoteUrl(stdout.trim(), remoteUrl)
} catch {
return false
}
@@ -553,6 +553,17 @@ describe('materializeWorktreePushTargetRemoteSsh', () => {
}
throw new Error('No such remote')
}
if (args[0] === 'remote' && args[1] === '-v') {
return {
stdout: [
'origin\thttps://github.com/stablyai/orca.git (fetch)',
'origin\thttps://github.com/stablyai/orca.git (push)',
`${SIBLING_REMOTE}\t${FORK_URL} (fetch)`,
`${SIBLING_REMOTE}\t${FORK_URL} (push)`
].join('\n'),
stderr: ''
}
}
if (args[0] === 'remote' && args.length === 1) {
return { stdout: `origin\n${SIBLING_REMOTE}\n`, stderr: '' }
}
+11
View File
@@ -46,6 +46,17 @@ function gitForConfig(config: {
}
return { stdout: `${config.base ?? ''}\n`, stderr: '' }
}
if (args[0] === 'remote' && args[1] === '-v') {
return {
stdout: (config.remotes ?? [])
.flatMap((name) => {
const url = config.remoteUrls?.[name] ?? ''
return [`${name}\t${url} (fetch)`, `${name}\t${url} (push)`]
})
.join('\n'),
stderr: ''
}
}
if (args[0] === 'remote' && args.length === 1) {
return { stdout: `${config.remotes?.join('\n') ?? ''}\n`, stderr: '' }
}
+15 -18
View File
@@ -1,5 +1,6 @@
import { assertGitPushTargetShape } from '../shared/git-push-target-validation'
import { gitRefTargetsBranchOnRemote } from '../shared/git-remote-branch-name'
import { findGitRemoteNameByFetchUrl } from '../shared/git-remote-url-index'
import type { GitPushTarget } from '../shared/worktree/types'
type RelayGit = (args: string[], cwd: string) => Promise<{ stdout: string; stderr: string }>
@@ -67,31 +68,19 @@ type ConfiguredPushRemote = {
branchRemote: string | null
}
// Host-side twin of `src/main/git/remote.ts`: one `git remote -v` instead of
// `git remote` plus a serial `git remote get-url` per remote.
async function findRemoteNameForUrl(
git: RelayGit,
worktreePath: string,
remoteUrl: string
): Promise<string | null> {
try {
const { stdout } = await git(['remote'], worktreePath)
const remotes = stdout
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean)
for (const remoteName of remotes) {
try {
const { stdout: urlStdout } = await git(['remote', 'get-url', remoteName], worktreePath)
if (urlStdout.trim() === remoteUrl) {
return remoteName
}
} catch {
// Ignore a remote that disappeared or has no fetch URL.
}
}
const { stdout } = await git(['remote', '-v'], worktreePath)
return findGitRemoteNameByFetchUrl(stdout, (candidateUrl) => candidateUrl === remoteUrl)
} catch {
return null
}
return null
}
async function normalizePushRemote(
@@ -120,9 +109,17 @@ async function getConfiguredPushRemote(
if (!remote) {
return null
}
const normalizedRemote = await normalizePushRemote(git, worktreePath, remote)
// The two usually name the same URL; resolving it twice reads the remote table twice.
if (!branchRemote) {
return { remote: normalizedRemote, branchRemote: null }
}
return {
remote: await normalizePushRemote(git, worktreePath, remote),
branchRemote: branchRemote ? await normalizePushRemote(git, worktreePath, branchRemote) : null
remote: normalizedRemote,
branchRemote:
branchRemote === remote
? normalizedRemote
: await normalizePushRemote(git, worktreePath, branchRemote)
}
}
@@ -15,6 +15,7 @@ import {
isUnsupportedWorktreeListZError
} from './git-worktree-command-capabilities'
import { gitCredentialPromptGuardEnv } from './git-credential-prompt-env'
import { parseGitRemoteFetchUrls } from './git-remote-url-index'
import { GIT_HISTORY_COMMIT_FORMAT, parseGitHistoryLog } from './git-history-log-parser'
import {
githubPullRequestHeadLocalRef,
@@ -214,6 +215,41 @@ describeBinaryCompatibility('real Git binary compatibility', () => {
await expect(runGit(['merge-base', '--end-of-options', head, unrelated])).rejects.toBeDefined()
})
// Why pin this: Orca answers "which remote has this URL" from one `git remote -v`
// instead of one `git remote get-url` per remote. That is only equivalent if both
// commands report the same URL — the insteadOf-expanded first `remote.<name>.url`,
// which a raw config read does not produce — on every supported Git.
it('reports the same fetch URL from remote -v as from remote get-url', async () => {
await runGit(['config', 'url.git@example.invalid:.insteadOf', 'https://example.invalid/'])
await runGit(['remote', 'add', 'compat-single', 'https://example.invalid/a/repo.git'])
await runGit(['remote', 'add', 'compat-multi', 'https://example.invalid/b/repo.git'])
await runGit([
'config',
'--add',
'remote.compat-multi.url',
'https://example.invalid/b2/repo.git'
])
await runGit([
'config',
'remote.compat-multi.pushurl',
'https://push.example.invalid/b/repo.git'
])
try {
const fetchUrls = parseGitRemoteFetchUrls((await runGit(['remote', '-v'])).stdout)
for (const name of ['compat-single', 'compat-multi']) {
const getUrl = (await runGit(['remote', 'get-url', name])).stdout.trim()
expect(fetchUrls.get(name)).toBe(getUrl)
}
expect(fetchUrls.get('compat-single')).toBe('git@example.invalid:a/repo.git')
// A `pushurl` must not displace the fetch URL the scan compares against.
expect(fetchUrls.get('compat-multi')).toBe('git@example.invalid:b/repo.git')
} finally {
await runGit(['remote', 'remove', 'compat-single'])
await runGit(['remote', 'remove', 'compat-multi'])
await runGit(['config', '--unset-all', 'url.git@example.invalid:.insteadOf'])
}
})
it('recognizes ref and merge-tree compatibility boundaries', async () => {
const fetchHeadPath = join(repoPath, '.git', 'FETCH_HEAD')
await writeFile(fetchHeadPath, 'sentinel\n')
@@ -0,0 +1,175 @@
// Why: resolving a URL-valued `branch.<name>.remote` (or `remote.pushDefault`) to a
// remote name used to cost `git remote` plus one serial `git remote get-url` per remote.
// `hasConfiguredBranchPushTarget` resolves up to two of them, so a 58-remote repo paid
// up to 118 subprocesses for one question. These tests pin the count and result parity.
import { describe, expect, it } from 'vitest'
import {
getConfiguredBranchRemoteUpstream,
hasConfiguredBranchPushTarget
} from './git-configured-branch-target'
const BRANCH = 'imp/translation'
const FORK_URL = 'https://github.com/contributor/orca.git'
const UPSTREAM_URL = 'https://github.com/stablyai/orca.git'
type RemoteRow = { name: string; fetchUrl: string; pushUrl?: string }
type Fixture = {
remotes: readonly RemoteRow[]
config: Readonly<Record<string, string>>
}
function makeRunner(fixture: Fixture): {
runGit: (args: string[]) => Promise<{ stdout: string }>
spawns: string[][]
} {
const spawns: string[][] = []
const runGit = async (args: string[]): Promise<{ stdout: string }> => {
spawns.push(args)
if (args[0] === 'config' && args[1] === '--get') {
const value = fixture.config[args[2]]
if (value === undefined) {
throw Object.assign(new Error('config key is not set'), { code: 1 })
}
return { stdout: `${value}\n` }
}
if (args[0] === 'remote' && args[1] === '-v') {
return {
stdout: fixture.remotes
.flatMap((remote) => [
`${remote.name}\t${remote.fetchUrl} (fetch)`,
`${remote.name}\t${remote.pushUrl ?? remote.fetchUrl} (push)`
])
.join('\n')
}
}
if (args[0] === 'remote' && args.length === 1) {
return { stdout: `${fixture.remotes.map((remote) => remote.name).join('\n')}\n` }
}
if (args[0] === 'remote' && args[1] === 'get-url') {
const match = fixture.remotes.find((remote) => remote.name === args[2])
if (!match) {
throw new Error(`No such remote ${args[2]}`)
}
return { stdout: `${match.fetchUrl}\n` }
}
throw new Error(`unexpected git command: ${args.join(' ')}`)
}
return { runGit, spawns }
}
const fiftyEightRemotes: RemoteRow[] = [
{ name: 'origin', fetchUrl: UPSTREAM_URL },
...Array.from({ length: 56 }, (_, index) => ({
name: `pr-user${index}-orca`,
fetchUrl: `https://github.com/user${index}/orca.git`
})),
{ name: 'pr-contributor-orca', fetchUrl: FORK_URL }
]
describe('hasConfiguredBranchPushTarget', () => {
it('resolves both URL-valued remotes from one remote table read at 58 remotes', async () => {
const { runGit, spawns } = makeRunner({
remotes: fiftyEightRemotes,
config: {
[`branch.${BRANCH}.pushRemote`]: FORK_URL,
[`branch.${BRANCH}.remote`]: FORK_URL,
[`branch.${BRANCH}.merge`]: `refs/heads/${BRANCH}`
}
})
await expect(hasConfiguredBranchPushTarget(runGit, BRANCH)).resolves.toBe(true)
// Both the push remote and the branch remote name the same URL, so one table read answers.
expect(spawns.filter((args) => args[0] === 'remote')).toEqual([['remote', '-v']])
expect(spawns.filter((args) => args[1] === 'get-url')).toEqual([])
})
it('keeps the not-set case false when no remote is configured', async () => {
const { runGit } = makeRunner({ remotes: fiftyEightRemotes, config: {} })
await expect(hasConfiguredBranchPushTarget(runGit, BRANCH)).resolves.toBe(false)
})
it('keeps the URL itself as the remote name when nothing matches', async () => {
const { runGit } = makeRunner({
remotes: [{ name: 'origin', fetchUrl: UPSTREAM_URL }],
config: {
[`branch.${BRANCH}.pushRemote`]: FORK_URL,
[`branch.${BRANCH}.remote`]: FORK_URL,
[`branch.${BRANCH}.merge`]: 'refs/heads/other'
}
})
// Unchanged no-match fallback: both remotes stay the raw URL, so they still agree
// and the differently named merge branch is still pushable.
await expect(hasConfiguredBranchPushTarget(runGit, BRANCH)).resolves.toBe(true)
})
})
describe('getConfiguredBranchRemoteUpstream', () => {
const remoteTrackingRefExists = async (): Promise<boolean> => true
it('picks the first remote holding a duplicated URL', async () => {
const { runGit, spawns } = makeRunner({
remotes: [
{ name: 'origin', fetchUrl: UPSTREAM_URL },
{ name: 'fork-a', fetchUrl: FORK_URL },
{ name: 'fork-b', fetchUrl: FORK_URL }
],
config: {
[`branch.${BRANCH}.remote`]: FORK_URL,
[`branch.${BRANCH}.merge`]: `refs/heads/${BRANCH}`
}
})
await expect(
getConfiguredBranchRemoteUpstream(runGit, BRANCH, remoteTrackingRefExists)
).resolves.toEqual({
upstreamName: `fork-a/${BRANCH}`,
remoteName: 'fork-a',
branchName: BRANCH,
isConfiguredUpstream: false
})
expect(spawns.filter((args) => args[0] === 'remote')).toEqual([['remote', '-v']])
})
it('ignores a push URL when fetch and push differ', async () => {
const { runGit } = makeRunner({
remotes: [{ name: 'split', fetchUrl: UPSTREAM_URL, pushUrl: FORK_URL }],
config: {
[`branch.${BRANCH}.remote`]: FORK_URL,
[`branch.${BRANCH}.merge`]: `refs/heads/${BRANCH}`
}
})
await expect(
getConfiguredBranchRemoteUpstream(runGit, BRANCH, remoteTrackingRefExists)
).resolves.toBeNull()
})
it('returns null with no remotes at all', async () => {
const { runGit } = makeRunner({
remotes: [],
config: {
[`branch.${BRANCH}.remote`]: FORK_URL,
[`branch.${BRANCH}.merge`]: `refs/heads/${BRANCH}`
}
})
await expect(
getConfiguredBranchRemoteUpstream(runGit, BRANCH, remoteTrackingRefExists)
).resolves.toBeNull()
})
it('keeps a plain named remote untouched', async () => {
const { runGit, spawns } = makeRunner({
remotes: fiftyEightRemotes,
config: {
[`branch.${BRANCH}.remote`]: 'origin',
[`branch.${BRANCH}.merge`]: `refs/heads/${BRANCH}`
}
})
await expect(
getConfiguredBranchRemoteUpstream(runGit, BRANCH, remoteTrackingRefExists)
).resolves.toMatchObject({ remoteName: 'origin' })
expect(spawns.filter((args) => args[0] === 'remote')).toEqual([])
})
})
+13 -21
View File
@@ -1,4 +1,5 @@
import { gitRefTargetsBranchOnRemote } from './git-remote-branch-name'
import { findGitRemoteNameByFetchUrl } from './git-remote-url-index'
type GitCommandRunner = (args: string[]) => Promise<{ stdout: string }>
@@ -25,30 +26,18 @@ function isUrlValuedRemote(remote: string): boolean {
return /^[A-Za-z][A-Za-z0-9+.-]*:\/\//.test(remote) || /^[^@/:]+@[^:]+:.+/.test(remote)
}
// `hasConfiguredBranchPushTarget` resolves up to two URL-valued remotes, so the old
// per-remote `get-url` scan cost up to 2 x (1 + remotes) subprocesses per call.
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.
}
}
const { stdout } = await runGit(['remote', '-v'])
return findGitRemoteNameByFetchUrl(stdout, (candidateUrl) => candidateUrl === remoteUrl)
} catch {
return null
}
return null
}
export async function getConfiguredBranchRemoteUpstream(
@@ -101,11 +90,14 @@ export async function hasConfiguredBranchPushTarget(
const pushRemoteName = isUrlValuedRemote(remote)
? ((await findRemoteNameForUrl(runGit, remote)) ?? remote)
: remote
const branchRemoteName = branchRemote
? isUrlValuedRemote(branchRemote)
? ((await findRemoteNameForUrl(runGit, branchRemote)) ?? branchRemote)
: branchRemote
: null
// The two usually name the same URL; resolving it twice reads the remote table twice.
const branchRemoteName = !branchRemote
? null
: branchRemote === remote
? pushRemoteName
: isUrlValuedRemote(branchRemote)
? ((await findRemoteNameForUrl(runGit, branchRemote)) ?? branchRemote)
: branchRemote
if (gitRefTargetsBranchOnRemote(baseRef, pushRemoteName, branchName)) {
return false
}
+112
View File
@@ -0,0 +1,112 @@
import { describe, expect, it } from 'vitest'
import {
findGitRemoteNameByFetchUrl,
parseGitRemoteFetchUrls,
parseGitRemoteVerboseLine
} from './git-remote-url-index'
const SSH_URL = 'git@github.com:contributor/orca.git'
const HTTPS_URL = 'https://github.com/contributor/orca.git'
function verbose(rows: readonly (readonly [string, string])[]): string {
return rows.map(([name, url]) => `${name}\t${url}`).join('\n')
}
describe('parseGitRemoteVerboseLine', () => {
it('reads the name, URL and direction', () => {
expect(parseGitRemoteVerboseLine(`origin\t${HTTPS_URL} (fetch)`)).toEqual({
name: 'origin',
url: HTTPS_URL,
direction: 'fetch'
})
})
it('keeps a URL that itself contains spaces and parentheses', () => {
const url = '/tmp/my repo (mirror)'
expect(parseGitRemoteVerboseLine(`local\t${url} (push)`)).toEqual({
name: 'local',
url,
direction: 'push'
})
})
it('rejects the URL-less row git prints for a pushurl-only remote', () => {
expect(parseGitRemoteVerboseLine('pushonly\t')).toBeNull()
expect(parseGitRemoteVerboseLine('not a remote row')).toBeNull()
})
})
describe('parseGitRemoteFetchUrls', () => {
it('returns nothing for a repo with no remotes', () => {
expect([...parseGitRemoteFetchUrls('')]).toEqual([])
})
it('keeps only fetch rows, in git remote order', () => {
const stdout = verbose([
['a', `${SSH_URL} (fetch)`],
['a', `${SSH_URL} (push)`],
['b', `${HTTPS_URL} (fetch)`],
['b', 'https://github.com/contributor/other.git (push)']
])
expect([...parseGitRemoteFetchUrls(stdout)]).toEqual([
['a', SSH_URL],
['b', HTTPS_URL]
])
})
it('parses CRLF output', () => {
const stdout = `a\t${SSH_URL} (fetch)\r\na\t${SSH_URL} (push)\r\n`
expect([...parseGitRemoteFetchUrls(stdout)]).toEqual([['a', SSH_URL]])
})
it('takes the first URL of a multi-URL remote, matching remote get-url', () => {
const stdout = verbose([
['multi', `${SSH_URL} (fetch)`],
['multi', `${SSH_URL} (push)`],
['multi', `${HTTPS_URL} (push)`]
])
expect(parseGitRemoteFetchUrls(stdout).get('multi')).toBe(SSH_URL)
})
it('scales to 58 remotes without losing order', () => {
const rows = Array.from({ length: 58 }, (_, index) => [
`r${index}`,
`https://example.com/o${index}/repo.git`
])
const stdout = rows
.flatMap(([name, url]) => [`${name}\t${url} (fetch)`, `${name}\t${url} (push)`])
.join('\n')
const parsed = [...parseGitRemoteFetchUrls(stdout)]
expect(parsed).toHaveLength(58)
expect(parsed[0]).toEqual(['r0', 'https://example.com/o0/repo.git'])
expect(parsed[57]).toEqual(['r57', 'https://example.com/o57/repo.git'])
})
})
describe('findGitRemoteNameByFetchUrl', () => {
const stdout = verbose([
['origin', 'https://github.com/stablyai/orca.git (fetch)'],
['origin', 'https://github.com/stablyai/orca.git (push)'],
['first-fork', `${SSH_URL} (fetch)`],
['first-fork', `${SSH_URL} (push)`],
['second-fork', `${SSH_URL} (fetch)`],
['second-fork', `${SSH_URL} (push)`]
])
it('returns the first remote holding a duplicated URL', () => {
expect(findGitRemoteNameByFetchUrl(stdout, (url) => url === SSH_URL)).toBe('first-fork')
})
it('returns null when nothing matches', () => {
expect(findGitRemoteNameByFetchUrl(stdout, (url) => url === HTTPS_URL)).toBeNull()
})
it('ignores push URLs when fetch and push differ', () => {
const split = verbose([
['split', `${SSH_URL} (fetch)`],
['split', `${HTTPS_URL} (push)`]
])
expect(findGitRemoteNameByFetchUrl(split, (url) => url === SSH_URL)).toBe('split')
expect(findGitRemoteNameByFetchUrl(split, (url) => url === HTTPS_URL)).toBeNull()
})
})
+64
View File
@@ -0,0 +1,64 @@
// Why: "which remote has this URL?" was answered with one `git remote get-url`
// subprocess per remote, awaited serially -- 58 spawns on a repo with 58 remotes,
// on every push-target resolution. `git remote -v` answers for every remote from
// one child.
//
// `remote -v` is the faithful one-command form, not `config --get-regexp '^remote\.'`:
// both `remote -v` and `remote get-url` print the URL *after* `url.<base>.insteadOf`
// expansion and pick the first of several `remote.<name>.url` values, while raw config
// reads return the unexpanded value and the last of the multiple values.
//
// `remote -v` also predates `remote get-url` (2.7), so this lowers rather than raises
// the Git floor and needs no capability gate.
import { iterateProcessOutputLines } from './process-output-field-scanner'
export type GitRemoteVerboseEntry = {
name: string
url: string
direction: 'fetch' | 'push'
}
// Greedy prefix so a URL containing spaces or parentheses keeps them.
const REMOTE_VERBOSE_URL_PATTERN = /^(.*) \((fetch|push)\)$/
/** Parse one `<name>\t<url> (fetch|push)` row. */
export function parseGitRemoteVerboseLine(line: string): GitRemoteVerboseEntry | null {
const tabIndex = line.indexOf('\t')
if (tabIndex === -1) {
return null
}
const name = line.slice(0, tabIndex)
const match = REMOTE_VERBOSE_URL_PATTERN.exec(line.slice(tabIndex + 1).trim())
return match ? { name, url: match[1], direction: match[2] as 'fetch' | 'push' } : null
}
/**
* Fetch URL per remote in `git remote` order -- the value `git remote get-url <name>`
* prints. A remote configured with only a `pushurl` has no fetch row and is absent
* here; `get-url` echoed the remote's own name for it, which no caller can match.
*/
export function parseGitRemoteFetchUrls(stdout: string): Map<string, string> {
const fetchUrls = new Map<string, string>()
for (const line of iterateProcessOutputLines(stdout)) {
const parsed = parseGitRemoteVerboseLine(line)
// First wins: `get-url` without `--all` prints the first `remote.<name>.url`.
if (parsed?.direction === 'fetch' && !fetchUrls.has(parsed.name)) {
fetchUrls.set(parsed.name, parsed.url)
}
}
return fetchUrls
}
/** First remote whose fetch URL matches, in the order the per-remote scan visited them. */
export function findGitRemoteNameByFetchUrl(
stdout: string,
matchesUrl: (url: string) => boolean
): string | null {
for (const [name, url] of parseGitRemoteFetchUrls(stdout)) {
if (matchesUrl(url)) {
return name
}
}
return null
}