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
74 changes: 71 additions & 3 deletions apps/server/src/vcs/GitVcsDriver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -112,3 +113,70 @@ it.effect("GitVcsDriver forwards execute env to the VCS process", () => {
),
);
});

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();
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: {
[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: "user.name",
GIT_CONFIG_VALUE_2: "outside-count",
},
});
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")),
);
16 changes: 12 additions & 4 deletions apps/server/src/vcs/GitVcsDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ import {
type VcsStatusInput,
type VcsStatusResult,
} from "@t3tools/contracts";
import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
import {
gitCommandEnv,
makeGitVcsDriverCore,
PATCH_RENDER_PREFIX_ARGS,
splitNullSeparatedGitStdoutPaths,
Expand Down Expand Up @@ -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,
Expand All @@ -434,15 +436,20 @@ const gitCommand = (
readonly outputMode?: VcsProcess.VcsProcessInput["outputMode"];
readonly appendTruncationMarker?: boolean;
},
) =>
process.run({
) {
const platform = yield* HostProcessPlatform;
let env = options?.env;
if (platform === "win32") {
env = gitCommandEnv(platform, globalThis.process.env, env);
}
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 }
: {}),
Expand All @@ -453,6 +460,7 @@ const gitCommand = (
? { appendTruncationMarker: options.appendTruncationMarker }
: {}),
});
});

export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* () {
const fileSystem = yield* FileSystem.FileSystem;
Expand Down
180 changes: 179 additions & 1 deletion apps/server/src/vcs/GitVcsDriverCore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";

import { GitCommandError, type ReviewDiffFileContentsInput } from "@t3tools/contracts";
import { ServerConfig } from "../config.ts";
import { makeGitVcsDriverCore, splitNullSeparatedGitStdoutPaths } from "./GitVcsDriverCore.ts";
import {
gitCommandEnv,
makeGitVcsDriverCore,
splitNullSeparatedGitStdoutPaths,
windowsLongPathConfigEnv,
} from "./GitVcsDriverCore.ts";
import * as GitVcsDriver from "./GitVcsDriver.ts";

const ServerConfigLayer = ServerConfig.layerTest(process.cwd(), {
Expand Down Expand Up @@ -2110,3 +2115,176 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => {
);
});
});

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),
Layer.provideMerge(NodeServices.layer),
Layer.provide(Layer.succeed(HostProcessPlatform, platform)),
);

return yield* Effect.gen(function* () {
const driver = yield* GitVcsDriver.GitVcsDriver;
const cwd = yield* makeTmpDir("git-longpath-test-");
const result = yield* driver.execute({
operation: "GitVcsDriverTest.readGitConfig",
cwd,
args: ["config", "--get", key],
// Override the suite's longpaths=true so it cannot mask missing injection.
env: {
[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: "user.name",
GIT_CONFIG_VALUE_2: "outside-count",
},
});
return result.stdout.trim();
}).pipe(Effect.provide(layer));
});

it.effect("enables long paths in Git on Windows while preserving inherited config", () =>
Effect.gen(function* () {
assert.equal(yield* readGitConfig("win32", "core.longpaths"), "true");
assert.equal(yield* readGitConfig("win32", "user.name"), "inherited-name");
}),
);

it.effect("preserves inherited Git config on platforms without MAX_PATH", () =>
Effect.gen(function* () {
assert.equal(yield* readGitConfig("linux", "core.longpaths"), "false");
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", () => {
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: "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" });

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", " ", "1 ", "1\n"]) {
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}`,
);
}
});
});
Loading
Loading