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:
parent
4d0ace87ec
commit
26224196f5
|
|
@ -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
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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 } : {})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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')]
|
||||
])
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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: '' }
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
Loading…
Reference in New Issue