Merge pull request #454 from wang-kaopu/fix/issue-440-old-git-worktree
fix(git): support worktree listing on older Git versions
This commit is contained in:
+17
-1
@@ -323,11 +323,27 @@ function pathIdentity(path: string): string {
|
|||||||
return process.platform === 'win32' ? absolute.toLowerCase() : absolute
|
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.
|
/** Raw usable checkout records, shared by inventory and target validation.
|
||||||
* Prunable records point at missing paths and are deliberately excluded from
|
* Prunable records point at missing paths and are deliberately excluded from
|
||||||
* both the selector and the command-target allowlist. */
|
* both the selector and the command-target allowlist. */
|
||||||
async function listedWorktrees(cwd: string): Promise<GitWorktreeRecord[]> {
|
async function listedWorktrees(cwd: string): Promise<GitWorktreeRecord[]> {
|
||||||
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)
|
return parseWorktreeList(raw).filter(entry => !entry.prunable)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+49
-1
@@ -6,7 +6,7 @@ const { spawnMock } = vi.hoisted(() => ({ spawnMock: vi.fn() }))
|
|||||||
|
|
||||||
vi.mock('node:child_process', () => ({ spawn: spawnMock }))
|
vi.mock('node:child_process', () => ({ spawn: spawnMock }))
|
||||||
|
|
||||||
import { isGitRepo } from '../src/git.ts'
|
import { isGitRepo, worktrees } from '../src/git.ts'
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
spawnMock.mockReset()
|
spawnMock.mockReset()
|
||||||
@@ -38,4 +38,52 @@ describe('git subprocess spawning', () => {
|
|||||||
expect.objectContaining({ windowsHide: true }),
|
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)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user