From db2e6c8b1510c5a03d10747328282e1ae49f6408 Mon Sep 17 00:00:00 2001 From: Keirara Date: Thu, 24 Sep 2026 21:07:34 -0400 Subject: [PATCH 1/2] fix(server): checkpoints work in Windows workspaces with paths over 260 characters Git for Windows cannot open paths longer than MAX_PATH unless core.longpaths is on, and it is off by default. In such a workspace, checkpoint capture's `git add -A` exited 128 with "Filename too long", so every turn failed to checkpoint. Checkpoint commands that read or write the working tree now pass `-c core.longpaths=true` on Windows. Co-authored-by: Claude Opus 5.5 (1M context) --- apps/server/src/vcs/GitVcsDriver.test.ts | 39 ++++++++++++++++++++++++ apps/server/src/vcs/GitVcsDriver.ts | 34 ++++++++++++++++++--- 2 files changed, 69 insertions(+), 4 deletions(-) diff --git a/apps/server/src/vcs/GitVcsDriver.test.ts b/apps/server/src/vcs/GitVcsDriver.test.ts index 4df6d0eefc5c..4594472ddb0c 100644 --- a/apps/server/src/vcs/GitVcsDriver.test.ts +++ b/apps/server/src/vcs/GitVcsDriver.test.ts @@ -15,6 +15,7 @@ import { ChildProcessSpawner } from "effect/unstable/process"; import { assert, it } from "@effect/vitest"; import { CheckpointRef, GitCommandError, VcsProcessExitError } from "@t3tools/contracts"; +import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; import * as ServerConfig from "../config.ts"; import * as CheckpointStore from "../checkpointing/CheckpointStore.ts"; import * as ProcessRunner from "../processRunner.ts"; @@ -1116,3 +1117,41 @@ it.effect("GitVcsDriver flushes checkpoint objects and refs to disk before publi ), ); }); + +it.effect.each(["win32", "linux"] as const)( + "checkpoint working-tree commands enable Git long paths only on Windows (%s)", + (platform) => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const liveProcess = yield* VcsProcess.VcsProcess; + const driver = yield* GitVcsDriver.makeVcsDriverShape(); + const cwd = yield* fs.makeTempDirectoryScoped({ prefix: "t3-checkpoint-long-paths-" }); + const { checkpointRef } = yield* makeCheckpointFixture(driver, cwd); + const observedArgs: ReadonlyArray[] = []; + const platformDriver = yield* GitVcsDriver.makeVcsDriverShape().pipe( + Effect.provideService(HostProcessPlatform, platform), + Effect.provideService(VcsProcess.VcsProcess, { + run: (input) => { + observedArgs.push(input.args); + return liveProcess.run(input); + }, + }), + ); + + yield* platformDriver.checkpoints.captureCheckpoint({ cwd, checkpointRef }); + yield* fs.writeFileString(path.join(cwd, "file.txt"), "changed\n"); + assert.isTrue(yield* platformDriver.checkpoints.restoreCheckpoint({ cwd, checkpointRef })); + + assert.strictEqual(yield* fs.readFileString(path.join(cwd, "file.txt")), "unstaged\n"); + const worktreeCommands = observedArgs.filter((args) => + ["add", "restore", "clean", "reset"].some((command) => args.includes(command)), + ); + assert.strictEqual(worktreeCommands.length, 4); + for (const args of worktreeCommands) { + const setting = args.indexOf("core.longpaths=true"); + if (platform === "win32") assert.strictEqual(args[setting - 1], "-c", args.join(" ")); + else assert.strictEqual(setting, -1, args.join(" ")); + } + }).pipe(Effect.scoped, Effect.provide(GitContractLayer)), +); diff --git a/apps/server/src/vcs/GitVcsDriver.ts b/apps/server/src/vcs/GitVcsDriver.ts index f450eef0dc24..57e0b2be5f82 100644 --- a/apps/server/src/vcs/GitVcsDriver.ts +++ b/apps/server/src/vcs/GitVcsDriver.ts @@ -32,6 +32,7 @@ import { type VcsStatusResult, type WorktreeSubmodules, } from "@t3tools/contracts"; +import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; import { makeGitVcsDriverCore, PATCH_RENDER_PREFIX_ARGS, @@ -775,6 +776,13 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( "core.fsyncMethod=fsync", ] as const; + // Git for Windows cannot open paths longer than MAX_PATH (260 characters) unless + // core.longpaths is on, and it is off by default. Without it, checkpoint `git add -A` exits + // 128 with "Filename too long" in any workspace holding such a path, and restore cannot write + // or clean those files. Checkpoints only snapshot and restore paths that already exist, so + // commands that touch the working tree turn it on even if the user's config says false. + const longPaths = (yield* HostProcessPlatform) === "win32" ? ["-c", "core.longpaths=true"] : []; + const checkpoints: VcsDriver.VcsCheckpointOps = { captureCheckpoint: Effect.fn("GitVcsDriver.checkpoints.captureCheckpoint")(function* (input) { const operation = VcsProcess.CHECKPOINT_CAPTURE_OPERATION; @@ -939,6 +947,7 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( args: [ ...indexConfig, ...durableWrite, + ...longPaths, "add", ...(sparseCheckout ? ["--sparse"] : []), "-A", @@ -957,7 +966,15 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( const untracked = yield* execute({ operation, cwd: input.cwd, - args: ["ls-files", "--others", "--exclude-standard", "-z", "--", "."], + args: [ + ...longPaths, + "ls-files", + "--others", + "--exclude-standard", + "-z", + "--", + ".", + ], env: commitEnv, maxOutputBytes: WORKSPACE_FILES_MAX_OUTPUT_BYTES, }); @@ -1072,7 +1089,16 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( yield* execute({ operation, cwd: input.cwd, - args: ["restore", "--source", commitOid, "--worktree", "--staged", "--", "."], + args: [ + ...longPaths, + "restore", + "--source", + commitOid, + "--worktree", + "--staged", + "--", + ".", + ], }); } // Restoring away the last tracked file can remove a nested workspace directory. @@ -1091,7 +1117,7 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( const cleaned = yield* execute({ operation, cwd: input.cwd, - args: ["clean", "-fd", "--", "."], + args: [...longPaths, "clean", "-fd", "--", "."], allowNonZeroExit: true, }); if (cleaned.exitCode !== 0) { @@ -1118,7 +1144,7 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( yield* execute({ operation, cwd: input.cwd, - args: ["reset", "--quiet", "--", "."], + args: [...longPaths, "reset", "--quiet", "--", "."], }); } From 468a95312c879ad61121d3a5e78492466d75366e Mon Sep 17 00:00:00 2001 From: Keirara Date: Thu, 24 Sep 2026 22:47:01 -0400 Subject: [PATCH 2/2] test(server): cover the Windows checkpoint recovery command An unborn nested repository in the long-paths fixture makes the first `git add` fail, so the nested-repo recovery listing now runs and the test checks that it also carries core.longpaths on Windows. Co-authored-by: Claude Opus 5.5 (1M context) --- apps/server/src/vcs/GitVcsDriver.test.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/server/src/vcs/GitVcsDriver.test.ts b/apps/server/src/vcs/GitVcsDriver.test.ts index 4594472ddb0c..c0ff1cfe2016 100644 --- a/apps/server/src/vcs/GitVcsDriver.test.ts +++ b/apps/server/src/vcs/GitVcsDriver.test.ts @@ -1127,7 +1127,9 @@ it.effect.each(["win32", "linux"] as const)( const liveProcess = yield* VcsProcess.VcsProcess; const driver = yield* GitVcsDriver.makeVcsDriverShape(); const cwd = yield* fs.makeTempDirectoryScoped({ prefix: "t3-checkpoint-long-paths-" }); - const { checkpointRef } = yield* makeCheckpointFixture(driver, cwd); + const { git, checkpointRef } = yield* makeCheckpointFixture(driver, cwd); + // An unborn nested repository fails the first `add`, so recovery's listing runs too. + yield* git(["init", "empty"]); const observedArgs: ReadonlyArray[] = []; const platformDriver = yield* GitVcsDriver.makeVcsDriverShape().pipe( Effect.provideService(HostProcessPlatform, platform), @@ -1144,10 +1146,12 @@ it.effect.each(["win32", "linux"] as const)( assert.isTrue(yield* platformDriver.checkpoints.restoreCheckpoint({ cwd, checkpointRef })); assert.strictEqual(yield* fs.readFileString(path.join(cwd, "file.txt")), "unstaged\n"); + assert.strictEqual((yield* git(["ls-tree", checkpointRef, "--", "empty"])).stdout, ""); const worktreeCommands = observedArgs.filter((args) => - ["add", "restore", "clean", "reset"].some((command) => args.includes(command)), + ["add", "--others", "restore", "clean", "reset"].some((command) => args.includes(command)), ); - assert.strictEqual(worktreeCommands.length, 4); + // Failed add, recovery listing, retried add, then restore, clean and reset. + assert.strictEqual(worktreeCommands.length, 6); for (const args of worktreeCommands) { const setting = args.indexOf("core.longpaths=true"); if (platform === "win32") assert.strictEqual(args[setting - 1], "-c", args.join(" "));