diff --git a/src/cli/selectors.ts b/src/cli/selectors.ts index 86ed2157ea2..0f90dc94508 100644 --- a/src/cli/selectors.ts +++ b/src/cli/selectors.ts @@ -4,7 +4,11 @@ import type { RuntimeWorktreeListResult, RuntimeWorktreeRecord } from '../shared/runtime-types' -import { isPathInsideOrEqual, isWslUncPathForCallerLinuxPath } from '../shared/cross-platform-path' +import { + isPathInsideOrEqual, + isWslUncPathForCallerLinuxPath, + isWslUncPathForLinuxMountedPath +} from '../shared/cross-platform-path' import { parseWslUncPath } from '../shared/wsl-paths' import type { RuntimeClient } from './runtime-client' import { RuntimeClientError } from './runtime/types' @@ -49,8 +53,9 @@ export async function resolveCallerDistroPathSelector( // the WSL launcher always sets and which arrives here as the invocation cwd, proves it. const callerDistro = parseWslUncPath(cwd)?.distro const linuxPath = selector.startsWith('path:') ? selector.slice(5) : '' + const isLinuxMountedPath = /^\/mnt\/[A-Za-z](?:\/|$)/.test(linuxPath) if ( - !callerDistro || + (!callerDistro && !isLinuxMountedPath) || client.isRemote || !linuxPath.startsWith('/') || linuxPath.startsWith('//') || @@ -63,7 +68,9 @@ export async function resolveCallerDistroPathSelector( limit: 10_000 }) const match = worktrees.result.worktrees.find((worktree) => - isWslUncPathForCallerLinuxPath(worktree.path, linuxPath, callerDistro) + isLinuxMountedPath + ? isWslUncPathForLinuxMountedPath(worktree.path, linuxPath) + : isWslUncPathForCallerLinuxPath(worktree.path, linuxPath, callerDistro!) ) // Why the stored spelling rather than a synthesized UNC path: an unmatched selector must // reach the runtime verbatim and fail as the caller typed it, never as a guessed distro. diff --git a/src/cli/worktree-selector-wsl-posix-path.test.ts b/src/cli/worktree-selector-wsl-posix-path.test.ts index 10d71c8e9ed..78ef1779c82 100644 --- a/src/cli/worktree-selector-wsl-posix-path.test.ts +++ b/src/cli/worktree-selector-wsl-posix-path.test.ts @@ -11,6 +11,7 @@ import { normalizeWorktreeSelectorForCaller } from './selectors' const UBUNTU = 'Ubuntu-24.04' const DEBIAN = 'Debian' const LINUX_PATH = '/home/neil/qa-repo' +const MOUNTED_PATH = '/mnt/c/Users/neil/qa-repo' function uncPath(distro: string, linuxPath: string): string { return `\\\\wsl.localhost\\${distro}${linuxPath.replace(/\//g, '\\')}` @@ -37,6 +38,15 @@ afterEach(() => { }) describe('normalizeWorktreeSelectorForCaller in a WSL shell (#16628)', () => { + it('resolves a shared /mnt drive without requiring the caller distro', async () => { + const storedPath = uncPath(UBUNTU, '/mnt/c/Users/neil/qa-repo') + const { client } = makeClient([storedPath]) + + await expect( + normalizeWorktreeSelectorForCaller(`path:${MOUNTED_PATH}`, '/mnt/c/Users/neil', client) + ).resolves.toBe(`path:${storedPath}`) + }) + it.each([ ['a backslash UNC registration', uncPath(UBUNTU, LINUX_PATH)], ['a forward-slash UNC registration', `//wsl.localhost/${UBUNTU}${LINUX_PATH}`], diff --git a/src/shared/cross-platform-path.test.ts b/src/shared/cross-platform-path.test.ts index f23892d98e3..6bdda5d064d 100644 --- a/src/shared/cross-platform-path.test.ts +++ b/src/shared/cross-platform-path.test.ts @@ -5,6 +5,7 @@ import { isPathInsideOrEqual, isRuntimePathAbsolute, isWslUncPathForCallerLinuxPath, + isWslUncPathForLinuxMountedPath, normalizeRuntimePathForComparison, relativePathInsideRoot, resolveRuntimePath @@ -99,6 +100,32 @@ describe('isWslUncPathForCallerLinuxPath', () => { }) }) +describe('isWslUncPathForLinuxMountedPath', () => { + it('matches a shared /mnt drive regardless of distro', () => { + expect( + isWslUncPathForLinuxMountedPath( + '\\\\wsl.localhost\\Ubuntu\\mnt\\c\\Users\\Neil\\repo', + '/mnt/c/users/neil/repo' + ) + ).toBe(true) + expect( + isWslUncPathForLinuxMountedPath( + '\\\\wsl.localhost\\Debian\\mnt\\c\\Users\\Neil\\repo', + '/mnt/c/users/neil/repo' + ) + ).toBe(true) + }) + + it('keeps non-mounted Linux paths out of the distro-independent match', () => { + expect( + isWslUncPathForLinuxMountedPath( + '\\\\wsl.localhost\\Ubuntu\\home\\Neil\\repo', + '/home/neil/repo' + ) + ).toBe(false) + }) +}) + describe('cross-platform path containment', () => { it('keeps POSIX sibling prefixes outside the root', () => { expect(isPathInsideOrEqual('/repo/app', '/repo/app')).toBe(true) diff --git a/src/shared/cross-platform-path.ts b/src/shared/cross-platform-path.ts index 679ee9e57ba..61308a70489 100644 --- a/src/shared/cross-platform-path.ts +++ b/src/shared/cross-platform-path.ts @@ -81,6 +81,26 @@ export function isWslUncPathForCallerLinuxPath( ) } +/** + * Whether a WSL UNC path fronts the same Windows-mounted `/mnt/` path. + * + * `/mnt/` is backed by the host drive and is shared across distros, so + * matching it does not require the caller's distro proof used for Linux paths. + */ +export function isWslUncPathForLinuxMountedPath(uncPath: string, linuxPath: string): boolean { + const parsed = parseWslUncPath(uncPath) + if (!parsed || !/^\/mnt\/[A-Za-z](?:\/|$)/.test(parsed.linuxPath)) { + return false + } + if (!/^\/mnt\/[A-Za-z](?:\/|$)/.test(linuxPath)) { + return false + } + return ( + normalizeRuntimePathForComparison(toWindowsWslPath(parsed.linuxPath, parsed.distro)) === + normalizeRuntimePathForComparison(toWindowsWslPath(linuxPath, parsed.distro)) + ) +} + export function areLocalWindowsWslPathAliases(left: string, right: string): boolean { const leftIdentity = getLocalWindowsWslPathIdentity(left) const rightIdentity = getLocalWindowsWslPathIdentity(right)