Skip to content
This repository was archived by the owner on Aug 6, 2026. It is now read-only.

Commit 2a3ccb0

Browse files
committed
consolidate file-list cap and timeout into listAllFiles
1 parent 5b6948c commit 2a3ccb0

4 files changed

Lines changed: 96 additions & 68 deletions

File tree

‎packages/git/src/queries.test.ts‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
getChangedFilesDetailed,
1414
getGitBusyState,
1515
getLinkedWorktreeMainPath,
16+
listAllFiles,
1617
remoteBranchExists,
1718
splitUnifiedDiffByFile,
1819
} from "./queries";
@@ -597,3 +598,44 @@ describe("getLinkedWorktreeMainPath", () => {
597598
expect(getLinkedWorktreeMainPath(worktreeDir)).toBeNull();
598599
});
599600
});
601+
602+
describe("listAllFiles", () => {
603+
let repoDir: string;
604+
605+
afterEach(async () => {
606+
if (repoDir) {
607+
await rm(repoDir, { recursive: true, force: true });
608+
}
609+
});
610+
611+
it("combines tracked and untracked files uncapped by default", async () => {
612+
repoDir = await setupRepo();
613+
await writeFile(path.join(repoDir, "untracked.txt"), "content");
614+
615+
const files = await listAllFiles(repoDir);
616+
617+
expect(files.sort()).toEqual(["file.txt", "untracked.txt"]);
618+
});
619+
620+
it("truncates to maxFiles", async () => {
621+
repoDir = await setupRepo();
622+
const git = createGitClient(repoDir);
623+
await writeFile(path.join(repoDir, "b.txt"), "content");
624+
await writeFile(path.join(repoDir, "c.txt"), "content");
625+
await git.add(["b.txt", "c.txt"]);
626+
await git.commit("add more files");
627+
628+
const files = await listAllFiles(repoDir, { maxFiles: 2 });
629+
630+
expect(files.length).toBe(2);
631+
});
632+
633+
it("keeps untracked files over tracked ones when truncating", async () => {
634+
repoDir = await setupRepo();
635+
await writeFile(path.join(repoDir, "untracked.txt"), "content");
636+
637+
const files = await listAllFiles(repoDir, { maxFiles: 1 });
638+
639+
expect(files).toEqual(["untracked.txt"]);
640+
});
641+
});

‎packages/git/src/queries.ts‎

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1181,15 +1181,39 @@ export async function listUntrackedFiles(
11811181
);
11821182
}
11831183

1184+
export interface ListAllFilesOptions {
1185+
maxFiles?: number;
1186+
timeoutMs?: number;
1187+
}
1188+
11841189
export async function listAllFiles(
11851190
baseDir: string,
1186-
options?: CreateGitClientOptions,
1191+
options?: ListAllFilesOptions,
11871192
): Promise<string[]> {
1188-
const [tracked, untracked] = await Promise.all([
1189-
listFiles(baseDir, options),
1190-
listUntrackedFiles(baseDir, options),
1191-
]);
1192-
return [...tracked, ...untracked];
1193+
const { maxFiles, timeoutMs } = options ?? {};
1194+
const controller =
1195+
timeoutMs !== undefined ? new AbortController() : undefined;
1196+
const timer =
1197+
controller && timeoutMs !== undefined
1198+
? setTimeout(() => controller.abort(), timeoutMs)
1199+
: undefined;
1200+
try {
1201+
const [tracked, untracked] = await Promise.all([
1202+
listFiles(baseDir, { abortSignal: controller?.signal }).catch(
1203+
(): string[] => [],
1204+
),
1205+
listUntrackedFiles(baseDir, { abortSignal: controller?.signal }).catch(
1206+
(): string[] => [],
1207+
),
1208+
]);
1209+
const combined = untracked.concat(tracked);
1210+
if (maxFiles !== undefined && combined.length > maxFiles) {
1211+
combined.splice(maxFiles);
1212+
}
1213+
return combined;
1214+
} finally {
1215+
if (timer) clearTimeout(timer);
1216+
}
11931217
}
11941218

11951219
// Tracked + untracked files containing `pattern` (literal, case-insensitive).

‎packages/workspace-server/src/services/fs/service.test.ts‎

Lines changed: 15 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,20 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
55

66
vi.mock("@posthog/git/queries", () => ({
77
getChangedFiles: vi.fn(async () => new Set<string>()),
8-
listFiles: vi.fn(async () => []),
9-
listUntrackedFiles: vi.fn(async () => []),
8+
listAllFiles: vi.fn(async () => []),
109
}));
1110

12-
import {
13-
getChangedFiles,
14-
listFiles,
15-
listUntrackedFiles,
16-
} from "@posthog/git/queries";
11+
import { getChangedFiles, listAllFiles } from "@posthog/git/queries";
1712
import { FsService } from "./service";
1813

1914
describe("FsService.listRepoFiles", () => {
2015
it("derives directory entries alongside files", async () => {
2116
vi.mocked(getChangedFiles).mockResolvedValue(new Set());
22-
vi.mocked(listFiles).mockResolvedValue([
17+
vi.mocked(listAllFiles).mockResolvedValue([
2318
"a.ts",
2419
"src/b.ts",
2520
"src/sub/c.ts",
2621
]);
27-
vi.mocked(listUntrackedFiles).mockResolvedValue([]);
2822

2923
const service = new FsService();
3024
const entries = await service.listRepoFiles("/repo");
@@ -40,12 +34,11 @@ describe("FsService.listRepoFiles", () => {
4034

4135
it("filters directories and files by query substring", async () => {
4236
vi.mocked(getChangedFiles).mockResolvedValue(new Set());
43-
vi.mocked(listFiles).mockResolvedValue([
37+
vi.mocked(listAllFiles).mockResolvedValue([
4438
"a.ts",
4539
"src/b.ts",
4640
"src/sub/c.ts",
4741
]);
48-
vi.mocked(listUntrackedFiles).mockResolvedValue([]);
4942

5043
const service = new FsService();
5144
const entries = await service.listRepoFiles("/repo", "sub");
@@ -56,26 +49,26 @@ describe("FsService.listRepoFiles", () => {
5649
]);
5750
});
5851

59-
it("caps file list at MAX_REPO_FILES when repo is very large", async () => {
52+
it("passes the file cap and timeout through to listAllFiles", async () => {
6053
vi.mocked(getChangedFiles).mockResolvedValue(new Set());
61-
const bigList = Array.from({ length: 60_000 }, (_, i) => `file${i}.ts`);
62-
vi.mocked(listFiles).mockResolvedValue(bigList);
63-
vi.mocked(listUntrackedFiles).mockResolvedValue([]);
54+
vi.mocked(listAllFiles).mockResolvedValue([]);
6455

6556
const service = new FsService();
66-
const entries = await service.listRepoFiles("/repo");
57+
await service.listRepoFiles("/repo");
6758

68-
expect(entries.length).toBe(50_000);
59+
expect(listAllFiles).toHaveBeenCalledWith("/repo", {
60+
maxFiles: 50_000,
61+
timeoutMs: 8_000,
62+
});
6963
});
7064

71-
it("total entries can exceed MAX_REPO_FILES when derived directories are included", async () => {
65+
it("total entries can exceed the file cap when derived directories are included", async () => {
7266
vi.mocked(getChangedFiles).mockResolvedValue(new Set());
73-
const bigList = Array.from(
74-
{ length: 60_000 },
67+
const cappedList = Array.from(
68+
{ length: 50_000 },
7569
(_, i) => `src/sub${i}/file.ts`,
7670
);
77-
vi.mocked(listFiles).mockResolvedValue(bigList);
78-
vi.mocked(listUntrackedFiles).mockResolvedValue([]);
71+
vi.mocked(listAllFiles).mockResolvedValue(cappedList);
7972

8073
const service = new FsService();
8174
const entries = await service.listRepoFiles("/repo");
@@ -84,17 +77,6 @@ describe("FsService.listRepoFiles", () => {
8477
expect(fileEntries.length).toBe(50_000);
8578
expect(entries.length).toBeGreaterThan(50_000);
8679
});
87-
88-
it("omits untracked files when git ls-files --others is aborted", async () => {
89-
vi.mocked(getChangedFiles).mockResolvedValue(new Set());
90-
vi.mocked(listFiles).mockResolvedValue(["tracked.ts"]);
91-
vi.mocked(listUntrackedFiles).mockRejectedValue(new Error("AbortError"));
92-
93-
const service = new FsService();
94-
const entries = await service.listRepoFiles("/repo");
95-
96-
expect(entries.some((e) => e.path === "tracked.ts")).toBe(true);
97-
});
9880
});
9981

10082
describe("FsService repo file IO", () => {

‎packages/workspace-server/src/services/fs/service.ts‎

Lines changed: 9 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
import fs from "node:fs/promises";
22
import path from "node:path";
3-
import {
4-
getChangedFiles,
5-
listFiles,
6-
listUntrackedFiles,
7-
} from "@posthog/git/queries";
3+
import { getChangedFiles, listAllFiles } from "@posthog/git/queries";
84
import { injectable } from "inversify";
95
import type { BoundedReadResult, DirectoryEntry, FileEntry } from "./schemas";
106

@@ -13,7 +9,7 @@ export class FsService {
139
private static readonly CACHE_TTL = 30000;
1410
private static readonly READ_REPO_FILES_CONCURRENCY = 24;
1511
private static readonly MAX_REPO_FILES = 50_000;
16-
private static readonly UNTRACKED_TIMEOUT_MS = 8_000;
12+
private static readonly LIST_FILES_TIMEOUT_MS = 8_000;
1713
private cache = new Map<string, { files: FileEntry[]; timestamp: number }>();
1814

1915
async listDirectory(dirPath: string): Promise<DirectoryEntry[]> {
@@ -49,7 +45,7 @@ export class FsService {
4945
const changedFiles = await getChangedFiles(repoPath);
5046

5147
if (query?.trim()) {
52-
const allFiles = await this.fetchAllFiles(repoPath);
48+
const allFiles = await this.listAllFilesBounded(repoPath);
5349
const directories = this.deriveDirectories(allFiles);
5450
const lowerQuery = query.toLowerCase();
5551
const matchingDirs = directories.filter((d) =>
@@ -70,7 +66,7 @@ export class FsService {
7066
return limit ? cached.files.slice(0, limit) : cached.files;
7167
}
7268

73-
const files = await this.fetchAllFiles(repoPath);
69+
const files = await this.listAllFilesBounded(repoPath);
7470
const directories = this.deriveDirectories(files);
7571
const entries = [
7672
...this.toDirectoryEntries(directories),
@@ -227,27 +223,11 @@ export class FsService {
227223
}));
228224
}
229225

230-
private async fetchAllFiles(repoPath: string): Promise<string[]> {
231-
const controller = new AbortController();
232-
const timer = setTimeout(
233-
() => controller.abort(),
234-
FsService.UNTRACKED_TIMEOUT_MS,
235-
);
236-
try {
237-
const [tracked, untracked] = await Promise.all([
238-
listFiles(repoPath),
239-
listUntrackedFiles(repoPath, { abortSignal: controller.signal }).catch(
240-
() => [],
241-
),
242-
]);
243-
const combined = tracked.concat(untracked);
244-
if (combined.length > FsService.MAX_REPO_FILES) {
245-
combined.length = FsService.MAX_REPO_FILES;
246-
}
247-
return combined;
248-
} finally {
249-
clearTimeout(timer);
250-
}
226+
private listAllFilesBounded(repoPath: string): Promise<string[]> {
227+
return listAllFiles(repoPath, {
228+
maxFiles: FsService.MAX_REPO_FILES,
229+
timeoutMs: FsService.LIST_FILES_TIMEOUT_MS,
230+
});
251231
}
252232

253233
private deriveDirectories(files: string[]): string[] {

0 commit comments

Comments
 (0)