mirror of
https://github.com/stablyai/orca.git
synced 2026-09-27 00:02:37 +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.
60 lines
2.1 KiB
TypeScript
60 lines
2.1 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
const { readFileMock, statMock } = vi.hoisted(() => ({
|
|
readFileMock: vi.fn(),
|
|
statMock: vi.fn()
|
|
}))
|
|
|
|
vi.mock('fs/promises', () => ({
|
|
readFile: readFileMock,
|
|
stat: statMock
|
|
}))
|
|
|
|
import { readWorktreeDiffStamp } from './source-control/worktree-diff-stamp'
|
|
|
|
const slashed = (value: unknown): string => String(value).replaceAll('\\', '/')
|
|
const missing = () => Object.assign(new Error('ENOENT'), { code: 'ENOENT' })
|
|
|
|
// Why this file: git in a WSL distro reports the worktree as `/mnt/c/...`, and the gitdir resolve
|
|
// reaches it on its own. If the working-tree read did not travel the same way, the stamp would be
|
|
// built from a real HEAD and a permanently absent file — a settled diff that survives every edit.
|
|
describe('readWorktreeDiffStamp on a drvfs-spelled WSL worktree', () => {
|
|
beforeEach(() => {
|
|
readFileMock.mockReset()
|
|
statMock.mockReset()
|
|
readFileMock.mockImplementation(async (target: string) => {
|
|
const value = slashed(target)
|
|
if (value === 'C:/repo/wt/.git') {
|
|
return 'gitdir: /mnt/c/repo/.git/worktrees/wt\n'
|
|
}
|
|
// Detached HEAD, so the stamp needs no ref-store walk.
|
|
if (value === 'C:/repo/.git/worktrees/wt/HEAD') {
|
|
return `${'a'.repeat(40)}\n`
|
|
}
|
|
throw missing()
|
|
})
|
|
statMock.mockImplementation(async (target: string) =>
|
|
slashed(target) === 'C:/repo/wt/src/a.ts'
|
|
? { mtimeMs: 1_000, size: 12, ino: 7 }
|
|
: Promise.reject(missing())
|
|
)
|
|
})
|
|
|
|
it('stamps the working-tree file through the same host spelling as the gitdir', async () => {
|
|
const platformSpy = vi.spyOn(process, 'platform', 'get').mockReturnValue('win32')
|
|
|
|
try {
|
|
const stamp = await readWorktreeDiffStamp('/mnt/c/repo/wt', 'src/a.ts', true)
|
|
|
|
expect(stamp).not.toBeNull()
|
|
expect(statMock.mock.calls.map(([target]) => slashed(target))).toContain(
|
|
'C:/repo/wt/src/a.ts'
|
|
)
|
|
// The working-tree component is what moves when the user edits, so it has to be present.
|
|
expect(stamp?.newestMtimeMs).toBe(1_000)
|
|
} finally {
|
|
platformSpy.mockRestore()
|
|
}
|
|
})
|
|
})
|