Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions apps/server/src/vcs/GitVcsDriver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -1116,3 +1117,45 @@ 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 { 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<string>[] = [];
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");
assert.strictEqual((yield* git(["ls-tree", checkpointRef, "--", "empty"])).stdout, "");
const worktreeCommands = observedArgs.filter((args) =>
["add", "--others", "restore", "clean", "reset"].some((command) => args.includes(command)),
);
// 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(" "));
else assert.strictEqual(setting, -1, args.join(" "));
}
}).pipe(Effect.scoped, Effect.provide(GitContractLayer)),
);
34 changes: 30 additions & 4 deletions apps/server/src/vcs/GitVcsDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
type VcsStatusResult,
type WorktreeSubmodules,
} from "@t3tools/contracts";
import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
import {
makeGitVcsDriverCore,
PATCH_RENDER_PREFIX_ARGS,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -939,6 +947,7 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* (
args: [
...indexConfig,
...durableWrite,
...longPaths,
"add",
...(sparseCheckout ? ["--sparse"] : []),
"-A",
Expand All @@ -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,
});
Expand Down Expand Up @@ -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.
Expand All @@ -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) {
Expand All @@ -1118,7 +1144,7 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* (
yield* execute({
operation,
cwd: input.cwd,
args: ["reset", "--quiet", "--", "."],
args: [...longPaths, "reset", "--quiet", "--", "."],
});
}

Expand Down
Loading