fix(git): resolve WSL drvfs Git metadata pointers on a Windows host (#17790)

When Orca's runtime is a WSL distro but the repo sits on a Windows drive, git
inside the distro writes `/mnt/c/...` into a worktree's `.git` gitfile and its
`commondir`, while Orca reads those files back through Win32.
`repo-git-marker-scan` returned the pointer verbatim, Windows read it as
drive-relative `C:\mnt\c\...`, and the worktree was reported `invalid`.

Move that resolver out of `repo-git-marker-scan` into
`src/shared/git-metadata-path.ts` and give it exactly one new case: on win32, a
drvfs pointer resolved against a base path that is not a WSL UNC path now gets
its drive spelling. Every other base/pointer/platform combination is
byte-identical to the deleted helper, verified differentially across a
base x pointer x platform matrix — macOS, Linux and native Windows are unchanged.

`toWindowsWslDrivePath` is factored out of `toWindowsWslPath` so the drvfs
matcher has one home; `toWindowsWslPath` itself is unchanged for all inputs,
including the line terminators JS `.` excludes (fuzzed 2M inputs, 0 divergences).

This changes the marker scan's verdict only. `resolve-git-dir.ts` and the relay's
own copy still `path.resolve` the same `/mnt/c/...` pointer in the Win32
namespace, so a worktree that is now accepted still degrades quietly in conflict
detection, sparse-checkout detection, the diff stamp and worktree listing. Those
parsers are deliberately untouched here; see the PR description.

Co-authored-by: Neil <neil@example.com>
This commit is contained in:
Neil
2026-08-31 20:32:46 -07:00
committed by GitHub
co-authored by Neil
parent 8b2d72114b
commit 7f63db7d7a
5 changed files with 171 additions and 23 deletions
+2 -15
View File
@@ -1,8 +1,7 @@
import { readFileSync, realpathSync, statSync } from 'node:fs'
import { dirname, isAbsolute, join, relative, resolve } from 'node:path'
import { dirname, isAbsolute, join, relative } from 'node:path'
import { normalizeRuntimePathSeparators } from '../../shared/cross-platform-path'
import { parseWslUncPath } from '../../shared/wsl-paths'
import { toWindowsWslPath } from '../wsl'
import { resolveGitMetadataPath } from '../../shared/git-metadata-path'
export type GitMarkerScanResult =
| { status: 'valid'; rootPath: string }
@@ -136,18 +135,6 @@ function parseGitdirFile(basePath: string, content: string): string | null {
return resolveGitMetadataPath(basePath, match[1])
}
function resolveGitMetadataPath(basePath: string, rawPath: string): string | null {
const value = rawPath.trim()
if (!value) {
return null
}
const baseWsl = parseWslUncPath(basePath)
if (baseWsl && value.startsWith('/')) {
return toWindowsWslPath(value, baseWsl.distro)
}
return isAbsolute(value) ? value : resolve(basePath, value)
}
function hasValidGitDirectorySync(gitDir: string): boolean {
return hasValidCommonGitDirectorySync(gitDir) || hasValidLinkedWorktreeGitDirectorySync(gitDir)
}
+75
View File
@@ -0,0 +1,75 @@
import { describe, expect, it } from 'vitest'
import { resolveGitMetadataPath } from './git-metadata-path'
describe('resolveGitMetadataPath', () => {
it('maps a drvfs pointer to its drive spelling on a Windows host with no distro context', () => {
expect(
resolveGitMetadataPath(
String.raw`C:\Users\me\repo`,
'/mnt/c/Users/me/repo/.git/worktrees/feature',
'win32'
)
).toBe(String.raw`C:\Users\me\repo\.git\worktrees\feature`)
})
it('maps a drvfs drive root to its drive spelling', () => {
expect(resolveGitMetadataPath(String.raw`D:\repo`, '/mnt/d', 'win32')).toBe('D:\\')
})
it('keeps a non-drvfs guest pointer verbatim when no distro names it', () => {
// Windows already reads this as drive-relative; guessing a distro would probe the wrong disk.
expect(resolveGitMetadataPath(String.raw`C:\repo`, '/home/me/repo/.git', 'win32')).toBe(
'/home/me/repo/.git'
)
})
it('maps a guest pointer through the distro encoded by a WSL UNC base', () => {
expect(
resolveGitMetadataPath(
String.raw`\\wsl.localhost\Debian\home\me\repo`,
'/home/me/repo/.git',
'win32'
)
).toBe(String.raw`\\wsl.localhost\Debian\home\me\repo\.git`)
})
it('resolves a relative pointer against a WSL UNC base', () => {
expect(
resolveGitMetadataPath(
String.raw`\\wsl.localhost\Debian\home\me\repo\.git\worktrees\feature`,
'../..',
'win32'
)
).toBe(String.raw`\\wsl.localhost\Debian\home\me\repo\.git`)
})
it('resolves relative pointers with the reading host path flavor', () => {
expect(resolveGitMetadataPath('/repo/worktree', '../.git/worktrees/feature', 'linux')).toBe(
'/repo/.git/worktrees/feature'
)
expect(
resolveGitMetadataPath(
String.raw`C:\repo\worktree`,
String.raw`..\.git\worktrees\feature`,
'win32'
)
).toBe(String.raw`C:\repo\.git\worktrees\feature`)
})
it('leaves absolute pointers alone on a POSIX host, drvfs spelling included', () => {
expect(
resolveGitMetadataPath('/repo/worktree', '/var/lib/git/worktrees/feature', 'linux')
).toBe('/var/lib/git/worktrees/feature')
expect(resolveGitMetadataPath('/repo/worktree', '/mnt/c/repo/.git', 'darwin')).toBe(
'/mnt/c/repo/.git'
)
})
it.each(['', ' ', '\t'])('rejects an empty metadata pointer %j', (rawPath) => {
expect(resolveGitMetadataPath('/repo', rawPath, 'linux')).toBeNull()
})
it('trims a padded pointer before resolving it', () => {
expect(resolveGitMetadataPath('/repo/worktree', ' ../.git ', 'linux')).toBe('/repo/.git')
})
})
+46
View File
@@ -0,0 +1,46 @@
import { posix, win32 } from 'node:path'
import { parseWslUncPath, toWindowsWslDrivePath, toWindowsWslPath } from './wsl-paths'
/**
* Resolve a Git metadata pointer (a `.git` gitfile payload or a `commondir`) in the path namespace
* of the host that reads it.
*
* Why: git running inside WSL writes these pointers in the guest namespace, but Node reads them
* back through Win32, where a drvfs pointer like `/mnt/c/repo/.git` silently means
* `C:\mnt\c\repo\.git`. Returns null only for an empty pointer.
*/
export function resolveGitMetadataPath(
basePath: string,
rawPath: string,
platform: NodeJS.Platform = process.platform
): string | null {
const value = rawPath.trim()
if (!value) {
return null
}
if (value.startsWith('/')) {
const translated = translateGuestPointer(value, basePath, platform)
if (translated) {
return translated
}
}
const host = platform === 'win32' ? win32 : posix
return host.isAbsolute(value) ? value : host.resolve(basePath, value)
}
/**
* The Win32 spelling of a POSIX-rooted pointer, or null to leave it alone. A WSL UNC base names the
* distro that wrote the pointer; failing that, only a drvfs mount has a spelling we can derive, and
* only a Windows host needs one.
*/
function translateGuestPointer(
value: string,
basePath: string,
platform: NodeJS.Platform
): string | null {
const distro = parseWslUncPath(basePath)?.distro
if (distro) {
return toWindowsWslPath(value, distro)
}
return platform === 'win32' ? toWindowsWslDrivePath(value) : null
}
+26
View File
@@ -6,6 +6,7 @@ import {
isWslUncPath,
parseWslUncPath,
resolveWslRepoWorktreeBasePath,
toWindowsWslDrivePath,
toWindowsWslPath,
toWindowsWslUncPath
} from './wsl-paths'
@@ -37,6 +38,31 @@ describe('wsl path helpers', () => {
expect(toWindowsWslPath(linuxPath, 'Ubuntu')).toBe(expected)
})
it.each([
['/mnt/c/Users/jin', 'C:\\Users\\jin'],
['/mnt/d', 'D:\\'],
['/mnt/d/', 'D:\\'],
['/MNT/c/Users/jin', null],
['/mnt/C/Repo', null],
['/home/jin', null],
// A drvfs prefix on a line that still carries a terminator is not a drive path.
['/mnt/c/Users/jin\r', null],
['/mnt/c/Users/jin\n', null],
['/mnt/c/Users/jin\u2028', null],
['/mnt/c/Users/jin\u2029', null]
] as const)('converts the DrvFs path %j without a distro lookup', (linuxPath, expected) => {
expect(toWindowsWslDrivePath(linuxPath)).toBe(expected)
})
it.each(['\r', '\n', '\u2028', '\u2029'])(
'leaves a DrvFs line ending in %j on the distro UNC view',
(terminator) => {
expect(toWindowsWslPath(`/mnt/c/Users/jin${terminator}`, 'Ubuntu')).toBe(
`\\\\wsl.localhost\\Ubuntu\\mnt\\c\\Users\\jin${terminator}`
)
}
)
it('keeps mounted-drive paths on the distro UNC view when requested', () => {
expect(toWindowsWslUncPath('/mnt/c/Users/jin', 'Ubuntu')).toBe(
'\\\\wsl.localhost\\Ubuntu\\mnt\\c\\Users\\jin'
+22 -8
View File
@@ -49,13 +49,7 @@ export function toLinuxPath(windowsPath: string): string {
/** Convert an absolute Linux path in a known WSL distro to its Windows form. */
export function toWindowsWslPath(linuxPath: string, distro: string): string {
const mntMatch = linuxPath.match(/^\/mnt\/([a-z])(\/.*)?$/)
if (mntMatch) {
const rest = (mntMatch[2] || '').replace(/\//g, '\\')
return `${mntMatch[1].toUpperCase()}:${rest || '\\'}`
}
return toWindowsWslUncPath(linuxPath, distro)
return toWindowsWslDrivePath(linuxPath) ?? toWindowsWslUncPath(linuxPath, distro)
}
/** Keep a Linux path addressable through its distro, including drvfs mounts. */
@@ -132,7 +126,12 @@ export function toWslExecutionSpace(path: string): string {
return parseWslUncPath(path)?.linuxPath ?? path
}
/** The drvfs automount is literally lowercase `/mnt/<letter>`; `/MNT` is an ordinary Linux dir. */
/**
* The drvfs automount is literally lowercase `/mnt/<letter>`; `/MNT` is an ordinary Linux dir.
* Deliberately looser than `toWindowsWslDrivePath`'s end-anchored matcher below: this one only
* classifies a prefix, so do not unify them — the anchoring there is what keeps a path carrying a
* stray line terminator off the drive spelling.
*/
const DRVFS_LINUX_PATH = /^\/mnt\/[a-z](?:\/|$)/
/** True for a Linux path that is really a Windows drive reached through drvfs. */
@@ -140,6 +139,21 @@ export function isDrvfsLinuxPath(linuxPath: string): boolean {
return DRVFS_LINUX_PATH.test(linuxPath)
}
/**
* The Windows drive spelling of a drvfs path, or null when the path is not one. Needs no distro:
* the bytes sit on the drive whichever distro mounted them.
*/
export function toWindowsWslDrivePath(linuxPath: string): string | null {
// `.` excludes every line terminator, so a drvfs prefix on a stray output line (an rg hit that
// still carries its CR) stays off the drive spelling.
const match = linuxPath.match(/^\/mnt\/([a-z])(\/.*)?$/)
if (!match) {
return null
}
const tail = (match[2] ?? '').replace(/\//g, '\\')
return `${match[1].toUpperCase()}:${tail || '\\'}`
}
/**
* The distro whose git would reach `projectPath` across the 9p/drvfs boundary, or null when it
* would not.