Skip to content
Closed
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
70 changes: 69 additions & 1 deletion apps/server/src/vcs/GitVcsDriver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,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 { 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,70 @@ it.effect("GitVcsDriver flushes checkpoint objects and refs to disk before publi
),
);
});

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")),
);
206 changes: 206 additions & 0 deletions apps/server/src/vcs/GitVcsDriverCore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
splitNullSeparatedGitStdoutPaths,
} from "./GitVcsDriverCore.ts";
import * as GitVcsDriver from "./GitVcsDriver.ts";
import { gitCommandEnv, windowsLongPathConfigEnv } from "./VcsProcess.ts";

const encodeGitCommandError = Schema.encodeEffect(Schema.fromJsonString(GitCommandError));

Expand Down Expand Up @@ -3263,3 +3264,208 @@ 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("lets caller config entries replace inherited ones regardless of casing", () => {
const host = {
GIT_CONFIG_COUNT: "2",
GIT_CONFIG_KEY_0: "user.name",
GIT_CONFIG_VALUE_0: "host-name",
GIT_CONFIG_KEY_1: "user.email",
GIT_CONFIG_VALUE_1: "host@example.com",
};
const caller = {
git_config_count: "1",
git_config_key_0: "user.name",
git_config_value_0: "caller-name",
};
assert.deepStrictEqual(gitCommandEnv("win32", host, caller), {
GIT_CONFIG_COUNT: "2",
GIT_CONFIG_KEY_0: "user.name",
GIT_CONFIG_VALUE_0: "caller-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("accepts a negative zero count, which Git parses as zero", () => {
for (const count of ["-0", "-00", " -0"]) {
assert.deepStrictEqual(windowsLongPathConfigEnv("win32", { GIT_CONFIG_COUNT: count }), {
GIT_CONFIG_COUNT: "1",
GIT_CONFIG_KEY_0: "core.longpaths",
GIT_CONFIG_VALUE_0: "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", "-01", "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}`,
);
}
});
});
8 changes: 3 additions & 5 deletions apps/server/src/vcs/GitVcsDriverCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import {
parseRemoteRefWithRemoteNames,
} from "../git/remoteRefs.ts";
import { ServerConfig } from "../config.ts";
import { gitCommandEnv } from "./VcsProcess.ts";

const DEFAULT_TIMEOUT_MS = 30_000;
const gitProcesses = Semaphore.makeUnsafe(8);
Expand Down Expand Up @@ -830,6 +831,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) {
Expand Down Expand Up @@ -858,11 +860,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
.spawn(
ChildProcess.make("git", commandInput.args, {
cwd: commandInput.cwd,
env: {
...process.env,
...input.env,
...trace2Monitor.env,
},
env: gitCommandEnv(hostPlatform, process.env, input.env, trace2Monitor.env),
}),
)
.pipe(
Expand Down
22 changes: 21 additions & 1 deletion apps/server/src/vcs/VcsProcess.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as NodeServices from "@effect/platform-node/NodeServices";
import { assert, describe, expect, it } from "@effect/vitest";
import { HostProcessWorkingDirectory } from "@t3tools/shared/hostProcess";
import { HostProcessPlatform, HostProcessWorkingDirectory } from "@t3tools/shared/hostProcess";
import * as Duration from "effect/Duration";
import * as Deferred from "effect/Deferred";
import * as Effect from "effect/Effect";
Expand Down Expand Up @@ -292,6 +292,26 @@ describe("VcsProcess.run", () => {
}).pipe(provideLive),
);

it.effect.each(["win32", "linux"] as const)(
"enables Git long paths only on Windows hosts (%s)",
(platform) =>
Effect.gen(function* () {
const result = yield* run({
operation: "test.longpaths",
command: "git",
args: ["config", "--get", "core.longpaths"],
cwd: process.cwd(),
env: {
GIT_CONFIG_COUNT: "1",
GIT_CONFIG_KEY_0: "core.longpaths",
GIT_CONFIG_VALUE_0: "false",
},
});

expect(result.stdout.trim()).toBe(platform === "win32" ? "true" : "false");
}).pipe(provideLive, Effect.provideService(HostProcessPlatform, platform)),
);

it.effect("writes stdin before waiting for exit", () =>
Effect.gen(function* () {
const result = yield* run({
Expand Down
Loading
Loading