diff --git a/src/main/worktree-removal-home-guard.test.ts b/src/main/worktree-removal-home-guard.test.ts index d06940acdd3..e9e434b2ed2 100644 --- a/src/main/worktree-removal-home-guard.test.ts +++ b/src/main/worktree-removal-home-guard.test.ts @@ -87,6 +87,25 @@ describe('path-shape home detection', () => { ])('WSL UNC %s -> %s', (worktreePath, expected) => { expect(isHome(worktreePath, CLIENT_REMOVAL_HOME)).toBe(expected) }) + + it.each([ + // `/mnt/` is the distro's drvfs view of a Windows volume, so this is + // `C:\Users\bob` wearing a Linux spelling, not a directory in the distro. + ['\\\\wsl.localhost\\Ubuntu\\mnt\\c\\Users\\bob', true], + ['\\\\wsl$\\Ubuntu\\mnt\\c\\Users', true], + // The volume itself, and the automount that holds every volume, contain the profile. + ['\\\\wsl.localhost\\Ubuntu\\mnt\\c', true], + ['\\\\wsl.localhost\\Ubuntu\\mnt\\c\\', true], + ['\\\\wsl.localhost\\Ubuntu\\mnt', true], + ['\\\\wsl.localhost\\Ubuntu\\mnt\\c\\Users\\bob\\ws\\wt', false], + ['\\\\wsl.localhost\\Ubuntu\\mnt\\c\\src\\repo', false], + // `/MNT` is an ordinary case-sensitive Linux directory, never the automount. + ['\\\\wsl.localhost\\Ubuntu\\MNT\\c\\Users\\bob', false] + ])('WSL drvfs %s -> %s', (worktreePath, expected) => { + expect(withProcessPlatform('darwin', () => isHome(worktreePath, CLIENT_REMOVAL_HOME))).toBe( + expected + ) + }) }) describe('whose home the guard consults', () => { diff --git a/src/main/worktree-removal-home-guard.ts b/src/main/worktree-removal-home-guard.ts index 004be97d6dc..7dde11b53d8 100644 --- a/src/main/worktree-removal-home-guard.ts +++ b/src/main/worktree-removal-home-guard.ts @@ -17,7 +17,7 @@ import { homedir } from 'node:os' import { posix, win32 } from 'node:path' import { isWindowsAbsolutePathLike } from '../shared/cross-platform-path' -import { parseWslUncPath } from '../shared/wsl-paths' +import { parseWslUncPath, toWindowsWslDrivePath } from '../shared/wsl-paths' export type PathOps = typeof posix @@ -172,13 +172,33 @@ function isLikelyWindowsUserProfileDirectory( /** * WSL UNC aliases front a Linux filesystem, so POSIX home shapes — not * `\Users` — are what protect `\\wsl.localhost\Ubuntu\home\alice`. + * + * Except under `/mnt/`: that tail is the distro's drvfs view of a Windows + * volume, so `\\wsl.localhost\Ubuntu\mnt\c\Users\bob` is the Windows profile with + * a Linux spelling. It takes the Windows rule on its drive form, which the UNC + * exclusion above would otherwise skip. */ function isLikelyWslDistroHomeDirectory(resolvedWorktreePath: string, pathOps: PathOps): boolean { if (pathOps !== win32) { return false } const wsl = parseWslUncPath(resolvedWorktreePath) - return !!wsl && (wsl.linuxPath === '/' || isPosixHomeRoot(trimTrailingSlash(wsl.linuxPath))) + if (!wsl) { + return false + } + const linuxPath = trimTrailingSlash(wsl.linuxPath) + const drivePath = toWindowsWslDrivePath(linuxPath) + if (drivePath) { + const resolvedDrivePath = win32.resolve(drivePath) + // Why: `/mnt/c` is the whole volume. `C:\` is refused as a root before this guard runs; its + // drvfs spelling has to be refused here, since its win32 root is the distro share. + return ( + win32.parse(resolvedDrivePath).root === resolvedDrivePath || + isLikelyWindowsUserProfileDirectory(resolvedDrivePath, win32) + ) + } + // Why `/mnt`: the automount parent holds every drvfs volume, so it contains every profile. + return wsl.linuxPath === '/' || linuxPath === '/mnt' || isPosixHomeRoot(linuxPath) } function isWslUncRemovalPath(resolvedWorktreePath: string): boolean {