perf(worktrees): avoid auto-maintenance in create fetches (#8039)

* perf(worktrees): avoid auto-maintenance in create fetches

Git's opportunistic maintenance can keep an already-complete exact-base fetch open for seconds. Disable it per command for create-base refreshes only, leaving ordinary fetch maintenance and exact-ref freshness unchanged.

* docs(worktrees): explain create fetch maintenance scope

* test(worktrees): account for fetch config prefixes

* fix(worktrees): cover Git 2.29 auto maintenance
This commit is contained in:
Neil 2026-07-10 02:31:47 -07:00 committed by GitHub
parent 4d0ace87ec
commit 26224196f5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 142 additions and 44 deletions

View File

@ -477,7 +477,11 @@ async function refreshRemoteTrackingBaseForWorktreeCreate(
return getOrStartSshWorktreeCreateFetch(
getSshWorktreeCreateBaseFetchKey(repo, base),
getSshWorktreeCreateRemoteQueueKey(repo, base.remote),
() => provider.fetchRemoteTrackingRef(repo.path, base.remote, base.branch, base.ref)
() =>
// Why: the exact-base refresh gates create; unrelated repo housekeeping must not extend it.
provider.fetchRemoteTrackingRef(repo.path, base.remote, base.branch, base.ref, {
skipAutoMaintenance: true
})
)
}

View File

@ -3733,7 +3733,8 @@ describe('registerWorktreeHandlers', () => {
'/remote/repo',
'origin',
'master',
'refs/remotes/origin/master'
'refs/remotes/origin/master',
{ skipAutoMaintenance: true }
)
})
@ -3800,7 +3801,8 @@ describe('registerWorktreeHandlers', () => {
'/remote/repo',
'origin',
'main',
'refs/remotes/origin/main'
'refs/remotes/origin/main',
{ skipAutoMaintenance: true }
)
expect(provider.addWorktree).toHaveBeenCalledWith(
'/remote/repo',
@ -4032,7 +4034,8 @@ describe('registerWorktreeHandlers', () => {
'/remote/repo',
'origin',
'main',
'refs/remotes/origin/main'
'refs/remotes/origin/main',
{ skipAutoMaintenance: true }
)
expect(provider.addWorktree).toHaveBeenCalledTimes(2)
})

View File

@ -850,14 +850,16 @@ describe('SshGitProvider', () => {
'/home/user/repo',
'origin',
'main',
'refs/remotes/origin/main'
'refs/remotes/origin/main',
{ skipAutoMaintenance: true }
)
expect(mux.request).toHaveBeenCalledWith('git.fetchRemoteTrackingRef', {
worktreePath: '/home/user/repo',
remote: 'origin',
branch: 'main',
ref: 'refs/remotes/origin/main'
ref: 'refs/remotes/origin/main',
skipAutoMaintenance: true
})
})

View File

@ -553,14 +553,16 @@ export class SshGitProvider implements IGitProvider {
worktreePath: string,
remote: string,
branch: string,
ref: string
ref: string,
options?: { skipAutoMaintenance?: boolean }
): Promise<void> {
await this.runWithDiffDedupeClear(async () => {
await this.mux.request('git.fetchRemoteTrackingRef', {
worktreePath,
remote,
branch,
ref
ref,
...(options?.skipAutoMaintenance ? { skipAutoMaintenance: true } : {})
})
})
}

View File

@ -22,10 +22,19 @@ vi.mock('../git/runner', async (importOriginal) => {
// normally — none of them trigger IO until a runtime method is called.
import { OrcaRuntimeService } from './orca-runtime'
function isFetchArgs(argv: unknown): argv is string[] {
if (!Array.isArray(argv)) {
return false
}
let commandIndex = 0
while (argv[commandIndex] === '-c' && typeof argv[commandIndex + 1] === 'string') {
commandIndex += 2
}
return argv[commandIndex] === 'fetch'
}
function fetchCallCount(): number {
return gitExecFileAsyncMock.mock.calls.filter(
([argv]) => Array.isArray(argv) && argv[0] === 'fetch'
).length
return gitExecFileAsyncMock.mock.calls.filter(([argv]) => isFetchArgs(argv)).length
}
function exactBaseRefreshOptions(cwd: string): {
@ -36,6 +45,21 @@ function exactBaseRefreshOptions(cwd: string): {
return { cwd, timeout: 60_000, useConfiguredSshCommandForNetwork: true }
}
function exactBaseRefreshArgs(branch = 'main'): string[] {
return [
'-c',
'maintenance.auto=false',
'-c',
'maintenance.commit-graph.auto=0',
'-c',
'gc.auto=0',
'fetch',
'--no-tags',
'origin',
`+refs/heads/${branch}:refs/remotes/origin/${branch}`
]
}
// Why (STA-1292): the broad create-path fetch must carry a timeout so a Windows
// credential-manager GUI hang can't wedge worktree creation forever.
function fullRemoteFetchOptions(cwd: string): { cwd: string; timeout: number } {
@ -188,11 +212,23 @@ describe('OrcaRuntimeService.fetchRemoteWithCache', () => {
})
expect(gitExecFileAsyncMock).toHaveBeenCalledWith(
['fetch', '--no-tags', 'origin', '+refs/heads/main:refs/remotes/origin/main'],
exactBaseRefreshArgs(),
exactBaseRefreshOptions('/repo/f')
)
})
it('keeps automatic maintenance enabled for ordinary full remote fetches', async () => {
mockFetchResults([{ stdout: '', stderr: '' }])
const runtime = new OrcaRuntimeService(null)
await runtime.getOrStartRemoteFetch('/repo/full-maintenance', 'origin')
expect(gitExecFileAsyncMock).toHaveBeenCalledWith(
['fetch', 'origin'],
fullRemoteFetchOptions('/repo/full-maintenance')
)
})
it('shares an in-flight remote-tracking base refresh and reuses exact-base freshness', async () => {
let resolveFetch!: () => void
const pending = new Promise<{ stdout: string; stderr: string }>((resolve) => {
@ -297,14 +333,9 @@ describe('OrcaRuntimeService.fetchRemoteWithCache', () => {
{ ok: true },
{ ok: true }
])
const fetchCalls = gitExecFileAsyncMock.mock.calls.filter(
([argv]) => Array.isArray(argv) && argv[0] === 'fetch'
)
const fetchCalls = gitExecFileAsyncMock.mock.calls.filter(([argv]) => isFetchArgs(argv))
expect(fetchCalls).toEqual([
[
['fetch', '--no-tags', 'origin', '+refs/heads/main:refs/remotes/origin/main'],
exactBaseRefreshOptions('/repo/h')
],
[exactBaseRefreshArgs(), exactBaseRefreshOptions('/repo/h')],
[['fetch', 'origin'], fullRemoteFetchOptions('/repo/h')]
])
})
@ -343,15 +374,10 @@ describe('OrcaRuntimeService.fetchRemoteWithCache', () => {
{ ok: true },
{ ok: true }
])
const fetchCalls = gitExecFileAsyncMock.mock.calls.filter(
([argv]) => Array.isArray(argv) && argv[0] === 'fetch'
)
const fetchCalls = gitExecFileAsyncMock.mock.calls.filter(([argv]) => isFetchArgs(argv))
expect(fetchCalls).toEqual([
[['fetch', 'origin'], fullRemoteFetchOptions('/repo/i')],
[
['fetch', '--no-tags', 'origin', '+refs/heads/main:refs/remotes/origin/main'],
exactBaseRefreshOptions('/repo/i')
]
[exactBaseRefreshArgs(), exactBaseRefreshOptions('/repo/i')]
])
})
@ -389,15 +415,10 @@ describe('OrcaRuntimeService.fetchRemoteWithCache', () => {
{ ok: false, errorKind: 'git_error' },
{ ok: true }
])
const fetchCalls = gitExecFileAsyncMock.mock.calls.filter(
([argv]) => Array.isArray(argv) && argv[0] === 'fetch'
)
const fetchCalls = gitExecFileAsyncMock.mock.calls.filter(([argv]) => isFetchArgs(argv))
expect(fetchCalls).toEqual([
[['fetch', 'origin'], fullRemoteFetchOptions('/repo/i-fail')],
[
['fetch', '--no-tags', 'origin', '+refs/heads/main:refs/remotes/origin/main'],
exactBaseRefreshOptions('/repo/i-fail')
]
[exactBaseRefreshArgs(), exactBaseRefreshOptions('/repo/i-fail')]
])
})
})

View File

@ -2312,7 +2312,7 @@ describe('OrcaRuntimeService', () => {
if (args[0] === 'rev-parse' && args[1] === '--verify') {
return { stdout: 'base-sha\n', stderr: '' }
}
if (args[0] === 'fetch') {
if (args.includes('fetch')) {
return refresh.promise
}
return { stdout: '', stderr: '' }
@ -2325,7 +2325,18 @@ describe('OrcaRuntimeService', () => {
await vi.waitFor(() => {
expect(gitSpy).toHaveBeenCalledWith(
['fetch', '--no-tags', 'origin', '+refs/heads/main:refs/remotes/origin/main'],
[
'-c',
'maintenance.auto=false',
'-c',
'maintenance.commit-graph.auto=0',
'-c',
'gc.auto=0',
'fetch',
'--no-tags',
'origin',
'+refs/heads/main:refs/remotes/origin/main'
],
{
cwd: TEST_REPO_PATH,
useConfiguredSshCommandForNetwork: true,
@ -2443,7 +2454,7 @@ describe('OrcaRuntimeService', () => {
if (args[0] === 'rev-parse' && args.includes('refs/remotes/origin/main^{commit}')) {
return { stdout: 'base-sha\n', stderr: '' }
}
if (args[0] === 'fetch') {
if (args.includes('fetch')) {
throw new Error('network unavailable')
}
return { stdout: '', stderr: '' }
@ -2505,7 +2516,7 @@ describe('OrcaRuntimeService', () => {
if (args[0] === 'rev-parse' && args.includes('refs/heads/develop^{commit}')) {
return { stdout: 'develop-sha\n', stderr: '' }
}
if (args[0] === 'fetch') {
if (args.includes('fetch')) {
throw new Error('network unavailable')
}
return { stdout: '', stderr: '' }
@ -2559,7 +2570,7 @@ describe('OrcaRuntimeService', () => {
if (args[0] === 'rev-parse' && args.includes('refs/heads/team/feature^{commit}')) {
return { stdout: 'team-feature-sha\n', stderr: '' }
}
if (args[0] === 'fetch') {
if (args.includes('fetch')) {
throw new Error('network unavailable')
}
return { stdout: '', stderr: '' }
@ -2580,7 +2591,18 @@ describe('OrcaRuntimeService', () => {
false
)
expect(gitSpy).not.toHaveBeenCalledWith(
['fetch', '--no-tags', 'team', '+refs/heads/feature:refs/remotes/team/feature'],
[
'-c',
'maintenance.auto=false',
'-c',
'maintenance.commit-graph.auto=0',
'-c',
'gc.auto=0',
'fetch',
'--no-tags',
'team',
'+refs/heads/feature:refs/remotes/team/feature'
],
expect.any(Object)
)
} finally {
@ -2604,7 +2626,7 @@ describe('OrcaRuntimeService', () => {
if (args[0] === 'rev-parse' && args[1] === '--verify') {
throw new Error('missing ref')
}
if (args[0] === 'fetch') {
if (args.includes('fetch')) {
throw new Error('network unavailable')
}
return { stdout: '', stderr: '' }

View File

@ -49,6 +49,7 @@ import {
getClonePathComparisonKey
} from '../git/repo-clone-path'
import { getGitCloneFailureMessage } from '../../shared/git-clone-failure-message'
import { GIT_FETCH_SKIP_AUTO_MAINTENANCE_CONFIG_ARGS } from '../../shared/git-fetch-auto-maintenance'
import { createHash, randomUUID } from 'node:crypto'
import { homedir } from 'node:os'
import { isAbsolute, join, resolve } from 'node:path'
@ -14620,8 +14621,15 @@ export class OrcaRuntimeService {
if (this.getFreshFetchCompletedAt(key) !== null) {
return { ok: true }
}
// Why: this exact refresh gates worktree create; ordinary fetches still own maintenance.
return gitExecFileAsync(
['fetch', '--no-tags', base.remote, `+refs/heads/${base.branch}:${base.ref}`],
[
...GIT_FETCH_SKIP_AUTO_MAINTENANCE_CONFIG_ARGS,
'fetch',
'--no-tags',
base.remote,
`+refs/heads/${base.branch}:${base.ref}`
],
{
cwd: repoPath,
...gitOptions,

View File

@ -1222,7 +1222,8 @@ describe('GitHandler', () => {
worktreePath: tmpDir,
remote: 'origin',
branch: 'main',
ref: 'refs/remotes/origin/main'
ref: 'refs/remotes/origin/main',
skipAutoMaintenance: true
})
const second = dispatcher.callRequest('git.diff', {
@ -1238,7 +1239,18 @@ describe('GitHandler', () => {
expect(gitBufferSpy).toHaveBeenCalledTimes(2)
expect(gitSpy).toHaveBeenCalledWith(
['fetch', '--no-tags', 'origin', '+refs/heads/main:refs/remotes/origin/main'],
[
'-c',
'maintenance.auto=false',
'-c',
'maintenance.commit-graph.auto=0',
'-c',
'gc.auto=0',
'fetch',
'--no-tags',
'origin',
'+refs/heads/main:refs/remotes/origin/main'
],
tmpDir
)
})

View File

@ -60,6 +60,7 @@ import {
import { getGitCloneFailureMessage } from '../shared/git-clone-failure-message'
import { syncForkDefaultBranch, validateGitForkSyncExpectedUpstream } from '../shared/git-fork-sync'
import { InFlightPromiseDedupe, stableInFlightKey } from '../shared/in-flight-promise-dedupe'
import { GIT_FETCH_SKIP_AUTO_MAINTENANCE_CONFIG_ARGS } from '../shared/git-fetch-auto-maintenance'
const execFileAsync = promisify(execFile)
const MAX_GIT_BUFFER = 10 * 1024 * 1024
@ -844,10 +845,14 @@ export class GitHandler {
const remote = params.remote
const branch = params.branch
const ref = params.ref
const skipAutoMaintenance = params.skipAutoMaintenance
try {
if (typeof remote !== 'string' || typeof branch !== 'string' || typeof ref !== 'string') {
throw new Error('Invalid remote-tracking fetch request.')
}
if (skipAutoMaintenance !== undefined && typeof skipAutoMaintenance !== 'boolean') {
throw new Error('Invalid remote-tracking fetch maintenance option.')
}
if (remote.startsWith('-') || branch.startsWith('-')) {
throw new Error('Remote-tracking fetch inputs must not start with "-".')
}
@ -866,7 +871,16 @@ export class GitHandler {
}
await this.git(['check-ref-format', `refs/heads/${branch}`], worktreePath)
await this.git(['check-ref-format', ref], worktreePath)
await this.git(['fetch', '--no-tags', remote, `+refs/heads/${branch}:${ref}`], worktreePath)
await this.git(
[
...(skipAutoMaintenance ? GIT_FETCH_SKIP_AUTO_MAINTENANCE_CONFIG_ARGS : []),
'fetch',
'--no-tags',
remote,
`+refs/heads/${branch}:${ref}`
],
worktreePath
)
} catch (error) {
// Why: create-worktree needs a write-capable fetch, but generic git.exec
// intentionally rejects fetch. This narrow RPC keeps the relay allowlist

View File

@ -0,0 +1,10 @@
// Why: Git 2.29 can auto-run commit-graph work before maintenance.auto became a gate.
// The other keys cover modern maintenance and legacy auto-gc without changing user config.
export const GIT_FETCH_SKIP_AUTO_MAINTENANCE_CONFIG_ARGS = [
'-c',
'maintenance.auto=false',
'-c',
'maintenance.commit-graph.auto=0',
'-c',
'gc.auto=0'
] as const