diff --git a/apps/server/src/vcs/GitVcsDriverCore.test.ts b/apps/server/src/vcs/GitVcsDriverCore.test.ts index 82d070f87cd0d8b7aa12d42bcb785892acbd6b66..c6e24caa62505c4c06bc01b03103727fbedbdef2 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.test.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.test.ts @@ -2159,6 +2159,28 @@ it.layer(layerTest)("GitVcsDriver core integration", (it) => { }), ); + it.effect("skips Changes totals instead of indexing thousands of untracked files", () => + Effect.gen(function* () { + const cwd = yield* makeTmpDir(); + yield* initRepoWithCommit(cwd); + const driver = yield* GitVcsDriver.GitVcsDriver; + yield* Effect.forEach( + Array.from({ length: 5_001 }, (_, index) => `bulk/${index}.txt`), + (file) => writeTextFile(cwd, file, "x\n"), + { concurrency: 32, discard: true }, + ); + yield* writeTextFile(cwd, "README.md", "changed\n"); + + const status = yield* driver.statusDetailsLocal(cwd, { includeBranchChanges: true }); + assert.isTrue(status.hasWorkingTreeChanges); + assert.isUndefined(status.branchChanges); + + const preview = yield* driver.getReviewDiffPreview({ cwd }); + assert.isNotEmpty(preview.sources); + assert.isTrue(preview.sources.every((source) => source.truncated)); + }), + ); + it.effect("Changes does not show newer base commits as deletions", () => Effect.gen(function* () { const cwd = yield* makeTmpDir(); diff --git a/apps/server/src/vcs/GitVcsDriverCore.ts b/apps/server/src/vcs/GitVcsDriverCore.ts index d07baa1727c15ca68dae08c9d90d7a5bf816ca7d..140786cce0a469a2bddbda6b50759e4110a6f3ef 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.ts @@ -59,6 +59,10 @@ 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_METADATA_MAX_OUTPUT_BYTES = 16 * 1024 * 1024; +// Every status refresh adds the untracked files to a temporary index, and git add's cost grows +// much faster than the file count (50k files took about 30 seconds). Past this many, the +// totals are reported incomplete instead. +const REVIEW_UNTRACKED_MAX_FILES = 5_000; const REVIEW_DIFF_FILE_MAX_OUTPUT_BYTES = 1024 * 1024; // Patches the clients render are parsed against git's default a/ and b/ path // prefixes. A repository or global diff.noprefix or diff.mnemonicPrefix would @@ -2618,7 +2622,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* }); // Lists untracked files and adds them to a temporary index, so a diff against any commit - // shows them as new. Returns null when the list is too big to read. Needs a Scope. + // shows them as new. Returns null when the list is too big to read or to index. Needs a Scope. const prepareUntrackedReviewIndex = Effect.fn("prepareUntrackedReviewIndex")(function* ( cwd: string, pathArgs: ReadonlyArray, @@ -2640,6 +2644,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* (candidate) => onlyPath === undefined || candidate === onlyPath, ); if (paths.length === 0) return { env: undefined }; + if (paths.length > REVIEW_UNTRACKED_MAX_FILES) return null; const env = yield* prepareReviewIndex(cwd, paths).pipe( Effect.catchTags({ PlatformError: (cause) =>