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
82 changes: 82 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 { symlinksSupported } from "@t3tools/shared/testing/symlinks";
import * as ServerConfig from "../config.ts";
import * as CheckpointStore from "../checkpointing/CheckpointStore.ts";
import * as ProcessRunner from "../processRunner.ts";
Expand Down Expand Up @@ -163,6 +164,87 @@ it.effect("checkpoint capture skips untracked nested repositories without a comm
}).pipe(Effect.scoped, Effect.provide(GitContractLayer)),
);

it.effect.skipIf(!symlinksSupported)(
"checkpoint capture writes its index into the real git dir when cwd is a symlinked subdirectory",
() =>
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const liveRunner = yield* ProcessRunner.ProcessRunner;
const indexFiles: string[] = [];
const captureProcess = yield* VcsProcess.make.pipe(
Effect.provideService(ProcessRunner.ProcessRunner, {
run: (input) => {
const indexFile = input.env?.GIT_INDEX_FILE;
if (indexFile !== undefined) indexFiles.push(indexFile);
return liveRunner.run(input);
},
}),
);
const driver = yield* GitVcsDriver.makeVcsDriverShape().pipe(
Effect.provideService(VcsProcess.VcsProcess, captureProcess),
);
const sandbox = yield* fileSystem.makeTempDirectoryScoped({
prefix: "t3-checkpoint-symlink-",
});
const repo = path.join(sandbox, "repo");
const subdir = path.join(repo, "sub", "dir");
const link = path.join(sandbox, "nest", "link");
yield* fileSystem.makeDirectory(subdir, { recursive: true });
yield* fileSystem.makeDirectory(path.dirname(link), { recursive: true });
yield* fileSystem.symlink(subdir, link);
const git = (cwd: string, args: ReadonlyArray<string>) =>
driver.execute({ operation: "checkpoint-test", cwd, args });

// Place the decoy where a lexical join of the symlink and Git's relative
// common dir lands, and do it before capture. Cleanup deletes the temp
// index afterward, so the recorded GIT_INDEX_FILE is what shows which
// repository was used.
yield* git(sandbox, ["init", "-b", "decoy-only"]);
yield* git(sandbox, ["config", "user.name", "Test"]);
yield* git(sandbox, ["config", "user.email", "test@test.com"]);
yield* git(sandbox, ["commit", "--allow-empty", "-m", "decoy"]);
yield* git(repo, ["init"]);
yield* git(repo, ["config", "user.name", "Test"]);
yield* git(repo, ["config", "user.email", "test@test.com"]);
yield* fileSystem.writeFileString(path.join(repo, "file.txt"), "initial\n");
yield* git(repo, ["add", "."]);
yield* git(repo, ["commit", "-m", "initial"]);
yield* fileSystem.writeFileString(path.join(subdir, "nested.txt"), "from-link\n");

const relativeCommonDir = (yield* git(link, ["rev-parse", "--git-common-dir"])).stdout.trim();
assert.isFalse(path.isAbsolute(relativeCommonDir));
const decoyGitDir = yield* fileSystem.realPath(path.join(sandbox, ".git"));
assert.strictEqual(
yield* fileSystem.realPath(path.resolve(link, relativeCommonDir)),
decoyGitDir,
);
const realCommonDir = yield* fileSystem.realPath(
(yield* git(repo, [
"rev-parse",
"--path-format=absolute",
"--git-common-dir",
])).stdout.trim(),
);
assert.notStrictEqual(realCommonDir, decoyGitDir);

const checkpointRef = CheckpointRef.make("refs/t3/checkpoints/symlink");
yield* driver.checkpoints.captureCheckpoint({ cwd: link, checkpointRef });

assert.isTrue(indexFiles.length > 0);
for (const indexFile of indexFiles) {
assert.isTrue(path.basename(indexFile).startsWith("t3-checkpoint-index"));
assert.strictEqual(yield* fileSystem.realPath(path.dirname(indexFile)), realCommonDir);
}
const decoyEntries = yield* fileSystem.readDirectory(decoyGitDir);
assert.isFalse(decoyEntries.some((entry) => entry.startsWith("t3-checkpoint-index")));
assert.strictEqual(
(yield* git(repo, ["show", `${checkpointRef}:sub/dir/nested.txt`])).stdout,
"from-link\n",
);
}).pipe(Effect.scoped, Effect.provide(GitCaptureContractLayer)),
);

it.effect("checkpoint recovery discovers nested HEAD independently of inherited GIT_DIR", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
Expand Down
7 changes: 6 additions & 1 deletion apps/server/src/vcs/GitVcsDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -753,12 +753,17 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* (
}),
);

// A relative `--git-common-dir` is computed from Git's physical working
// directory. `path.resolve` applies `..` to the project path string, so a
// symlink into a repository subdirectory climbs out of the link and the
// checkpoint index is created in the wrong directory. `--path-format=absolute`
// is the same request already used for `--git-path index`.
const resolveGitCommonDir = (cwd: string) =>
Effect.gen(function* () {
const result = yield* execute({
operation: "GitVcsDriver.checkpoints.resolveGitCommonDir",
cwd,
args: ["rev-parse", "--git-common-dir"],
args: ["rev-parse", "--path-format=absolute", "--git-common-dir"],
});
const gitCommonDir = result.stdout.trim();
return path.isAbsolute(gitCommonDir) ? gitCommonDir : path.resolve(cwd, gitCommonDir);
Expand Down
43 changes: 42 additions & 1 deletion apps/server/src/vcs/GitVcsDriverCore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
type ReviewDiffFileContentsInput,
type WorktreeSubmodules,
} from "@t3tools/contracts";
import { symlinksSupported } from "@t3tools/shared/testing/symlinks";
import { ServerConfig } from "../config.ts";
import { gitCommandDuration } from "../observability/Metrics.ts";
import {
Expand Down Expand Up @@ -334,7 +335,7 @@ it.effect("uses stable diagnostics for every parsed non-repository command", ()
{ args: ["rev-parse", "--git-path", "index"], lcAll: "C" },
{ args: ["status", "--porcelain=2", "--branch"], lcAll: "C" },
{ args: ["rev-parse", "--abbrev-ref", "HEAD"], lcAll: "C" },
{ args: ["rev-parse", "--git-common-dir"], lcAll: "C" },
{ args: ["rev-parse", "--path-format=absolute", "--git-common-dir"], lcAll: "C" },
]);
}).pipe(Effect.provide(layer));
});
Expand Down Expand Up @@ -1771,6 +1772,46 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => {
});

describe("repository status", () => {
it.effect.skipIf(!symlinksSupported)(
"lists the real repository through a symlink when a decoy repo occupies the lexical common dir",
() =>
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const pathService = yield* Path.Path;
const sandbox = yield* makeTmpDir("git-symlink-subdir-");
const repo = pathService.join(sandbox, "repo");
const link = pathService.join(sandbox, "nest", "link");
yield* fileSystem.makeDirectory(pathService.join(repo, "sub", "dir"), {
recursive: true,
});
yield* fileSystem.makeDirectory(pathService.dirname(link), { recursive: true });
yield* fileSystem.symlink(pathService.join(repo, "sub", "dir"), link);

// The decoy has to exist before the only lookup. Refreshing listRefs
// on the same cwd coalesces path resolution, so a second call would
// replay a result captured before this repository existed.
yield* git(sandbox, ["init", "-b", "decoy-only"]);
yield* git(sandbox, ["config", "user.email", "test@test.com"]);
yield* git(sandbox, ["config", "user.name", "Test"]);
yield* git(sandbox, ["commit", "--allow-empty", "-m", "decoy"]);
yield* initRepoWithCommit(repo);
yield* git(repo, ["checkout", "-b", "real-only"]);

const relativeCommonDir = yield* git(link, ["rev-parse", "--git-common-dir"]);
assert.isFalse(pathService.isAbsolute(relativeCommonDir));
const decoyGitDir = pathService.join(sandbox, ".git");
assert.strictEqual(
yield* fileSystem.realPath(pathService.resolve(link, relativeCommonDir)),
yield* fileSystem.realPath(decoyGitDir),
);

const refs = yield* (yield* GitVcsDriver.GitVcsDriver).listRefs({ cwd: link });
assert.isTrue(refs.isRepo);
assert.isFalse(refs.refs.some((ref) => ref.name === "decoy-only"));
assert.isTrue(refs.refs.some((ref) => ref.name === "real-only"));
}),
);

it.effect("reports non-repository directories without failing", () =>
Effect.gen(function* () {
const cwd = yield* makeTmpDir();
Expand Down
11 changes: 8 additions & 3 deletions apps/server/src/vcs/GitVcsDriverCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1146,10 +1146,15 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
const resolveRepositoryPathsUncached = Effect.fn("resolveRepositoryPathsUncached")(function* (
cwd: string,
) {
// Same absolute common-dir request as checkpoint capture. A relative
// answer joined with `path.resolve` walks `..` out of a symlinked
// subdirectory and can cache a missing path or another repository.
// `realPath` still runs afterward so Windows 8.3 short names collapse.
const gitCommonDirArgs = ["rev-parse", "--path-format=absolute", "--git-common-dir"] as const;
const commonDirResult = yield* executeGitWithStableDiagnostics(
"GitVcsDriver.resolveRepositoryPaths.commonDir",
cwd,
["rev-parse", "--git-common-dir"],
gitCommonDirArgs,
{
timeoutMs: 5_000,
allowNonZeroExit: true,
Expand All @@ -1164,7 +1169,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
...gitCommandContext({
operation: "GitVcsDriver.resolveRepositoryPaths.commonDir",
cwd,
args: ["rev-parse", "--git-common-dir"],
args: gitCommonDirArgs,
}),
detail: "Failed to resolve the Git common directory.",
exitCode: commonDirResult.exitCode,
Expand Down Expand Up @@ -1324,7 +1329,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
...gitCommandContext({
operation: "GitVcsDriver.resolveGitCommonDir",
cwd,
args: ["rev-parse", "--git-common-dir"],
args: ["rev-parse", "--path-format=absolute", "--git-common-dir"],
}),
detail: "Cannot resolve a Git common directory outside a repository.",
});
Expand Down
Loading