From 49cdebf8a5d8f0ab27dd3a96e6177f1b27c9918a Mon Sep 17 00:00:00 2001 From: Neil Date: Thu, 25 Jun 2026 16:36:15 -0700 Subject: [PATCH] Clear diff dedupe for worktree ref mutations --- src/main/providers/ssh-git-provider.ts | 44 +++++++---- src/relay/git-handler.ts | 105 ++++++++++++++----------- 2 files changed, 88 insertions(+), 61 deletions(-) diff --git a/src/main/providers/ssh-git-provider.ts b/src/main/providers/ssh-git-provider.ts index 9a59c562140..0c02b66e798 100644 --- a/src/main/providers/ssh-git-provider.ts +++ b/src/main/providers/ssh-git-provider.ts @@ -507,10 +507,13 @@ export class SshGitProvider implements IGitProvider { worktreePath: string, expectedUpstream: GitForkSyncExpectedUpstream ): Promise { - return (await this.mux.request('git.forkSync', { - worktreePath, - ...(expectedUpstream ? { expectedUpstream } : {}) - })) as GitForkSyncResult + return this.runWithDiffDedupeClear( + async () => + (await this.mux.request('git.forkSync', { + worktreePath, + ...(expectedUpstream ? { expectedUpstream } : {}) + })) as GitForkSyncResult + ) } async fetchRemoteTrackingRef( @@ -607,11 +610,13 @@ export class SshGitProvider implements IGitProvider { targetDir: string, options?: { base?: string; checkoutExistingBranch?: boolean; noCheckout?: boolean } ): Promise { - await this.mux.request('git.addWorktree', { - repoPath, - branchName, - targetDir, - ...options + await this.runWithDiffDedupeClear(async () => { + await this.mux.request('git.addWorktree', { + repoPath, + branchName, + targetDir, + ...options + }) }) } @@ -620,11 +625,14 @@ export class SshGitProvider implements IGitProvider { force?: boolean, options?: { deleteBranch?: boolean; forceBranchDelete?: boolean } ): Promise { - return ((await this.mux.request('git.removeWorktree', { - worktreePath, - force, - ...options - })) ?? {}) as RemoveWorktreeResult + return this.runWithDiffDedupeClear( + async () => + ((await this.mux.request('git.removeWorktree', { + worktreePath, + force, + ...options + })) ?? {}) as RemoveWorktreeResult + ) } async worktreeIsClean( @@ -676,11 +684,15 @@ export class SshGitProvider implements IGitProvider { ownerWorktreePath?: string checkOnly?: boolean }): Promise { - await this.mux.request('git.refreshLocalBaseRefForWorktreeCreate', args) + await this.runWithDiffDedupeClear(async () => { + await this.mux.request('git.refreshLocalBaseRefForWorktreeCreate', args) + }) } async renameCurrentBranch(worktreePath: string, newBranch: string): Promise { - await this.mux.request('git.renameCurrentBranch', { worktreePath, newBranch }) + await this.runWithDiffDedupeClear(async () => { + await this.mux.request('git.renameCurrentBranch', { worktreePath, newBranch }) + }) } async exec( diff --git a/src/relay/git-handler.ts b/src/relay/git-handler.ts index 7c9ba2c8c77..d9cbb605e6d 100644 --- a/src/relay/git-handler.ts +++ b/src/relay/git-handler.ts @@ -143,6 +143,15 @@ export class GitHandler { this.dispatcher.onRequest('git.isGitRepo', (p) => this.isGitRepo(p)) } + private async runWithDiffDedupeClear(run: () => Promise): Promise { + this.gitDiffReadDedupe.clear() + try { + return await run() + } finally { + this.gitDiffReadDedupe.clear() + } + } + private async git( args: string[], cwd: string, @@ -609,33 +618,35 @@ export class GitHandler { } private async forkSync(params: Record, context?: RequestContext) { - const worktreePath = params.worktreePath as string - const expectedUpstream = validateGitForkSyncExpectedUpstream(params.expectedUpstream, { - required: true + return this.runWithDiffDedupeClear(async () => { + const worktreePath = params.worktreePath as string + const expectedUpstream = validateGitForkSyncExpectedUpstream(params.expectedUpstream, { + required: true + }) + const controller = new AbortController() + const abortFromContext = () => controller.abort() + if (context?.signal?.aborted) { + controller.abort() + } else { + context?.signal?.addEventListener('abort', abortFromContext, { once: true }) + } + const timeout = setTimeout(() => controller.abort(), 60_000) + try { + return await syncForkDefaultBranch( + (args) => + this.git(args, worktreePath, { + nonInteractive: true, + signal: controller.signal + }), + { expectedUpstream } + ) + } catch (error) { + throw new Error(normalizeGitErrorMessage(error, 'push')) + } finally { + clearTimeout(timeout) + context?.signal?.removeEventListener('abort', abortFromContext) + } }) - const controller = new AbortController() - const abortFromContext = () => controller.abort() - if (context?.signal?.aborted) { - controller.abort() - } else { - context?.signal?.addEventListener('abort', abortFromContext, { once: true }) - } - const timeout = setTimeout(() => controller.abort(), 60_000) - try { - return await syncForkDefaultBranch( - (args) => - this.git(args, worktreePath, { - nonInteractive: true, - signal: controller.signal - }), - { expectedUpstream } - ) - } catch (error) { - throw new Error(normalizeGitErrorMessage(error, 'push')) - } finally { - clearTimeout(timeout) - context?.signal?.removeEventListener('abort', abortFromContext) - } } private async fetchRemoteTrackingRef(params: Record) { @@ -955,22 +966,24 @@ export class GitHandler { } private async renameCurrentBranch(params: Record) { - const worktreePath = params.worktreePath - const newBranch = params.newBranch - if (typeof worktreePath !== 'string' || typeof newBranch !== 'string') { - throw new Error('Invalid branch rename request.') - } - if (newBranch.startsWith('-')) { - throw new Error('Branch name must not start with "-".') - } - try { - // Why: generic git.exec intentionally blocks destructive branch flags. - // This narrow RPC permits only the already-checked current-branch rename. - await this.git(['check-ref-format', '--branch', newBranch], worktreePath) - await this.git(['branch', '-m', newBranch], worktreePath) - } catch (error) { - throw new Error(normalizeGitErrorMessage(error)) - } + return this.runWithDiffDedupeClear(async () => { + const worktreePath = params.worktreePath + const newBranch = params.newBranch + if (typeof worktreePath !== 'string' || typeof newBranch !== 'string') { + throw new Error('Invalid branch rename request.') + } + if (newBranch.startsWith('-')) { + throw new Error('Branch name must not start with "-".') + } + try { + // Why: generic git.exec intentionally blocks destructive branch flags. + // This narrow RPC permits only the already-checked current-branch rename. + await this.git(['check-ref-format', '--branch', newBranch], worktreePath) + await this.git(['branch', '-m', newBranch], worktreePath) + } catch (error) { + throw new Error(normalizeGitErrorMessage(error)) + } + }) } private async isGitRepo(params: Record) { @@ -1005,11 +1018,11 @@ export class GitHandler { } private async addWorktree(params: Record) { - return addWorktreeOp(this.git.bind(this), params) + return this.runWithDiffDedupeClear(() => addWorktreeOp(this.git.bind(this), params)) } private async removeWorktree(params: Record) { - return removeWorktreeOp(this.git.bind(this), params) + return this.runWithDiffDedupeClear(() => removeWorktreeOp(this.git.bind(this), params)) } private async worktreeIsClean(params: Record) { @@ -1017,6 +1030,8 @@ export class GitHandler { } private async refreshLocalBaseRefForWorktreeCreate(params: Record) { - return refreshLocalBaseRefForWorktreeCreateOp(this.git.bind(this), params) + return this.runWithDiffDedupeClear(() => + refreshLocalBaseRefForWorktreeCreateOp(this.git.bind(this), params) + ) } }