mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
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:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user