From 463471771c2da3a296f6ed7c7d58fa1d9ed92560 Mon Sep 17 00:00:00 2001 From: That1Drifter Date: Wed, 12 Aug 2026 08:17:39 -0500 Subject: [PATCH 1/4] fix(server): enable git core.longpaths on Windows Git on Windows refuses to create or delete a path longer than MAX_PATH (260) unless core.longpaths is set. The OS-level LongPathsEnabled setting does not cover it: git opts in per repository, and it is off by default. Worktrees are where this bites. A worktree base path is longer than the repository root, so a repository that clones fine can still fail to check out into a worktree, and fail to be removed afterwards. Removal is the worse half: git drops its administrative record before deleting files, so a partial delete strands the directory where `git worktree list` can no longer see it. Measured on Windows 11 (git 2.52, LongPathsEnabled=1, core.longpaths unset at every scope): git worktree add into a 268-char target error: unable to create file ...: Filename too long fatal: Could not reset index file to revision 'HEAD' the same command with core.longpaths=true exit 0, checkout complete git worktree remove --force over a pnpm node_modules tree fails in 7s with "Filename too long", after git has already deregistered the worktree, leaving 206k files behind The config is injected per invocation through GIT_CONFIG_* rather than argv, so it reaches every git subcommand without changing the command line and nothing is written to the user's config. It is appended after any inherited GIT_CONFIG_COUNT entries so a caller's own injected config keeps working, and a GIT_CONFIG_COUNT that is not a count is left alone rather than overwritten, so git still reports it. Closes #635 Co-Authored-By: Claude Opus 5 (1M context) --- apps/server/src/vcs/GitVcsDriverCore.test.ts | 124 ++++++++++++++++++- apps/server/src/vcs/GitVcsDriverCore.ts | 51 +++++++- 2 files changed, 171 insertions(+), 4 deletions(-) diff --git a/apps/server/src/vcs/GitVcsDriverCore.test.ts b/apps/server/src/vcs/GitVcsDriverCore.test.ts index fb741b28ee7c..1325c08cbe63 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.test.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.test.ts @@ -19,8 +19,13 @@ import * as TestClock from "effect/testing/TestClock"; import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; import { GitCommandError, type ReviewDiffFileContentsInput } from "@t3tools/contracts"; +import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; import { ServerConfig } from "../config.ts"; -import { makeGitVcsDriverCore, splitNullSeparatedGitStdoutPaths } from "./GitVcsDriverCore.ts"; +import { + makeGitVcsDriverCore, + splitNullSeparatedGitStdoutPaths, + windowsLongPathConfigEnv, +} from "./GitVcsDriverCore.ts"; import * as GitVcsDriver from "./GitVcsDriver.ts"; const ServerConfigLayer = ServerConfig.layerTest(process.cwd(), { @@ -2110,3 +2115,120 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => { ); }); }); + +describe("Windows long path configuration", () => { + const captureGitEnv = (platform: NodeJS.Platform) => { + const captured: Array = []; + const spawner = ChildProcessSpawner.make((command) => + Effect.sync(() => { + if (!ChildProcess.isStandardCommand(command)) { + return assert.fail("expected a standard Git command"); + } + captured.push(command.options.env ?? {}); + return makeSuccessfulHandle(""); + }), + ); + const layer = GitVcsDriver.layer.pipe( + Layer.provide(ServerConfigLayer), + Layer.provideMerge( + Layer.merge( + NodeServices.layer, + Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner), + ), + ), + Layer.provide(Layer.succeed(HostProcessPlatform, platform)), + ); + + return Effect.gen(function* () { + const driver = yield* GitVcsDriver.GitVcsDriver; + const cwd = yield* makeTmpDir("git-longpath-test-"); + + yield* driver.execute({ + operation: "GitVcsDriverTest.execute", + cwd, + args: ["status"], + }); + + assert.equal(captured.length, 1); + return captured[0] ?? {}; + }).pipe(Effect.provide(layer)); + }; + + /** + * Asserted by locating the injected entry rather than by pinning it to index + * zero: the spawned environment inherits the host's, so a machine that already + * exports GIT_CONFIG_* shifts the index without changing the behaviour here. + */ + const findLongPathIndex = (env: NodeJS.ProcessEnv): string | undefined => { + const key = Object.keys(env).find( + (candidate) => candidate.startsWith("GIT_CONFIG_KEY_") && env[candidate] === "core.longpaths", + ); + return key?.slice("GIT_CONFIG_KEY_".length); + }; + + it.effect("reaches the spawned git process on Windows", () => + Effect.gen(function* () { + const env = yield* captureGitEnv("win32"); + + const index = findLongPathIndex(env); + assert.notEqual(index, undefined); + assert.equal(env[`GIT_CONFIG_VALUE_${index}`], "true"); + }), + ); + + it.effect("is left off platforms without MAX_PATH", () => + Effect.gen(function* () { + const env = yield* captureGitEnv("linux"); + + assert.equal(findLongPathIndex(env), undefined); + }), + ); +}); + +describe("windowsLongPathConfigEnv", () => { + it("enables core.longpaths on Windows", () => { + assert.deepStrictEqual(windowsLongPathConfigEnv("win32", {}), { + GIT_CONFIG_COUNT: "1", + GIT_CONFIG_KEY_0: "core.longpaths", + GIT_CONFIG_VALUE_0: "true", + }); + }); + + it("adds nothing on platforms without MAX_PATH", () => { + assert.deepStrictEqual(windowsLongPathConfigEnv("linux", {}), {}); + assert.deepStrictEqual(windowsLongPathConfigEnv("darwin", { GIT_CONFIG_COUNT: "2" }), {}); + }); + + it("appends after inherited entries instead of overwriting them", () => { + assert.deepStrictEqual(windowsLongPathConfigEnv("win32", { GIT_CONFIG_COUNT: "2" }), { + GIT_CONFIG_COUNT: "3", + GIT_CONFIG_KEY_2: "core.longpaths", + GIT_CONFIG_VALUE_2: "true", + }); + }); + + it("treats an absent or empty count as no inherited entries", () => { + const expected = { + GIT_CONFIG_COUNT: "1", + GIT_CONFIG_KEY_0: "core.longpaths", + GIT_CONFIG_VALUE_0: "true", + }; + assert.deepStrictEqual(windowsLongPathConfigEnv("win32", { GIT_CONFIG_COUNT: "" }), expected); + assert.deepStrictEqual(windowsLongPathConfigEnv("win32", { GIT_CONFIG_COUNT: " " }), expected); + assert.deepStrictEqual(windowsLongPathConfigEnv("win32", { GIT_CONFIG_COUNT: "0" }), expected); + }); + + it("leaves a malformed count alone so git still reports it", () => { + for (const malformed of ["not-a-number", "2x", "-1", "1.5"]) { + assert.deepStrictEqual( + windowsLongPathConfigEnv("win32", { + GIT_CONFIG_COUNT: malformed, + GIT_CONFIG_KEY_0: "user.name", + GIT_CONFIG_VALUE_0: "inherited", + }), + {}, + `expected no injection for GIT_CONFIG_COUNT=${malformed}`, + ); + } + }); +}); diff --git a/apps/server/src/vcs/GitVcsDriverCore.ts b/apps/server/src/vcs/GitVcsDriverCore.ts index 3c7e018ddea7..8d4bc9729705 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.ts @@ -27,6 +27,7 @@ import { type VcsRef, } from "@t3tools/contracts"; import { dedupeRemoteBranchesWithLocalMatches, normalizeGitRemoteUrl } from "@t3tools/shared/git"; +import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; import { compactTraceAttributes } from "@t3tools/shared/observability"; import { decodeJsonResult } from "@t3tools/shared/schemaJson"; import { gitCommandDuration, gitCommandsTotal, withMetrics } from "../observability/Metrics.ts"; @@ -108,6 +109,45 @@ const NON_REPOSITORY_REMOTE_STATUS_DETAILS = Object.freeze { + if (platform !== "win32") { + return {}; + } + const inherited = env.GIT_CONFIG_COUNT?.trim(); + if (inherited !== undefined && inherited !== "" && !/^\d+$/.test(inherited)) { + return {}; + } + const count = inherited === undefined || inherited === "" ? 0 : Number.parseInt(inherited, 10); + return { + GIT_CONFIG_COUNT: String(count + 1), + [`GIT_CONFIG_KEY_${count}`]: "core.longpaths", + [`GIT_CONFIG_VALUE_${count}`]: "true", + }; +}; + type TraceTailState = { processedChars: number; remainder: string; @@ -727,6 +767,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* const commandSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; const { worktreesDir } = yield* ServerConfig; const crypto = yield* Crypto.Crypto; + const hostPlatform = yield* HostProcessPlatform; const executeRaw: GitVcsDriver.GitVcsDriver["Service"]["execute"] = Effect.fnUntraced( function* (input) { @@ -751,14 +792,18 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* }), ), ); + const spawnEnv = { + ...process.env, + ...input.env, + ...trace2Monitor.env, + }; const child = yield* commandSpawner .spawn( ChildProcess.make("git", commandInput.args, { cwd: commandInput.cwd, env: { - ...process.env, - ...input.env, - ...trace2Monitor.env, + ...spawnEnv, + ...windowsLongPathConfigEnv(hostPlatform, spawnEnv), }, }), ) From cdb608e935a8a59c55c7e54c505c0a1b71470d82 Mon Sep 17 00:00:00 2001 From: That1Drifter Date: Wed, 12 Aug 2026 09:14:20 -0500 Subject: [PATCH 2/4] fix(server): find inherited GIT_CONFIG_COUNT case-insensitively Windows environment variable names ignore case, but spreading process.env keeps the host's casing, so an inherited git_config_count was treated as absent. The helper then added GIT_CONFIG_COUNT=1 beside it, spawn kept only one of the case-duplicated entries, and the caller's GIT_CONFIG_* entries were silently dropped. Locate the count case-insensitively and reuse its name when incrementing. Co-Authored-By: Claude Fable 5 --- apps/server/src/vcs/GitVcsDriverCore.test.ts | 20 ++++++++++++++++++++ apps/server/src/vcs/GitVcsDriverCore.ts | 12 ++++++++++-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/apps/server/src/vcs/GitVcsDriverCore.test.ts b/apps/server/src/vcs/GitVcsDriverCore.test.ts index 1325c08cbe63..b82915f2a5a8 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.test.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.test.ts @@ -2218,6 +2218,26 @@ describe("windowsLongPathConfigEnv", () => { assert.deepStrictEqual(windowsLongPathConfigEnv("win32", { GIT_CONFIG_COUNT: "0" }), expected); }); + it("reuses an inherited count whose name differs only in case", () => { + const result = windowsLongPathConfigEnv("win32", { git_config_count: "2" }); + + assert.deepStrictEqual(result, { + git_config_count: "3", + GIT_CONFIG_KEY_2: "core.longpaths", + GIT_CONFIG_VALUE_2: "true", + }); + // Two entries differing only in case collapse on spawn, and the survivor + // would decide whether the caller's config or this one is honoured. + assert.equal( + Object.keys(result).filter((key) => key.toUpperCase() === "GIT_CONFIG_COUNT").length, + 1, + ); + }); + + it("leaves a malformed count alone regardless of its casing", () => { + assert.deepStrictEqual(windowsLongPathConfigEnv("win32", { git_config_count: "nope" }), {}); + }); + it("leaves a malformed count alone so git still reports it", () => { for (const malformed of ["not-a-number", "2x", "-1", "1.5"]) { assert.deepStrictEqual( diff --git a/apps/server/src/vcs/GitVcsDriverCore.ts b/apps/server/src/vcs/GitVcsDriverCore.ts index 8d4bc9729705..f52c87c92aea 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.ts @@ -128,6 +128,12 @@ const NON_REPOSITORY_REMOTE_STATUS_DETAILS = Object.freeze key.toUpperCase() === "GIT_CONFIG_COUNT") ?? "GIT_CONFIG_COUNT"; + const inherited = env[countKey]?.trim(); if (inherited !== undefined && inherited !== "" && !/^\d+$/.test(inherited)) { return {}; } const count = inherited === undefined || inherited === "" ? 0 : Number.parseInt(inherited, 10); return { - GIT_CONFIG_COUNT: String(count + 1), + [countKey]: String(count + 1), [`GIT_CONFIG_KEY_${count}`]: "core.longpaths", [`GIT_CONFIG_VALUE_${count}`]: "true", }; From 16dae40a4fc068f7d2aa5159874a6baf23f0c4a8 Mon Sep 17 00:00:00 2001 From: That1Drifter Date: Sun, 6 Sep 2026 21:48:06 -0500 Subject: [PATCH 3/4] fix(server): cover checkpoint long paths and refresh regression tests --- apps/server/src/vcs/GitVcsDriver.test.ts | 70 ++++++++++++++- apps/server/src/vcs/GitVcsDriver.ts | 17 +++- apps/server/src/vcs/GitVcsDriverCore.test.ts | 89 +++++++++----------- apps/server/src/vcs/GitVcsDriverCore.ts | 13 +-- 4 files changed, 126 insertions(+), 63 deletions(-) diff --git a/apps/server/src/vcs/GitVcsDriver.test.ts b/apps/server/src/vcs/GitVcsDriver.test.ts index 031055a3b6cb..40ace88d4c35 100644 --- a/apps/server/src/vcs/GitVcsDriver.test.ts +++ b/apps/server/src/vcs/GitVcsDriver.test.ts @@ -5,9 +5,10 @@ import * as Layer from "effect/Layer"; import * as Path from "effect/Path"; import * as PlatformError from "effect/PlatformError"; import { ChildProcessSpawner } from "effect/unstable/process"; -import { assert, it } from "@effect/vitest"; +import { assert, it, vi } from "@effect/vitest"; -import { GitCommandError } from "@t3tools/contracts"; +import { CheckpointRef, GitCommandError } from "@t3tools/contracts"; +import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; import * as ServerConfig from "../config.ts"; import * as GitVcsDriver from "./GitVcsDriver.ts"; import * as VcsProcess from "./VcsProcess.ts"; @@ -84,7 +85,7 @@ it.effect("GitVcsDriver forwards execute env to the VCS process", () => { outputMode: "error", }); - assert.deepStrictEqual(observedEnv, { + assert.deepInclude(observedEnv, { GIT_INDEX_FILE: "/tmp/t3-index", }); assert.strictEqual(observedAppendTruncationMarker, true); @@ -112,3 +113,66 @@ it.effect("GitVcsDriver forwards execute env to the VCS process", () => { ), ); }); + +for (const platform of ["win32", "linux"] as const) { + it.effect(`GitVcsDriver applies long path configuration for ${platform}`, () => + Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const cwd = yield* fileSystem.makeTempDirectoryScoped(); + const driver = yield* GitVcsDriver.makeVcsDriverShape(); + const readConfig = Effect.fn("readConfig")(function* (key: string) { + const result = yield* driver.execute({ + operation: "GitVcsDriver.test.longpaths", + cwd, + args: ["config", "--get", key], + env: { + GIT_CONFIG_COUNT: "2", + GIT_CONFIG_KEY_0: "user.name", + GIT_CONFIG_VALUE_0: "inherited-name", + GIT_CONFIG_KEY_1: "core.longpaths", + GIT_CONFIG_VALUE_1: "false", + GIT_CONFIG_KEY_2: "unused.fixture", + GIT_CONFIG_VALUE_2: "stale", + }, + }); + return result.stdout.trim(); + }); + + assert.equal(yield* readConfig("core.longpaths"), platform === "win32" ? "true" : "false"); + assert.equal(yield* readConfig("user.name"), "inherited-name"); + }).pipe(Effect.provide(GitContractLayer), Effect.provideService(HostProcessPlatform, platform)), + ); +} + +it.effect("captures and restores checkpoints with paths beyond MAX_PATH", () => + Effect.gen(function* () { + yield* Effect.acquireRelease( + Effect.sync(() => { + const count = Number(process.env.GIT_CONFIG_COUNT ?? "0"); + vi.stubEnv(`GIT_CONFIG_KEY_${count}`, "core.longpaths"); + vi.stubEnv(`GIT_CONFIG_VALUE_${count}`, "false"); + vi.stubEnv("GIT_CONFIG_COUNT", String(count + 1)); + }), + () => Effect.sync(() => vi.unstubAllEnvs()), + ); + const fileSystem = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const cwd = yield* fileSystem.makeTempDirectoryScoped(); + const driver = yield* GitVcsDriver.makeVcsDriverShape(); + yield* driver.initRepository({ cwd }); + const filePath = path.join( + cwd, + ...Array.from({ length: 6 }, () => "nested-".repeat(6)), + "file.txt", + ); + assert.isAbove(filePath.length, 260); + yield* fileSystem.makeDirectory(path.dirname(filePath), { recursive: true }); + yield* fileSystem.writeFileString(filePath, "checkpoint content\n"); + + const input = { cwd, checkpointRef: CheckpointRef.make("refs/t3/checkpoints/longpaths") }; + yield* driver.checkpoints.captureCheckpoint(input); + yield* fileSystem.writeFileString(filePath, "changed content\n"); + assert.isTrue(yield* driver.checkpoints.restoreCheckpoint(input)); + assert.equal(yield* fileSystem.readFileString(filePath), "checkpoint content\n"); + }).pipe(Effect.provide(GitContractLayer), Effect.provideService(HostProcessPlatform, "win32")), +); diff --git a/apps/server/src/vcs/GitVcsDriver.ts b/apps/server/src/vcs/GitVcsDriver.ts index f1e48a24d6fe..e36ff980192e 100644 --- a/apps/server/src/vcs/GitVcsDriver.ts +++ b/apps/server/src/vcs/GitVcsDriver.ts @@ -30,10 +30,12 @@ import { type VcsStatusInput, type VcsStatusResult, } from "@t3tools/contracts"; +import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; import { makeGitVcsDriverCore, PATCH_RENDER_PREFIX_ARGS, splitNullSeparatedGitStdoutPaths, + windowsLongPathConfigEnv, } from "./GitVcsDriverCore.ts"; import * as VcsDriver from "./VcsDriver.ts"; import * as VcsProcess from "./VcsProcess.ts"; @@ -420,7 +422,7 @@ function parseGitRemoteVerboseOutput( return remotes; } -const gitCommand = ( +const gitCommand = Effect.fn("GitVcsDriver.gitCommand")(function* ( process: VcsProcess.VcsProcess["Service"], operation: string, cwd: string, @@ -434,15 +436,21 @@ const gitCommand = ( readonly outputMode?: VcsProcess.VcsProcessInput["outputMode"]; readonly appendTruncationMarker?: boolean; }, -) => - process.run({ +) { + const platform = yield* HostProcessPlatform; + let env = options?.env; + if (platform === "win32") { + const inheritedEnv = { ...globalThis.process.env, ...env }; + env = { ...inheritedEnv, ...windowsLongPathConfigEnv(platform, inheritedEnv) }; + } + return yield* process.run({ operation, command: "git", args: ["-C", cwd, ...args], cwd, spawnCwd: globalThis.process.cwd(), ...(options?.stdin !== undefined ? { stdin: options.stdin } : {}), - ...(options?.env !== undefined ? { env: options.env } : {}), + ...(env !== undefined ? { env } : {}), ...(options?.allowNonZeroExit !== undefined ? { allowNonZeroExit: options.allowNonZeroExit } : {}), @@ -453,6 +461,7 @@ const gitCommand = ( ? { appendTruncationMarker: options.appendTruncationMarker } : {}), }); +}); export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* () { const fileSystem = yield* FileSystem.FileSystem; diff --git a/apps/server/src/vcs/GitVcsDriverCore.test.ts b/apps/server/src/vcs/GitVcsDriverCore.test.ts index b82915f2a5a8..4ebf67fb4254 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.test.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.test.ts @@ -19,7 +19,6 @@ import * as TestClock from "effect/testing/TestClock"; import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; import { GitCommandError, type ReviewDiffFileContentsInput } from "@t3tools/contracts"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; import { ServerConfig } from "../config.ts"; import { makeGitVcsDriverCore, @@ -2117,70 +2116,49 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => { }); describe("Windows long path configuration", () => { - const captureGitEnv = (platform: NodeJS.Platform) => { - const captured: Array = []; - const spawner = ChildProcessSpawner.make((command) => - Effect.sync(() => { - if (!ChildProcess.isStandardCommand(command)) { - return assert.fail("expected a standard Git command"); - } - captured.push(command.options.env ?? {}); - return makeSuccessfulHandle(""); - }), - ); + const readGitConfig = Effect.fn("readGitConfig")(function* ( + platform: NodeJS.Platform, + key: string, + ) { const layer = GitVcsDriver.layer.pipe( Layer.provide(ServerConfigLayer), - Layer.provideMerge( - Layer.merge( - NodeServices.layer, - Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner), - ), - ), + Layer.provideMerge(NodeServices.layer), Layer.provide(Layer.succeed(HostProcessPlatform, platform)), ); - return Effect.gen(function* () { + return yield* Effect.gen(function* () { const driver = yield* GitVcsDriver.GitVcsDriver; const cwd = yield* makeTmpDir("git-longpath-test-"); - - yield* driver.execute({ - operation: "GitVcsDriverTest.execute", + const result = yield* driver.execute({ + operation: "GitVcsDriverTest.readGitConfig", cwd, - args: ["status"], + args: ["config", "--get", key], + // Override the suite's longpaths=true so it cannot mask missing injection. + env: { + GIT_CONFIG_COUNT: "2", + GIT_CONFIG_KEY_0: "user.name", + GIT_CONFIG_VALUE_0: "inherited-name", + GIT_CONFIG_KEY_1: "core.longpaths", + GIT_CONFIG_VALUE_1: "false", + GIT_CONFIG_KEY_2: "unused.fixture", + GIT_CONFIG_VALUE_2: "stale", + }, }); - - assert.equal(captured.length, 1); - return captured[0] ?? {}; + return result.stdout.trim(); }).pipe(Effect.provide(layer)); - }; - - /** - * Asserted by locating the injected entry rather than by pinning it to index - * zero: the spawned environment inherits the host's, so a machine that already - * exports GIT_CONFIG_* shifts the index without changing the behaviour here. - */ - const findLongPathIndex = (env: NodeJS.ProcessEnv): string | undefined => { - const key = Object.keys(env).find( - (candidate) => candidate.startsWith("GIT_CONFIG_KEY_") && env[candidate] === "core.longpaths", - ); - return key?.slice("GIT_CONFIG_KEY_".length); - }; + }); - it.effect("reaches the spawned git process on Windows", () => + it.effect("enables long paths in Git on Windows while preserving inherited config", () => Effect.gen(function* () { - const env = yield* captureGitEnv("win32"); - - const index = findLongPathIndex(env); - assert.notEqual(index, undefined); - assert.equal(env[`GIT_CONFIG_VALUE_${index}`], "true"); + assert.equal(yield* readGitConfig("win32", "core.longpaths"), "true"); + assert.equal(yield* readGitConfig("win32", "user.name"), "inherited-name"); }), ); - it.effect("is left off platforms without MAX_PATH", () => + it.effect("preserves inherited Git config on platforms without MAX_PATH", () => Effect.gen(function* () { - const env = yield* captureGitEnv("linux"); - - assert.equal(findLongPathIndex(env), undefined); + assert.equal(yield* readGitConfig("linux", "core.longpaths"), "false"); + assert.equal(yield* readGitConfig("linux", "user.name"), "inherited-name"); }), ); }); @@ -2214,10 +2192,19 @@ describe("windowsLongPathConfigEnv", () => { GIT_CONFIG_VALUE_0: "true", }; assert.deepStrictEqual(windowsLongPathConfigEnv("win32", { GIT_CONFIG_COUNT: "" }), expected); - assert.deepStrictEqual(windowsLongPathConfigEnv("win32", { GIT_CONFIG_COUNT: " " }), expected); assert.deepStrictEqual(windowsLongPathConfigEnv("win32", { GIT_CONFIG_COUNT: "0" }), expected); }); + it("accepts Git's leading whitespace and optional plus sign in a count", () => { + for (const count of [" 2", "+2", "\t+2", "02"]) { + assert.deepStrictEqual(windowsLongPathConfigEnv("win32", { GIT_CONFIG_COUNT: count }), { + GIT_CONFIG_COUNT: "3", + GIT_CONFIG_KEY_2: "core.longpaths", + GIT_CONFIG_VALUE_2: "true", + }); + } + }); + it("reuses an inherited count whose name differs only in case", () => { const result = windowsLongPathConfigEnv("win32", { git_config_count: "2" }); @@ -2239,7 +2226,7 @@ describe("windowsLongPathConfigEnv", () => { }); it("leaves a malformed count alone so git still reports it", () => { - for (const malformed of ["not-a-number", "2x", "-1", "1.5"]) { + for (const malformed of ["not-a-number", "2x", "-1", "1.5", " ", "1 ", "1\n"]) { assert.deepStrictEqual( windowsLongPathConfigEnv("win32", { GIT_CONFIG_COUNT: malformed, diff --git a/apps/server/src/vcs/GitVcsDriverCore.ts b/apps/server/src/vcs/GitVcsDriverCore.ts index f52c87c92aea..f30b1beb81b3 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.ts @@ -116,9 +116,8 @@ const NON_REPOSITORY_REMOTE_STATUS_DETAILS = Object.freeze key.toUpperCase() === "GIT_CONFIG_COUNT") ?? "GIT_CONFIG_COUNT"; - const inherited = env[countKey]?.trim(); - if (inherited !== undefined && inherited !== "" && !/^\d+$/.test(inherited)) { + const inherited = env[countKey]; + if ( + inherited !== undefined && + inherited !== "" && + /^[ \t\r\n\v\f]*\+?\d+/.exec(inherited)?.[0] !== inherited + ) { return {}; } const count = inherited === undefined || inherited === "" ? 0 : Number.parseInt(inherited, 10); From 2d097f0220226ba01a11d4141f8702156a6cbf29 Mon Sep 17 00:00:00 2001 From: That1Drifter Date: Sun, 6 Sep 2026 22:03:46 -0500 Subject: [PATCH 4/4] fix(server): honor case-insensitive Git config count overrides --- apps/server/src/vcs/GitVcsDriver.test.ts | 14 +++-- apps/server/src/vcs/GitVcsDriver.ts | 5 +- apps/server/src/vcs/GitVcsDriverCore.test.ts | 55 ++++++++++++++++++-- apps/server/src/vcs/GitVcsDriverCore.ts | 34 ++++++------ 4 files changed, 82 insertions(+), 26 deletions(-) diff --git a/apps/server/src/vcs/GitVcsDriver.test.ts b/apps/server/src/vcs/GitVcsDriver.test.ts index 40ace88d4c35..7caf2ae49f74 100644 --- a/apps/server/src/vcs/GitVcsDriver.test.ts +++ b/apps/server/src/vcs/GitVcsDriver.test.ts @@ -114,8 +114,12 @@ it.effect("GitVcsDriver forwards execute env to the VCS process", () => { ); }); -for (const platform of ["win32", "linux"] as const) { - it.effect(`GitVcsDriver applies long path configuration for ${platform}`, () => +for (const [platform, countKey] of [ + ["win32", "GIT_CONFIG_COUNT"], + ["win32", "git_config_count"], + ["linux", "GIT_CONFIG_COUNT"], +] as const) { + it.effect(`GitVcsDriver applies long path configuration for ${platform} with ${countKey}`, () => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const cwd = yield* fileSystem.makeTempDirectoryScoped(); @@ -126,13 +130,13 @@ for (const platform of ["win32", "linux"] as const) { cwd, args: ["config", "--get", key], env: { - GIT_CONFIG_COUNT: "2", + [countKey]: "2", GIT_CONFIG_KEY_0: "user.name", GIT_CONFIG_VALUE_0: "inherited-name", GIT_CONFIG_KEY_1: "core.longpaths", GIT_CONFIG_VALUE_1: "false", - GIT_CONFIG_KEY_2: "unused.fixture", - GIT_CONFIG_VALUE_2: "stale", + GIT_CONFIG_KEY_2: "user.name", + GIT_CONFIG_VALUE_2: "outside-count", }, }); return result.stdout.trim(); diff --git a/apps/server/src/vcs/GitVcsDriver.ts b/apps/server/src/vcs/GitVcsDriver.ts index e36ff980192e..681074be7aec 100644 --- a/apps/server/src/vcs/GitVcsDriver.ts +++ b/apps/server/src/vcs/GitVcsDriver.ts @@ -32,10 +32,10 @@ import { } from "@t3tools/contracts"; import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; import { + gitCommandEnv, makeGitVcsDriverCore, PATCH_RENDER_PREFIX_ARGS, splitNullSeparatedGitStdoutPaths, - windowsLongPathConfigEnv, } from "./GitVcsDriverCore.ts"; import * as VcsDriver from "./VcsDriver.ts"; import * as VcsProcess from "./VcsProcess.ts"; @@ -440,8 +440,7 @@ const gitCommand = Effect.fn("GitVcsDriver.gitCommand")(function* ( const platform = yield* HostProcessPlatform; let env = options?.env; if (platform === "win32") { - const inheritedEnv = { ...globalThis.process.env, ...env }; - env = { ...inheritedEnv, ...windowsLongPathConfigEnv(platform, inheritedEnv) }; + env = gitCommandEnv(platform, globalThis.process.env, env); } return yield* process.run({ operation, diff --git a/apps/server/src/vcs/GitVcsDriverCore.test.ts b/apps/server/src/vcs/GitVcsDriverCore.test.ts index 4ebf67fb4254..16df73bcf0d8 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.test.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.test.ts @@ -21,6 +21,7 @@ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; import { GitCommandError, type ReviewDiffFileContentsInput } from "@t3tools/contracts"; import { ServerConfig } from "../config.ts"; import { + gitCommandEnv, makeGitVcsDriverCore, splitNullSeparatedGitStdoutPaths, windowsLongPathConfigEnv, @@ -2119,6 +2120,7 @@ describe("Windows long path configuration", () => { const readGitConfig = Effect.fn("readGitConfig")(function* ( platform: NodeJS.Platform, key: string, + countKey = "GIT_CONFIG_COUNT", ) { const layer = GitVcsDriver.layer.pipe( Layer.provide(ServerConfigLayer), @@ -2135,13 +2137,13 @@ describe("Windows long path configuration", () => { args: ["config", "--get", key], // Override the suite's longpaths=true so it cannot mask missing injection. env: { - GIT_CONFIG_COUNT: "2", + [countKey]: "2", GIT_CONFIG_KEY_0: "user.name", GIT_CONFIG_VALUE_0: "inherited-name", GIT_CONFIG_KEY_1: "core.longpaths", GIT_CONFIG_VALUE_1: "false", - GIT_CONFIG_KEY_2: "unused.fixture", - GIT_CONFIG_VALUE_2: "stale", + GIT_CONFIG_KEY_2: "user.name", + GIT_CONFIG_VALUE_2: "outside-count", }, }); return result.stdout.trim(); @@ -2161,6 +2163,53 @@ describe("Windows long path configuration", () => { assert.equal(yield* readGitConfig("linux", "user.name"), "inherited-name"); }), ); + + it.effect("honors a caller count with different casing from the host", () => + Effect.gen(function* () { + assert.equal( + yield* readGitConfig("win32", "user.name", "git_config_count"), + "inherited-name", + ); + assert.equal(yield* readGitConfig("win32", "core.longpaths", "git_config_count"), "true"); + }), + ); +}); + +describe("gitCommandEnv", () => { + it("merges count overrides regardless of casing without changing the sources", () => { + for (const [hostKey, callerKey] of [ + ["GIT_CONFIG_COUNT", "git_config_count"], + ["git_config_count", "GIT_CONFIG_COUNT"], + ] as const) { + const host = Object.freeze({ + [hostKey]: "3", + GIT_CONFIG_KEY_0: "user.name", + GIT_CONFIG_VALUE_0: "inherited-name", + }); + const caller = Object.freeze({ [callerKey]: "1" }); + assert.deepStrictEqual(gitCommandEnv("win32", host, caller), { + GIT_CONFIG_COUNT: "2", + GIT_CONFIG_KEY_0: "user.name", + GIT_CONFIG_VALUE_0: "inherited-name", + GIT_CONFIG_KEY_1: "core.longpaths", + GIT_CONFIG_VALUE_1: "true", + }); + } + }); + + it("preserves a malformed caller override for Git to reject", () => { + assert.deepStrictEqual( + gitCommandEnv("win32", { GIT_CONFIG_COUNT: "2" }, { git_config_count: "nope" }), + { GIT_CONFIG_COUNT: "nope" }, + ); + }); + + it("keeps non-Windows environment variable names case-sensitive", () => { + assert.deepStrictEqual( + gitCommandEnv("linux", { GIT_CONFIG_COUNT: "2" }, undefined, { git_config_count: "1" }), + { GIT_CONFIG_COUNT: "2", git_config_count: "1" }, + ); + }); }); describe("windowsLongPathConfigEnv", () => { diff --git a/apps/server/src/vcs/GitVcsDriverCore.ts b/apps/server/src/vcs/GitVcsDriverCore.ts index f30b1beb81b3..39a6f6904876 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.ts @@ -127,12 +127,6 @@ const NON_REPOSITORY_REMOTE_STATUS_DETAILS = Object.freeze +): NodeJS.ProcessEnv => { + const env: NodeJS.ProcessEnv = {}; + for (const source of sources) { + if (platform !== "win32") { + Object.assign(env, source); + continue; + } + for (const [key, value] of Object.entries(source ?? {})) { + env[key.toUpperCase() === "GIT_CONFIG_COUNT" ? "GIT_CONFIG_COUNT" : key] = value; + } + } + return { ...env, ...windowsLongPathConfigEnv(platform, env) }; +}; + type TraceTailState = { processedChars: number; remainder: string; @@ -803,19 +815,11 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* }), ), ); - const spawnEnv = { - ...process.env, - ...input.env, - ...trace2Monitor.env, - }; const child = yield* commandSpawner .spawn( ChildProcess.make("git", commandInput.args, { cwd: commandInput.cwd, - env: { - ...spawnEnv, - ...windowsLongPathConfigEnv(hostPlatform, spawnEnv), - }, + env: gitCommandEnv(hostPlatform, process.env, input.env, trace2Monitor.env), }), ) .pipe(