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
86 changes: 86 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,91 @@ 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);
/** Runs git in `cwd` through the checkpoint driver under test. */
const git = (cwd: string, args: ReadonlyArray<string>) =>
driver.execute({ operation: "checkpoint-test", cwd, args });
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 realCommonDir = yield* fileSystem.realPath(
(yield* git(repo, [
"rev-parse",
"--path-format=absolute",
"--git-common-dir",
])).stdout.trim(),
);
/**
* Checkpoint Git commands must keep the private index in the repository
* that owns `cwd`, including when a different repository sits where a
* lexical `path.resolve` of the relative common dir would land.
*/
const assertIndexesStayInRealCommonDir = Effect.gen(function* () {
assert.isTrue(indexFiles.length > 0);
for (const indexFile of indexFiles) {
assert.strictEqual(yield* fileSystem.realPath(path.dirname(indexFile)), realCommonDir);
}
});

const checkpointRef = CheckpointRef.make("refs/t3/checkpoints/symlink");
yield* driver.checkpoints.captureCheckpoint({ cwd: link, checkpointRef });
yield* assertIndexesStayInRealCommonDir;
assert.strictEqual(
(yield* git(repo, ["show", `${checkpointRef}:sub/dir/nested.txt`])).stdout,
"from-link\n",
);

indexFiles.length = 0;
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* fileSystem.writeFileString(path.join(subdir, "nested.txt"), "again\n");
const decoyRef = CheckpointRef.make("refs/t3/checkpoints/symlink-decoy");
yield* driver.checkpoints.captureCheckpoint({ cwd: link, checkpointRef: decoyRef });
yield* assertIndexesStayInRealCommonDir;
const decoyEntries = yield* fileSystem.readDirectory(path.join(sandbox, ".git"));
assert.isFalse(decoyEntries.some((entry) => entry.startsWith("t3-checkpoint-index")));
assert.strictEqual(
(yield* git(repo, ["show", `${decoyRef}:sub/dir/nested.txt`])).stdout,
"again\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
13 changes: 12 additions & 1 deletion apps/server/src/vcs/GitVcsDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -753,12 +753,23 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* (
}),
);

/**
* Absolute path of the repository's common git directory, where checkpoint
* capture places its private index.
*
* A relative `--git-common-dir` is computed from Git's physical working
* directory. Joining it with `path.resolve` applies `..` to the project path
* string, so a symlink into a repository subdirectory climbs out of the
* symlink and `GIT_INDEX_FILE` points at a directory that does not exist
* (exit 128). `--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
38 changes: 37 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 @@ -1688,6 +1689,41 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => {
});

describe("repository status", () => {
it.effect.skipIf(!symlinksSupported)(
"resolves the common dir through a symlink into a repository subdirectory",
() =>
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);
yield* initRepoWithCommit(repo);
yield* git(repo, ["checkout", "-b", "real-only"]);

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

// The lexical join of the symlink with ../../.git is sandbox/.git.
// That directory must not be treated as this repository.
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"]);

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

it.effect("reports non-repository directories without failing", () =>
Effect.gen(function* () {
const cwd = yield* makeTmpDir();
Expand Down
14 changes: 12 additions & 2 deletions apps/server/src/vcs/GitVcsDriverCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1142,13 +1142,23 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
).pipe(Effect.asVoid);
};

/**
* Resolves the common git directory, worktree root, and current branch.
*
* `--git-common-dir` is requested with `--path-format=absolute` because a
* relative answer is computed from Git's physical working directory.
* `path.resolve` would apply `..` to a symlinked project path and climb out
* of a symlink that points at a repository subdirectory. `realPath` still
* runs afterward so Windows 8.3 short names collapse to one directory.
*/
const resolveRepositoryPathsUncached = Effect.fn("resolveRepositoryPathsUncached")(function* (
cwd: string,
) {
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 @@ -1163,7 +1173,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
Loading