From 7a1afb5434331f1ddc669064e35a2ab89771e211 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Fri, 10 Jul 2026 00:04:51 -0700 Subject: [PATCH] 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. --- src/main/ipc/worktree-remote.ts | 5 +- src/main/ipc/worktrees.test.ts | 9 ++- src/main/providers/ssh-git-provider.test.ts | 6 +- src/main/providers/ssh-git-provider.ts | 6 +- src/main/runtime/fetch-remote-cache.test.ts | 69 +++++++++++++-------- src/main/runtime/orca-runtime.ts | 9 ++- src/relay/git-handler.test.ts | 14 ++++- src/relay/git-handler.ts | 16 ++++- src/shared/git-fetch-auto-maintenance.ts | 8 +++ 9 files changed, 105 insertions(+), 37 deletions(-) create mode 100644 src/shared/git-fetch-auto-maintenance.ts diff --git a/src/main/ipc/worktree-remote.ts b/src/main/ipc/worktree-remote.ts index e0a2bbf5db1..6217a3338da 100644 --- a/src/main/ipc/worktree-remote.ts +++ b/src/main/ipc/worktree-remote.ts @@ -477,7 +477,10 @@ async function refreshRemoteTrackingBaseForWorktreeCreate( return getOrStartSshWorktreeCreateFetch( getSshWorktreeCreateBaseFetchKey(repo, base), getSshWorktreeCreateRemoteQueueKey(repo, base.remote), - () => provider.fetchRemoteTrackingRef(repo.path, base.remote, base.branch, base.ref) + () => + provider.fetchRemoteTrackingRef(repo.path, base.remote, base.branch, base.ref, { + skipAutoMaintenance: true + }) ) } diff --git a/src/main/ipc/worktrees.test.ts b/src/main/ipc/worktrees.test.ts index 43c94325789..7b4f699cd62 100644 --- a/src/main/ipc/worktrees.test.ts +++ b/src/main/ipc/worktrees.test.ts @@ -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) }) diff --git a/src/main/providers/ssh-git-provider.test.ts b/src/main/providers/ssh-git-provider.test.ts index acfaefaed12..0974367ef81 100644 --- a/src/main/providers/ssh-git-provider.test.ts +++ b/src/main/providers/ssh-git-provider.test.ts @@ -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 }) }) diff --git a/src/main/providers/ssh-git-provider.ts b/src/main/providers/ssh-git-provider.ts index 5ba6a29e380..0cde62d534f 100644 --- a/src/main/providers/ssh-git-provider.ts +++ b/src/main/providers/ssh-git-provider.ts @@ -553,14 +553,16 @@ export class SshGitProvider implements IGitProvider { worktreePath: string, remote: string, branch: string, - ref: string + ref: string, + options?: { skipAutoMaintenance?: boolean } ): Promise { await this.runWithDiffDedupeClear(async () => { await this.mux.request('git.fetchRemoteTrackingRef', { worktreePath, remote, branch, - ref + ref, + ...(options?.skipAutoMaintenance ? { skipAutoMaintenance: true } : {}) }) }) } diff --git a/src/main/runtime/fetch-remote-cache.test.ts b/src/main/runtime/fetch-remote-cache.test.ts index fce2a5e6d59..43f95b0797c 100644 --- a/src/main/runtime/fetch-remote-cache.test.ts +++ b/src/main/runtime/fetch-remote-cache.test.ts @@ -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,19 @@ function exactBaseRefreshOptions(cwd: string): { return { cwd, timeout: 60_000, useConfiguredSshCommandForNetwork: true } } +function exactBaseRefreshArgs(branch = 'main'): string[] { + return [ + '-c', + 'maintenance.auto=false', + '-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 +210,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 +331,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 +372,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 +413,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')] ]) }) }) diff --git a/src/main/runtime/orca-runtime.ts b/src/main/runtime/orca-runtime.ts index 305a15a2127..c7e55378c2f 100644 --- a/src/main/runtime/orca-runtime.ts +++ b/src/main/runtime/orca-runtime.ts @@ -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' @@ -14621,7 +14622,13 @@ export class OrcaRuntimeService { return { ok: true } } 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, diff --git a/src/relay/git-handler.test.ts b/src/relay/git-handler.test.ts index 07b84db43f9..5bc6db582fd 100644 --- a/src/relay/git-handler.test.ts +++ b/src/relay/git-handler.test.ts @@ -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,16 @@ 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', + 'gc.auto=0', + 'fetch', + '--no-tags', + 'origin', + '+refs/heads/main:refs/remotes/origin/main' + ], tmpDir ) }) diff --git a/src/relay/git-handler.ts b/src/relay/git-handler.ts index 2d4bb2bb09b..61134d5d037 100644 --- a/src/relay/git-handler.ts +++ b/src/relay/git-handler.ts @@ -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 diff --git a/src/shared/git-fetch-auto-maintenance.ts b/src/shared/git-fetch-auto-maintenance.ts new file mode 100644 index 00000000000..5a61ddcf27c --- /dev/null +++ b/src/shared/git-fetch-auto-maintenance.ts @@ -0,0 +1,8 @@ +// Why: repo housekeeping is unrelated to ref freshness; per-command config also works on old Git. +// Cover both modern maintenance and its legacy auto-gc predecessor without changing user config. +export const GIT_FETCH_SKIP_AUTO_MAINTENANCE_CONFIG_ARGS = [ + '-c', + 'maintenance.auto=false', + '-c', + 'gc.auto=0' +] as const