From ecf6ecd10834b1876e93cf2dc9cf8ab9128893dd Mon Sep 17 00:00:00 2001
From: luis-7654 <103049391+luis-7654@users.noreply.github.com>
Date: Sat, 7 Nov 2026 13:17:49 +0100
Subject: [PATCH 1/5] fix(server): bound untracked files before review diffs
---
apps/server/src/vcs/GitVcsDriverCore.test.ts | 102 +++++++++++++++++++
apps/server/src/vcs/GitVcsDriverCore.ts | 45 ++++++--
2 files changed, 139 insertions(+), 8 deletions(-)
diff --git a/apps/server/src/vcs/GitVcsDriverCore.test.ts b/apps/server/src/vcs/GitVcsDriverCore.test.ts
index 728c20c034ee2..8c149db61bdd5 100644
--- a/apps/server/src/vcs/GitVcsDriverCore.test.ts
+++ b/apps/server/src/vcs/GitVcsDriverCore.test.ts
@@ -2644,6 +2644,108 @@ it.layer(layerTest)("GitVcsDriver core integration", (it) => {
}),
);
+ it.effect("bounds untracked review input without hiding ordinary edits", () =>
+ Effect.gen(function* () {
+ const cwd = yield* makeTmpDir();
+ yield* initRepoWithCommit(cwd);
+ const fileSystem = yield* FileSystem.FileSystem;
+ const pathService = yield* Path.Path;
+ yield* writeTextFile(cwd, "README.md", "# test\ntracked edit\n");
+ yield* writeTextFile(cwd, "ordinary.txt", "ordinary edit\n");
+ yield* writeTextFile(cwd, "large.data", "");
+ // Sparse fixture exercises logical size without allocating the input budget.
+ yield* fileSystem.truncate(pathService.join(cwd, "large.data"), 11 * 1024 * 1024);
+ if (symlinksSupported) {
+ yield* fileSystem.symlink("large.data", pathService.join(cwd, "ordinary-link"));
+ }
+ const indexBefore = yield* git(cwd, ["ls-files", "--stage"]);
+ const delegate = yield* ChildProcessSpawner.ChildProcessSpawner;
+ const spawner = ChildProcessSpawner.make((command) =>
+ Effect.gen(function* () {
+ if (
+ ChildProcess.isStandardCommand(command) &&
+ (command.args.includes("--numstat") || command.args.includes("--patch"))
+ ) {
+ const result = yield* delegate.spawn(
+ ChildProcess.make("git", ["ls-files", "--error-unmatch", "large.data"], {
+ cwd,
+ env: command.options.env,
+ }),
+ );
+ assert.notEqual(Number(yield* result.exitCode), 0);
+ }
+ return yield* delegate.spawn(command);
+ }),
+ );
+ const driver = yield* makeGitVcsDriverCore().pipe(
+ Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
+ );
+ const preview = yield* driver.getReviewDiffPreview({ cwd, metadataOnly: true });
+ const source = preview.sources.find((item) => item.kind === "working-tree")!;
+ assert.isTrue(source.truncated);
+ assert.isUndefined(source.files);
+ assert.include(source.diff, "+tracked edit");
+ assert.include(source.diff, "+ordinary edit");
+ assert.notInclude(source.diff, "b/large.data");
+ if (symlinksSupported) assert.include(source.diff, "b/ordinary-link");
+ assert.isUndefined(
+ (yield* driver.statusDetailsLocal(cwd, { includeBranchChanges: true })).branchChanges,
+ );
+ const selected = yield* driver.getReviewDiffPreview({
+ cwd,
+ file: { path: "ordinary.txt", previousPath: null, sourceKind: "working-tree" },
+ });
+ assert.isFalse(selected.sources[0]!.truncated);
+ assert.include(selected.sources[0]!.diff, "+ordinary edit");
+ const oversized = yield* driver.getReviewDiffPreview({
+ cwd,
+ file: { path: "large.data", previousPath: null, sourceKind: "working-tree" },
+ });
+ assert.isTrue(oversized.sources[0]!.truncated);
+ assert.isEmpty(oversized.sources[0]!.diff);
+ assert.strictEqual(yield* git(cwd, ["ls-files", "--stage"]), indexBefore);
+ }),
+ );
+
+ it.effect("bounds aggregate untracked input while keeping later small files", () =>
+ Effect.gen(function* () {
+ const cwd = yield* makeTmpDir();
+ yield* initRepoWithCommit(cwd);
+ const fileSystem = yield* FileSystem.FileSystem;
+ const pathService = yield* Path.Path;
+ for (let index = 0; index < 8; index++) {
+ const name = `large-${index}.data`;
+ yield* writeTextFile(cwd, name, "");
+ yield* fileSystem.truncate(pathService.join(cwd, name), 9 * 1024 * 1024);
+ }
+ yield* writeTextFile(cwd, "z-small.txt", "still reviewable\n");
+ const delegate = yield* ChildProcessSpawner.ChildProcessSpawner;
+ const spawner = ChildProcessSpawner.make((command) =>
+ Effect.gen(function* () {
+ if (ChildProcess.isStandardCommand(command) && command.args.includes("--numstat")) {
+ const result = yield* delegate.spawn(
+ ChildProcess.make("git", ["ls-files", "--error-unmatch", "large-7.data"], {
+ cwd,
+ env: command.options.env,
+ }),
+ );
+ assert.notEqual(Number(yield* result.exitCode), 0);
+ }
+ return yield* delegate.spawn(command);
+ }),
+ );
+ const driver = yield* makeGitVcsDriverCore().pipe(
+ Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
+ );
+ const preview = yield* driver.getReviewDiffPreview({ cwd, metadataOnly: true });
+ const source = preview.sources.find((item) => item.kind === "working-tree")!;
+ assert.isTrue(source.truncated);
+ assert.isUndefined(source.files);
+ assert.notInclude(source.diff, "b/large-7.data");
+ assert.include(source.diff, "+still reviewable");
+ }),
+ );
+
it.effect("reviews untracked files outside the sparse-checkout cone", () =>
Effect.gen(function* () {
const cwd = yield* makeTmpDir();
diff --git a/apps/server/src/vcs/GitVcsDriverCore.ts b/apps/server/src/vcs/GitVcsDriverCore.ts
index 85a7e524b1a63..19cb1057e9e15 100644
--- a/apps/server/src/vcs/GitVcsDriverCore.ts
+++ b/apps/server/src/vcs/GitVcsDriverCore.ts
@@ -75,6 +75,8 @@ const REVIEW_DIFF_PATCH_MAX_OUTPUT_BYTES = 120_000;
const REVIEW_METADATA_MAX_OUTPUT_BYTES = 16 * 1024 * 1024;
// Temporary-index git add scales poorly (50k files took ~30 s); report incomplete totals above this cap.
const REVIEW_UNTRACKED_MAX_FILES = 5_000;
+const REVIEW_UNTRACKED_MAX_FILE_BYTES = 10n * 1024n * 1024n;
+const REVIEW_UNTRACKED_MAX_TOTAL_BYTES = 64n * 1024n * 1024n;
const REVIEW_DIFF_FILE_MAX_OUTPUT_BYTES = 1024 * 1024;
const REVIEW_BRANCH_COMMIT_LIMIT = 100;
const REVIEW_BRANCH_SUBJECT_LIMIT = 300;
@@ -3137,9 +3139,35 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
(candidate) =>
onlyPath === undefined || candidate === onlyPath || candidate === `${onlyPath}/`,
);
- if (paths.length === 0) return { env: undefined };
+ if (paths.length === 0) return { env: undefined, complete: true };
if (paths.length > REVIEW_UNTRACKED_MAX_FILES) return null;
- const env = yield* prepareReviewIndex(cwd, paths).pipe(
+ const boundedPaths: string[] = [];
+ let bytes = 0n;
+ let complete = true;
+ for (const relativePath of paths) {
+ // lstat bounds sparse files by logical size without reading symlink targets.
+ const info = yield* fileSystem
+ .lstat(path.resolve(cwd, relativePath))
+ .pipe(Effect.orElseSucceed(() => undefined));
+ if (
+ info === undefined ||
+ (info.type !== "File" && info.type !== "Directory" && info.type !== "SymbolicLink")
+ ) {
+ complete = false;
+ continue;
+ }
+ const size = info.type === "Directory" ? 0n : info.size;
+ if (
+ size > REVIEW_UNTRACKED_MAX_FILE_BYTES ||
+ bytes + size > REVIEW_UNTRACKED_MAX_TOTAL_BYTES
+ ) {
+ complete = false;
+ continue;
+ }
+ bytes += size;
+ boundedPaths.push(relativePath);
+ }
+ const env = yield* prepareReviewIndex(cwd, boundedPaths).pipe(
Effect.catchTags({
PlatformError: (cause) =>
Effect.fail(
@@ -3153,7 +3181,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
),
}),
);
- return { env };
+ return { env, complete };
});
const resolveReviewMergeBase = Effect.fn("resolveReviewMergeBase")(function* (
@@ -3230,12 +3258,12 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
: [];
// Empty status can mean untracked files are hidden; retain the full scan.
const untracked = yield* prepareUntrackedReviewIndex(cwd, pathspecs);
- if (untracked === null) {
+ if (untracked === null || !untracked.complete) {
return yield* new GitCommandError({
operation: "GitVcsDriver.readBranchChangeTotals",
command: "git ls-files",
cwd,
- detail: "Too many untracked files to count.",
+ detail: "Untracked files exceed the review input limits.",
});
}
const readNumstat = (ref: string) =>
@@ -3564,9 +3592,10 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
});
const [dirtyTrackedResult, baseResult] = yield* Effect.gen(function* () {
const untracked = input.trackedOnly
- ? { env: undefined }
+ ? { env: undefined, complete: true }
: yield* prepareUntrackedReviewIndex(cwd, pathArgs, input.file?.path);
- const metadataOnly = input.metadataOnly === true && !input.file && untracked !== null;
+ const metadataOnly =
+ input.metadataOnly === true && !input.file && untracked?.complete === true;
const [dirty, base] =
review.mergeBase === dirtyRef
? yield* readTrackedDiff(dirtyRef, untracked?.env, metadataOnly).pipe(
@@ -3579,7 +3608,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
],
{ concurrency: 2 },
);
- if (untracked !== null) return [dirty, base] as const;
+ if (untracked?.complete) return [dirty, base] as const;
const incomplete = (ref: string | null, result: typeof dirty) =>
ref === null ? result : { ...result, files: undefined, stdoutTruncated: true };
return [incomplete(dirtyRef, dirty), incomplete(review.mergeBase, base)] as const;
From 889e7c0e26876e5c297c9b7f9e4f4780f8422ebb Mon Sep 17 00:00:00 2001
From: Julius Marminge
Date: Wed, 11 Nov 2026 04:11:01 +0000
Subject: [PATCH 2/5] test(server): drop redundant spawner probes from
untracked bound tests
AI-assisted; verified with the tests and tools listed above.
---
apps/server/src/vcs/GitVcsDriverCore.test.ts | 43 +-------------------
1 file changed, 2 insertions(+), 41 deletions(-)
diff --git a/apps/server/src/vcs/GitVcsDriverCore.test.ts b/apps/server/src/vcs/GitVcsDriverCore.test.ts
index bfea0917ce81e..3e0aa390c85f5 100644
--- a/apps/server/src/vcs/GitVcsDriverCore.test.ts
+++ b/apps/server/src/vcs/GitVcsDriverCore.test.ts
@@ -2659,28 +2659,7 @@ it.layer(layerTest)("GitVcsDriver core integration", (it) => {
if (symlinksSupported) {
yield* fileSystem.symlink("large.data", pathService.join(cwd, "ordinary-link"));
}
- const indexBefore = yield* git(cwd, ["ls-files", "--stage"]);
- const delegate = yield* ChildProcessSpawner.ChildProcessSpawner;
- const spawner = ChildProcessSpawner.make((command) =>
- Effect.gen(function* () {
- if (
- ChildProcess.isStandardCommand(command) &&
- (command.args.includes("--numstat") || command.args.includes("--patch"))
- ) {
- const result = yield* delegate.spawn(
- ChildProcess.make("git", ["ls-files", "--error-unmatch", "large.data"], {
- cwd,
- env: command.options.env,
- }),
- );
- assert.notEqual(Number(yield* result.exitCode), 0);
- }
- return yield* delegate.spawn(command);
- }),
- );
- const driver = yield* makeGitVcsDriverCore().pipe(
- Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
- );
+ const driver = yield* GitVcsDriver.GitVcsDriver;
const preview = yield* driver.getReviewDiffPreview({ cwd, metadataOnly: true });
const source = preview.sources.find((item) => item.kind === "working-tree")!;
assert.isTrue(source.truncated);
@@ -2704,7 +2683,6 @@ it.layer(layerTest)("GitVcsDriver core integration", (it) => {
});
assert.isTrue(oversized.sources[0]!.truncated);
assert.isEmpty(oversized.sources[0]!.diff);
- assert.strictEqual(yield* git(cwd, ["ls-files", "--stage"]), indexBefore);
}),
);
@@ -2720,24 +2698,7 @@ it.layer(layerTest)("GitVcsDriver core integration", (it) => {
yield* fileSystem.truncate(pathService.join(cwd, name), 9 * 1024 * 1024);
}
yield* writeTextFile(cwd, "z-small.txt", "still reviewable\n");
- const delegate = yield* ChildProcessSpawner.ChildProcessSpawner;
- const spawner = ChildProcessSpawner.make((command) =>
- Effect.gen(function* () {
- if (ChildProcess.isStandardCommand(command) && command.args.includes("--numstat")) {
- const result = yield* delegate.spawn(
- ChildProcess.make("git", ["ls-files", "--error-unmatch", "large-7.data"], {
- cwd,
- env: command.options.env,
- }),
- );
- assert.notEqual(Number(yield* result.exitCode), 0);
- }
- return yield* delegate.spawn(command);
- }),
- );
- const driver = yield* makeGitVcsDriverCore().pipe(
- Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
- );
+ const driver = yield* GitVcsDriver.GitVcsDriver;
const preview = yield* driver.getReviewDiffPreview({ cwd, metadataOnly: true });
const source = preview.sources.find((item) => item.kind === "working-tree")!;
assert.isTrue(source.truncated);
From 4ca6cc9d58e8d0c0d31122f814330c1cc783e270 Mon Sep 17 00:00:00 2001
From: Julius Marminge
Date: Wed, 11 Nov 2026 04:27:02 +0000
Subject: [PATCH 3/5] fix(server): omit oversized untracked files without
dropping review totals
Raise the untracked review budget to 256 MiB total and leave files past it
out of the temporary index instead of falling back to the >5,000-file path,
so the per-file list, deferred patches and Changes totals stay available.
AI-assisted; verified with the tests and tools listed above.
---
apps/server/src/vcs/GitVcsDriverCore.test.ts | 49 +++++---------------
apps/server/src/vcs/GitVcsDriverCore.ts | 46 +++++++-----------
2 files changed, 29 insertions(+), 66 deletions(-)
diff --git a/apps/server/src/vcs/GitVcsDriverCore.test.ts b/apps/server/src/vcs/GitVcsDriverCore.test.ts
index 3e0aa390c85f5..febaef3e09063 100644
--- a/apps/server/src/vcs/GitVcsDriverCore.test.ts
+++ b/apps/server/src/vcs/GitVcsDriverCore.test.ts
@@ -2645,38 +2645,33 @@ it.layer(layerTest)("GitVcsDriver core integration", (it) => {
}),
);
- it.effect("bounds untracked review input without hiding ordinary edits", () =>
+ it.effect("leaves oversized untracked files out of review input", () =>
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, "README.md", "# test\ntracked edit\n");
yield* writeTextFile(cwd, "ordinary.txt", "ordinary edit\n");
yield* writeTextFile(cwd, "large.data", "");
// Sparse fixture exercises logical size without allocating the input budget.
- yield* fileSystem.truncate(pathService.join(cwd, "large.data"), 11 * 1024 * 1024);
+ yield* fileSystem.truncate(pathService.join(cwd, "large.data"), 257 * 1024 * 1024);
if (symlinksSupported) {
yield* fileSystem.symlink("large.data", pathService.join(cwd, "ordinary-link"));
}
- const driver = yield* GitVcsDriver.GitVcsDriver;
+
const preview = yield* driver.getReviewDiffPreview({ cwd, metadataOnly: true });
const source = preview.sources.find((item) => item.kind === "working-tree")!;
assert.isTrue(source.truncated);
- assert.isUndefined(source.files);
- assert.include(source.diff, "+tracked edit");
- assert.include(source.diff, "+ordinary edit");
- assert.notInclude(source.diff, "b/large.data");
- if (symlinksSupported) assert.include(source.diff, "b/ordinary-link");
- assert.isUndefined(
- (yield* driver.statusDetailsLocal(cwd, { includeBranchChanges: true })).branchChanges,
+ assert.deepStrictEqual(
+ source.files?.map((file) => file.path),
+ symlinksSupported
+ ? ["README.md", "ordinary-link", "ordinary.txt"]
+ : ["README.md", "ordinary.txt"],
);
- const selected = yield* driver.getReviewDiffPreview({
- cwd,
- file: { path: "ordinary.txt", previousPath: null, sourceKind: "working-tree" },
- });
- assert.isFalse(selected.sources[0]!.truncated);
- assert.include(selected.sources[0]!.diff, "+ordinary edit");
+ const status = yield* driver.statusDetailsLocal(cwd, { includeBranchChanges: true });
+ assert.strictEqual(status.branchChanges?.insertions, symlinksSupported ? 3 : 2);
const oversized = yield* driver.getReviewDiffPreview({
cwd,
file: { path: "large.data", previousPath: null, sourceKind: "working-tree" },
@@ -2686,28 +2681,6 @@ it.layer(layerTest)("GitVcsDriver core integration", (it) => {
}),
);
- it.effect("bounds aggregate untracked input while keeping later small files", () =>
- Effect.gen(function* () {
- const cwd = yield* makeTmpDir();
- yield* initRepoWithCommit(cwd);
- const fileSystem = yield* FileSystem.FileSystem;
- const pathService = yield* Path.Path;
- for (let index = 0; index < 8; index++) {
- const name = `large-${index}.data`;
- yield* writeTextFile(cwd, name, "");
- yield* fileSystem.truncate(pathService.join(cwd, name), 9 * 1024 * 1024);
- }
- yield* writeTextFile(cwd, "z-small.txt", "still reviewable\n");
- const driver = yield* GitVcsDriver.GitVcsDriver;
- const preview = yield* driver.getReviewDiffPreview({ cwd, metadataOnly: true });
- const source = preview.sources.find((item) => item.kind === "working-tree")!;
- assert.isTrue(source.truncated);
- assert.isUndefined(source.files);
- assert.notInclude(source.diff, "b/large-7.data");
- assert.include(source.diff, "+still reviewable");
- }),
- );
-
it.effect("reviews untracked files outside the sparse-checkout cone", () =>
Effect.gen(function* () {
const cwd = yield* makeTmpDir();
diff --git a/apps/server/src/vcs/GitVcsDriverCore.ts b/apps/server/src/vcs/GitVcsDriverCore.ts
index 72fbaba898ae8..fa6a16b0ac9a1 100644
--- a/apps/server/src/vcs/GitVcsDriverCore.ts
+++ b/apps/server/src/vcs/GitVcsDriverCore.ts
@@ -76,8 +76,8 @@ const REVIEW_DIFF_PATCH_MAX_OUTPUT_BYTES = 120_000;
const REVIEW_METADATA_MAX_OUTPUT_BYTES = 16 * 1024 * 1024;
// Temporary-index git add scales poorly (50k files took ~30 s); report incomplete totals above this cap.
const REVIEW_UNTRACKED_MAX_FILES = 5_000;
-const REVIEW_UNTRACKED_MAX_FILE_BYTES = 10n * 1024n * 1024n;
-const REVIEW_UNTRACKED_MAX_TOTAL_BYTES = 64n * 1024n * 1024n;
+// git diff --numstat reads ~5 ms/MB of untracked text; larger files are left out of the review index.
+const REVIEW_UNTRACKED_MAX_TOTAL_BYTES = 256n * 1024n * 1024n;
const REVIEW_DIFF_FILE_MAX_OUTPUT_BYTES = 1024 * 1024;
const REVIEW_BRANCH_COMMIT_LIMIT = 100;
const REVIEW_BRANCH_SUBJECT_LIMIT = 300;
@@ -3171,34 +3171,22 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
(candidate) =>
onlyPath === undefined || candidate === onlyPath || candidate === `${onlyPath}/`,
);
- if (paths.length === 0) return { env: undefined, complete: true };
+ if (paths.length === 0) return { env: undefined, omitted: false };
if (paths.length > REVIEW_UNTRACKED_MAX_FILES) return null;
const boundedPaths: string[] = [];
let bytes = 0n;
- let complete = true;
for (const relativePath of paths) {
- // lstat bounds sparse files by logical size without reading symlink targets.
+ // lstat reads logical size without following symlinks.
const info = yield* fileSystem
.lstat(path.resolve(cwd, relativePath))
.pipe(Effect.orElseSucceed(() => undefined));
- if (
- info === undefined ||
- (info.type !== "File" && info.type !== "Directory" && info.type !== "SymbolicLink")
- ) {
- complete = false;
- continue;
- }
- const size = info.type === "Directory" ? 0n : info.size;
- if (
- size > REVIEW_UNTRACKED_MAX_FILE_BYTES ||
- bytes + size > REVIEW_UNTRACKED_MAX_TOTAL_BYTES
- ) {
- complete = false;
- continue;
- }
+ const size = info?.type === "File" || info?.type === "SymbolicLink" ? info.size : 0n;
+ if (bytes + size > REVIEW_UNTRACKED_MAX_TOTAL_BYTES) continue;
bytes += size;
boundedPaths.push(relativePath);
}
+ const omitted = boundedPaths.length < paths.length;
+ if (boundedPaths.length === 0) return { env: undefined, omitted };
const env = yield* prepareReviewIndex(cwd, boundedPaths).pipe(
Effect.catchTags({
PlatformError: (cause) =>
@@ -3213,7 +3201,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
),
}),
);
- return { env, complete };
+ return { env, omitted };
});
const resolveReviewMergeBase = Effect.fn("resolveReviewMergeBase")(function* (
@@ -3290,12 +3278,12 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
: [];
// Empty status can mean untracked files are hidden; retain the full scan.
const untracked = yield* prepareUntrackedReviewIndex(cwd, pathspecs);
- if (untracked === null || !untracked.complete) {
+ if (untracked === null) {
return yield* new GitCommandError({
operation: "GitVcsDriver.readBranchChangeTotals",
command: "git ls-files",
cwd,
- detail: "Untracked files exceed the review input limits.",
+ detail: "Too many untracked files to count.",
});
}
const readNumstat = (ref: string) =>
@@ -3624,10 +3612,9 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
});
const [dirtyTrackedResult, baseResult] = yield* Effect.gen(function* () {
const untracked = input.trackedOnly
- ? { env: undefined, complete: true }
+ ? { env: undefined, omitted: false }
: yield* prepareUntrackedReviewIndex(cwd, pathArgs, input.file?.path);
- const metadataOnly =
- input.metadataOnly === true && !input.file && untracked?.complete === true;
+ const metadataOnly = input.metadataOnly === true && !input.file && untracked !== null;
const [dirty, base] =
review.mergeBase === dirtyRef
? yield* readTrackedDiff(dirtyRef, untracked?.env, metadataOnly).pipe(
@@ -3640,9 +3627,12 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
],
{ concurrency: 2 },
);
- if (untracked?.complete) return [dirty, base] as const;
+ if (untracked?.omitted === false) return [dirty, base] as const;
+ // Too many untracked files drops per-file totals; omitted oversized files only mark it partial.
const incomplete = (ref: string | null, result: typeof dirty) =>
- ref === null ? result : { ...result, files: undefined, stdoutTruncated: true };
+ ref === null
+ ? result
+ : { ...result, stdoutTruncated: true, files: untracked ? result.files : undefined };
return [incomplete(dirtyRef, dirty), incomplete(review.mergeBase, base)] as const;
}).pipe(Effect.scoped);
const dirtyFiles = dirtyTrackedResult.files;
From 329a8eaeb8fa912eef0a63d44d1ed3ec9b8e1d51 Mon Sep 17 00:00:00 2001
From: Julius Marminge
Date: Wed, 11 Nov 2026 04:56:26 +0000
Subject: [PATCH 4/5] fix(server): flag omitted untracked files so clients warn
about them
Review sources now carry untrackedOmitted when untracked files past the
size budget were left out. Web and mobile show a notice for it, instead
of reusing truncated, which the lazy per-file view hides.
AI-assisted; verified with the tests and tools listed above.
---
apps/mobile/src/features/review/reviewModel.ts | 12 +++++++++---
.../mobile/src/features/review/useReviewDiffData.ts | 3 ++-
apps/server/src/vcs/GitVcsDriverCore.test.ts | 4 ++--
apps/server/src/vcs/GitVcsDriverCore.ts | 13 ++++++-------
apps/web/src/components/DiffPanel.tsx | 10 +++++++++-
packages/contracts/src/review.ts | 2 ++
6 files changed, 30 insertions(+), 14 deletions(-)
diff --git a/apps/mobile/src/features/review/reviewModel.ts b/apps/mobile/src/features/review/reviewModel.ts
index df3c7c51324b4..960e3a69783ec 100644
--- a/apps/mobile/src/features/review/reviewModel.ts
+++ b/apps/mobile/src/features/review/reviewModel.ts
@@ -664,14 +664,20 @@ export function buildReviewParsedDiff(
}
}
+export const UNTRACKED_OMITTED_NOTICE =
+ "Some untracked files are too large to review. They are not shown or counted.";
+
export function applyReviewDiffMetadata(
previewDiff: ReviewParsedDiff,
- selectedSection: Pick | null,
+ selectedSection: Pick | null,
): ReviewParsedDiff {
if (previewDiff.kind === "empty") return previewDiff;
+ const untrackedOmitted = selectedSection?.source?.untrackedOmitted === true;
const notice = selectedSection?.truncated
- ? `This preview exceeds the size limit. Changes shown are incomplete.${selectedSection.files ? " Counts include all changes." : ""}`
- : previewDiff.notice;
+ ? `This preview exceeds the size limit. Changes shown are incomplete.${selectedSection.files && !untrackedOmitted ? " Counts include all changes." : ""}`
+ : untrackedOmitted
+ ? UNTRACKED_OMITTED_NOTICE
+ : previewDiff.notice;
if (previewDiff.kind !== "files" || !selectedSection?.files) return { ...previewDiff, notice };
const totals = selectedSection.files.reduce(
(total, file) => ({
diff --git a/apps/mobile/src/features/review/useReviewDiffData.ts b/apps/mobile/src/features/review/useReviewDiffData.ts
index d9ec656cbc773..d45ab1ad2524b 100644
--- a/apps/mobile/src/features/review/useReviewDiffData.ts
+++ b/apps/mobile/src/features/review/useReviewDiffData.ts
@@ -6,6 +6,7 @@ import { getCachedNativeReviewDiffData } from "./nativeReviewDiffAdapter";
import { getCachedReviewParsedDiff } from "./reviewState";
import {
applyReviewDiffMetadata,
+ UNTRACKED_OMITTED_NOTICE,
buildReviewParsedDiff,
type ReviewParsedDiff,
type ReviewRenderableFile,
@@ -225,7 +226,7 @@ export function useReviewDiffData(input: {
fileCount: files.length,
additions: files.reduce((total, file) => total + file.additions, 0),
deletions: files.reduce((total, file) => total + file.deletions, 0),
- notice: null,
+ notice: lazySource.untrackedOmitted ? UNTRACKED_OMITTED_NOTICE : null,
};
}, [lazySource, previewDiff, selectedSection, patches]);
const headerDiffSummary = useMemo(
diff --git a/apps/server/src/vcs/GitVcsDriverCore.test.ts b/apps/server/src/vcs/GitVcsDriverCore.test.ts
index febaef3e09063..9e429df3c2b1b 100644
--- a/apps/server/src/vcs/GitVcsDriverCore.test.ts
+++ b/apps/server/src/vcs/GitVcsDriverCore.test.ts
@@ -2663,7 +2663,7 @@ it.layer(layerTest)("GitVcsDriver core integration", (it) => {
const preview = yield* driver.getReviewDiffPreview({ cwd, metadataOnly: true });
const source = preview.sources.find((item) => item.kind === "working-tree")!;
- assert.isTrue(source.truncated);
+ assert.isTrue(source.untrackedOmitted);
assert.deepStrictEqual(
source.files?.map((file) => file.path),
symlinksSupported
@@ -2676,7 +2676,7 @@ it.layer(layerTest)("GitVcsDriver core integration", (it) => {
cwd,
file: { path: "large.data", previousPath: null, sourceKind: "working-tree" },
});
- assert.isTrue(oversized.sources[0]!.truncated);
+ assert.isTrue(oversized.sources[0]!.untrackedOmitted);
assert.isEmpty(oversized.sources[0]!.diff);
}),
);
diff --git a/apps/server/src/vcs/GitVcsDriverCore.ts b/apps/server/src/vcs/GitVcsDriverCore.ts
index fa6a16b0ac9a1..02480468af2a3 100644
--- a/apps/server/src/vcs/GitVcsDriverCore.ts
+++ b/apps/server/src/vcs/GitVcsDriverCore.ts
@@ -3610,7 +3610,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
);
return { ...patch, files: stat.files };
});
- const [dirtyTrackedResult, baseResult] = yield* Effect.gen(function* () {
+ const [dirtyTrackedResult, baseResult, untrackedOmitted] = yield* Effect.gen(function* () {
const untracked = input.trackedOnly
? { env: undefined, omitted: false }
: yield* prepareUntrackedReviewIndex(cwd, pathArgs, input.file?.path);
@@ -3627,13 +3627,10 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
],
{ concurrency: 2 },
);
- if (untracked?.omitted === false) return [dirty, base] as const;
- // Too many untracked files drops per-file totals; omitted oversized files only mark it partial.
+ if (untracked !== null) return [dirty, base, untracked.omitted] as const;
const incomplete = (ref: string | null, result: typeof dirty) =>
- ref === null
- ? result
- : { ...result, stdoutTruncated: true, files: untracked ? result.files : undefined };
- return [incomplete(dirtyRef, dirty), incomplete(review.mergeBase, base)] as const;
+ ref === null ? result : { ...result, files: undefined, stdoutTruncated: true };
+ return [incomplete(dirtyRef, dirty), incomplete(review.mergeBase, base), false] as const;
}).pipe(Effect.scoped);
const dirtyFiles = dirtyTrackedResult.files;
const baseFiles = baseResult.files;
@@ -3655,6 +3652,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
...(dirtyFiles === undefined ? {} : { files: dirtyFiles }),
diffHash: dirtyDiffHash,
truncated: dirtyTrackedResult.stdoutTruncated,
+ ...(untrackedOmitted ? { untrackedOmitted } : {}),
...(input.metadataOnly && !input.file && dirtyFiles !== undefined
? { patchesDeferred: true }
: {}),
@@ -3669,6 +3667,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
...(baseFiles === undefined ? {} : { files: baseFiles }),
diffHash: baseDiffHash,
truncated: baseResult.stdoutTruncated,
+ ...(untrackedOmitted ? { untrackedOmitted } : {}),
...(input.metadataOnly && !input.file && baseFiles !== undefined
? { patchesDeferred: true }
: {}),
diff --git a/apps/web/src/components/DiffPanel.tsx b/apps/web/src/components/DiffPanel.tsx
index e7e6d614ef77f..f5d9a01775fc2 100644
--- a/apps/web/src/components/DiffPanel.tsx
+++ b/apps/web/src/components/DiffPanel.tsx
@@ -1308,10 +1308,18 @@ export default function DiffPanel({
: "Showing tracked changes. Checking untracked files..."}
)}
+ {isGitSelection && selectedGitSource?.untrackedOmitted && (
+
+ Some untracked files are too large to review. They are not shown or counted.
+
+ )}
{isSelectedPatchTruncated && !lazySource && (
This preview exceeds the size limit. Changes shown are incomplete.
- {isGitSelection && selectedGitSource?.files && !preview?.incomplete
+ {isGitSelection &&
+ selectedGitSource?.files &&
+ !preview?.incomplete &&
+ !selectedGitSource.untrackedOmitted
? " Totals include all changes."
: ""}
diff --git a/packages/contracts/src/review.ts b/packages/contracts/src/review.ts
index 2608e635ba906..7d5d082ef8b11 100644
--- a/packages/contracts/src/review.ts
+++ b/packages/contracts/src/review.ts
@@ -65,6 +65,8 @@ export const ReviewDiffPreviewSource = Schema.Struct({
diffHash: TrimmedNonEmptyString,
truncated: Schema.Boolean,
patchesDeferred: Schema.optionalKey(Schema.Boolean),
+ // Untracked files past the review size budget are left out of the diff, files and totals.
+ untrackedOmitted: Schema.optionalKey(Schema.Boolean),
files: Schema.optionalKey(Schema.Array(ReviewDiffFileStat)),
});
export type ReviewDiffPreviewSource = typeof ReviewDiffPreviewSource.Type;
From b2507708a1abb533caecd16b17ea69474525b9ee Mon Sep 17 00:00:00 2001
From: Julius Marminge
Date: Wed, 11 Nov 2026 05:10:04 +0000
Subject: [PATCH 5/5] fix(server): keep omitted untracked files from looking
like complete totals
Changes totals are unavailable when untracked files were left out, as they
are above the file-count cap, and mobile's empty review state now explains
that oversized untracked files were omitted.
AI-assisted; verified with the tests and tools listed above.
---
apps/mobile/src/features/review/ReviewSheet.tsx | 6 ++++--
apps/server/src/vcs/GitVcsDriverCore.test.ts | 2 +-
apps/server/src/vcs/GitVcsDriverCore.ts | 4 ++--
3 files changed, 7 insertions(+), 5 deletions(-)
diff --git a/apps/mobile/src/features/review/ReviewSheet.tsx b/apps/mobile/src/features/review/ReviewSheet.tsx
index a953ca80901fa..777e121c89ead 100644
--- a/apps/mobile/src/features/review/ReviewSheet.tsx
+++ b/apps/mobile/src/features/review/ReviewSheet.tsx
@@ -6,7 +6,7 @@ import { useNavigation, type StaticScreenProps } from "@react-navigation/native"
import { AppActivityIndicator } from "../../components/ui/AppActivityIndicator";
import { AppBar } from "../../components/ui/AppBar";
import type { AppBarMenuItem } from "../../components/ui/AppBar.types";
-import type { ReviewSectionItem } from "./reviewModel";
+import { UNTRACKED_OMITTED_NOTICE, type ReviewSectionItem } from "./reviewModel";
import { useReviewHeaderPresentation } from "./useReviewHeaderPresentation";
import { type Ref, type ReactElement, useImperativeHandle, useRef, useState } from "react";
import {
@@ -766,7 +766,9 @@ export function ReviewSheet(props: ReviewSheetProps) {
Platform.OS === "android" && "mt-2 text-center",
)}
>
- {selectedSection.subtitle ?? "This diff is empty."}
+ {selectedSection.source?.untrackedOmitted
+ ? UNTRACKED_OMITTED_NOTICE
+ : (selectedSection.subtitle ?? "This diff is empty.")}
) : parsedDiff.kind === "raw" ? (
diff --git a/apps/server/src/vcs/GitVcsDriverCore.test.ts b/apps/server/src/vcs/GitVcsDriverCore.test.ts
index 9e429df3c2b1b..c26e07834b87b 100644
--- a/apps/server/src/vcs/GitVcsDriverCore.test.ts
+++ b/apps/server/src/vcs/GitVcsDriverCore.test.ts
@@ -2671,7 +2671,7 @@ it.layer(layerTest)("GitVcsDriver core integration", (it) => {
: ["README.md", "ordinary.txt"],
);
const status = yield* driver.statusDetailsLocal(cwd, { includeBranchChanges: true });
- assert.strictEqual(status.branchChanges?.insertions, symlinksSupported ? 3 : 2);
+ assert.isUndefined(status.branchChanges);
const oversized = yield* driver.getReviewDiffPreview({
cwd,
file: { path: "large.data", previousPath: null, sourceKind: "working-tree" },
diff --git a/apps/server/src/vcs/GitVcsDriverCore.ts b/apps/server/src/vcs/GitVcsDriverCore.ts
index 02480468af2a3..a5b498bf63981 100644
--- a/apps/server/src/vcs/GitVcsDriverCore.ts
+++ b/apps/server/src/vcs/GitVcsDriverCore.ts
@@ -3278,12 +3278,12 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
: [];
// Empty status can mean untracked files are hidden; retain the full scan.
const untracked = yield* prepareUntrackedReviewIndex(cwd, pathspecs);
- if (untracked === null) {
+ if (untracked === null || untracked.omitted) {
return yield* new GitCommandError({
operation: "GitVcsDriver.readBranchChangeTotals",
command: "git ls-files",
cwd,
- detail: "Too many untracked files to count.",
+ detail: "Untracked files exceed the review input limits.",
});
}
const readNumstat = (ref: string) =>