fix(git): force LC_ALL=C in GIT_NO_PROMPT_ENV to support non-English locales (#2035)
On non-English systems (e.g. LANG=de_DE.UTF-8), git produces localized stderr output. GSD's stderr.includes() guards are hardcoded to English strings and never match, causing every git add with exclusions to throw GSD_GIT_ERROR and merge failures to be misclassified. - Add LC_ALL: "C" to GIT_NO_PROMPT_ENV in git-constants.ts - Add env: GIT_NO_PROMPT_ENV to nativeMergeSquash fallback execFileSync - Add regression tests for both fixes Fixes #1997 Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8d5cadd53b
commit
a7cf125970
3 changed files with 135 additions and 0 deletions
|
|
@ -8,4 +8,5 @@ export const GIT_NO_PROMPT_ENV = {
|
|||
GIT_TERMINAL_PROMPT: "0",
|
||||
GIT_ASKPASS: "",
|
||||
GIT_SVN_ID: "",
|
||||
LC_ALL: "C", // force English git output so stderr string checks work on all locales (#1997)
|
||||
};
|
||||
|
|
|
|||
|
|
@ -847,6 +847,7 @@ export function nativeMergeSquash(basePath: string, branch: string): GitMergeRes
|
|||
cwd: basePath,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
encoding: "utf-8",
|
||||
env: GIT_NO_PROMPT_ENV,
|
||||
});
|
||||
return { success: true, conflicts: [] };
|
||||
} catch (err: unknown) {
|
||||
|
|
|
|||
133
src/resources/extensions/gsd/tests/git-locale.test.ts
Normal file
133
src/resources/extensions/gsd/tests/git-locale.test.ts
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
/**
|
||||
* Regression tests for #1997: git locale not forced to C.
|
||||
*
|
||||
* Validates that GIT_NO_PROMPT_ENV includes LC_ALL=C so git always produces
|
||||
* English output, and that nativeMergeSquash passes the env to execFileSync.
|
||||
*/
|
||||
|
||||
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { execFileSync } from "node:child_process";
|
||||
|
||||
import { GIT_NO_PROMPT_ENV } from "../git-constants.ts";
|
||||
import { nativeAddAllWithExclusions } from "../native-git-bridge.ts";
|
||||
import { RUNTIME_EXCLUSION_PATHS } from "../git-service.ts";
|
||||
import { createTestContext } from "./test-helpers.ts";
|
||||
|
||||
const { assertEq, assertTrue, report } = createTestContext();
|
||||
|
||||
function git(cwd: string, ...args: string[]): string {
|
||||
return execFileSync("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" }).trim();
|
||||
}
|
||||
|
||||
function initTempRepo(): string {
|
||||
const dir = mkdtempSync(join(tmpdir(), "gsd-locale-"));
|
||||
git(dir, "init");
|
||||
git(dir, "config", "user.email", "test@test.com");
|
||||
git(dir, "config", "user.name", "Test");
|
||||
// Initial commit so HEAD exists
|
||||
writeFileSync(join(dir, "init.txt"), "init");
|
||||
git(dir, "add", "-A");
|
||||
git(dir, "commit", "-m", "init");
|
||||
return dir;
|
||||
}
|
||||
|
||||
function createFile(base: string, relPath: string, content: string): void {
|
||||
const full = join(base, relPath);
|
||||
mkdirSync(join(full, ".."), { recursive: true });
|
||||
writeFileSync(full, content);
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
// ─── GIT_NO_PROMPT_ENV includes LC_ALL=C ─────────────────────────────
|
||||
|
||||
console.log("\n=== GIT_NO_PROMPT_ENV includes LC_ALL=C ===");
|
||||
|
||||
assertEq(
|
||||
GIT_NO_PROMPT_ENV.LC_ALL,
|
||||
"C",
|
||||
"GIT_NO_PROMPT_ENV must set LC_ALL to 'C' to force English git output"
|
||||
);
|
||||
|
||||
assertTrue(
|
||||
"GIT_TERMINAL_PROMPT" in GIT_NO_PROMPT_ENV,
|
||||
"GIT_NO_PROMPT_ENV still contains GIT_TERMINAL_PROMPT"
|
||||
);
|
||||
|
||||
// ─── nativeAddAllWithExclusions: non-English locale does not throw ───
|
||||
|
||||
console.log("\n=== nativeAddAllWithExclusions: non-English locale does not throw ===");
|
||||
|
||||
{
|
||||
// Simulate what happens on a German system: .gsd is gitignored,
|
||||
// exclusion pathspecs trigger an advisory warning exit code 1.
|
||||
// With LC_ALL=C the English stderr guard should match and suppress.
|
||||
const repo = initTempRepo();
|
||||
|
||||
writeFileSync(join(repo, ".gitignore"), ".gsd\n");
|
||||
createFile(repo, ".gsd/STATE.md", "# State");
|
||||
createFile(repo, "src/app.ts", "export const x = 1;");
|
||||
|
||||
// Save original LC_ALL / LANG and force German locale env
|
||||
const origLcAll = process.env.LC_ALL;
|
||||
const origLang = process.env.LANG;
|
||||
process.env.LANG = "de_DE.UTF-8";
|
||||
delete process.env.LC_ALL;
|
||||
|
||||
let threw = false;
|
||||
try {
|
||||
nativeAddAllWithExclusions(repo, RUNTIME_EXCLUSION_PATHS);
|
||||
} catch (e) {
|
||||
threw = true;
|
||||
console.error(" unexpected error:", e);
|
||||
}
|
||||
|
||||
// Restore
|
||||
if (origLcAll !== undefined) process.env.LC_ALL = origLcAll;
|
||||
else delete process.env.LC_ALL;
|
||||
if (origLang !== undefined) process.env.LANG = origLang;
|
||||
else delete process.env.LANG;
|
||||
|
||||
assertTrue(
|
||||
!threw,
|
||||
"nativeAddAllWithExclusions must not throw on non-English locale when .gsd is gitignored (#1997)"
|
||||
);
|
||||
|
||||
const staged = git(repo, "diff", "--cached", "--name-only");
|
||||
assertTrue(staged.includes("src/app.ts"), "real file staged despite German locale");
|
||||
|
||||
rmSync(repo, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
// ─── nativeMergeSquash: env is passed (merge-squash stderr is English) ─
|
||||
|
||||
console.log("\n=== nativeMergeSquash fallback uses GIT_NO_PROMPT_ENV ===");
|
||||
|
||||
{
|
||||
// We verify indirectly: the source code must pass env: GIT_NO_PROMPT_ENV.
|
||||
// Read the source and check for the pattern. This is a static check.
|
||||
const src = readFileSync(
|
||||
join(import.meta.dirname, "..", "native-git-bridge.ts"),
|
||||
"utf-8"
|
||||
);
|
||||
|
||||
// Find the nativeMergeSquash function and check it uses GIT_NO_PROMPT_ENV
|
||||
const fnStart = src.indexOf("export function nativeMergeSquash");
|
||||
assertTrue(fnStart !== -1, "nativeMergeSquash function exists in source");
|
||||
|
||||
const fnBody = src.slice(fnStart, src.indexOf("\nexport function", fnStart + 1));
|
||||
const hasEnv = fnBody.includes("env: GIT_NO_PROMPT_ENV");
|
||||
assertTrue(
|
||||
hasEnv,
|
||||
"nativeMergeSquash fallback must pass env: GIT_NO_PROMPT_ENV to execFileSync (#1997)"
|
||||
);
|
||||
}
|
||||
|
||||
report();
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue