mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 16:02:32 +00:00
Git running inside a WSL distro writes `.git` gitdir pointers, and answers
`status --porcelain`, in the guest namespace. Node reads both back in the
Windows main process, where `/mnt/c/repo/.git` resolves to `C:\mnt\c\repo\.git`
and `/home/me/wt` names nothing at all. Four fs probes were built on those
fabricated paths and always came back "absent":
- `detectConflictOperation`'s four marker probes, so merge/rebase/cherry-pick
badges silently went missing.
- `parseUnmergedEntry`'s compat existence check, so every `deleted_by_us` /
`added_by_them` conflict rendered as 'deleted' regardless of the working tree.
- `findExistingWorktreeSymlinkPaths`' `lstat` from status, so Orca's own shared
symlinks (node_modules and friends) showed as user changes.
- the same `lstat` from the hosted-review dirty preflight, which fails closed:
an unreadable shared symlink read as uncommitted work and blocked PR/MR
creation outright.
`resolveGitDir` computes the host spelling of the worktree once and uses it for
both the gitfile read and the pointer resolve, so a guest-spelled worktree path
is reached at all, and a relative pointer (`worktree.useRelativePaths`, git
2.48+) resolves against a spelling Win32 understands. The pointer itself now
goes through the already-landed `resolveGitMetadataPath`, and the function gains
an optional `{ wslDistro }` for a caller whose base path does not encode a
distro. `detectConflictOperation` forwards it, and the three callers that reach
it -- status-read, the runtime RPC, the `git:conflictOperation` IPC -- pass the
git options they already hold. The return type stays `Promise<string>`.
`resolveWorktreeHostPath` is the same rule applied to a worktree path, used by
status-read for the two working-tree probes and by the review preflight. Both it
and `resolveGitMetadataPath` now treat only a single-leading-slash path as guest
namespace: `//wsl.localhost/...` is already a host UNC spelling, and translating
it prepended a second share prefix.
`readWorktreeDiffStamp` needed the same one-namespace guarantee, since moving
translation inside `resolveGitDir` would otherwise make its HEAD and index real
while the working-tree stat stayed fabricated, letting a settled diff survive
every edit. #17896 landed that change first, so it is no longer in this diff;
its version is a superset and all four components already resolve from one
`hostWorktreePath`. What remains here is the `resolveGitDir` gitfile-pointer
fix that #17896 explicitly deferred, which `worktree-diff-stamp-host-paths.test.ts`
pins.
`getConflictCompatibilityStatus` moves from `existsSync` to async `access`, for
the same reason `detectConflictOperation` did: once these paths are real they
are `\\wsl.localhost\...` shares, and a sync probe per asymmetric conflict
blocks the Electron main thread for a 9p round trip on every status poll.
Per-platform delta:
- native Windows, no WSL: no behavioral change. Nothing here starts with a
single `/`, so no path is translated. An absolute pointer is now returned
verbatim rather than separator-normalized; every consumer re-joins or
normalizes it before use.
- macOS/Linux: no change. Guest-pointer translation is gated to win32, and a
caller-named distro is ignored off Windows.
- Windows + WSL: drvfs pointers and drvfs-spelled worktrees now resolve to their
drive spelling instead of `C:\mnt\...`; a non-drvfs guest path resolves
through the named distro's UNC share, or stays verbatim (ENOENT -> existing
fail-safe) when none is named.
- SSH/relay: none. Those paths return before any of this via the provider
branch; `src/relay/git-handler-status-ops.ts` keeps its own resolveGitDir.
- folder workspaces, GitLab: none. Neither is on these code paths.
223 lines
8.6 KiB
TypeScript
223 lines
8.6 KiB
TypeScript
import { posix, win32 } from 'node:path'
|
|
import { describe, expect, it } from 'vitest'
|
|
import { resolveGitMetadataPath, resolveWorktreeHostPath } from './git-metadata-path'
|
|
|
|
const hostIsWindows = process.platform === 'win32'
|
|
|
|
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',
|
|
{ platform: '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', { platform: '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', { platform: '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',
|
|
{ platform: 'win32' }
|
|
)
|
|
).toBe(String.raw`\\wsl.localhost\Debian\home\me\repo\.git`)
|
|
})
|
|
|
|
it('maps a guest pointer through a caller-named distro when the base encodes none', () => {
|
|
expect(
|
|
resolveGitMetadataPath(String.raw`C:\Users\me\repo`, '/home/me/repo/.git', {
|
|
platform: 'win32',
|
|
wslDistro: 'Ubuntu'
|
|
})
|
|
).toBe(String.raw`\\wsl.localhost\Ubuntu\home\me\repo\.git`)
|
|
})
|
|
|
|
// Git for Windows spells a UNC gitdir with forward slashes; that is already a host path, and
|
|
// translating it would produce `\\wsl.localhost\Ubuntu\\wsl.localhost\Ubuntu\...`.
|
|
it('keeps a forward-slash UNC pointer verbatim rather than re-prefixing the share', () => {
|
|
expect(
|
|
resolveGitMetadataPath(
|
|
String.raw`\\wsl.localhost\Ubuntu\home\me\repo\feature`,
|
|
'//wsl.localhost/Ubuntu/home/me/repo/.git/worktrees/feature',
|
|
{ platform: 'win32', wslDistro: 'Ubuntu' }
|
|
)
|
|
).toBe('//wsl.localhost/Ubuntu/home/me/repo/.git/worktrees/feature')
|
|
})
|
|
|
|
it('lets the distro encoded by a WSL UNC base outrank the caller-named one', () => {
|
|
expect(
|
|
resolveGitMetadataPath(
|
|
String.raw`\\wsl.localhost\Debian\home\me\repo`,
|
|
'/home/me/repo/.git',
|
|
{ platform: 'win32', wslDistro: 'Ubuntu' }
|
|
)
|
|
).toBe(String.raw`\\wsl.localhost\Debian\home\me\repo\.git`)
|
|
})
|
|
|
|
it('ignores a caller-named distro on a POSIX host', () => {
|
|
// A POSIX reader is already in the pointer's namespace; a UNC path there would be a filename.
|
|
expect(
|
|
resolveGitMetadataPath('/repo', '/home/me/repo/.git', {
|
|
platform: 'linux',
|
|
wslDistro: 'Ubuntu'
|
|
})
|
|
).toBe('/home/me/repo/.git')
|
|
expect(
|
|
resolveGitMetadataPath('/repo', '/mnt/c/repo/.git', {
|
|
platform: 'darwin',
|
|
wslDistro: 'Ubuntu'
|
|
})
|
|
).toBe('/mnt/c/repo/.git')
|
|
})
|
|
|
|
it('keeps a drvfs pointer on its drive spelling even when a distro is named', () => {
|
|
expect(
|
|
resolveGitMetadataPath(String.raw`C:\repo`, '/mnt/c/repo/.git', {
|
|
platform: 'win32',
|
|
wslDistro: 'Ubuntu'
|
|
})
|
|
).toBe(String.raw`C:\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`,
|
|
'../..',
|
|
{ platform: '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', { platform: 'linux' })
|
|
).toBe('/repo/.git/worktrees/feature')
|
|
expect(
|
|
resolveGitMetadataPath(String.raw`C:\repo\worktree`, String.raw`..\.git\worktrees\feature`, {
|
|
platform: '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', {
|
|
platform: 'linux'
|
|
})
|
|
).toBe('/var/lib/git/worktrees/feature')
|
|
expect(
|
|
resolveGitMetadataPath('/repo/worktree', '/mnt/c/repo/.git', { platform: 'darwin' })
|
|
).toBe('/mnt/c/repo/.git')
|
|
})
|
|
|
|
it.each(['', ' ', '\t'])('rejects an empty metadata pointer %j', (rawPath) => {
|
|
expect(resolveGitMetadataPath('/repo', rawPath, { platform: 'linux' })).toBeNull()
|
|
})
|
|
|
|
it('trims a padded pointer before resolving it', () => {
|
|
expect(resolveGitMetadataPath('/repo/worktree', ' ../.git ', { platform: 'linux' })).toBe(
|
|
'/repo/.git'
|
|
)
|
|
})
|
|
|
|
// Both production callers omit options entirely, so the defaults must stay the old behavior:
|
|
// the reading host's own path flavor, and no distro.
|
|
it.each([
|
|
[
|
|
String.raw`\\wsl.localhost\Debian\home\me\repo`,
|
|
'/home/me/repo/.git',
|
|
String.raw`\\wsl.localhost\Debian\home\me\repo\.git`
|
|
],
|
|
['/repo', '/mnt/c/repo/.git', hostIsWindows ? String.raw`C:\repo\.git` : '/mnt/c/repo/.git'],
|
|
['/repo', '/var/lib/git/worktrees/feature', '/var/lib/git/worktrees/feature'],
|
|
['/repo', ' ', null]
|
|
])('defaults to the current host with no distro for %j -> %j', (basePath, rawPath, expected) => {
|
|
expect(resolveGitMetadataPath(basePath, rawPath)).toBe(expected)
|
|
})
|
|
|
|
it('resolves a relative pointer with no options in the reading host flavor', () => {
|
|
// Expectation comes from node's own path module, not from a second call under test.
|
|
expect(resolveGitMetadataPath('/repo/worktree', '../.git/worktrees/feature')).toBe(
|
|
(hostIsWindows ? win32 : posix).resolve('/repo/worktree', '../.git/worktrees/feature')
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('resolveWorktreeHostPath', () => {
|
|
// A gitfile payload ends in a newline; a directory name can end in a space, and trimming one
|
|
// away points every read at a directory that does not exist.
|
|
it.each(['/repo/my feature ', ' /repo/my feature'])(
|
|
'keeps whitespace that belongs to the directory name (%j)',
|
|
(worktreePath) => {
|
|
expect(resolveWorktreeHostPath(worktreePath, { platform: 'linux' })).toBe(worktreePath)
|
|
expect(resolveWorktreeHostPath(worktreePath, { platform: 'win32' })).toBe(worktreePath)
|
|
}
|
|
)
|
|
|
|
it('still translates a guest directory for a Windows reader', () => {
|
|
expect(
|
|
resolveWorktreeHostPath('/home/me/repo', { platform: 'win32', wslDistro: 'Ubuntu' })
|
|
).toBe(String.raw`\\wsl.localhost\Ubuntu\home\me\repo`)
|
|
expect(resolveWorktreeHostPath('/mnt/c/repo', { platform: 'win32' })).toBe(String.raw`C:\repo`)
|
|
})
|
|
|
|
it.each(['', ' '])('has no spelling for an empty worktree path %j', (worktreePath) => {
|
|
expect(resolveWorktreeHostPath(worktreePath, { platform: 'win32' })).toBeNull()
|
|
})
|
|
|
|
it('maps a drvfs worktree to its drive spelling on a Windows host', () => {
|
|
expect(resolveWorktreeHostPath('/mnt/c/Users/me/repo/feature', { platform: 'win32' })).toBe(
|
|
String.raw`C:\Users\me\repo\feature`
|
|
)
|
|
})
|
|
|
|
it('maps a guest worktree through a caller-named distro', () => {
|
|
expect(
|
|
resolveWorktreeHostPath('/home/me/repo/feature', { platform: 'win32', wslDistro: 'Ubuntu' })
|
|
).toBe(String.raw`\\wsl.localhost\Ubuntu\home\me\repo\feature`)
|
|
})
|
|
|
|
it('keeps a guest worktree verbatim on Windows when no distro names it', () => {
|
|
expect(resolveWorktreeHostPath('/home/me/repo/feature', { platform: 'win32' })).toBe(
|
|
'/home/me/repo/feature'
|
|
)
|
|
})
|
|
|
|
it('ignores a caller-named distro on a POSIX host', () => {
|
|
expect(
|
|
resolveWorktreeHostPath('/home/me/repo/feature', { platform: 'linux', wslDistro: 'Ubuntu' })
|
|
).toBe('/home/me/repo/feature')
|
|
expect(
|
|
resolveWorktreeHostPath('/mnt/c/repo/feature', { platform: 'darwin', wslDistro: 'Ubuntu' })
|
|
).toBe('/mnt/c/repo/feature')
|
|
})
|
|
|
|
// `//x` is a host UNC spelling, not a guest path: translating it would prepend a second share.
|
|
// A relative path is deliberately absent: the wrapper resolves one against the cwd.
|
|
it.each([
|
|
String.raw`C:\Users\me\repo\feature`,
|
|
String.raw`\\wsl.localhost\Ubuntu\home\me\repo\feature`,
|
|
'//wsl.localhost/Ubuntu/home/me/repo/feature'
|
|
])('leaves a non-guest worktree path untouched: %j', (worktreePath) => {
|
|
expect(resolveWorktreeHostPath(worktreePath, { platform: 'win32', wslDistro: 'Ubuntu' })).toBe(
|
|
worktreePath
|
|
)
|
|
})
|
|
})
|