mirror of
https://github.com/stablyai/orca.git
synced 2026-10-17 00:03:08 +00:00
Consolidate untranslated-git-locale into runner and relay primitives
Replace the five per-site LC_ALL=C patches with one shared UNTRANSLATED_GIT_OUTPUT_ENV (LANGUAGE=en LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8) injected inside the git runner primitives (promptGuardGitEnv, gitSpawn, gitExecFileSync, gitExecFileAsyncBuffer) and a relay buildRelayGitEnv() helper, so every current and future machine-parsed git spawn is covered by construction — including the fs-handler-git-fallback sites the per-site approach missed. The UTF-8 English locale keeps a UTF-8 LC_CTYPE for hooks git spawns; LANGUAGE is pinned because gettext consults it before LC_ALL. WSL-routed git gets the same values as a shell assignment prefix built in resolveCommand, since spawn env cannot cross the wsl.exe boundary — closing the WSL gap the first pass accepted. Also scrub credential-bearing remote URLs from the probe-failed message surfaced on the worktree card. 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -66,6 +66,20 @@ describe('probeBranchUpstream', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('scrubs credential-bearing URLs from the probe-failed message', async () => {
|
||||
// The message surfaces on the worktree card, so an embedded remote URL
|
||||
// must not leak a token or password into the UI.
|
||||
const exec: GitExec = vi
|
||||
.fn()
|
||||
.mockRejectedValue(
|
||||
new Error('fatal: unable to access https://user:hunter2@example.com/repo.git/: timed out')
|
||||
)
|
||||
expect(await probeBranchUpstream(exec)).toEqual({
|
||||
outcome: 'probe-failed',
|
||||
message: 'fatal: unable to access https://example.com/repo.git/: timed out'
|
||||
})
|
||||
})
|
||||
|
||||
it('reports probe-failed, not has-upstream, for localized git diagnostics (issue #7808)', async () => {
|
||||
// A gettext-enabled git under de_DE translates even the `fatal:` prefix.
|
||||
const exec: GitExec = vi.fn(async (args: string[]) => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isNoUpstreamError } from '../../shared/git-remote-error'
|
||||
import { isNoUpstreamError, stripCredentialsFromMessage } from '../../shared/git-remote-error'
|
||||
import {
|
||||
resolveEffectiveGitUpstream,
|
||||
type GitCommandRunner
|
||||
@@ -31,9 +31,10 @@ export async function probeBranchUpstream(exec: GitExec): Promise<BranchUpstream
|
||||
}
|
||||
// Why: an unexpected failure is not proof either way — report it as its own
|
||||
// outcome so callers skip the rename but stay retryable (issue #7808).
|
||||
// The message surfaces in the UI, so scrub credential-bearing remote URLs.
|
||||
return {
|
||||
outcome: 'probe-failed',
|
||||
message: error instanceof Error ? error.message : String(error)
|
||||
message: stripCredentialsFromMessage(error instanceof Error ? error.message : String(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,8 @@ import {
|
||||
nonInteractiveGitEnv,
|
||||
parseRetryAfterMs,
|
||||
promptGuardGitEnv,
|
||||
redirectPortedHostnameToEnv
|
||||
redirectPortedHostnameToEnv,
|
||||
untranslatedGitOutputEnv
|
||||
} from './runner'
|
||||
|
||||
// Reads git config injected via the GIT_CONFIG_COUNT/KEY/VALUE env protocol
|
||||
@@ -224,11 +225,19 @@ describe('git env forces untranslated diagnostics (issue #7808)', () => {
|
||||
// A gettext-enabled git under de_DE translates even the `fatal:` prefix,
|
||||
// breaking isNoUpstreamError and every other stderr phrase match.
|
||||
const env = promptGuardGitEnv({ PATH: '/usr/bin', LC_ALL: 'de_DE.UTF-8' })
|
||||
expect(env.LC_ALL).toBe('C')
|
||||
expect(env.LC_ALL).toBe('en_US.UTF-8')
|
||||
expect(env.LANG).toBe('en_US.UTF-8')
|
||||
})
|
||||
|
||||
it('pins LANGUAGE, which outranks LC_ALL in gettext lookups', () => {
|
||||
const env = untranslatedGitOutputEnv({ PATH: '/usr/bin', LANGUAGE: 'de:en' })
|
||||
expect(env.LANGUAGE).toBe('en')
|
||||
expect(env.LC_ALL).toBe('en_US.UTF-8')
|
||||
})
|
||||
|
||||
it('applies to nonInteractiveGitEnv as well', () => {
|
||||
const env = nonInteractiveGitEnv({ PATH: '/usr/bin', LANG: 'fr_FR.UTF-8' })
|
||||
expect(env.LC_ALL).toBe('C')
|
||||
expect(env.LC_ALL).toBe('en_US.UTF-8')
|
||||
expect(env.LANGUAGE).toBe('en')
|
||||
})
|
||||
})
|
||||
|
||||
+30
-9
@@ -35,9 +35,17 @@ import {
|
||||
escapeWslShCommandForWindows,
|
||||
quotePosixShell
|
||||
} from '../../shared/wsl-login-shell-command'
|
||||
import { UNTRANSLATED_GIT_OUTPUT_ENV } from '../../shared/git-output-locale'
|
||||
|
||||
// ─── Core resolution ────────────────────────────────────────────────
|
||||
|
||||
// `LANGUAGE=en LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8` — env-assignment prefix for
|
||||
// WSL-routed git, where spawn env cannot cross the wsl.exe boundary. Values are
|
||||
// shell-safe unquoted (alnum, dot, dash, underscore only).
|
||||
const GIT_OUTPUT_LOCALE_SHELL_PREFIX = Object.entries(UNTRANSLATED_GIT_OUTPUT_ENV)
|
||||
.map(([key, value]) => `${key}=${value}`)
|
||||
.join(' ')
|
||||
|
||||
type ResolvedCommand = {
|
||||
binary: string
|
||||
args: string[]
|
||||
@@ -202,6 +210,10 @@ function resolveCommand(
|
||||
}
|
||||
|
||||
const translatedArgs = translateArgsForWsl(args)
|
||||
// Why: env set on wsl.exe stays on the Windows side (WSLENV forwards only
|
||||
// named vars), so the untranslated-output locale must ride the command
|
||||
// string for git stderr parsers to work inside the distro (issue #7808).
|
||||
const localePrefix = command === 'git' ? `${GIT_OUTPUT_LOCALE_SHELL_PREFIX} ` : ''
|
||||
const escapedCommand = quotePosixShell(command)
|
||||
// Why: shell-escape each argument to prevent word splitting / glob expansion
|
||||
// inside the bash -c string. Single quotes are safe for all chars except
|
||||
@@ -214,8 +226,8 @@ function resolveCommand(
|
||||
// doesn't need a particular cwd for global calls like `api rate_limit`.
|
||||
const linuxCwd = cwdWsl?.linuxPath ?? (cwd && wslDistroOverride ? translateArgForWsl(cwd) : null)
|
||||
const shellCmd = linuxCwd
|
||||
? `cd ${quotePosixShell(linuxCwd)} && ${escapedCommand} ${escapedArgs.join(' ')}`
|
||||
: `${escapedCommand} ${escapedArgs.join(' ')}`
|
||||
? `cd ${quotePosixShell(linuxCwd)} && ${localePrefix}${escapedCommand} ${escapedArgs.join(' ')}`
|
||||
: `${localePrefix}${escapedCommand} ${escapedArgs.join(' ')}`
|
||||
|
||||
if (options.useWslLoginShell) {
|
||||
return {
|
||||
@@ -560,15 +572,21 @@ export function appendGitConfigEnv(
|
||||
return next
|
||||
}
|
||||
|
||||
/**
|
||||
* Pin Orca-spawned git to untranslated English output so stderr/progress
|
||||
* parsers keep working under any user locale (issue #7808; see
|
||||
* UNTRANSLATED_GIT_OUTPUT_ENV for the full rationale). Terminal git is
|
||||
* untouched. Injected by every git runner in this module; WSL-routed spawns
|
||||
* get the same values via GIT_OUTPUT_LOCALE_SHELL_PREFIX instead.
|
||||
*/
|
||||
export function untranslatedGitOutputEnv(env: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
|
||||
return { ...env, ...UNTRANSLATED_GIT_OUTPUT_ENV }
|
||||
}
|
||||
|
||||
export function promptGuardGitEnv(env: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
|
||||
return appendGitConfigEnv(
|
||||
{
|
||||
...env,
|
||||
// Why: Orca parses git's stderr diagnostics (isNoUpstreamError & friends);
|
||||
// a gettext-enabled git under a non-English locale translates even the
|
||||
// `fatal:` prefix and silently breaks those parsers (issue #7808). Force
|
||||
// the C locale for Orca-spawned git only — terminal git is untouched.
|
||||
LC_ALL: 'C',
|
||||
...untranslatedGitOutputEnv(env),
|
||||
GIT_TERMINAL_PROMPT: '0',
|
||||
GIT_ASKPASS: env.GIT_ASKPASS ?? '',
|
||||
SSH_ASKPASS: env.SSH_ASKPASS ?? '',
|
||||
@@ -875,7 +893,8 @@ export async function gitExecFileAsyncBuffer(
|
||||
const { stdout } = (await execFileCapture(resolved.binary, resolved.args, {
|
||||
cwd: resolved.cwd,
|
||||
encoding: 'buffer',
|
||||
maxBuffer: options.maxBuffer
|
||||
maxBuffer: options.maxBuffer,
|
||||
env: untranslatedGitOutputEnv()
|
||||
})) as { stdout: Buffer }
|
||||
return { stdout }
|
||||
}
|
||||
@@ -1056,6 +1075,7 @@ export function gitExecFileSync(
|
||||
return execFileSync(resolved.binary, resolved.args, {
|
||||
cwd: resolved.cwd,
|
||||
encoding: options.encoding ?? 'utf-8',
|
||||
env: untranslatedGitOutputEnv(),
|
||||
stdio: options.stdio ?? ['pipe', 'pipe', 'pipe'],
|
||||
timeout: options.timeout ?? GIT_EXEC_SYNC_TIMEOUT_MS
|
||||
}) as string
|
||||
@@ -1081,6 +1101,7 @@ export function gitSpawn(
|
||||
const spawnStartedAt = performance.now()
|
||||
const child = spawn(resolved.binary, resolved.args, {
|
||||
...spawnOptions,
|
||||
env: untranslatedGitOutputEnv(spawnOptions.env ?? process.env),
|
||||
cwd: resolved.cwd
|
||||
})
|
||||
recordSubprocessSpawn(resolved.binary, resolved.args, performance.now() - spawnStartedAt)
|
||||
|
||||
@@ -2253,10 +2253,6 @@ export function registerRepoHandlers(mainWindow: BrowserWindow, store: Store): v
|
||||
try {
|
||||
proc = gitSpawn(['clone', '--progress', '--', args.url, clonePath], {
|
||||
cwd: args.destination,
|
||||
// Why: emitCloneProgressFromText and getGitCloneFailureMessage
|
||||
// parse untranslated progress/`fatal:` lines, which a gettext
|
||||
// git localizes (issue #7808). Locale only — auth is unchanged.
|
||||
env: { ...process.env, LC_ALL: 'C' },
|
||||
stdio: ['ignore', 'ignore', 'pipe']
|
||||
})
|
||||
} catch (err) {
|
||||
|
||||
@@ -5452,7 +5452,7 @@ describe('OrcaRuntimeService', () => {
|
||||
it('keeps project clone setup on the cloned host-qualified repo', async () => {
|
||||
const destination = await mkdtemp(join(tmpdir(), 'orca-runtime-project-clone-'))
|
||||
const clonePath = join(destination, 'orca')
|
||||
const spawnSpy = vi.spyOn(gitRunner, 'wslAwareSpawn')
|
||||
const spawnSpy = vi.spyOn(gitRunner, 'gitSpawn')
|
||||
const repos: Record<string, unknown>[] = []
|
||||
getRepoUpstreamMock.mockResolvedValue({ owner: 'stablyai', repo: 'orca' })
|
||||
const runtimeStore = {
|
||||
@@ -5514,7 +5514,7 @@ describe('OrcaRuntimeService', () => {
|
||||
it('adopts public clone repos into host-qualified project setup', async () => {
|
||||
const destination = await mkdtemp(join(tmpdir(), 'orca-runtime-project-clone-'))
|
||||
const clonePath = join(destination, 'orca')
|
||||
const spawnSpy = vi.spyOn(gitRunner, 'wslAwareSpawn')
|
||||
const spawnSpy = vi.spyOn(gitRunner, 'gitSpawn')
|
||||
const repos: Record<string, unknown>[] = []
|
||||
getRepoUpstreamMock.mockResolvedValue({ owner: 'stablyai', repo: 'orca' })
|
||||
const runtimeStore = {
|
||||
@@ -5584,7 +5584,7 @@ describe('OrcaRuntimeService', () => {
|
||||
it('keeps project clone repos split by runtime host on the same clone path', async () => {
|
||||
const destination = await mkdtemp(join(tmpdir(), 'orca-runtime-project-clone-'))
|
||||
const clonePath = join(destination, 'orca')
|
||||
const spawnSpy = vi.spyOn(gitRunner, 'wslAwareSpawn')
|
||||
const spawnSpy = vi.spyOn(gitRunner, 'gitSpawn')
|
||||
const repos: Record<string, unknown>[] = [
|
||||
{
|
||||
id: 'repo-host-a',
|
||||
@@ -5755,7 +5755,7 @@ describe('OrcaRuntimeService', () => {
|
||||
})
|
||||
|
||||
it('defaults runtime cloneRepo badgeColor to DEFAULT_REPO_BADGE_COLOR', async () => {
|
||||
const spawnSpy = vi.spyOn(gitRunner, 'wslAwareSpawn')
|
||||
const spawnSpy = vi.spyOn(gitRunner, 'gitSpawn')
|
||||
const added: Record<string, unknown>[] = []
|
||||
const colorStore = {
|
||||
...store,
|
||||
@@ -5791,7 +5791,7 @@ describe('OrcaRuntimeService', () => {
|
||||
})
|
||||
|
||||
it('preserves existing badgeColor on runtime cloneRepo folder->git dedupe upgrade', async () => {
|
||||
const spawnSpy = vi.spyOn(gitRunner, 'wslAwareSpawn')
|
||||
const spawnSpy = vi.spyOn(gitRunner, 'gitSpawn')
|
||||
spawnSpy.mockImplementation(() => {
|
||||
const proc = new EventEmitter() as EventEmitter & { stderr: EventEmitter }
|
||||
proc.stderr = new EventEmitter()
|
||||
@@ -5889,7 +5889,7 @@ describe('OrcaRuntimeService', () => {
|
||||
})
|
||||
|
||||
it('rejects runtime cloneRepo dot-segment URLs before spawning git', async () => {
|
||||
const spawnSpy = vi.spyOn(gitRunner, 'wslAwareSpawn')
|
||||
const spawnSpy = vi.spyOn(gitRunner, 'gitSpawn')
|
||||
const runtime = createRuntime()
|
||||
const tempRoot = await mkdtemp(join(tmpdir(), 'orca-runtime-clone-'))
|
||||
const destination = join(tempRoot, 'destination')
|
||||
@@ -5906,7 +5906,7 @@ describe('OrcaRuntimeService', () => {
|
||||
})
|
||||
|
||||
it('rejects runtime cloneRepo parent-segment URLs before spawning git', async () => {
|
||||
const spawnSpy = vi.spyOn(gitRunner, 'wslAwareSpawn')
|
||||
const spawnSpy = vi.spyOn(gitRunner, 'gitSpawn')
|
||||
const runtime = createRuntime()
|
||||
const tempRoot = await mkdtemp(join(tmpdir(), 'orca-runtime-clone-'))
|
||||
const destination = join(tempRoot, 'destination')
|
||||
@@ -5923,7 +5923,7 @@ describe('OrcaRuntimeService', () => {
|
||||
})
|
||||
|
||||
it('removes an owned runtime clone target when git exits unsuccessfully', async () => {
|
||||
const spawnSpy = vi.spyOn(gitRunner, 'wslAwareSpawn')
|
||||
const spawnSpy = vi.spyOn(gitRunner, 'gitSpawn')
|
||||
const proc = new EventEmitter() as EventEmitter & { stderr: EventEmitter }
|
||||
proc.stderr = new EventEmitter()
|
||||
spawnSpy.mockReturnValue(proc as never)
|
||||
@@ -5950,7 +5950,7 @@ describe('OrcaRuntimeService', () => {
|
||||
})
|
||||
|
||||
it('preserves an existing runtime clone target when git exits unsuccessfully', async () => {
|
||||
const spawnSpy = vi.spyOn(gitRunner, 'wslAwareSpawn')
|
||||
const spawnSpy = vi.spyOn(gitRunner, 'gitSpawn')
|
||||
const proc = new EventEmitter() as EventEmitter & { stderr: EventEmitter }
|
||||
proc.stderr = new EventEmitter()
|
||||
spawnSpy.mockReturnValue(proc as never)
|
||||
@@ -5977,7 +5977,7 @@ describe('OrcaRuntimeService', () => {
|
||||
})
|
||||
|
||||
it('skips runtime clone failure cleanup when the owned target is replaced', async () => {
|
||||
const spawnSpy = vi.spyOn(gitRunner, 'wslAwareSpawn')
|
||||
const spawnSpy = vi.spyOn(gitRunner, 'gitSpawn')
|
||||
const proc = new EventEmitter() as EventEmitter & { stderr: EventEmitter }
|
||||
proc.stderr = new EventEmitter()
|
||||
spawnSpy.mockReturnValue(proc as never)
|
||||
@@ -6006,7 +6006,7 @@ describe('OrcaRuntimeService', () => {
|
||||
})
|
||||
|
||||
it('serializes concurrent runtime clones for the same target', async () => {
|
||||
const spawnSpy = vi.spyOn(gitRunner, 'wslAwareSpawn')
|
||||
const spawnSpy = vi.spyOn(gitRunner, 'gitSpawn')
|
||||
const firstProc = new EventEmitter() as EventEmitter & { stderr: EventEmitter }
|
||||
firstProc.stderr = new EventEmitter()
|
||||
spawnSpy.mockReturnValueOnce(firstProc as never)
|
||||
|
||||
@@ -41,7 +41,7 @@ import {
|
||||
AGENT_PROMPT_SUBMIT_DELAY_MS,
|
||||
buildAgentPromptPasteBytes
|
||||
} from '../../shared/agent-prompt-injection'
|
||||
import { gitExecFileAsync, wslAwareSpawn } from '../git/runner'
|
||||
import { gitExecFileAsync, gitSpawn } from '../git/runner'
|
||||
import {
|
||||
cleanupClaimedCloneTarget,
|
||||
claimCloneTarget,
|
||||
@@ -10662,14 +10662,10 @@ export class OrcaRuntimeService {
|
||||
await mkdir(trimmedDestination, { recursive: true })
|
||||
const claimedTarget = await claimCloneTarget(clonePath)
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
let proc: ReturnType<typeof wslAwareSpawn>
|
||||
let proc: ReturnType<typeof gitSpawn>
|
||||
try {
|
||||
proc = wslAwareSpawn('git', ['clone', '--progress', '--', trimmedUrl, clonePath], {
|
||||
proc = gitSpawn(['clone', '--progress', '--', trimmedUrl, clonePath], {
|
||||
cwd: trimmedDestination,
|
||||
// Why: getGitCloneFailureMessage matches `fatal:`/`error:` lines,
|
||||
// which a gettext-enabled git translates under a non-English locale
|
||||
// (issue #7808). Locale only — clone auth behavior stays unchanged.
|
||||
env: { ...process.env, LC_ALL: 'C' },
|
||||
stdio: ['ignore', 'ignore', 'pipe']
|
||||
})
|
||||
} catch (err) {
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
ingestGitGrepLine,
|
||||
SEARCH_TIMEOUT_MS
|
||||
} from '../shared/text-search'
|
||||
import { buildRelayCommandEnv } from './relay-command-env'
|
||||
import { buildRelayGitEnv } from './relay-command-env'
|
||||
|
||||
/**
|
||||
* List files using `git ls-files`. Fallback when rg is not installed.
|
||||
@@ -61,7 +61,7 @@ export function listFilesWithGit(
|
||||
|
||||
const child = spawn('git', ['ls-files', ...args], {
|
||||
cwd: rootPath,
|
||||
env: buildRelayCommandEnv(),
|
||||
env: buildRelayGitEnv(),
|
||||
stdio: ['ignore', 'pipe', 'pipe']
|
||||
})
|
||||
let timer: ReturnType<typeof setTimeout> | null = null
|
||||
@@ -209,7 +209,7 @@ export function searchWithGitGrep(
|
||||
|
||||
const child = spawn('git', gitArgs, {
|
||||
cwd: rootPath,
|
||||
env: buildRelayCommandEnv(),
|
||||
env: buildRelayGitEnv(),
|
||||
stdio: ['ignore', 'pipe', 'pipe']
|
||||
})
|
||||
let killTimeout: ReturnType<typeof setTimeout>
|
||||
|
||||
@@ -52,7 +52,7 @@ import {
|
||||
resolveEffectiveGitUpstream
|
||||
} from '../shared/git-effective-upstream'
|
||||
import { loadGitHistoryFromExecutor } from '../shared/git-history'
|
||||
import { buildRelayCommandEnv } from './relay-command-env'
|
||||
import { buildRelayGitEnv } from './relay-command-env'
|
||||
import {
|
||||
removeSafeUntrackedDiscardTarget,
|
||||
removeSafeUntrackedDiscardTargets
|
||||
@@ -254,11 +254,7 @@ export class GitHandler {
|
||||
stdin?: string
|
||||
}
|
||||
): Promise<{ stdout: string; stderr: string }> {
|
||||
const env = buildRelayCommandEnv()
|
||||
// Why: callers parse git's stderr diagnostics and progress lines; a
|
||||
// gettext-enabled git under a non-English remote locale translates them
|
||||
// and breaks the parsers (issue #7808). Relay git is machine-driven only.
|
||||
env.LC_ALL = 'C'
|
||||
const env = buildRelayGitEnv()
|
||||
if (opts?.disableOptionalLocks) {
|
||||
env.GIT_OPTIONAL_LOCKS = '0'
|
||||
}
|
||||
@@ -285,7 +281,7 @@ export class GitHandler {
|
||||
private async gitBuffer(args: string[], cwd: string): Promise<Buffer> {
|
||||
const { stdout } = (await execFileAsync('git', args, {
|
||||
cwd,
|
||||
env: { ...buildRelayCommandEnv(), LC_ALL: 'C' },
|
||||
env: buildRelayGitEnv(),
|
||||
encoding: 'buffer',
|
||||
maxBuffer: MAX_GIT_BUFFER
|
||||
})) as { stdout: Buffer }
|
||||
@@ -1101,8 +1097,7 @@ export class GitHandler {
|
||||
return await new Promise((resolve, reject) => {
|
||||
const child = spawn('git', args, {
|
||||
cwd: expandTilde(cwd),
|
||||
// LC_ALL=C: the progress regex below only matches untranslated lines.
|
||||
env: { ...buildRelayCommandEnv(), LC_ALL: 'C' },
|
||||
env: buildRelayGitEnv(),
|
||||
stdio: ['ignore', 'pipe', 'pipe']
|
||||
})
|
||||
let stdout = ''
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { homedir } from 'node:os'
|
||||
import { buildRelayCommandEnv } from './relay-command-env'
|
||||
import { buildRelayCommandEnv, buildRelayGitEnv } from './relay-command-env'
|
||||
|
||||
// homedir() is the fallback when the relay env carries no HOME; mock it so the
|
||||
// fallback path is deterministic and the "no resolvable home" branch is reachable.
|
||||
@@ -228,3 +228,20 @@ describe('buildRelayCommandEnv', () => {
|
||||
expect(env.PATH?.split(':')).toContain('/home/fallback/.local/bin')
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildRelayGitEnv', () => {
|
||||
it('pins an untranslated locale on top of the command env (issue #7808)', () => {
|
||||
// Relay git stderr/progress is machine-parsed; a non-English host locale
|
||||
// with a gettext git would translate it and break the parsers.
|
||||
const env = buildRelayGitEnv(
|
||||
{ HOME: '/home/me', PATH: '/usr/bin', LC_ALL: 'de_DE.UTF-8' },
|
||||
'linux'
|
||||
)
|
||||
|
||||
expect(env.LC_ALL).toBe('en_US.UTF-8')
|
||||
expect(env.LANG).toBe('en_US.UTF-8')
|
||||
expect(env.LANGUAGE).toBe('en')
|
||||
// PATH fallback behavior is unchanged.
|
||||
expect(env.PATH?.split(':')).toEqual(expect.arrayContaining(['/usr/bin', '/usr/local/bin']))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { homedir } from 'node:os'
|
||||
import { posix, win32 } from 'node:path'
|
||||
import { UNTRANSLATED_GIT_OUTPUT_ENV } from '../shared/git-output-locale'
|
||||
|
||||
const POSIX_RELAY_PATH_FALLBACKS = ['/usr/local/bin', '/opt/homebrew/bin', '/usr/bin', '/bin']
|
||||
const WINDOWS_RELAY_PATH_FALLBACKS = [
|
||||
@@ -154,3 +155,16 @@ export function buildRelayCommandEnv(
|
||||
[key]: [...segments].join(delimiter)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Env for relay-spawned git specifically: the PATH fallbacks above plus a
|
||||
* pinned English UTF-8 locale, because relay git output is machine-parsed
|
||||
* (stderr phrase matching, progress regexes) and a gettext-enabled git under
|
||||
* a non-English host locale translates it (issue #7808).
|
||||
*/
|
||||
export function buildRelayGitEnv(
|
||||
baseEnv: NodeJS.ProcessEnv = process.env,
|
||||
platform: NodeJS.Platform = process.platform
|
||||
): NodeJS.ProcessEnv {
|
||||
return { ...buildRelayCommandEnv(baseEnv, platform), ...UNTRANSLATED_GIT_OUTPUT_ENV }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Env pinning git subprocess output to untranslated English (issue #7808).
|
||||
*
|
||||
* Why: Orca parses git's stderr diagnostics and progress lines (e.g.
|
||||
* isNoUpstreamError, clone progress/`fatal:` matching); a gettext-enabled git
|
||||
* under a non-English locale translates even the `fatal:` prefix and silently
|
||||
* breaks those parsers. An English UTF-8 locale rather than plain `C` keeps a
|
||||
* UTF-8 LC_CTYPE for hooks git spawns (commit, push); a host without
|
||||
* en_US.UTF-8 falls back to C, which still emits untranslated messages.
|
||||
* LANGUAGE outranks LC_ALL in gettext's lookup, so it is pinned too.
|
||||
*/
|
||||
export const UNTRANSLATED_GIT_OUTPUT_ENV = {
|
||||
LANGUAGE: 'en',
|
||||
LC_ALL: 'en_US.UTF-8',
|
||||
LANG: 'en_US.UTF-8'
|
||||
} as const
|
||||
Reference in New Issue
Block a user