fix(git): bypass WSL login shells for status reads

This commit is contained in:
OrcaWin
2026-08-08 07:34:10 -07:00
parent 54a9b5840b
commit b6b59dbc26
8 changed files with 1178 additions and 127 deletions
+368
View File
@@ -0,0 +1,368 @@
import { spawn } from 'node:child_process'
import { performance } from 'node:perf_hooks'
import process from 'node:process'
import { createJiti } from 'jiti'
const DEFAULT_SAMPLES = 20
const DEFAULT_WARMUPS = 3
function parseArgs(argv) {
const options = {
distro: null,
nativeRepo: null,
mountedRepo: null,
samples: DEFAULT_SAMPLES,
warmups: DEFAULT_WARMUPS,
loginDelayMs: 0
}
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index]
const value = argv[++index]
if (!value) {
throw new Error(`${arg} requires a value`)
}
if (arg === '--distro') {
options.distro = value
} else if (arg === '--native-repo') {
options.nativeRepo = value
} else if (arg === '--mounted-repo') {
options.mountedRepo = value
} else if (arg === '--samples') {
options.samples = Number(value)
} else if (arg === '--warmups') {
options.warmups = Number(value)
} else if (arg === '--login-delay-ms') {
options.loginDelayMs = Number(value)
} else {
throw new Error(`Unknown argument: ${arg}`)
}
}
for (const [name, value, minimum] of [
['samples', options.samples, 5],
['warmups', options.warmups, 0],
['login-delay-ms', options.loginDelayMs, 0]
]) {
if (!Number.isInteger(value) || value < minimum || value > 1_000) {
throw new Error(`--${name} must be an integer between ${minimum} and 1000`)
}
}
if (!options.nativeRepo || !options.mountedRepo) {
throw new Error('--native-repo and --mounted-repo are required')
}
return options
}
function run(command, args, options = {}) {
return new Promise((resolve, reject) => {
const child = spawn(command, args, {
env: options.env,
stdio: ['ignore', 'pipe', 'pipe'],
windowsHide: true
})
const stdout = []
const stderr = []
child.stdout.on('data', (chunk) => stdout.push(chunk))
child.stderr.on('data', (chunk) => stderr.push(chunk))
child.on('error', reject)
child.on('close', (status) => {
const result = {
status,
stdout: Buffer.concat(stdout),
stderr: Buffer.concat(stderr)
}
if (status === 0 || options.allowFailure) {
resolve(result)
} else {
reject(new Error(`${command} exited ${status}: ${result.stderr.toString('utf8').trim()}`))
}
})
})
}
function wslArgs(distro, args) {
return ['-d', distro, '--exec', ...args]
}
function wslShellArgs(distro, args) {
return ['-d', distro, '--', ...args]
}
async function resolveDistro(requested) {
if (requested) {
return requested
}
const result = await run('wsl.exe', ['--list', '--quiet'], {
env: { ...process.env, WSL_UTF8: '1' }
})
const distro = result.stdout
.toString('utf8')
.replaceAll('\0', '')
.split(/\r?\n/)
.map((value) => value.trim())
.find(Boolean)
if (!distro) {
throw new Error('No WSL distro is installed')
}
return distro
}
function percentile(samples, value) {
const sorted = [...samples].sort((left, right) => left - right)
return sorted[Math.ceil((value / 100) * sorted.length) - 1]
}
function summarize(samples) {
return {
samples: samples.length,
medianMs: Number(percentile(samples, 50).toFixed(1)),
p95Ms: Number(percentile(samples, 95).toFixed(1)),
minMs: Number(Math.min(...samples).toFixed(1)),
maxMs: Number(Math.max(...samples).toFixed(1))
}
}
function assertRepoPath(path, expectedPrefix) {
if (!path.startsWith(expectedPrefix) || path.includes('\0') || path.includes('\n')) {
throw new Error(`Unexpected benchmark repository path: ${path}`)
}
}
async function main() {
if (process.platform !== 'win32') {
throw new Error('This benchmark requires a Windows host with WSL')
}
const options = parseArgs(process.argv.slice(2))
assertRepoPath(options.nativeRepo, '/')
assertRepoPath(options.mountedRepo, '/mnt/')
const distro = await resolveDistro(options.distro)
const jiti = createJiti(import.meta.url)
const { buildWslLoginShellCommand, escapeWslShCommandForWindows, quotePosixShell } =
await jiti.import('../../src/shared/wsl-login-shell-command.ts')
const loginProbe = buildWslLoginShellCommand(
`printf '\\n__ORCA_PATH__%s\\n__ORCA_GIT__%s\\n' "$PATH" "$(command -v git)"`
)
const probe = await run(
'wsl.exe',
wslShellArgs(distro, ['/bin/sh', '-lc', escapeWslShCommandForWindows(loginProbe)])
)
const probeText = probe.stdout.toString('utf8')
const loginPath = /__ORCA_PATH__(.*)/.exec(probeText)?.[1]?.trim()
const gitPath = /__ORCA_GIT__(.*)/.exec(probeText)?.[1]?.trim()
if (!loginPath || !gitPath?.startsWith('/')) {
throw new Error(`Could not resolve login-shell Git environment: ${probeText.trim()}`)
}
const outputMarker = `__ORCA_GIT_OUTPUT_${process.pid}__\n`
const runGit = async (mode, repo, args) => {
const startedAt = performance.now()
let result
if (mode === 'login') {
const command = [
`cd ${quotePosixShell(repo)} &&`,
`printf ${quotePosixShell(outputMarker)} &&`,
`LC_ALL=C LANG=C ${quotePosixShell('git')}`,
...args.map(quotePosixShell)
].join(' ')
const delay = options.loginDelayMs > 0 ? `sleep ${options.loginDelayMs / 1_000}; ` : ''
const script = `${delay}${buildWslLoginShellCommand(command)}`
result = await run(
'wsl.exe',
wslShellArgs(distro, ['/bin/sh', '-lc', escapeWslShCommandForWindows(script)])
)
const markerOffset = result.stdout.indexOf(outputMarker)
if (markerOffset < 0) {
throw new Error('Login shell did not emit the Git output marker')
}
result.stdout = result.stdout.subarray(markerOffset + Buffer.byteLength(outputMarker))
} else {
result = await run(
'wsl.exe',
wslArgs(distro, [
'/usr/bin/env',
`PATH=${loginPath}`,
'LC_ALL=C',
'LANG=C',
gitPath,
'-C',
repo,
...args
])
)
}
return { ...result, elapsedMs: performance.now() - startedAt }
}
const measure = async (label, runArm) => {
for (let index = 0; index < options.warmups; index += 1) {
await runArm('login')
await runArm('fast')
}
const samples = { login: [], fast: [] }
for (let index = 0; index < options.samples; index += 1) {
const order = index % 2 === 0 ? ['login', 'fast'] : ['fast', 'login']
const results = []
for (const mode of order) {
const result = await runArm(mode)
samples[mode].push(result.elapsedMs)
results.push(result)
}
if (results[0].status !== results[1].status || !results[0].stdout.equals(results[1].stdout)) {
throw new Error(
`${label} correctness mismatch between ${order.join(' and ')} arms:\n` +
`${order[0]}=${JSON.stringify(results[0].stdout.toString('utf8'))}\n` +
`${order[1]}=${JSON.stringify(results[1].stdout.toString('utf8'))}`
)
}
}
const login = summarize(samples.login)
const fast = summarize(samples.fast)
return {
login,
fast,
medianSpeedup: Number((login.medianMs / fast.medianMs).toFixed(2)),
medianSavedMs: Number((login.medianMs - fast.medianMs).toFixed(1))
}
}
const benchmarkRepo = async (repo) => {
const branch = (
await runGit('fast', repo, ['symbolic-ref', '--quiet', '--short', 'HEAD'])
).stdout
.toString('utf8')
.trim()
const fixtureName = `.orca-wsl-git-shell-benchmark-${process.pid}.txt`
const fixturePath = `${repo}/${fixtureName}`
const prepareStage = async () => {
await runGit('fast', repo, ['reset', '--quiet', '--', fixtureName])
await run(
'wsl.exe',
wslArgs(distro, [
'/bin/sh',
'-c',
'printf "benchmark\\n" > "$1"',
'orca-wsl-git-shell-benchmark',
fixturePath
])
)
}
const cleanupStage = async () => {
await runGit('fast', repo, ['reset', '--quiet', '--', fixtureName])
await run('wsl.exe', wslArgs(distro, ['/bin/rm', '-f', '--', fixturePath]))
}
const operations = {
status: [
'-c',
'core.quotePath=false',
'status',
'--porcelain=v2',
'--branch',
'--untracked-files=all'
],
numstat: ['-c', 'core.quotePath=false', 'diff', '-z', '--numstat', '-M'],
upstream: [
'for-each-ref',
'--format=%(refname)%00%(upstream)%00%(upstream:short)',
'--count=1',
`refs/heads/${branch}`
]
}
const result = {}
for (const [name, args] of Object.entries(operations)) {
result[name] = await measure(`${repo}:${name}`, (mode) => runGit(mode, repo, args))
}
try {
result.stageRefresh = await measure(`${repo}:stage-refresh`, async (mode) => {
await prepareStage()
const startedAt = performance.now()
await runGit('login', repo, ['add', '--', fixtureName])
const status = await runGit(mode, repo, operations.status)
const numstat = await runGit(mode, repo, [
'-c',
'core.quotePath=false',
'diff',
'-z',
'--cached',
'--numstat',
'-M'
])
return {
status: 0,
stdout: Buffer.concat([status.stdout, numstat.stdout]),
stderr: Buffer.concat([status.stderr, numstat.stderr]),
elapsedMs: performance.now() - startedAt
}
})
} finally {
await cleanupStage()
}
return result
}
const verifyProductionRunner = async (repo) => {
const windowsRepo = (
await run('wsl.exe', wslArgs(distro, ['/usr/bin/wslpath', '-w', repo]))
).stdout
.toString('utf8')
.trim()
const args = [
'-c',
'core.quotePath=false',
'status',
'--porcelain=v2',
'--branch',
'--untracked-files=all'
]
const [{ gitExecFileAsync }, { getWslGitReadEnvironment }, expected] = await Promise.all([
jiti.import('../../src/main/git/runner.ts'),
jiti.import('../../src/main/git/wsl-git-read-environment.ts'),
runGit('fast', repo, args)
])
await getWslGitReadEnvironment(distro)
const startedAt = performance.now()
const actual = await gitExecFileAsync(args, {
cwd: windowsRepo,
preferWslDirectGit: true,
wslDistro: distro
})
if (!Buffer.from(actual.stdout).equals(expected.stdout)) {
throw new Error(`Production runner output mismatch for ${windowsRepo}`)
}
return {
windowsRepo,
environmentPrimed: true,
elapsedMs: Number((performance.now() - startedAt).toFixed(1)),
outputBytes: expected.stdout.length
}
}
const result = {
distro,
gitPath,
gitVersion: (await runGit('fast', options.nativeRepo, ['--version'])).stdout
.toString('utf8')
.trim(),
samples: options.samples,
warmups: options.warmups,
injectedLoginDelayMs: options.loginDelayMs,
loginProbePreambleBytes: Buffer.byteLength(probeText.split('__ORCA_PATH__', 1)[0]),
guestProcessShape: {
login: 'sh -> interactive login shell -> git',
fast: 'env -> git'
},
native: await benchmarkRepo(options.nativeRepo),
mounted: await benchmarkRepo(options.mountedRepo),
productionRunnerVerification: {
native: await verifyProductionRunner(options.nativeRepo),
mounted: await verifyProductionRunner(options.mountedRepo)
}
}
console.log(JSON.stringify(result, null, 2))
}
main().catch((error) => {
console.error(error instanceof Error ? error.message : String(error))
process.exitCode = 1
})
+1
View File
@@ -110,6 +110,7 @@
"bench:startup": "pnpm run ensure:electron-runtime && node tests/tools/benchmarks/startup-time-bench.mjs",
"bench:daemon-coldstart": "pnpm run ensure:electron-runtime && node tests/tools/benchmarks/daemon-coldstart-bench.mjs",
"bench:wsl-hook-relay-reattach": "pnpm run build:relay && node config/scripts/wsl-hook-relay-reattach-benchmark.mjs",
"bench:wsl-git-shell": "node config/scripts/wsl-git-shell-benchmark.mjs",
"bench:hang-watchdog-memory": "pnpm run ensure:electron-runtime && node config/scripts/hang-watchdog-memory-benchmark.mjs",
"bench:main-thread-jank": "pnpm run ensure:electron-runtime && node tests/tools/benchmarks/main-thread-jank-bench.mjs",
"bench:worktree-deletion": "node tests/tools/benchmarks/worktree-deletion-dev-bench.mjs",
+12
View File
@@ -13,3 +13,15 @@ export function gitOptionsForWorktree(
...(options.signal ? { signal: options.signal } : {})
}
}
export function gitStatusReadOptionsForWorktree(
cwd: string,
options: GitRuntimeOptions = {}
): {
cwd: string
wslDistro?: string
signal?: AbortSignal
preferWslDirectGit: true
} {
return { ...gitOptionsForWorktree(cwd, options), preferWslDirectGit: true }
}
+391
View File
@@ -0,0 +1,391 @@
import { EventEmitter } from 'node:events'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const { execFileMock, execFileSyncMock, spawnMock } = vi.hoisted(() => ({
execFileMock: vi.fn(),
execFileSyncMock: vi.fn(),
spawnMock: vi.fn()
}))
vi.mock('node:child_process', () => ({
execFile: execFileMock,
execFileSync: execFileSyncMock,
spawn: spawnMock
}))
vi.mock('../observability/instrumentation', () => ({
withGitSpan: (_attributes: unknown, run: () => unknown) => run()
}))
vi.mock('../diagnostics/main-thread-churn-probe', () => ({ recordSubprocessSpawn: vi.fn() }))
import { gitExecFileAsync, gitStreamStdout } from './runner'
import {
getWslGitReadEnvironment,
resetWslGitReadEnvironmentForTests,
seedWslGitReadEnvironmentForTests
} from './wsl-git-read-environment'
const DISTRO = 'Ubuntu'
const LOGIN_ENVIRONMENT = { gitPath: '/home/user/bin/git', path: '/home/user/bin:/usr/bin:/bin' }
type MockChild = EventEmitter & {
stdout: EventEmitter
stderr: EventEmitter
pid: number
kill: ReturnType<typeof vi.fn>
}
function createMockChild(): MockChild {
const child = new EventEmitter() as MockChild
child.stdout = new EventEmitter()
child.stderr = new EventEmitter()
child.pid = 1234
child.kill = vi.fn()
return child
}
async function withPlatform<T>(platform: NodeJS.Platform, run: () => Promise<T>): Promise<T> {
const original = process.platform
Object.defineProperty(process, 'platform', { configurable: true, value: platform })
try {
return await run()
} finally {
Object.defineProperty(process, 'platform', { configurable: true, value: original })
}
}
function succeedExecFile(stdout = 'ok'): void {
execFileMock.mockImplementation((_command, _args, _options, callback) => {
const child = createMockChild()
queueMicrotask(() => callback?.(null, stdout, ''))
return child
})
}
describe('WSL direct Git reads', () => {
beforeEach(() => {
execFileMock.mockReset()
execFileSyncMock.mockReset()
spawnMock.mockReset()
resetWslGitReadEnvironmentForTests()
})
afterEach(() => {
resetWslGitReadEnvironmentForTests()
})
it('coalesces the login-shell environment probe per distro', async () => {
let completeProbe: ((error: Error | null, stdout: string, stderr: string) => void) | undefined
execFileMock.mockImplementation((_command, _args, _options, callback) => {
completeProbe = callback
return createMockChild()
})
const first = getWslGitReadEnvironment(DISTRO)
const second = getWslGitReadEnvironment(DISTRO)
expect(execFileMock).toHaveBeenCalledTimes(1)
completeProbe?.(
null,
'profile banner\n\0ORCA_WSL_GIT_READ_ENV_V1\0/home/user/bin:/usr/bin\0/home/user/bin/git\0',
''
)
await expect(Promise.all([first, second])).resolves.toEqual([
{ gitPath: '/home/user/bin/git', path: '/home/user/bin:/usr/bin' },
{ gitPath: '/home/user/bin/git', path: '/home/user/bin:/usr/bin' }
])
expect(execFileMock.mock.calls[0]?.[1]?.slice(3, 5)).toEqual(['sh', '-lc'])
expect(execFileMock.mock.calls[0]?.[1]?.[5]).toContain('^GIT_')
})
it('runs an opted-in read directly with translated cwd and arguments', async () => {
await withPlatform('win32', async () => {
seedWslGitReadEnvironmentForTests(DISTRO, LOGIN_ENVIRONMENT)
succeedExecFile()
await gitExecFileAsync(['status', '--short', String.raw`C:\repo\file.txt`], {
cwd: String.raw`C:\repo`,
env: { GIT_OPTIONAL_LOCKS: '0' },
preferWslDirectGit: true,
wslDistro: DISTRO
})
expect(execFileMock.mock.calls[0]?.[0]).toBe('wsl.exe')
expect(execFileMock.mock.calls[0]?.[1]).toEqual([
'-d',
DISTRO,
'--exec',
'/usr/bin/env',
`PATH=${LOGIN_ENVIRONMENT.path}`,
'LANGUAGE=en',
'LC_ALL=en_US.UTF-8',
'LANG=en_US.UTF-8',
'GIT_OPTIONAL_LOCKS=0',
LOGIN_ENVIRONMENT.gitPath,
'-C',
'/mnt/c/repo',
'status',
'--short',
'/mnt/c/repo/file.txt'
])
})
})
it('keeps the first read on the login shell while priming the fast path', async () => {
await withPlatform('win32', async () => {
let completeProbe: ((error: Error | null, stdout: string, stderr: string) => void) | undefined
execFileMock.mockImplementation((_command, args, _options, callback) => {
const child = createMockChild()
if ((args as string[])[5]?.includes('ORCA_WSL_GIT_READ_ENV_V1')) {
completeProbe = callback
} else {
queueMicrotask(() => callback?.(null, 'ok', ''))
}
return child
})
const options = {
cwd: String.raw`C:\repo`,
preferWslDirectGit: true as const,
wslDistro: DISTRO
}
await gitExecFileAsync(['status', '--short'], options)
expect(execFileMock).toHaveBeenCalledTimes(2)
expect(execFileMock.mock.calls[1]?.[1]?.slice(3, 5)).toEqual(['sh', '-lc'])
completeProbe?.(
null,
`\0ORCA_WSL_GIT_READ_ENV_V1\0${LOGIN_ENVIRONMENT.path}\0${LOGIN_ENVIRONMENT.gitPath}\0`,
''
)
await Promise.resolve()
await gitExecFileAsync(['status', '--short'], options)
expect(execFileMock.mock.calls[2]?.[1]).toContain('--exec')
})
})
it('keeps unopted commands on the WSL login shell', async () => {
await withPlatform('win32', async () => {
seedWslGitReadEnvironmentForTests(DISTRO, LOGIN_ENVIRONMENT)
succeedExecFile()
await gitExecFileAsync(['add', '--', 'file.txt'], {
cwd: String.raw`C:\repo`,
wslDistro: DISTRO
})
expect(execFileMock.mock.calls[0]?.[1]?.slice(3, 5)).toEqual(['sh', '-lc'])
})
})
it('keeps reads with custom Git environment policy on the login shell', async () => {
await withPlatform('win32', async () => {
seedWslGitReadEnvironmentForTests(DISTRO, LOGIN_ENVIRONMENT)
succeedExecFile()
await gitExecFileAsync(['status', '--short'], {
cwd: String.raw`C:\repo`,
env: { GIT_CONFIG_GLOBAL: '/home/user/custom.gitconfig' },
preferWslDirectGit: true,
wslDistro: DISTRO
})
expect(execFileMock.mock.calls[0]?.[1]?.slice(3, 5)).toEqual(['sh', '-lc'])
})
})
it('leaves override-less WSL UNC routing on its existing non-login shell', async () => {
await withPlatform('win32', async () => {
seedWslGitReadEnvironmentForTests(DISTRO, LOGIN_ENVIRONMENT)
succeedExecFile()
await gitExecFileAsync(['status', '--short'], {
cwd: String.raw`\\wsl.localhost\Ubuntu\repo`,
preferWslDirectGit: true
})
expect(execFileMock.mock.calls[0]?.[1]?.slice(3, 5)).toEqual(['bash', '-c'])
})
})
it('invalidates a missing direct executable and retries through the login shell', async () => {
await withPlatform('win32', async () => {
seedWslGitReadEnvironmentForTests(DISTRO, LOGIN_ENVIRONMENT)
execFileMock.mockImplementationOnce((_command, _args, _options, callback) => {
const child = createMockChild()
queueMicrotask(() =>
callback?.(
Object.assign(new Error('exit 127'), { code: 127 }),
'',
'/usr/bin/env: No such file or directory'
)
)
return child
})
execFileMock.mockImplementationOnce((_command, _args, _options, callback) => {
const child = createMockChild()
queueMicrotask(() => callback?.(null, 'ok', ''))
return child
})
await expect(
gitExecFileAsync(['status', '--short'], {
cwd: String.raw`C:\repo`,
preferWslDirectGit: true,
wslDistro: DISTRO
})
).resolves.toEqual({ stdout: 'ok', stderr: '' })
expect(execFileMock.mock.calls[0]?.[1]).toContain('--exec')
expect(execFileMock.mock.calls[1]?.[1]?.slice(3, 5)).toEqual(['sh', '-lc'])
})
})
it('falls back and disables the fast path after another direct Git failure', async () => {
await withPlatform('win32', async () => {
seedWslGitReadEnvironmentForTests(DISTRO, LOGIN_ENVIRONMENT)
execFileMock.mockImplementationOnce((_command, _args, _options, callback) => {
const child = createMockChild()
queueMicrotask(() =>
callback?.(Object.assign(new Error('exit 128'), { code: 128 }), '', 'helper failed')
)
return child
})
execFileMock.mockImplementation((_command, _args, _options, callback) => {
const child = createMockChild()
queueMicrotask(() => callback?.(null, 'ok', ''))
return child
})
const options = {
cwd: String.raw`C:\repo`,
preferWslDirectGit: true as const,
wslDistro: DISTRO
}
await gitExecFileAsync(['status', '--short'], options)
await gitExecFileAsync(['status', '--short'], options)
expect(execFileMock.mock.calls[0]?.[1]).toContain('--exec')
expect(execFileMock.mock.calls[1]?.[1]?.slice(3, 5)).toEqual(['sh', '-lc'])
expect(execFileMock.mock.calls[2]?.[1]?.slice(3, 5)).toEqual(['sh', '-lc'])
})
})
it('streams opted-in reads directly', async () => {
await withPlatform('win32', async () => {
seedWslGitReadEnvironmentForTests(DISTRO, LOGIN_ENVIRONMENT)
spawnMock.mockImplementation(() => {
const child = createMockChild()
queueMicrotask(() => {
child.stdout.emit('data', Buffer.from('# branch.head main\n'))
child.emit('close', 0)
})
return child
})
const chunks: string[] = []
await gitStreamStdout(['status', '--porcelain=v2'], {
cwd: String.raw`C:\repo`,
onStdout: (chunk) => {
chunks.push(chunk)
},
preferWslDirectGit: true,
wslDistro: DISTRO
})
expect(chunks).toEqual(['# branch.head main\n'])
expect(spawnMock.mock.calls[0]?.[1]).toContain('--exec')
})
})
it('retries a direct stream only when no stdout was delivered', async () => {
await withPlatform('win32', async () => {
seedWslGitReadEnvironmentForTests(DISTRO, LOGIN_ENVIRONMENT)
spawnMock
.mockImplementationOnce(() => {
const child = createMockChild()
queueMicrotask(() => {
child.stderr.emit('data', Buffer.from('/usr/bin/env: No such file or directory'))
child.emit('close', 127)
})
return child
})
.mockImplementationOnce(() => {
const child = createMockChild()
queueMicrotask(() => {
child.stdout.emit('data', Buffer.from('# branch.head main\n'))
child.emit('close', 0)
})
return child
})
await gitStreamStdout(['status', '--porcelain=v2'], {
cwd: String.raw`C:\repo`,
onStdout: () => {},
preferWslDirectGit: true,
wslDistro: DISTRO
})
expect(spawnMock).toHaveBeenCalledTimes(2)
expect(spawnMock.mock.calls[0]?.[1]).toContain('--exec')
expect(spawnMock.mock.calls[1]?.[1]?.slice(3, 5)).toEqual(['sh', '-lc'])
})
})
it('does not retry a failed direct stream after delivering output', async () => {
await withPlatform('win32', async () => {
seedWslGitReadEnvironmentForTests(DISTRO, LOGIN_ENVIRONMENT)
spawnMock.mockImplementation(() => {
const child = createMockChild()
queueMicrotask(() => {
child.stdout.emit('data', Buffer.from('partial'))
child.stderr.emit('data', Buffer.from('/usr/bin/env: No such file or directory'))
child.emit('close', 127)
})
return child
})
await expect(
gitStreamStdout(['status', '--porcelain=v2'], {
cwd: String.raw`C:\repo`,
onStdout: () => {},
preferWslDirectGit: true,
wslDistro: DISTRO
})
).rejects.toThrow('git exited with 127')
expect(spawnMock).toHaveBeenCalledTimes(1)
})
})
it('keeps network reads on the login shell even if opted in', async () => {
await withPlatform('win32', async () => {
seedWslGitReadEnvironmentForTests(DISTRO, LOGIN_ENVIRONMENT)
succeedExecFile()
await gitExecFileAsync(['fetch', '--dry-run'], {
cwd: String.raw`C:\repo`,
preferWslDirectGit: true,
useConfiguredSshCommandForNetwork: true,
wslDistro: DISTRO
})
expect(
execFileMock.mock.calls.every((call) => call[1]?.slice(3, 5).join(' ') === 'sh -lc')
).toBe(true)
})
})
it('leaves non-Windows execution unchanged', async () => {
await withPlatform('linux', async () => {
seedWslGitReadEnvironmentForTests(DISTRO, LOGIN_ENVIRONMENT)
succeedExecFile()
await gitExecFileAsync(['status', '--short'], {
cwd: '/repo',
preferWslDirectGit: true,
wslDistro: DISTRO
})
expect(execFileMock.mock.calls[0]?.[0]).toBe('git')
})
})
})
+285 -121
View File
@@ -41,6 +41,13 @@ import {
} from '../../shared/wsl-login-shell-command'
import { UNTRANSLATED_GIT_OUTPUT_ENV } from '../../shared/git-output-locale'
import { endSubprocessStdin } from '../../shared/subprocess-stdin-write'
import {
disableWslGitReadEnvironment,
getWslGitReadEnvironment,
invalidateWslGitReadEnvironment,
peekWslGitReadEnvironment,
type WslGitReadEnvironment
} from './wsl-git-read-environment'
// Re-exported for existing importers; lightweight consumers should import from './exec-error' to avoid this heavy module.
import { extractExecError, parseRetryAfterMs } from './exec-error'
export { extractExecError, parseRetryAfterMs }
@@ -51,6 +58,9 @@ export { extractExecError, parseRetryAfterMs }
const GIT_OUTPUT_LOCALE_SHELL_PREFIX = Object.entries(UNTRANSLATED_GIT_OUTPUT_ENV)
.map(([key, value]) => `${key}=${value}`)
.join(' ')
const GIT_OUTPUT_LOCALE_ENV_ARGS = Object.entries(UNTRANSLATED_GIT_OUTPUT_ENV).map(
([key, value]) => `${key}=${value}`
)
type ResolvedCommand = {
binary: string
@@ -58,6 +68,7 @@ type ResolvedCommand = {
cwd: string | undefined
/** Non-null when the command was routed through WSL. */
wsl: WslPathInfo | null
wslMode: 'direct-git' | 'login-shell' | 'non-login-shell' | null
}
/**
@@ -160,7 +171,8 @@ function resolveHostGitHubCli(command: 'gh', args: string[]): ResolvedCommand {
args,
// Why: host gh can't use a WSL UNC cwd; we only fall back for commands with explicit repo/API context, so none is needed.
cwd: undefined,
wsl: null
wsl: null,
wslMode: null
}
}
@@ -202,10 +214,14 @@ function resolveCommand(
args: string[],
cwd: string | undefined,
wslDistroOverride?: string,
options: { useWslLoginShell?: boolean } = {}
options: {
useWslLoginShell?: boolean
wslGitReadEnvironment?: WslGitReadEnvironment
env?: NodeJS.ProcessEnv
} = {}
): ResolvedCommand {
if (process.platform !== 'win32') {
return { binary: command, args, cwd, wsl: null }
return { binary: command, args, cwd, wsl: null, wslMode: null }
}
// Why: global gh callers (rate_limit, listAccessibleProjects) have no cwd to derive a distro from; a distro hint still routes through wsl.exe.
@@ -214,7 +230,7 @@ function resolveCommand(
const wsl: WslPathInfo | null =
cwdWsl ?? (wslDistroOverride ? { distro: wslDistroOverride, linuxPath: '' } : null)
if (!wsl) {
return { binary: command, args, cwd, wsl: null }
return { binary: command, args, cwd, wsl: null, wslMode: null }
}
const translatedArgs = translateArgsForWsl(args)
@@ -229,6 +245,28 @@ function resolveCommand(
? `cd ${quotePosixShell(linuxCwd)} && ${localePrefix}${escapedCommand} ${escapedArgs.join(' ')}`
: `${localePrefix}${escapedCommand} ${escapedArgs.join(' ')}`
if (command === 'git' && options.wslGitReadEnvironment) {
const optionalLocks = options.env?.GIT_OPTIONAL_LOCKS
return {
binary: 'wsl.exe',
args: [
'-d',
wsl.distro,
'--exec',
'/usr/bin/env',
`PATH=${options.wslGitReadEnvironment.path}`,
...GIT_OUTPUT_LOCALE_ENV_ARGS,
...(optionalLocks !== undefined ? [`GIT_OPTIONAL_LOCKS=${optionalLocks}`] : []),
options.wslGitReadEnvironment.gitPath,
...(linuxCwd ? ['-C', linuxCwd] : []),
...translatedArgs
],
cwd: undefined,
wsl,
wslMode: 'direct-git'
}
}
if (options.useWslLoginShell) {
return {
binary: 'wsl.exe',
@@ -241,7 +279,8 @@ function resolveCommand(
escapeWslShCommandForWindows(buildWslLoginShellCommand(shellCmd))
],
cwd: undefined,
wsl
wsl,
wslMode: 'login-shell'
}
}
@@ -250,7 +289,8 @@ function resolveCommand(
args: ['-d', wsl.distro, '--', 'bash', '-c', shellCmd],
// Why: the `cd` inside bash -c handles the directory; a UNC cwd on the Node process is redundant and can break Node internals.
cwd: undefined,
wsl
wsl,
wslMode: 'non-login-shell'
}
}
@@ -268,6 +308,7 @@ type GitExecOptions = {
env?: NodeJS.ProcessEnv
signal?: AbortSignal
wslDistro?: string
preferWslDirectGit?: boolean
useConfiguredSshCommandForNetwork?: boolean
}
@@ -281,6 +322,82 @@ type CommandExecOptions = {
wslDistro?: string
}
function wslDistroForCommand(cwd: string | undefined, override?: string): string | null {
if (process.platform !== 'win32') {
return null
}
return (cwd ? parseWslPath(cwd)?.distro : undefined) ?? override ?? null
}
function resolveGitCommand(
args: string[],
options: GitExecOptions,
forceLoginShell = false
): ResolvedCommand {
if (!forceLoginShell && shouldAttemptWslDirectGit(options)) {
const distro = wslDistroForCommand(options.cwd, options.wslDistro)
const environment = distro ? peekWslGitReadEnvironment(distro) : undefined
if (environment) {
return resolveCommand('git', args, options.cwd, options.wslDistro, {
wslGitReadEnvironment: environment,
env: options.env
})
}
if (distro) {
void getWslGitReadEnvironment(distro)
}
}
return resolveCommand('git', args, options.cwd, options.wslDistro, {
useWslLoginShell: Boolean(options.wslDistro)
})
}
function shouldAttemptWslDirectGit(options: GitExecOptions): boolean {
return Boolean(
process.platform === 'win32' &&
options.preferWslDirectGit &&
!options.useConfiguredSshCommandForNetwork &&
!Object.keys(options.env ?? {}).some(
(key) => key.startsWith('GIT_') && key !== 'GIT_OPTIONAL_LOCKS'
) &&
options.wslDistro
)
}
function resolveGitCommandWithoutProbe(args: string[], options: GitExecOptions): ResolvedCommand {
return resolveCommand('git', args, options.cwd, options.wslDistro, {
useWslLoginShell: Boolean(options.wslDistro)
})
}
function isDirectWslGitNotFound(error: unknown, resolved: ResolvedCommand): boolean {
if (resolved.wslMode !== 'direct-git' || !error || typeof error !== 'object') {
return false
}
const { code, stderr } = error as { code?: unknown; stderr?: unknown }
const message = typeof stderr === 'string' ? stderr : String(stderr ?? '')
return code === 127 && (message.includes('not found') || message.includes('No such file'))
}
function directWslGitExitCode(error: unknown, resolved: ResolvedCommand): number | null {
if (resolved.wslMode !== 'direct-git' || !error || typeof error !== 'object') {
return null
}
const code = (error as { code?: unknown }).code
return typeof code === 'number' ? code : null
}
function disableOrInvalidateDirectWslGit(error: unknown, resolved: ResolvedCommand): void {
if (!resolved.wsl) {
return
}
if (isDirectWslGitNotFound(error, resolved)) {
invalidateWslGitReadEnvironment(resolved.wsl.distro)
} else {
disableWslGitReadEnvironment(resolved.wsl.distro)
}
}
function isMissingCommandError(error: unknown): boolean {
return Boolean(
error && typeof error === 'object' && (error as { code?: unknown }).code === 'ENOENT'
@@ -841,25 +958,32 @@ export async function gitExecFileAsync(
return withGitSpan(
{ args, ...(options.cwd !== undefined ? { cwd: options.cwd } : {}) },
async () => {
const resolved = resolveCommand('git', args, options.cwd, options.wslDistro, {
useWslLoginShell: Boolean(options.wslDistro)
})
const resolved = resolveGitCommand(args, options)
const policy = options.useConfiguredSshCommandForNetwork
? await buildNetworkSshPolicyEnv(options)
: { env: nonInteractiveGitEnv(options.env), mode: 'default' as const }
let result: { stdout: string | Buffer; stderr: string | Buffer }
try {
result = await execFileCapture(resolved.binary, resolved.args, {
cwd: resolved.cwd,
const capture = (
command: ResolvedCommand
): Promise<{ stdout: string | Buffer; stderr: string | Buffer }> =>
execFileCapture(command.binary, command.args, {
cwd: command.cwd,
encoding: (options.encoding ?? 'utf-8') as BufferEncoding,
maxBuffer: options.maxBuffer,
timeout: options.timeout,
stdin: options.stdin,
// Why: never let a git read-path call block on an interactive prompt (issue #5308) — fail fast.
env: policy.env,
signal: options.signal
})
let result: { stdout: string | Buffer; stderr: string | Buffer }
try {
result = await capture(resolved)
} catch (error) {
if (directWslGitExitCode(error, resolved) !== null && !options.signal?.aborted) {
disableOrInvalidateDirectWslGit(error, resolved)
result = await capture(resolveGitCommand(args, options, true))
const { stdout, stderr } = result
return { stdout: stdout as string, stderr: stderr as string }
}
if (options.useConfiguredSshCommandForNetwork && error && typeof error === 'object') {
Object.assign(error, { gitSshPolicyMode: policy.mode })
}
@@ -942,6 +1066,7 @@ type GitStreamOptions = {
cwd: string
env?: NodeJS.ProcessEnv
wslDistro?: string
preferWslDirectGit?: boolean
signal?: AbortSignal
/** Byte backstop; defaults to DEFAULT_GIT_MAX_BUFFER. */
maxBuffer?: number
@@ -965,115 +1090,154 @@ export async function gitStreamStdout(
): Promise<GitStreamResult> {
const maxBuffer = options.maxBuffer ?? DEFAULT_GIT_MAX_BUFFER
return withGitSpan({ args, cwd: options.cwd }, async () => {
return new Promise<GitStreamResult>((resolve, reject) => {
if (options.signal?.aborted) {
reject(createAbortError())
return
}
const child = gitSpawn(args, {
cwd: options.cwd,
env: nonInteractiveGitEnv(options.env),
stdio: ['ignore', 'pipe', 'pipe'],
wslDistro: options.wslDistro,
windowsHide: true
const gitOptions: GitExecOptions = {
cwd: options.cwd,
...(options.env ? { env: options.env } : {}),
...(options.wslDistro ? { wslDistro: options.wslDistro } : {}),
...(options.preferWslDirectGit ? { preferWslDirectGit: true } : {}),
...(options.signal ? { signal: options.signal } : {})
}
let resolved = resolveGitCommand(args, gitOptions)
const stream = (command: ResolvedCommand): Promise<GitStreamResult> =>
new Promise<GitStreamResult>((resolve, reject) => {
if (options.signal?.aborted) {
reject(createAbortError())
return
}
const stdio: SpawnOptions['stdio'] = ['ignore', 'pipe', 'pipe']
const spawnOptions = {
cwd: options.cwd,
env: nonInteractiveGitEnv(options.env),
stdio,
wslDistro: options.wslDistro,
windowsHide: true
}
let child: ChildProcess
if (command.wslMode === 'direct-git') {
const spawnStartedAt = performance.now()
child = spawn(command.binary, command.args, {
cwd: command.cwd,
env: untranslatedGitOutputEnv(spawnOptions.env),
stdio: spawnOptions.stdio,
windowsHide: true
})
recordSubprocessSpawn(command.binary, command.args, performance.now() - spawnStartedAt)
} else {
child = gitSpawn(args, spawnOptions)
}
let settled = false
let stoppedEarly = false
let stdoutBytes = 0
let stderr = ''
let stderrBytes = 0
// Why: decode statefully so a multibyte UTF-8 char split across chunks isn't corrupted into replacement chars.
const stdoutDecoder = new StringDecoder('utf8')
const stderrDecoder = new StringDecoder('utf8')
const cleanup = (): void => {
child.stdout?.off('data', onStdoutData)
child.stderr?.off('data', onStderrData)
child.off('error', onError)
child.off('close', onClose)
options.signal?.removeEventListener('abort', onAbort)
// Flush any bytes the decoders were holding for an incomplete sequence.
stdoutDecoder.end()
stderrDecoder.end()
}
const finish = (error: Error | null): void => {
if (settled) {
return
}
settled = true
cleanup()
if (error) {
reject(Object.assign(error, { stderr, stdoutBytes }))
return
}
resolve({ stoppedEarly })
}
function onStdoutData(chunk: Buffer): void {
stdoutBytes += chunk.byteLength
if (stdoutBytes > maxBuffer) {
void killSpawnedCommandTree(child)
finish(new Error('git stdout exceeded maxBuffer.'))
return
}
const decoded = stdoutDecoder.write(chunk)
if (decoded.length === 0) {
return
}
// Why: a throw from the caller's parser would escape this event handler and crash main; convert to a rejection.
let shouldStop: boolean | void
try {
shouldStop = options.onStdout(decoded)
} catch (error) {
void killSpawnedCommandTree(child)
finish(error instanceof Error ? error : new Error(String(error)))
return
}
if (shouldStop === true) {
// Parser hit its limit: kill git and resolve cleanly with the partial output.
stoppedEarly = true
void killSpawnedCommandTree(child)
finish(null)
}
}
function onStderrData(chunk: Buffer): void {
stderrBytes += chunk.byteLength
if (stderrBytes > maxBuffer) {
void killSpawnedCommandTree(child)
finish(new Error('git stderr exceeded maxBuffer.'))
return
}
stderr += stderrDecoder.write(chunk)
}
function onError(error: Error): void {
finish(error)
}
function onClose(code: number | null): void {
if (stoppedEarly || code === 0) {
finish(null)
return
}
finish(Object.assign(new Error(`git exited with ${code}: ${stderr}`), { code }))
}
function onAbort(): void {
if (!child.pid) {
// Why: failed spawn reports ENOENT after abort cleanup; retain a listener so it cannot crash main.
child.once('error', () => {})
}
void killSpawnedCommandTree(child)
finish(createAbortError())
}
child.stdout?.on('data', onStdoutData)
child.stderr?.on('data', onStderrData)
child.on('error', onError)
child.on('close', onClose)
options.signal?.addEventListener('abort', onAbort, { once: true })
if (options.signal?.aborted) {
onAbort()
}
})
let settled = false
let stoppedEarly = false
let stdoutBytes = 0
let stderr = ''
let stderrBytes = 0
// Why: decode statefully so a multibyte UTF-8 char split across chunks isn't corrupted into replacement chars.
const stdoutDecoder = new StringDecoder('utf8')
const stderrDecoder = new StringDecoder('utf8')
const cleanup = (): void => {
child.stdout?.off('data', onStdoutData)
child.stderr?.off('data', onStderrData)
child.off('error', onError)
child.off('close', onClose)
options.signal?.removeEventListener('abort', onAbort)
// Flush any bytes the decoders were holding for an incomplete sequence.
stdoutDecoder.end()
stderrDecoder.end()
try {
return await stream(resolved)
} catch (error) {
const stdoutBytes =
error && typeof error === 'object' ? (error as { stdoutBytes?: unknown }).stdoutBytes : null
if (
stdoutBytes === 0 &&
directWslGitExitCode(error, resolved) !== null &&
!options.signal?.aborted
) {
disableOrInvalidateDirectWslGit(error, resolved)
resolved = resolveGitCommandWithoutProbe(args, gitOptions)
return stream(resolved)
}
const finish = (error: Error | null): void => {
if (settled) {
return
}
settled = true
cleanup()
if (error) {
reject(Object.assign(error, { stderr }))
return
}
resolve({ stoppedEarly })
}
function onStdoutData(chunk: Buffer): void {
stdoutBytes += chunk.byteLength
if (stdoutBytes > maxBuffer) {
void killSpawnedCommandTree(child)
finish(new Error('git stdout exceeded maxBuffer.'))
return
}
const decoded = stdoutDecoder.write(chunk)
if (decoded.length === 0) {
return
}
// Why: a throw from the caller's parser would escape this event handler and crash main; convert to a rejection.
let shouldStop: boolean | void
try {
shouldStop = options.onStdout(decoded)
} catch (error) {
void killSpawnedCommandTree(child)
finish(error instanceof Error ? error : new Error(String(error)))
return
}
if (shouldStop === true) {
// Parser hit its limit: kill git and resolve cleanly with the partial output.
stoppedEarly = true
void killSpawnedCommandTree(child)
finish(null)
}
}
function onStderrData(chunk: Buffer): void {
stderrBytes += chunk.byteLength
if (stderrBytes > maxBuffer) {
void killSpawnedCommandTree(child)
finish(new Error('git stderr exceeded maxBuffer.'))
return
}
stderr += stderrDecoder.write(chunk)
}
function onError(error: Error): void {
finish(error)
}
function onClose(code: number | null): void {
if (stoppedEarly || code === 0) {
finish(null)
return
}
finish(new Error(`git exited with ${code}: ${stderr}`))
}
function onAbort(): void {
if (!child.pid) {
// Why: failed spawn reports ENOENT after abort cleanup; retain a listener so it cannot crash main.
child.once('error', () => {})
}
void killSpawnedCommandTree(child)
finish(createAbortError())
}
child.stdout?.on('data', onStdoutData)
child.stderr?.on('data', onStderrData)
child.on('error', onError)
child.on('close', onClose)
options.signal?.addEventListener('abort', onAbort, { once: true })
if (options.signal?.aborted) {
onAbort()
}
})
throw error
}
})
}
+17 -1
View File
@@ -1120,6 +1120,22 @@ describe('getStatus', () => {
gitExecFileAsyncMock.mockResolvedValue({ stdout: '' })
})
it('opts status reads into direct WSL Git without changing mutation options', async () => {
readFileMock.mockResolvedValue('gitdir: /repo/.git/worktrees/feature\n')
existsSyncMock.mockReturnValue(false)
await getStatus('/repo', { wslDistro: 'Ubuntu' })
await stageFile('/repo', 'src/file.ts', { wslDistro: 'Ubuntu' })
expect(gitStreamOptionsMock).toHaveBeenCalledWith(
expect.objectContaining({ preferWslDirectGit: true, wslDistro: 'Ubuntu' })
)
const addOptions = gitExecFileAsyncMock.mock.calls.find(([args]) =>
(args as string[]).includes('add')
)?.[1] as { preferWslDirectGit?: boolean }
expect(addOptions.preferWslDirectGit).toBeUndefined()
})
it('benchmarks concurrent status burst subprocess pressure', async () => {
const benchPath = process.env.ORCA_GIT_STATUS_COALESCING_BENCH_JSON
if (!benchPath) {
@@ -1594,7 +1610,7 @@ describe('getStatus', () => {
expect(gitExecFileAsyncMock).toHaveBeenCalledWith(
['rev-parse', '--verify', '--quiet', 'refs/remotes/origin/feature/prompts'],
{ cwd: '/repo' }
{ cwd: '/repo', preferWslDirectGit: true }
)
expect(result.upstreamStatus).toEqual({ hasUpstream: false, ahead: 0, behind: 0 })
})
+9 -5
View File
@@ -51,7 +51,7 @@ import { resolveWorktreeBaseCommitOid } from './worktree-base-ref-probe'
import { getLargeDiffRenderLimit } from '../../shared/large-diff-render-limit'
import { InFlightPromiseDedupe, stableInFlightKey } from '../../shared/in-flight-promise-dedupe'
import type { GitRuntimeOptions } from './git-runtime-options'
import { gitOptionsForWorktree } from './git-runtime-options'
import { gitOptionsForWorktree, gitStatusReadOptionsForWorktree } from './git-runtime-options'
import { GitStatusReadLeaseOwner } from './git-status-read-lease-owner'
import { parseGitRevListFirstParentOid } from '../../shared/git-rev-list-output'
import {
@@ -321,6 +321,7 @@ async function runGetStatus(
const { stoppedEarly } = await gitStreamStdout(statusArgs, {
cwd: worktreePath,
wslDistro: options.wslDistro,
preferWslDirectGit: true,
// Why: status polling is read-like; disable optional locks to avoid racing terminal Git on index.lock.
env: gitOptionalLocksDisabledEnv(),
signal: options.signal,
@@ -467,7 +468,7 @@ function createBranchLineTotalInput(
.map((entry) => entry.path),
runDiffNumstat: (args, signal) =>
gitExecFileAsync(args, {
...gitOptionsForWorktree(worktreePath, options),
...gitStatusReadOptionsForWorktree(worktreePath, options),
// Why: after the spread, so the shared lease signal wins over this caller's own.
signal,
env: gitOptionalLocksDisabledEnv(),
@@ -618,7 +619,10 @@ async function runNumstat(
'--numstat',
'-M'
],
{ ...gitOptionsForWorktree(worktreePath, options), env: gitOptionalLocksDisabledEnv() }
{
...gitStatusReadOptionsForWorktree(worktreePath, options),
env: gitOptionalLocksDisabledEnv()
}
)
return parseNumstat(stdout)
} catch (error) {
@@ -852,7 +856,7 @@ async function probeOrRevalidateEffectiveUpstreamStatus(
} else if (cached) {
try {
const status = await getGitUpstreamStatusForUpstreamName(
(args) => gitExecFileAsync(args, gitOptionsForWorktree(worktreePath, options)),
(args) => gitExecFileAsync(args, gitStatusReadOptionsForWorktree(worktreePath, options)),
cached.upstreamName
)
return { status, probedSameNameOriginRef: false }
@@ -889,7 +893,7 @@ async function probeEffectiveUpstreamStatus(
): Promise<{ status: GitUpstreamStatus; probedSameNameOriginRef: boolean }> {
let probedSameNameOriginRef = false
const snapshotRunner = createGitConfigSnapshotRunner((args) =>
gitExecFileAsync(args, gitOptionsForWorktree(worktreePath, options))
gitExecFileAsync(args, gitStatusReadOptionsForWorktree(worktreePath, options))
)
const status = await getEffectiveGitUpstreamStatus((args) => {
if (args[0] === 'rev-parse' && args.includes(`refs/remotes/origin/${branchName}`)) {
+95
View File
@@ -0,0 +1,95 @@
import { execFile } from 'node:child_process'
import {
buildWslLoginShellCommand,
escapeWslShCommandForWindows
} from '../../shared/wsl-login-shell-command'
export type WslGitReadEnvironment = { gitPath: string; path: string }
const PROBE_MARKER = 'ORCA_WSL_GIT_READ_ENV_V1'
const PROBE_TIMEOUT_MS = 10_000
const PROBE_MAX_BUFFER = 64 * 1024
const environmentByDistro = new Map<string, Promise<WslGitReadEnvironment | null>>()
const resolvedEnvironmentByDistro = new Map<string, WslGitReadEnvironment>()
function parseProbe(stdout: string): WslGitReadEnvironment | null {
const fields = stdout.split('\0')
const markerIndex = fields.lastIndexOf(PROBE_MARKER)
const path = fields[markerIndex + 1] ?? ''
const gitPath = fields[markerIndex + 2] ?? ''
if (
markerIndex < 0 ||
!path.includes('/') ||
path.length > 32_768 ||
!gitPath.startsWith('/') ||
gitPath.includes('\n') ||
gitPath.includes('\r')
) {
return null
}
return { gitPath, path }
}
function probeWslGitReadEnvironment(distro: string): Promise<WslGitReadEnvironment | null> {
const probeCommand = [
'_orca_git_path=$(command -v git 2>/dev/null || true)',
'case "$_orca_git_path" in /*) [ -x "$_orca_git_path" ] || exit 127 ;; *) exit 127 ;; esac',
'if [ -n "${XDG_CONFIG_HOME:-}" ] || [ -n "${LD_LIBRARY_PATH:-}" ] || env | grep -q \'^GIT_\'; then exit 78; fi',
`printf '\\0${PROBE_MARKER}\\0%s\\0%s\\0' "$PATH" "$_orca_git_path"`
].join('\n')
const script = escapeWslShCommandForWindows(buildWslLoginShellCommand(probeCommand))
return new Promise((resolve) => {
execFile(
'wsl.exe',
['-d', distro, '--', 'sh', '-lc', script],
{
encoding: 'utf8',
maxBuffer: PROBE_MAX_BUFFER,
timeout: PROBE_TIMEOUT_MS,
windowsHide: true
},
(error, stdout) => resolve(error ? null : parseProbe(String(stdout)))
)
})
}
export function getWslGitReadEnvironment(distro: string): Promise<WslGitReadEnvironment | null> {
let environment = environmentByDistro.get(distro)
if (!environment) {
environment = probeWslGitReadEnvironment(distro).then((resolved) => {
if (resolved && environmentByDistro.get(distro) === environment) {
resolvedEnvironmentByDistro.set(distro, resolved)
}
return resolved
})
environmentByDistro.set(distro, environment)
}
return environment
}
export function peekWslGitReadEnvironment(distro: string): WslGitReadEnvironment | undefined {
return resolvedEnvironmentByDistro.get(distro)
}
export function invalidateWslGitReadEnvironment(distro: string): void {
environmentByDistro.delete(distro)
resolvedEnvironmentByDistro.delete(distro)
}
export function disableWslGitReadEnvironment(distro: string): void {
environmentByDistro.set(distro, Promise.resolve(null))
resolvedEnvironmentByDistro.delete(distro)
}
export function resetWslGitReadEnvironmentForTests(): void {
environmentByDistro.clear()
resolvedEnvironmentByDistro.clear()
}
export function seedWslGitReadEnvironmentForTests(
distro: string,
environment: WslGitReadEnvironment
): void {
environmentByDistro.set(distro, Promise.resolve(environment))
resolvedEnvironmentByDistro.set(distro, environment)
}