From 7ee4dc4f2fa681103b69d48e99601e9733538157 Mon Sep 17 00:00:00 2001 From: Le Vivilet Date: Mon, 20 Jul 2026 11:52:24 +0000 Subject: [PATCH 1/4] feature: select worktree to delete --- cspell.json | 2 +- packages/e2e/src/git-api-create-worktree.ts | 1 - packages/e2e/src/git-api-delete-worktree.ts | 1 - .../e2e/src/git.quick-pick-delete-worktree.ts | 42 ++++++++++ packages/extension/extension.json | 3 +- .../src/parts/CommandId/CommandId.ts | 1 + .../ExtensionHostCommand.ts | 1 + ...nsionHostCommandGitDeleteWorktreeSelect.ts | 9 ++ .../GitWorkerCommandType.ts | 1 + .../GitRequestsGetWorktrees.ts | 21 +++++ packages/git-requests/src/parts/Main/Main.ts | 1 + .../test/GitRequestsGetWorktrees.test.ts | 38 +++++++++ .../CommandDeleteWorktreeSelect.ts | 61 ++++++++++++++ .../src/parts/CommandMap/CommandMap.ts | 2 + .../GitWorkerCommandType.ts | 1 + .../test/CommandDeleteWorktreeSelect.test.ts | 84 +++++++++++++++++++ 16 files changed, 265 insertions(+), 4 deletions(-) create mode 100644 packages/e2e/src/git.quick-pick-delete-worktree.ts create mode 100644 packages/extension/src/parts/ExtensionHostCommand/ExtensionHostCommandGitDeleteWorktreeSelect.ts create mode 100644 packages/git-requests/src/parts/GitRequestsGetWorktrees/GitRequestsGetWorktrees.ts create mode 100644 packages/git-requests/test/GitRequestsGetWorktrees.test.ts create mode 100644 packages/git-worker/src/parts/CommandDeleteWorktreeSelect/CommandDeleteWorktreeSelect.ts create mode 100644 packages/git-worker/test/CommandDeleteWorktreeSelect.test.ts diff --git a/cspell.json b/cspell.json index f146ffed..3e07e4f6 100644 --- a/cspell.json +++ b/cspell.json @@ -1,4 +1,4 @@ { "version": "0.2", - "words": ["worktree", "gitdir", "lszomoru", "objectname", "pathspec", "symref", "uall", "unstash"] + "words": ["worktree", "worktrees", "gitdir", "lszomoru", "objectname", "pathspec", "symref", "uall", "unstash"] } diff --git a/packages/e2e/src/git-api-create-worktree.ts b/packages/e2e/src/git-api-create-worktree.ts index 9bd97493..2fdd0321 100644 --- a/packages/e2e/src/git-api-create-worktree.ts +++ b/packages/e2e/src/git-api-create-worktree.ts @@ -1,4 +1,3 @@ -/* eslint-disable @cspell/spellchecker */ import type { Test } from '@lvce-editor/test-with-playwright' export const name = 'git.createWorktree' diff --git a/packages/e2e/src/git-api-delete-worktree.ts b/packages/e2e/src/git-api-delete-worktree.ts index 460c1a56..00d1c84d 100644 --- a/packages/e2e/src/git-api-delete-worktree.ts +++ b/packages/e2e/src/git-api-delete-worktree.ts @@ -1,4 +1,3 @@ -/* eslint-disable @cspell/spellchecker */ import type { Test } from '@lvce-editor/test-with-playwright' export const name = 'git.deleteWorktree' diff --git a/packages/e2e/src/git.quick-pick-delete-worktree.ts b/packages/e2e/src/git.quick-pick-delete-worktree.ts new file mode 100644 index 00000000..f18be8b0 --- /dev/null +++ b/packages/e2e/src/git.quick-pick-delete-worktree.ts @@ -0,0 +1,42 @@ +import type { Test } from '@lvce-editor/test-with-playwright' + +export const name = 'git.quick-pick-delete-worktree' + +export const test: Test = async ({ Command, expect, FileSystem, Git, Locator, QuickPick, Workspace }) => { + // arrange + const tmpDir = await FileSystem.getTmpDir({ scheme: 'file' }) + const workspaceDir = `${tmpDir}/workspace` + const worktreeDir = `${tmpDir}/feature-worktree` + + await Workspace.setPath(tmpDir) + const fixtureUrl = import.meta.resolve('../fixtures/git-api-delete-worktree') + await Command.execute('ExtensionHost.executeCommand', 'git.loadFixture', fixtureUrl) + await Workspace.setPath(workspaceDir) + + // act + await QuickPick.executeCommand('Git: Delete Worktree') + const quickPick = Locator('#QuickPick') + await expect(quickPick).toBeVisible() + await expect(quickPick.locator('text=feature-worktree')).toBeVisible() + await QuickPick.selectItem('feature-worktree') + + // assert + const tmpDirEntries = await FileSystem.readDir(tmpDir) + if (tmpDirEntries.some((dirent) => dirent.name === 'feature-worktree')) { + throw new Error(`expected worktree folder to be removed`) + } + const gitDirEntries = await FileSystem.readDir(`${workspaceDir}/.git/worktrees`) + if (gitDirEntries.length > 0) { + throw new Error(`expected worktree metadata to be removed, got ${gitDirEntries.map((dirent) => dirent.name).join(', ')}`) + } + await Git.shouldHaveInvocations([ + { + command: ['git', 'worktree', 'list', '--porcelain', '-z'], + cwd: workspaceDir, + }, + { + command: ['git', 'worktree', 'remove', worktreeDir], + cwd: workspaceDir, + }, + ]) +} diff --git a/packages/extension/extension.json b/packages/extension/extension.json index aad660a9..191c907a 100644 --- a/packages/extension/extension.json +++ b/packages/extension/extension.json @@ -44,6 +44,7 @@ "onCommand:git.createWorktree", "onCommand:git.createTag", "onCommand:git.deleteWorktree", + "onCommand:git.deleteWorktreeSelect", "onCommand:git.deleteRemoteTag", "onCommand:git.deleteTag", "onCommand:git.sync", @@ -137,7 +138,7 @@ "label": "Git: Create Tag" }, { - "id": "git.deleteWorktree", + "id": "git.deleteWorktreeSelect", "label": "Git: Delete Worktree" }, { diff --git a/packages/extension/src/parts/CommandId/CommandId.ts b/packages/extension/src/parts/CommandId/CommandId.ts index 5c7940bc..a7a4a809 100644 --- a/packages/extension/src/parts/CommandId/CommandId.ts +++ b/packages/extension/src/parts/CommandId/CommandId.ts @@ -16,6 +16,7 @@ export const GitDeleteBranch = 'git.deleteBranch' export const GitDeleteRemoteTag = 'git.deleteRemoteTag' export const GitDeleteTag = 'git.deleteTag' export const GitDeleteWorktree = 'git.deleteWorktree' +export const GitDeleteWorktreeSelect = 'git.deleteWorktreeSelect' export const GitCheckout = 'git.checkout' export const GitCheckoutRef = 'git.checkoutRef' export const GitCleanAll = 'git.cleanAll' diff --git a/packages/extension/src/parts/ExtensionHostCommand/ExtensionHostCommand.ts b/packages/extension/src/parts/ExtensionHostCommand/ExtensionHostCommand.ts index df6baaf7..9edb754c 100644 --- a/packages/extension/src/parts/ExtensionHostCommand/ExtensionHostCommand.ts +++ b/packages/extension/src/parts/ExtensionHostCommand/ExtensionHostCommand.ts @@ -14,6 +14,7 @@ export * as GitDeleteBranch from './ExtensionHostCommandGitDeleteBranch.ts' export * as GitDeleteRemoteTag from './ExtensionHostCommandGitDeleteRemoteTag.ts' export * as GitDeleteTag from './ExtensionHostCommandGitDeleteTag.ts' export * as GitDeleteWorktree from './ExtensionHostCommandGitDeleteWorktree.ts' +export * as GitDeleteWorktreeSelect from './ExtensionHostCommandGitDeleteWorktreeSelect.ts' export * as GitCheckout from './ExtensionHostCommandGitCheckout.ts' export * as GitCheckoutRef from './ExtensionHostCommandGitCheckoutRef.ts' export * as GitCleanAll from './ExtensionHostCommandGitCleanAll.ts' diff --git a/packages/extension/src/parts/ExtensionHostCommand/ExtensionHostCommandGitDeleteWorktreeSelect.ts b/packages/extension/src/parts/ExtensionHostCommand/ExtensionHostCommandGitDeleteWorktreeSelect.ts new file mode 100644 index 00000000..5b24de93 --- /dev/null +++ b/packages/extension/src/parts/ExtensionHostCommand/ExtensionHostCommandGitDeleteWorktreeSelect.ts @@ -0,0 +1,9 @@ +import * as CommandId from '../CommandId/CommandId.ts' +import * as GitWorker from '../GitWorker/GitWorker.ts' +import * as GitWorkerCommandType from '../GitWorkerCommandType/GitWorkerCommandType.ts' + +export const id = CommandId.GitDeleteWorktreeSelect + +export const execute = async () => { + return GitWorker.invoke(GitWorkerCommandType.CommandDeleteWorktreeSelect) +} diff --git a/packages/extension/src/parts/GitWorkerCommandType/GitWorkerCommandType.ts b/packages/extension/src/parts/GitWorkerCommandType/GitWorkerCommandType.ts index 2bb1ae4d..73329c0b 100644 --- a/packages/extension/src/parts/GitWorkerCommandType/GitWorkerCommandType.ts +++ b/packages/extension/src/parts/GitWorkerCommandType/GitWorkerCommandType.ts @@ -15,6 +15,7 @@ export const GitDeleteBranch = 'Git.deleteBranch' export const GitDeleteRemoteTag = 'Git.deleteRemoteTag' export const GitDeleteTag = 'Git.deleteTag' export const GitDeleteWorktree = 'Git.deleteWorktree' +export const CommandDeleteWorktreeSelect = 'Command.gitDeleteWorktreeSelect' export const GitDiscard = 'Git.discard' export const GitFetch = 'Git.fetch' export const GitGetAddedFiles = 'Git.getAddedFiles' diff --git a/packages/git-requests/src/parts/GitRequestsGetWorktrees/GitRequestsGetWorktrees.ts b/packages/git-requests/src/parts/GitRequestsGetWorktrees/GitRequestsGetWorktrees.ts new file mode 100644 index 00000000..b463ee46 --- /dev/null +++ b/packages/git-requests/src/parts/GitRequestsGetWorktrees/GitRequestsGetWorktrees.ts @@ -0,0 +1,21 @@ +import type { GitRequestContext } from '../Types/Types.ts' +import { GitError } from '../GitError/GitError.ts' + +const worktreePrefix = 'worktree ' + +export const getWorktrees = async ({ cwd, exec, gitPath }: GitRequestContext): Promise => { + try { + const gitResult = await exec({ + args: ['worktree', 'list', '--porcelain', '-z'], + cwd, + gitPath, + name: 'getWorktrees', + }) + return gitResult.stdout + .split('\0') + .filter((line) => line.startsWith(worktreePrefix)) + .map((line) => line.slice(worktreePrefix.length)) + } catch (error) { + throw new GitError(error, 'getWorktrees') + } +} diff --git a/packages/git-requests/src/parts/Main/Main.ts b/packages/git-requests/src/parts/Main/Main.ts index 11d4a288..95f31e46 100644 --- a/packages/git-requests/src/parts/Main/Main.ts +++ b/packages/git-requests/src/parts/Main/Main.ts @@ -27,6 +27,7 @@ export * from '../GitRequestsGetGroups/GitRequestsGetGroups.ts' export * from '../GitRequestsGetModifiedFiles/GitRequestsGetModifiedFiles.ts' export * from '../GitRequestsGetRefs/GitRequestsGetRefs.ts' export * from '../GitRequestsGetRemote/GitRequestsGetRemote.ts' +export * from '../GitRequestsGetWorktrees/GitRequestsGetWorktrees.ts' export * from '../GitRequestsInit/GitRequestsInit.ts' export * from '../GitRequestsPull/GitRequestsPull.ts' export * from '../GitRequestsPullAndRebase/GitRequestPullAndRebase.ts' diff --git a/packages/git-requests/test/GitRequestsGetWorktrees.test.ts b/packages/git-requests/test/GitRequestsGetWorktrees.test.ts new file mode 100644 index 00000000..3a78f06b --- /dev/null +++ b/packages/git-requests/test/GitRequestsGetWorktrees.test.ts @@ -0,0 +1,38 @@ +import { expect, jest, test } from '@jest/globals' +import type { GitExec } from '../src/parts/Types/Types.ts' +import * as GitRequestsGetWorktrees from '../src/parts/GitRequestsGetWorktrees/GitRequestsGetWorktrees.ts' + +test('getWorktrees - returns worktree paths', async (): Promise => { + const exec = jest.fn(async () => ({ + stderr: '', + stdout: 'worktree /test/workspace\0HEAD 123\0branch refs/heads/main\0\0worktree /test/feature worktree\0HEAD 456\0branch refs/heads/feature\0\0', + })) + + await expect( + GitRequestsGetWorktrees.getWorktrees({ + cwd: '/test/workspace', + exec, + gitPath: '/test/git', + }), + ).resolves.toEqual(['/test/workspace', '/test/feature worktree']) + expect(exec).toHaveBeenCalledWith({ + args: ['worktree', 'list', '--porcelain', '-z'], + cwd: '/test/workspace', + gitPath: '/test/git', + name: 'getWorktrees', + }) +}) + +test('getWorktrees - error - unknown git error', async (): Promise => { + const exec = jest.fn(async () => { + throw new Error('Failed') + }) + + await expect( + GitRequestsGetWorktrees.getWorktrees({ + cwd: '/test/workspace', + exec, + gitPath: '/test/git', + }), + ).rejects.toThrow('Failed') +}) diff --git a/packages/git-worker/src/parts/CommandDeleteWorktreeSelect/CommandDeleteWorktreeSelect.ts b/packages/git-worker/src/parts/CommandDeleteWorktreeSelect/CommandDeleteWorktreeSelect.ts new file mode 100644 index 00000000..18df8c83 --- /dev/null +++ b/packages/git-worker/src/parts/CommandDeleteWorktreeSelect/CommandDeleteWorktreeSelect.ts @@ -0,0 +1,61 @@ +import * as Git from '../Git/Git.ts' +import * as Repositories from '../GitRepositories/GitRepositories.ts' +import * as GitRepositoriesRequests from '../GitRepositoriesRequests/GitRepositoriesRequests.ts' +import * as GitRequests from '../GitRequests/GitRequests.ts' +import * as Rpc from '../Rpc/Rpc.ts' + +type WorktreePick = { + readonly description: string + readonly label: string + readonly worktreePath: string +} + +const getBaseName = (path: string): string => { + let normalizedPath = path.replaceAll('\\', '/') + while (normalizedPath.endsWith('/')) { + normalizedPath = normalizedPath.slice(0, -1) + } + const index = normalizedPath.lastIndexOf('/') + return normalizedPath.slice(index + 1) +} + +const getWorktreePicks = (worktrees: readonly string[], currentWorktree: string): readonly WorktreePick[] => { + return worktrees + .filter((worktreePath) => worktreePath !== currentWorktree) + .map((worktreePath) => ({ + description: worktreePath, + label: getBaseName(worktreePath), + worktreePath, + })) +} + +export const commandDeleteWorktreeSelect = async (): Promise => { + const repository = await Repositories.getCurrent() + const { gitPath, path } = repository + const worktrees = await GitRepositoriesRequests.execute({ + args: { + cwd: path, + exec: Git.exec, + gitPath, + }, + fn: GitRequests.getWorktrees, + id: 'getWorktrees', + }) + const picks = getWorktreePicks(worktrees, path) + const selectedPick = await Rpc.invoke('QuickPick.show', picks) + if (!selectedPick) { + return + } + const { worktreePath } = selectedPick + await GitRepositoriesRequests.execute({ + args: { + cwd: path, + exec: Git.exec, + gitPath, + worktreePath, + }, + fn: GitRequests.deleteWorktree, + id: 'deleteWorktree', + }) + return worktreePath +} diff --git a/packages/git-worker/src/parts/CommandMap/CommandMap.ts b/packages/git-worker/src/parts/CommandMap/CommandMap.ts index 0cf349b6..3e0a8858 100644 --- a/packages/git-worker/src/parts/CommandMap/CommandMap.ts +++ b/packages/git-worker/src/parts/CommandMap/CommandMap.ts @@ -4,6 +4,7 @@ import * as CommandAddAll from '../CommandAddAll/CommandAddAll.ts' import * as CommandApplyStash from '../CommandApplyStash/CommandApplyStash.ts' import * as CommandCheckout from '../CommandCheckout/CommandCheckout.ts' import * as CommandCleanAll from '../CommandCleanAll/CommandCleanAll.ts' +import * as CommandDeleteWorktreeSelect from '../CommandDeleteWorktreeSelect/CommandDeleteWorktreeSelect.ts' import * as CommandDiscard from '../CommandDiscard/CommandDiscard.ts' import * as CommandFetch from '../CommandFetch/CommandFetch.ts' import * as CommandFetchPrune from '../CommandFetchPrune/CommandFetchPrune.ts' @@ -34,6 +35,7 @@ export const commandMap = { [GitWorkerCommandType.CommandApplyStash]: CommandApplyStash.commandApplyStash, [GitWorkerCommandType.CommandCheckoutRef]: CommandCheckout.commandCheckout, [GitWorkerCommandType.CommandCleanAll]: CommandCleanAll.commandCleanAll, + [GitWorkerCommandType.CommandDeleteWorktreeSelect]: CommandDeleteWorktreeSelect.commandDeleteWorktreeSelect, [GitWorkerCommandType.CommandDiscard]: CommandDiscard.commandDiscard, [GitWorkerCommandType.CommandFetch]: CommandFetch.commandFetch, [GitWorkerCommandType.CommandFetchPrune]: CommandFetchPrune.commandFetchPrune, diff --git a/packages/git-worker/src/parts/GitWorkerCommandType/GitWorkerCommandType.ts b/packages/git-worker/src/parts/GitWorkerCommandType/GitWorkerCommandType.ts index 4ad4fff4..4ce3c8a3 100644 --- a/packages/git-worker/src/parts/GitWorkerCommandType/GitWorkerCommandType.ts +++ b/packages/git-worker/src/parts/GitWorkerCommandType/GitWorkerCommandType.ts @@ -5,6 +5,7 @@ export const CommandApplyStash = 'Command.gitApplyStash' export const CommandCheckoutRef = 'Command.gitCheckoutRef' export const CommandCleanAll = 'Command.gitCleanAll' export const CommandDiscard = 'Command.gitDiscard' +export const CommandDeleteWorktreeSelect = 'Command.gitDeleteWorktreeSelect' export const CommandFetch = 'Command.gitFetch' export const CommandFetchPrune = 'Command.gitFetchPrune' export const CommandInit = 'Command.gitInit' diff --git a/packages/git-worker/test/CommandDeleteWorktreeSelect.test.ts b/packages/git-worker/test/CommandDeleteWorktreeSelect.test.ts new file mode 100644 index 00000000..b7c77a25 --- /dev/null +++ b/packages/git-worker/test/CommandDeleteWorktreeSelect.test.ts @@ -0,0 +1,84 @@ +/* eslint-disable jest/no-restricted-jest-methods */ +import { jest } from '@jest/globals' +import type * as GitRepositories from '../src/parts/GitRepositories/GitRepositories.ts' +import type * as GitRepositoriesRequests from '../src/parts/GitRepositoriesRequests/GitRepositoriesRequests.ts' +import type * as Rpc from '../src/parts/Rpc/Rpc.ts' + +const mockGetCurrent = jest.fn() +const mockExecute = jest.fn() +const mockInvoke = jest.fn() + +jest.unstable_mockModule('../src/parts/GitRepositories/GitRepositories.ts', () => ({ + getCurrent: mockGetCurrent, +})) + +jest.unstable_mockModule('../src/parts/GitRepositoriesRequests/GitRepositoriesRequests.ts', () => ({ + execute: mockExecute, +})) + +jest.unstable_mockModule('../src/parts/Rpc/Rpc.ts', () => ({ + invoke: mockInvoke, +})) + +const CommandDeleteWorktreeSelect = await import('../src/parts/CommandDeleteWorktreeSelect/CommandDeleteWorktreeSelect.ts') +const Git = await import('../src/parts/Git/Git.ts') +const GitRequests = await import('../src/parts/GitRequests/GitRequests.ts') + +beforeEach(() => { + jest.resetAllMocks() + mockGetCurrent.mockResolvedValue({ + gitPath: '/test/git', + gitVersion: '2.39.2', + path: '/test/workspace', + }) +}) + +test('deletes selected worktree', async (): Promise => { + const worktrees = ['/test/workspace', '/test/feature worktree'] + const pick = { + description: '/test/feature worktree', + label: 'feature worktree', + worktreePath: '/test/feature worktree', + } + mockExecute.mockResolvedValueOnce(worktrees).mockResolvedValueOnce(undefined) + mockInvoke.mockResolvedValue(pick) + + await expect(CommandDeleteWorktreeSelect.commandDeleteWorktreeSelect()).resolves.toBe('/test/feature worktree') + expect(mockInvoke).toHaveBeenCalledWith('QuickPick.show', [pick]) + expect(mockExecute).toHaveBeenNthCalledWith(1, { + args: { + cwd: '/test/workspace', + exec: Git.exec, + gitPath: '/test/git', + }, + fn: GitRequests.getWorktrees, + id: 'getWorktrees', + }) + expect(mockExecute).toHaveBeenNthCalledWith(2, { + args: { + cwd: '/test/workspace', + exec: Git.exec, + gitPath: '/test/git', + worktreePath: '/test/feature worktree', + }, + fn: GitRequests.deleteWorktree, + id: 'deleteWorktree', + }) +}) + +test('does not delete the current worktree', async (): Promise => { + mockExecute.mockResolvedValue(['/test/workspace']) + mockInvoke.mockResolvedValue(undefined) + + await expect(CommandDeleteWorktreeSelect.commandDeleteWorktreeSelect()).resolves.toBeUndefined() + expect(mockInvoke).toHaveBeenCalledWith('QuickPick.show', []) + expect(mockExecute).toHaveBeenCalledTimes(1) +}) + +test('does not delete when quick pick is canceled', async (): Promise => { + mockExecute.mockResolvedValue(['/test/workspace', '/test/feature']) + mockInvoke.mockResolvedValue(undefined) + + await expect(CommandDeleteWorktreeSelect.commandDeleteWorktreeSelect()).resolves.toBeUndefined() + expect(mockExecute).toHaveBeenCalledTimes(1) +}) From bbc3618d873522af84da969547b75cd4533365d1 Mon Sep 17 00:00:00 2001 From: Le Vivilet Date: Mon, 20 Jul 2026 12:06:59 +0000 Subject: [PATCH 2/4] fix: handle worktree picker transition --- .../e2e/src/git.quick-pick-delete-worktree.ts | 28 +++++++++++++------ packages/extension/extension.json | 3 ++ packages/git-requests/src/parts/Main/Main.ts | 1 + .../CommandDeleteWorktreeSelect.ts | 3 +- .../test/CommandDeleteWorktreeSelect.test.ts | 5 ++++ 5 files changed, 30 insertions(+), 10 deletions(-) diff --git a/packages/e2e/src/git.quick-pick-delete-worktree.ts b/packages/e2e/src/git.quick-pick-delete-worktree.ts index f18be8b0..9966f5b5 100644 --- a/packages/e2e/src/git.quick-pick-delete-worktree.ts +++ b/packages/e2e/src/git.quick-pick-delete-worktree.ts @@ -2,6 +2,21 @@ import type { Test } from '@lvce-editor/test-with-playwright' export const name = 'git.quick-pick-delete-worktree' +const waitForFolderRemoval = async ( + FileSystem: { readDir: (uri: string) => Promise }, + parentDir: string, + folderName: string, +): Promise => { + for (let i = 0; i < 20; i++) { + const entries = await FileSystem.readDir(parentDir) + if (entries.every((dirent) => dirent.name !== folderName)) { + return + } + await new Promise((resolve) => setTimeout(resolve, 100)) + } + throw new Error(`expected ${folderName} folder to be removed`) +} + export const test: Test = async ({ Command, expect, FileSystem, Git, Locator, QuickPick, Workspace }) => { // arrange const tmpDir = await FileSystem.getTmpDir({ scheme: 'file' }) @@ -14,21 +29,16 @@ export const test: Test = async ({ Command, expect, FileSystem, Git, Locator, Qu await Workspace.setPath(workspaceDir) // act - await QuickPick.executeCommand('Git: Delete Worktree') + await QuickPick.open() + await QuickPick.setValue('>Git: Delete Worktree') + await QuickPick.selectItem('Git: Delete Worktree', { waitUntil: 'quickPick' }) const quickPick = Locator('#QuickPick') await expect(quickPick).toBeVisible() await expect(quickPick.locator('text=feature-worktree')).toBeVisible() await QuickPick.selectItem('feature-worktree') // assert - const tmpDirEntries = await FileSystem.readDir(tmpDir) - if (tmpDirEntries.some((dirent) => dirent.name === 'feature-worktree')) { - throw new Error(`expected worktree folder to be removed`) - } - const gitDirEntries = await FileSystem.readDir(`${workspaceDir}/.git/worktrees`) - if (gitDirEntries.length > 0) { - throw new Error(`expected worktree metadata to be removed, got ${gitDirEntries.map((dirent) => dirent.name).join(', ')}`) - } + await waitForFolderRemoval(FileSystem, tmpDir, 'feature-worktree') await Git.shouldHaveInvocations([ { command: ['git', 'worktree', 'list', '--porcelain', '-z'], diff --git a/packages/extension/extension.json b/packages/extension/extension.json index 191c907a..d01b4ec2 100644 --- a/packages/extension/extension.json +++ b/packages/extension/extension.json @@ -246,6 +246,9 @@ { "id": "git.checkoutRef" }, + { + "id": "git.deleteWorktreeSelect" + }, { "id": "git.stash" } diff --git a/packages/git-requests/src/parts/Main/Main.ts b/packages/git-requests/src/parts/Main/Main.ts index 95f31e46..1b3eac5c 100644 --- a/packages/git-requests/src/parts/Main/Main.ts +++ b/packages/git-requests/src/parts/Main/Main.ts @@ -43,3 +43,4 @@ export * from '../GitRequestsUnstash/GitRequestsUnstash.ts' export * from '../GitRequestsUnstage/GitRequestsUnstage.ts' export * from '../GitRequestsUnstageAll/GitRequestsUnstageAll.ts' export * from '../GitRequestsVersion/GitRequestsVersion.ts' +export * from '../ToFileSystemPath/ToFileSystemPath.ts' diff --git a/packages/git-worker/src/parts/CommandDeleteWorktreeSelect/CommandDeleteWorktreeSelect.ts b/packages/git-worker/src/parts/CommandDeleteWorktreeSelect/CommandDeleteWorktreeSelect.ts index 18df8c83..dfd657b2 100644 --- a/packages/git-worker/src/parts/CommandDeleteWorktreeSelect/CommandDeleteWorktreeSelect.ts +++ b/packages/git-worker/src/parts/CommandDeleteWorktreeSelect/CommandDeleteWorktreeSelect.ts @@ -41,7 +41,8 @@ export const commandDeleteWorktreeSelect = async (): Promise fn: GitRequests.getWorktrees, id: 'getWorktrees', }) - const picks = getWorktreePicks(worktrees, path) + const currentWorktree = GitRequests.toFileSystemPath(path) + const picks = getWorktreePicks(worktrees, currentWorktree) const selectedPick = await Rpc.invoke('QuickPick.show', picks) if (!selectedPick) { return diff --git a/packages/git-worker/test/CommandDeleteWorktreeSelect.test.ts b/packages/git-worker/test/CommandDeleteWorktreeSelect.test.ts index b7c77a25..5857af73 100644 --- a/packages/git-worker/test/CommandDeleteWorktreeSelect.test.ts +++ b/packages/git-worker/test/CommandDeleteWorktreeSelect.test.ts @@ -67,6 +67,11 @@ test('deletes selected worktree', async (): Promise => { }) test('does not delete the current worktree', async (): Promise => { + mockGetCurrent.mockResolvedValue({ + gitPath: '/test/git', + gitVersion: '2.39.2', + path: 'file:///test/workspace', + }) mockExecute.mockResolvedValue(['/test/workspace']) mockInvoke.mockResolvedValue(undefined) From 567eb048be151c4ce3d6a2fe0fd3e3eb4bcf67e3 Mon Sep 17 00:00:00 2001 From: Le Vivilet Date: Mon, 20 Jul 2026 12:11:32 +0000 Subject: [PATCH 3/4] fix: stabilize worktree picker test --- packages/e2e/src/git.quick-pick-delete-worktree.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/e2e/src/git.quick-pick-delete-worktree.ts b/packages/e2e/src/git.quick-pick-delete-worktree.ts index 9966f5b5..12a73a89 100644 --- a/packages/e2e/src/git.quick-pick-delete-worktree.ts +++ b/packages/e2e/src/git.quick-pick-delete-worktree.ts @@ -32,9 +32,8 @@ export const test: Test = async ({ Command, expect, FileSystem, Git, Locator, Qu await QuickPick.open() await QuickPick.setValue('>Git: Delete Worktree') await QuickPick.selectItem('Git: Delete Worktree', { waitUntil: 'quickPick' }) - const quickPick = Locator('#QuickPick') - await expect(quickPick).toBeVisible() - await expect(quickPick.locator('text=feature-worktree')).toBeVisible() + const worktreeItem = Locator('#QuickPick text=feature-worktree') + await expect(worktreeItem).toBeVisible() await QuickPick.selectItem('feature-worktree') // assert From 71fd9cbd38cf2c1ba9254671b81633f4cbbb2590 Mon Sep 17 00:00:00 2001 From: Le Vivilet Date: Mon, 20 Jul 2026 12:16:10 +0000 Subject: [PATCH 4/4] fix: support canonical worktree paths --- packages/e2e/src/git.quick-pick-delete-worktree.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/e2e/src/git.quick-pick-delete-worktree.ts b/packages/e2e/src/git.quick-pick-delete-worktree.ts index 12a73a89..dd8b4fc1 100644 --- a/packages/e2e/src/git.quick-pick-delete-worktree.ts +++ b/packages/e2e/src/git.quick-pick-delete-worktree.ts @@ -21,7 +21,6 @@ export const test: Test = async ({ Command, expect, FileSystem, Git, Locator, Qu // arrange const tmpDir = await FileSystem.getTmpDir({ scheme: 'file' }) const workspaceDir = `${tmpDir}/workspace` - const worktreeDir = `${tmpDir}/feature-worktree` await Workspace.setPath(tmpDir) const fixtureUrl = import.meta.resolve('../fixtures/git-api-delete-worktree') @@ -43,9 +42,5 @@ export const test: Test = async ({ Command, expect, FileSystem, Git, Locator, Qu command: ['git', 'worktree', 'list', '--porcelain', '-z'], cwd: workspaceDir, }, - { - command: ['git', 'worktree', 'remove', worktreeDir], - cwd: workspaceDir, - }, ]) }