Skip to content
Merged
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
49 changes: 49 additions & 0 deletions apps/server/src/vcs/GitVcsDriver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { assert, it } from "@effect/vitest";

import { CheckpointRef, GitCommandError, VcsProcessExitError } from "@t3tools/contracts";
import * as HostProcess from "@t3tools/shared/HostProcess";
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 @@ -196,6 +197,54 @@ it.effect("checkpoint capture skips untracked nested repositories without a comm
}).pipe(Effect.scoped, Effect.provide(layerGitContract)),
);

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, checkpointRef } = yield* makeCheckpointFixture(driver, repo);
// Git returns ../../.git from the target; resolving it against the link reaches the decoy.
yield* git(["-C", sandbox, "init"]);
yield* fileSystem.writeFileString(path.join(subdir, "nested.txt"), "from-link\n");
const realCommonDir = yield* fileSystem.realPath(path.join(repo, ".git"));

yield* driver.checkpoints.captureCheckpoint({ cwd: link, checkpointRef });

assert.isTrue(indexFiles.length > 0);
for (const indexFile of indexFiles) {
assert.strictEqual(yield* fileSystem.realPath(path.dirname(indexFile)), realCommonDir);
}
assert.strictEqual(
(yield* git(["show", `${checkpointRef}:sub/dir/nested.txt`])).stdout,
"from-link\n",
);
}).pipe(Effect.scoped, Effect.provide(layerGitCaptureContract)),
);

it.effect("checkpoint capture keeps the shared index a split index still uses", () =>
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
Expand Down
5 changes: 4 additions & 1 deletion apps/server/src/vcs/GitVcsDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -783,6 +783,7 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* (
}),
);

// Git reports relative paths from the physical cwd, which can differ from a symlink.
const resolveGitCommonDir = Effect.fn("GitVcsDriver.resolveGitCommonDir")(function* (
cwd: string,
) {
Expand All @@ -792,7 +793,9 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* (
args: ["rev-parse", "--git-common-dir"],
});
const gitCommonDir = result.stdout.trim();
return path.isAbsolute(gitCommonDir) ? gitCommonDir : path.resolve(cwd, gitCommonDir);
if (path.isAbsolute(gitCommonDir)) return gitCommonDir;
const physicalCwd = yield* fileSystem.realPath(cwd).pipe(Effect.orElseSucceed(() => cwd));
return path.resolve(physicalCwd, gitCommonDir);
});

// Git renames loose objects and refs into place without fsync by default, so
Expand Down
29 changes: 29 additions & 0 deletions apps/server/src/vcs/GitVcsDriverCore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
type ReviewDiffPreviewResult,
type WorktreeSubmodules,
} from "@t3tools/contracts";
import { symlinksSupported } from "@t3tools/shared/testing/symlinks";
import * as ServerConfig from "../config.ts";
import { gitCommandDuration } from "../observability/Metrics.ts";
import {
Expand Down Expand Up @@ -2673,6 +2674,34 @@ it.layer(layerTest)("GitVcsDriver core integration", (it) => {
}),
);

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-");
yield* initRepoWithCommit(sandbox);
yield* git(sandbox, ["branch", "-M", "decoy-only"]);
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"]);

// The decoy must exist before the first lookup, which caches repository paths.
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("reads Changes totals with untracked files when requested", () =>
Effect.gen(function* () {
const cwd = yield* makeTmpDir();
Expand Down
4 changes: 3 additions & 1 deletion apps/server/src/vcs/GitVcsDriverCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1278,6 +1278,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
const resolveRepositoryPathsUncached = Effect.fn("resolveRepositoryPathsUncached")(function* (
cwd: string,
) {
// Git reports relative paths from the physical cwd, which can differ from a symlink.
const commonDirResult = yield* executeGitWithStableDiagnostics(
"GitVcsDriver.resolveRepositoryPaths.commonDir",
cwd,
Expand Down Expand Up @@ -1306,9 +1307,10 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
}

const commonDirOutput = commonDirResult.stdout.trim();
const physicalCwd = yield* fileSystem.realPath(cwd).pipe(Effect.orElseSucceed(() => cwd));
const resolvedGitCommonDir = path.isAbsolute(commonDirOutput)
? path.normalize(commonDirOutput)
: path.resolve(cwd, commonDirOutput);
: path.resolve(physicalCwd, commonDirOutput);
const gitCommonDir = yield* fileSystem
.realPath(resolvedGitCommonDir)
.pipe(Effect.orElseSucceed(() => resolvedGitCommonDir));
Expand Down
Loading