Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
146 changes: 145 additions & 1 deletion apps/server/src/vcs/GitVcsDriver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import * as PlatformError from "effect/PlatformError";
import { ChildProcessSpawner } from "effect/unstable/process";
import { assert, it } from "@effect/vitest";

import { GitCommandError } from "@t3tools/contracts";
import { CheckpointRef, GitCommandError } from "@t3tools/contracts";
import * as ServerConfig from "../config.ts";
import * as GitVcsDriver from "./GitVcsDriver.ts";
import * as VcsProcess from "./VcsProcess.ts";
Expand Down Expand Up @@ -108,3 +108,147 @@ it.effect("GitVcsDriver forwards execute env to the VCS process", () => {
),
);
});

it.effect("GitVcsDriver stages only changed paths when capturing a checkpoint", () => {
const commands: Array<VcsProcess.VcsProcessInput> = [];
const changedPaths = ["changed.ts", "deleted.ts", "dir/new\nfile.ts", ":literal.ts"];

return Effect.gen(function* () {
const driver = yield* GitVcsDriver.makeVcsDriverShape();
assert.isDefined(driver.checkpoints);

yield* driver.checkpoints.captureCheckpoint({
cwd: process.cwd(),
checkpointRef: CheckpointRef.make("refs/t3/checkpoints/test/turn/1"),
});

const statusCommand = commands.find((command) => command.args[2] === "status");
assert.isDefined(statusCommand);
assert.deepStrictEqual(statusCommand.args.slice(2), [
"status",
"--porcelain=v1",
"-z",
"--untracked-files=all",
"--no-renames",
"--",
".",
]);
assert.strictEqual(statusCommand.env?.GIT_OPTIONAL_LOCKS, "0");

const addCommand = commands.find((command) => command.args[3] === "add");
assert.isDefined(addCommand);
assert.deepStrictEqual(addCommand.args.slice(2), [
"--literal-pathspecs",
"add",
"-A",
"--pathspec-from-file=-",
"--pathspec-file-nul",
]);
assert.strictEqual(addCommand.stdin, `${changedPaths.join("\0")}\0`);
}).pipe(
Effect.provide(
Layer.mergeAll(
NodeServices.layer,
Layer.mock(VcsProcess.VcsProcess)({
run: (input) =>
Effect.sync(() => {
commands.push(input);
const args = input.args.slice(2);
const stdout =
args[0] === "status"
? ` M ${changedPaths[0]}\0 D ${changedPaths[1]}\0?? ${changedPaths[2]}\0?? ${changedPaths[3]}\0`
: args[0] === "rev-parse" && args[1] === "--git-common-dir"
? ".git\n"
: args[0] === "rev-parse" && args[1] === "--show-toplevel"
? `${process.cwd()}\n`
: args[0] === "write-tree"
? "tree-oid\n"
: args[0] === "commit-tree"
? "commit-oid\n"
: "";
return {
exitCode: ChildProcessSpawner.ExitCode(0),
stdout,
stderr: "",
stdoutTruncated: false,
stderrTruncated: false,
};
}),
}),
),
),
);
});

it.effect("GitVcsDriver captures scoped working tree changes without changing the real index", () =>
Effect.scoped(
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const driver = yield* GitVcsDriver.makeVcsDriverShape();
const cwd = yield* fileSystem.makeTempDirectoryScoped({
prefix: "t3-git-checkpoint-paths-",
});
const scopeCwd = path.join(cwd, "scope");
const checkpointRef = CheckpointRef.make("refs/t3/checkpoints/test/turn/1");
assert.isDefined(driver.checkpoints);

const git = (gitCwd: string, args: ReadonlyArray<string>, allowNonZeroExit = false) =>
driver.execute({
operation: "GitVcsDriver.test.checkpointPaths",
cwd: gitCwd,
args,
allowNonZeroExit,
timeoutMs: 10_000,
});
const write = (relativePath: string, contents: string) =>
fileSystem.writeFileString(path.join(cwd, relativePath), contents);

yield* git(cwd, ["init"]);
yield* git(cwd, ["config", "user.email", "test@test.com"]);
yield* git(cwd, ["config", "user.name", "Test"]);
yield* fileSystem.makeDirectory(scopeCwd);
yield* write("scope/changed.txt", "original changed\n");
yield* write("scope/deleted.txt", "original deleted\n");
yield* write("scope/staged.txt", "original staged\n");
yield* write("outside.txt", "original outside\n");
yield* git(cwd, ["add", "."]);
yield* git(cwd, ["commit", "-m", "initial"]);

yield* write("scope/staged.txt", "staged version\n");
yield* git(cwd, ["add", "scope/staged.txt"]);
const indexTreeBefore = (yield* git(cwd, ["write-tree"])).stdout.trim();

yield* write("scope/changed.txt", "working changed\n");
yield* write("scope/staged.txt", "working version\n");
yield* write("scope/new file [1].txt", "new file\n");
yield* write("outside.txt", "working outside\n");
yield* fileSystem.remove(path.join(scopeCwd, "deleted.txt"));

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

const indexTreeAfter = (yield* git(cwd, ["write-tree"])).stdout.trim();
assert.strictEqual(indexTreeAfter, indexTreeBefore);
assert.strictEqual(
(yield* git(cwd, ["show", `${checkpointRef}:scope/changed.txt`])).stdout,
"working changed\n",
);
assert.strictEqual(
(yield* git(cwd, ["show", `${checkpointRef}:scope/staged.txt`])).stdout,
"working version\n",
);
assert.strictEqual(
(yield* git(cwd, ["show", `${checkpointRef}:scope/new file [1].txt`])).stdout,
"new file\n",
);
assert.strictEqual(
(yield* git(cwd, ["show", `${checkpointRef}:outside.txt`])).stdout,
"original outside\n",
);
assert.notStrictEqual(
(yield* git(cwd, ["cat-file", "-e", `${checkpointRef}:scope/deleted.txt`], true)).exitCode,
0,
);
}).pipe(Effect.provide(GitContractLayer)),
),
);
68 changes: 59 additions & 9 deletions apps/server/src/vcs/GitVcsDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,12 @@ function splitNullSeparatedPaths(input: string, truncated: boolean): string[] {
return parts.filter((value) => value.length > 0);
}

function parseCheckpointStatusPaths(output: string): string[] {
return splitNullSeparatedPaths(output, false).flatMap((entry) =>
entry.length > 3 && entry[2] === " " ? [entry.slice(3)] : [],
);
}

function chunkPathsForGitCheckIgnore(relativePaths: ReadonlyArray<string>): string[][] {
const chunks: string[][] = [];
let chunk: string[] = [];
Expand Down Expand Up @@ -708,10 +714,18 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* (
return path.isAbsolute(gitCommonDir) ? gitCommonDir : path.resolve(cwd, gitCommonDir);
});

const resolveRepositoryRoot = (cwd: string) =>
execute({
operation: "GitVcsDriver.checkpoints.resolveRepositoryRoot",
cwd,
args: ["rev-parse", "--show-toplevel"],
}).pipe(Effect.map((result) => result.stdout.trim()));

const checkpoints: VcsDriver.VcsCheckpointOps = {
captureCheckpoint: Effect.fn("GitVcsDriver.checkpoints.captureCheckpoint")(function* (input) {
const operation = "GitVcsDriver.checkpoints.captureCheckpoint";
const gitCommonDir = yield* resolveGitCommonDir(input.cwd);
const repositoryRoot = yield* resolveRepositoryRoot(input.cwd);
const tempIndexPath = path.join(
gitCommonDir,
`t3-checkpoint-index-${NodeCrypto.randomUUID()}`,
Expand All @@ -731,22 +745,58 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* (

yield* Effect.gen(function* () {
const headExists = yield* hasHeadCommit(input.cwd);
if (headExists) {
yield* execute({
operation,
cwd: input.cwd,
args: headExists ? ["read-tree", "HEAD"] : ["read-tree", "--empty"],
env: commitEnv,
});

const statusResult = yield* execute({
operation,
cwd: input.cwd,
args: [
"status",
"--porcelain=v1",
"-z",
"--untracked-files=all",
"--no-renames",
"--",
".",
],
env: {
...process.env,
GIT_OPTIONAL_LOCKS: "0",
},
maxOutputBytes: WORKSPACE_FILES_MAX_OUTPUT_BYTES,
});

if (statusResult.stdoutTruncated) {
yield* execute({
operation,
cwd: input.cwd,
args: ["read-tree", "HEAD"],
args: ["add", "-A", "--", "."],
env: commitEnv,
});
} else {
const changedPaths = parseCheckpointStatusPaths(statusResult.stdout);
if (changedPaths.length > 0) {
yield* execute({
operation,
cwd: repositoryRoot,
args: [
"--literal-pathspecs",
"add",
"-A",
"--pathspec-from-file=-",
"--pathspec-file-nul",
],
stdin: `${changedPaths.join("\0")}\0`,
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
Outdated
env: commitEnv,
});
Comment thread
cursor[bot] marked this conversation as resolved.
}
}

yield* execute({
operation,
cwd: input.cwd,
args: ["add", "-A", "--", "."],
env: commitEnv,
});

const writeTreeResult = yield* execute({
operation,
cwd: input.cwd,
Expand Down
Loading