From 5d8c33832c89d36013c0eb61d60333c5668eeb26 Mon Sep 17 00:00:00 2001 From: Kunping Wang <146500293+wang-kaopu@users.noreply.github.com> Date: Sat, 29 Aug 2026 09:19:39 +0800 Subject: [PATCH] =?UTF-8?q?fix(git):=20=E5=85=BC=E5=AE=B9=E6=97=A7?= =?UTF-8?q?=E7=89=88=20Git=20=E7=9A=84=20worktree=20=E5=88=97=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/git.ts | 18 ++++++++++++++- tests/git-spawn.spec.ts | 50 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/src/git.ts b/src/git.ts index 9b9f151..29e5aad 100644 --- a/src/git.ts +++ b/src/git.ts @@ -323,11 +323,27 @@ function pathIdentity(path: string): string { return process.platform === 'win32' ? absolute.toLowerCase() : absolute } +/** Whether the current Git binary supports NUL-framed `worktree list` output. + * Git < 2.36 rejects `-z`; cache the capability after the first attempt so + * the SCM panel's polling does not repeatedly spawn a command known to fail. */ +let worktreeListSupportsZ: boolean | undefined + /** Raw usable checkout records, shared by inventory and target validation. * Prunable records point at missing paths and are deliberately excluded from * both the selector and the command-target allowlist. */ async function listedWorktrees(cwd: string): Promise { - const raw = await runGit(cwd, ['worktree', 'list', '--porcelain', '-z']) + let raw: string + if (worktreeListSupportsZ === false) { + raw = await runGit(cwd, ['worktree', 'list', '--porcelain']) + } else { + try { + raw = await runGit(cwd, ['worktree', 'list', '--porcelain', '-z']) + worktreeListSupportsZ = true + } catch { + worktreeListSupportsZ = false + raw = await runGit(cwd, ['worktree', 'list', '--porcelain']) + } + } return parseWorktreeList(raw).filter(entry => !entry.prunable) } diff --git a/tests/git-spawn.spec.ts b/tests/git-spawn.spec.ts index eab01ec..569df70 100644 --- a/tests/git-spawn.spec.ts +++ b/tests/git-spawn.spec.ts @@ -6,7 +6,7 @@ const { spawnMock } = vi.hoisted(() => ({ spawnMock: vi.fn() })) vi.mock('node:child_process', () => ({ spawn: spawnMock })) -import { isGitRepo } from '../src/git.ts' +import { isGitRepo, worktrees } from '../src/git.ts' afterEach(() => { spawnMock.mockReset() @@ -38,4 +38,52 @@ describe('git subprocess spawning', () => { expect.objectContaining({ windowsHide: true }), ) }) + + it('falls back when worktree list does not support -z', async () => { + const gitChild = (stdoutText = '', stderrText = '', code = 0): EventEmitter => { + const child = new EventEmitter() + const stdout = new PassThrough() + const stderr = new PassThrough() + Object.assign(child, { stdout, stderr, kill: vi.fn() }) + queueMicrotask(() => { + stdout.end(stdoutText) + stderr.end(stderrText) + child.emit('close', code) + }) + return child + } + + spawnMock.mockImplementation((_file: string, args: string[]) => { + const command = args.slice(5) + if (command[0] === 'rev-parse' && command[1] === '--is-inside-work-tree') return gitChild('true\n') + if (command[0] === 'rev-parse' && command[1] === '--show-toplevel') return gitChild('C:\\repo\n') + if (command[0] === 'rev-parse' && command[1] === '--abbrev-ref') return gitChild('main\n') + if (command[0] === 'status') return gitChild('') + if (command[0] === 'worktree' && command.includes('-z')) { + return gitChild('', "error: unknown switch `z'\n", 129) + } + if (command[0] === 'worktree') { + return gitChild([ + 'worktree C:\\repo', + 'HEAD abc', + 'branch refs/heads/main', + '', + ].join('\n')) + } + throw new Error(`unexpected git command: ${command.join(' ')}`) + }) + + await expect(worktrees('C:\\repo')).resolves.toEqual([ + { path: 'C:\\repo', branch: 'main', current: true, changes: 0 }, + ]) + await expect(worktrees('C:\\repo')).resolves.toEqual([ + { path: 'C:\\repo', branch: 'main', current: true, changes: 0 }, + ]) + + const nulAttempts = spawnMock.mock.calls.filter((call) => { + const args = call[1] as string[] + return args.includes('worktree') && args.includes('-z') + }) + expect(nulAttempts).toHaveLength(1) + }) })