diff --git a/src/main/repo-worktrees.test.ts b/src/main/repo-worktrees.test.ts index 31bdd90b0a7..9ed407956bc 100644 --- a/src/main/repo-worktrees.test.ts +++ b/src/main/repo-worktrees.test.ts @@ -1,9 +1,16 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -const { listWorktreeGraphMock, listWorktreesMock, listWorktreesStrictMock } = vi.hoisted(() => ({ - listWorktreeGraphMock: vi.fn(), - listWorktreesMock: vi.fn(), - listWorktreesStrictMock: vi.fn() +const { listWorktreeGraphMock, listWorktreesMock, listWorktreesStrictMock, statMock } = vi.hoisted( + () => ({ + listWorktreeGraphMock: vi.fn(), + listWorktreesMock: vi.fn(), + listWorktreesStrictMock: vi.fn(), + statMock: vi.fn() + }) +) + +vi.mock('./local-worktree-filesystem', () => ({ + getLocalWorktreePathAccess: () => ({ statPath: statMock }) })) vi.mock('./git/worktree', () => ({ @@ -23,11 +30,32 @@ import { } from './repo-worktrees' import { WorktreeCatalogUnavailableError } from '../shared/worktree/worktree-catalog-availability' +const testGitRepo = { + id: 'repo-1', + path: '/workspace/repo', + displayName: 'repo', + badgeColor: '#000', + addedAt: 0, + kind: 'git' as const +} + describe('repo-worktrees', () => { beforeEach(() => { listWorktreeGraphMock.mockReset() listWorktreesMock.mockReset() listWorktreesStrictMock.mockReset() + statMock.mockReset().mockResolvedValue({}) + }) + + it('skips Git on each missing-folder scan and resumes when the folder returns', async () => { + statMock.mockRejectedValueOnce(Object.assign(new Error('missing'), { code: 'ENOENT' })) + statMock.mockRejectedValueOnce(Object.assign(new Error('missing'), { code: 'ENOENT' })) + expect(await listRepoWorktreesForDetectedScan(testGitRepo)).toEqual([]) + expect(await listRepoWorktreesForDetectedScan(testGitRepo)).toEqual([]) + expect(listWorktreesStrictMock).not.toHaveBeenCalled() + listWorktreesStrictMock.mockResolvedValueOnce([]) + await listRepoWorktreesForDetectedScan(testGitRepo) + expect(listWorktreesStrictMock).toHaveBeenCalledOnce() }) it('creates a stable synthetic worktree for folder repos', () => { @@ -100,17 +128,7 @@ describe('repo-worktrees', () => { ]) const signal = new AbortController().signal - const result = await listRepoWorktrees( - { - id: 'repo-1', - path: '/workspace/repo', - displayName: 'repo', - badgeColor: '#000', - addedAt: 0, - kind: 'git' - }, - { signal } - ) + const result = await listRepoWorktrees(testGitRepo, { signal }) expect(listWorktreesMock).toHaveBeenCalledWith('/workspace/repo', { signal }) expect(result).toHaveLength(1) @@ -122,14 +140,7 @@ describe('repo-worktrees', () => { { path: '/workspace/repo', head: 'abc', branch: '', isBare: false, isMainWorktree: true } ]) - const result = await listRepoWorktreeGraph({ - id: 'repo-1', - path: '/workspace/repo', - displayName: 'repo', - badgeColor: '#000', - addedAt: 0, - kind: 'git' - }) + const result = await listRepoWorktreeGraph(testGitRepo) expect(listWorktreeGraphMock).toHaveBeenCalledWith('/workspace/repo') expect(listWorktreesMock).not.toHaveBeenCalled() @@ -165,17 +176,10 @@ describe('repo-worktrees', () => { ]) const signal = new AbortController().signal - const result = await listLocalRepoWorktreesStrict( - { - id: 'repo-1', - path: '/workspace/repo', - displayName: 'repo', - badgeColor: '#000', - addedAt: 0, - kind: 'git' - }, - { signal, wslDistro: 'Ubuntu' } - ) + const result = await listLocalRepoWorktreesStrict(testGitRepo, { + signal, + wslDistro: 'Ubuntu' + }) expect(listWorktreesStrictMock).toHaveBeenCalledWith('/workspace/repo', { signal, diff --git a/src/main/repo-worktrees.ts b/src/main/repo-worktrees.ts index e92a8e50612..a6753eb1c29 100644 --- a/src/main/repo-worktrees.ts +++ b/src/main/repo-worktrees.ts @@ -12,6 +12,8 @@ import { getRepoExecutionHostId, LOCAL_EXECUTION_HOST_ID } from '../shared/execu import { resolveGitRouteForHost } from './providers/execution-host-provider-dispatch' import { areWorktreePathsEqual } from './ipc/worktree-logic' import { WorktreeCatalogUnavailableError } from '../shared/worktree/worktree-catalog-availability' +import { isDefinitiveAbsence } from '../shared/definitive-filesystem-absence' +import { getLocalWorktreePathAccess } from './local-worktree-filesystem' type LocalRepoWorktreeListOptions = { wslDistro?: string @@ -61,7 +63,18 @@ export async function listRepoWorktreesForDetectedScan( repo: Repo, options?: LocalRepoWorktreeListOptions ): Promise { - return listRoutedRepoWorktrees(repo, options, listWorktreesSharedStrictAllowingTrueEmpty) + return listRoutedRepoWorktrees(repo, options, async (repoPath, localOptions) => { + try { + // The local route runs on the execution host, including the server for SSH repositories. + await getLocalWorktreePathAccess(localOptions).statPath(repoPath) + } catch (error) { + if (isDefinitiveAbsence(error)) { + return [] + } + throw error + } + return listWorktreesSharedStrictAllowingTrueEmpty(repoPath, localOptions) + }) } async function listRoutedRepoWorktrees(