Skip to content
Open
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
168 changes: 168 additions & 0 deletions apps/server/src/vcs/GitVcsDriverCore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,37 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => {
}),
);

it.effect("includes untracked files before the repository has its first commit", () =>
Effect.gen(function* () {
const cwd = yield* makeTmpDir();
const driver = yield* GitVcsDriver.GitVcsDriver;
yield* driver.initRepo({ cwd });
yield* writeTextFile(cwd, "first.txt", "first\n");

const preview = yield* driver.getReviewDiffPreview({ cwd, ignoreWhitespace: false });
const diff = preview.sources.find((source) => source.kind === "working-tree")?.diff ?? "";

assert.include(diff, "diff --git a/first.txt b/first.txt");
assert.include(diff, "+first");
}),
);

it.effect("includes staged files before the repository has its first commit", () =>
Effect.gen(function* () {
const cwd = yield* makeTmpDir();
const driver = yield* GitVcsDriver.GitVcsDriver;
yield* driver.initRepo({ cwd });
yield* writeTextFile(cwd, "staged.txt", "staged\n");
yield* git(cwd, ["add", "staged.txt"]);

const preview = yield* driver.getReviewDiffPreview({ cwd, ignoreWhitespace: false });
const diff = preview.sources.find((source) => source.kind === "working-tree")?.diff ?? "";

assert.include(diff, "diff --git a/staged.txt b/staged.txt");
assert.include(diff, "+staged");
}),
);

it.effect("honors whitespace filtering for worktree and branch previews", () =>
Effect.gen(function* () {
const cwd = yield* makeTmpDir();
Expand Down Expand Up @@ -243,6 +274,143 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => {
);
}),
);

it.effect("detects an unstaged rename with edits as one working-tree diff", () =>
Effect.gen(function* () {
const cwd = yield* makeTmpDir();
yield* initRepoWithCommit(cwd);
const fileSystem = yield* FileSystem.FileSystem;
const pathService = yield* Path.Path;
const driver = yield* GitVcsDriver.GitVcsDriver;
yield* writeTextFile(
cwd,
"before.ts",
[
"export const first = 1;",
"export const second = 2;",
"export const third = 3;",
"export const fourth = 4;",
"",
].join("\n"),
);
yield* git(cwd, ["add", "before.ts"]);
yield* git(cwd, ["commit", "-m", "add source file"]);
yield* fileSystem.rename(
pathService.join(cwd, "before.ts"),
pathService.join(cwd, "after.ts"),
);
yield* writeTextFile(
cwd,
"after.ts",
[
"export const first = 1;",
"export const second = 2;",
"export const third = 30;",
"export const fourth = 4;",
"",
].join("\n"),
);

const preview = yield* driver.getReviewDiffPreview({ cwd, ignoreWhitespace: false });
const diff = preview.sources.find((source) => source.kind === "working-tree")?.diff ?? "";

assert.include(diff, "rename from before.ts");
assert.include(diff, "rename to after.ts");
assert.include(diff, "-export const third = 3;");
assert.include(diff, "+export const third = 30;");
assert.notInclude(diff, "--- /dev/null");
assert.notInclude(diff, "+++ /dev/null");
}),
);

it.effect("preserves sparse-checkout entries in the working-tree diff", () =>
Effect.gen(function* () {
const cwd = yield* makeTmpDir();
yield* initRepoWithCommit(cwd);
const driver = yield* GitVcsDriver.GitVcsDriver;
yield* writeTextFile(cwd, "included/file.ts", "export const included = 1;\n");
yield* writeTextFile(cwd, "excluded/file.ts", "export const excluded = 1;\n");
yield* git(cwd, ["add", "."]);
yield* git(cwd, ["commit", "-m", "add sparse files"]);
yield* git(cwd, ["sparse-checkout", "set", "included"]);
yield* writeTextFile(cwd, "included/file.ts", "export const included = 2;\n");

const preview = yield* driver.getReviewDiffPreview({ cwd, ignoreWhitespace: false });
const diff = preview.sources.find((source) => source.kind === "working-tree")?.diff ?? "";

assert.include(diff, "diff --git a/included/file.ts b/included/file.ts");
assert.include(diff, "+export const included = 2;");
assert.notInclude(diff, "excluded/file.ts");
}),
);

it.effect("includes staged index-only deletions in the working-tree diff", () =>
Effect.gen(function* () {
const cwd = yield* makeTmpDir();
yield* initRepoWithCommit(cwd);
const driver = yield* GitVcsDriver.GitVcsDriver;
yield* writeTextFile(cwd, ".gitignore", "secret.env\n");
yield* writeTextFile(cwd, "secret.env", "SECRET=value\n");
yield* git(cwd, ["add", "-f", ".gitignore", "secret.env"]);
yield* git(cwd, ["commit", "-m", "add ignored tracked file"]);
yield* git(cwd, ["rm", "--cached", "secret.env"]);

const preview = yield* driver.getReviewDiffPreview({ cwd, ignoreWhitespace: false });
const diff = preview.sources.find((source) => source.kind === "working-tree")?.diff ?? "";

assert.include(diff, "diff --git a/secret.env b/secret.env");
assert.include(diff, "deleted file mode");
assert.include(diff, "-SECRET=value");
}),
);

it.effect("detects a committed rename with edits in the branch diff", () =>
Effect.gen(function* () {
const cwd = yield* makeTmpDir();
const { initialBranch } = yield* initRepoWithCommit(cwd);
const driver = yield* GitVcsDriver.GitVcsDriver;
yield* writeTextFile(
cwd,
"before.ts",
[
"export const first = 1;",
"export const second = 2;",
"export const third = 3;",
"export const fourth = 4;",
"",
].join("\n"),
);
yield* git(cwd, ["add", "before.ts"]);
yield* git(cwd, ["commit", "-m", "add source file"]);
yield* git(cwd, ["checkout", "-b", "feature/rename"]);
yield* git(cwd, ["mv", "before.ts", "after.ts"]);
yield* writeTextFile(
cwd,
"after.ts",
[
"export const first = 1;",
"export const second = 2;",
"export const third = 30;",
"export const fourth = 4;",
"",
].join("\n"),
);
yield* git(cwd, ["add", "after.ts"]);
yield* git(cwd, ["commit", "-m", "rename source file"]);

const preview = yield* driver.getReviewDiffPreview({
cwd,
baseRef: initialBranch,
ignoreWhitespace: false,
});
const diff = preview.sources.find((source) => source.kind === "branch-range")?.diff ?? "";

assert.include(diff, "rename from before.ts");
assert.include(diff, "rename to after.ts");
assert.include(diff, "-export const third = 3;");
assert.include(diff, "+export const third = 30;");
}),
);
});

describe("repository status", () => {
Expand Down
157 changes: 98 additions & 59 deletions apps/server/src/vcs/GitVcsDriverCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,6 @@ const RANGE_COMMIT_SUMMARY_MAX_OUTPUT_BYTES = 19_000;
const RANGE_DIFF_SUMMARY_MAX_OUTPUT_BYTES = 19_000;
const RANGE_DIFF_PATCH_MAX_OUTPUT_BYTES = 59_000;
const REVIEW_DIFF_PATCH_MAX_OUTPUT_BYTES = 120_000;
const REVIEW_UNTRACKED_DIFF_MAX_OUTPUT_BYTES = 80_000;
const WORKSPACE_FILES_MAX_OUTPUT_BYTES = 120_000;
const STATUS_UPSTREAM_REFRESH_INTERVAL = Duration.seconds(15);
const STATUS_UPSTREAM_REFRESH_TIMEOUT = Duration.seconds(5);

Expand Down Expand Up @@ -1807,43 +1805,103 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
};
});

const readUntrackedReviewDiffs = Effect.fn("readUntrackedReviewDiffs")(function* (cwd: string) {
const untrackedResult = yield* executeGit(
"GitVcsDriver.readUntrackedReviewDiffs.list",
cwd,
["ls-files", "--others", "--exclude-standard", "-z"],
{
maxOutputBytes: WORKSPACE_FILES_MAX_OUTPUT_BYTES,
appendTruncationMarker: true,
},
);
const untrackedPaths = splitNullSeparatedGitStdoutPaths(untrackedResult);
if (untrackedPaths.length === 0) {
return { diff: "", truncated: untrackedResult.stdoutTruncated };
}
const readWorkingTreeReviewDiff = Effect.fn("readWorkingTreeReviewDiff")(function* (
cwd: string,
ignoreWhitespace: boolean | undefined,
) {
const tempDirectory = yield* fileSystem
.makeTempDirectory({ prefix: "t3code-review-index-" })
.pipe(
Effect.mapError(
(cause) =>
new GitCommandError({
operation: "GitVcsDriver.readWorkingTreeReviewDiff.createTempIndex",
command: "git diff",
cwd,
detail: "Failed to create a temporary Git index for the review diff.",
cause,
}),
),
);
const indexPath = path.join(tempDirectory, "index");
const env = { GIT_INDEX_FILE: indexPath } satisfies NodeJS.ProcessEnv;

return yield* Effect.gen(function* () {
const headResult = yield* executeGit(
"GitVcsDriver.readWorkingTreeReviewDiff.resolveHead",
cwd,
["rev-parse", "--verify", "--quiet", "HEAD"],
{ allowNonZeroExit: true },
);
const hasHead = headResult.exitCode === 0;
const baseline = hasHead
? "HEAD"
: (yield* executeGit(
"GitVcsDriver.readWorkingTreeReviewDiff.resolveEmptyTree",
cwd,
["hash-object", "-t", "tree", "--stdin"],
{ stdin: "" },
)).stdout.trim();
const gitIndexResult = yield* executeGit(
"GitVcsDriver.readWorkingTreeReviewDiff.resolveIndex",
cwd,
["rev-parse", "--git-path", "index"],
);
const gitIndexPath = path.isAbsolute(gitIndexResult.stdout.trim())
? gitIndexResult.stdout.trim()
: path.resolve(cwd, gitIndexResult.stdout.trim());

const diffs = yield* Effect.forEach(
untrackedPaths,
(relativePath) =>
executeGit(
"GitVcsDriver.readUntrackedReviewDiffs.diff",
if (yield* fileSystem.exists(gitIndexPath)) {
yield* fileSystem.copyFile(gitIndexPath, indexPath).pipe(
Effect.mapError(
(cause) =>
new GitCommandError({
operation: "GitVcsDriver.readWorkingTreeReviewDiff.copyIndex",
command: "git diff",
cwd,
detail: "Failed to copy the Git index for the review diff.",
cause,
}),
),
);
} else {
yield* executeGit(
"GitVcsDriver.readWorkingTreeReviewDiff.readEmptyTree",
cwd,
["diff", "--no-index", "--patch", "--minimal", "--", "/dev/null", relativePath],
{
allowNonZeroExit: true,
maxOutputBytes: REVIEW_UNTRACKED_DIFF_MAX_OUTPUT_BYTES,
appendTruncationMarker: true,
},
),
{ concurrency: 4 },
);
["read-tree", "--empty"],
{ env },
);
}
yield* executeGit(
"GitVcsDriver.readWorkingTreeReviewDiff.add",
cwd,
["add", "--intent-to-add", "-A", "--", "."],
Comment thread
jakeleventhal marked this conversation as resolved.
{ env },
);

return {
diff: Arr.filterMap(diffs, (result) =>
result.stdout.trim().length > 0 ? Result.succeed(result.stdout) : Result.failVoid,
).join("\n"),
truncated: untrackedResult.stdoutTruncated || diffs.some((result) => result.stdoutTruncated),
};
return yield* executeGit(
"GitVcsDriver.readWorkingTreeReviewDiff.diff",
cwd,
[
"diff",
"--patch",
"--minimal",
"--find-renames",
...(ignoreWhitespace ? ["--ignore-all-space"] : []),
baseline,
"--",
],
{
env,
maxOutputBytes: REVIEW_DIFF_PATCH_MAX_OUTPUT_BYTES,
appendTruncationMarker: true,
},
);
}).pipe(
Effect.ensuring(
fileSystem.remove(tempDirectory, { recursive: true, force: true }).pipe(Effect.ignore),
),
);
});

const getReviewDiffPreview = Effect.fn("getReviewDiffPreview")(function* (
Expand All @@ -1867,22 +1925,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
)
: null);

const dirtyTrackedResult = yield* executeGit(
"GitVcsDriver.getReviewDiffPreview.dirtyTracked",
input.cwd,
[
"diff",
"--patch",
"--minimal",
...(input.ignoreWhitespace ? ["--ignore-all-space"] : []),
"HEAD",
"--",
],
{
maxOutputBytes: REVIEW_DIFF_PATCH_MAX_OUTPUT_BYTES,
appendTruncationMarker: true,
},
).pipe(
const dirtyResult = yield* readWorkingTreeReviewDiff(input.cwd, input.ignoreWhitespace).pipe(
Effect.orElseSucceed(() => ({
exitCode: 0,
stdout: "",
Expand All @@ -1891,12 +1934,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
stderrTruncated: false,
})),
);
const dirtyUntracked = yield* readUntrackedReviewDiffs(input.cwd).pipe(
Effect.orElseSucceed(() => ({ diff: "", truncated: false })),
);
const dirtyDiff = [dirtyTrackedResult.stdout.trimEnd(), dirtyUntracked.diff.trimEnd()]
.filter((diff) => diff.length > 0)
.join("\n");
const dirtyDiff = dirtyResult.stdout;

const baseResult =
baseRef && branch
Expand All @@ -1907,6 +1945,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
"diff",
"--patch",
"--minimal",
"--find-renames",
...(input.ignoreWhitespace ? ["--ignore-all-space"] : []),
`${baseRef}...HEAD`,
],
Expand Down Expand Up @@ -1953,7 +1992,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
headRef: null,
diff: dirtyDiff,
diffHash: dirtyDiffHash,
truncated: dirtyTrackedResult.stdoutTruncated || dirtyUntracked.truncated,
truncated: dirtyResult.stdoutTruncated,
},
{
id: "branch-range",
Expand Down
Loading