authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-10-07 01:25:21-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-07 01:25:21-07:00
log10395bcc59707302c1476e19186532ff5a9d3557
treed30377b01ff7f502bc05e1db1abdc15430152182
parentb77108bc41506ad7ba28839d9a8e04ba79fe340b
signature Signed by PGP key B5690EEEBB952194

fix(server): status refresh no longer pegs CPU in repos with thousands of untracked files (#16771)

Co-authored-by: Braulio Oliveira <brauliobo@gmail.com> Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

2 files changed, 28 insertions(+), 1 deletions(-)

apps/server/src/vcs/GitVcsDriverCore.test.ts+22
......@@ -2159,6 +2159,28 @@ it.layer(layerTest)("GitVcsDriver core integration", (it) => {
21592159 }),
21602160 );
21612161
2162 it.effect("skips Changes totals instead of indexing thousands of untracked files", () =>
2163 Effect.gen(function* () {
2164 const cwd = yield* makeTmpDir();
2165 yield* initRepoWithCommit(cwd);
2166 const driver = yield* GitVcsDriver.GitVcsDriver;
2167 yield* Effect.forEach(
2168 Array.from({ length: 5_001 }, (_, index) => `bulk/${index}.txt`),
2169 (file) => writeTextFile(cwd, file, "x\n"),
2170 { concurrency: 32, discard: true },
2171 );
2172 yield* writeTextFile(cwd, "README.md", "changed\n");
2173
2174 const status = yield* driver.statusDetailsLocal(cwd, { includeBranchChanges: true });
2175 assert.isTrue(status.hasWorkingTreeChanges);
2176 assert.isUndefined(status.branchChanges);
2177
2178 const preview = yield* driver.getReviewDiffPreview({ cwd });
2179 assert.isNotEmpty(preview.sources);
2180 assert.isTrue(preview.sources.every((source) => source.truncated));
2181 }),
2182 );
2183
21622184 it.effect("Changes does not show newer base commits as deletions", () =>
21632185 Effect.gen(function* () {
21642186 const cwd = yield* makeTmpDir();
apps/server/src/vcs/GitVcsDriverCore.ts+6-1
......@@ -59,6 +59,10 @@ const RANGE_DIFF_SUMMARY_MAX_OUTPUT_BYTES = 19_000;
5959const RANGE_DIFF_PATCH_MAX_OUTPUT_BYTES = 59_000;
6060const REVIEW_DIFF_PATCH_MAX_OUTPUT_BYTES = 120_000;
6161const REVIEW_METADATA_MAX_OUTPUT_BYTES = 16 * 1024 * 1024;
62// Every status refresh adds the untracked files to a temporary index, and git add's cost grows
63// much faster than the file count (50k files took about 30 seconds). Past this many, the
64// totals are reported incomplete instead.
65const REVIEW_UNTRACKED_MAX_FILES = 5_000;
6266const REVIEW_DIFF_FILE_MAX_OUTPUT_BYTES = 1024 * 1024;
6367// Patches the clients render are parsed against git's default a/ and b/ path
6468// prefixes. A repository or global diff.noprefix or diff.mnemonicPrefix would
......@@ -2618,7 +2622,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
26182622 });
26192623
26202624 // Lists untracked files and adds them to a temporary index, so a diff against any commit
2621 // shows them as new. Returns null when the list is too big to read. Needs a Scope.
2625 // shows them as new. Returns null when the list is too big to read or to index. Needs a Scope.
26222626 const prepareUntrackedReviewIndex = Effect.fn("prepareUntrackedReviewIndex")(function* (
26232627 cwd: string,
26242628 pathArgs: ReadonlyArray<string>,
......@@ -2640,6 +2644,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
26402644 (candidate) => onlyPath === undefined || candidate === onlyPath,
26412645 );
26422646 if (paths.length === 0) return { env: undefined };
2647 if (paths.length > REVIEW_UNTRACKED_MAX_FILES) return null;
26432648 const env = yield* prepareReviewIndex(cwd, paths).pipe(
26442649 Effect.catchTags({
26452650 PlatformError: (cause) =>