Skip to content

Commit 470ab3d

Browse files
committed
add gitIgnore module tests
1 parent 223638f commit 470ab3d

File tree

2 files changed

+103
-1
lines changed

2 files changed

+103
-1
lines changed

apps/server/src/gitIgnore.test.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { assert, beforeEach, describe, it, vi } from "vitest";
2+
3+
import type { ProcessRunOptions, ProcessRunResult } from "./processRunner";
4+
5+
const { runProcessMock } = vi.hoisted(() => ({
6+
runProcessMock:
7+
vi.fn<
8+
(
9+
command: string,
10+
args: readonly string[],
11+
options?: ProcessRunOptions,
12+
) => Promise<ProcessRunResult>
13+
>(),
14+
}));
15+
16+
vi.mock("./processRunner", () => ({
17+
runProcess: runProcessMock,
18+
}));
19+
20+
function processResult(
21+
overrides: Partial<ProcessRunResult> & Pick<ProcessRunResult, "stdout" | "code">,
22+
): ProcessRunResult {
23+
return {
24+
stdout: overrides.stdout,
25+
code: overrides.code,
26+
stderr: overrides.stderr ?? "",
27+
signal: overrides.signal ?? null,
28+
timedOut: overrides.timedOut ?? false,
29+
stdoutTruncated: overrides.stdoutTruncated ?? false,
30+
stderrTruncated: overrides.stderrTruncated ?? false,
31+
};
32+
}
33+
34+
describe("gitIgnore", () => {
35+
beforeEach(() => {
36+
runProcessMock.mockReset();
37+
vi.resetModules();
38+
});
39+
40+
it("chunks large git check-ignore requests and filters ignored matches", async () => {
41+
const ignoredPaths = Array.from(
42+
{ length: 320 },
43+
(_, index) => `ignored/${index.toString().padStart(4, "0")}/${"x".repeat(1024)}.ts`,
44+
);
45+
const keptPaths = ["src/keep.ts", "docs/readme.md"];
46+
const relativePaths = [...ignoredPaths, ...keptPaths];
47+
let checkIgnoreCalls = 0;
48+
49+
runProcessMock.mockImplementation(async (_command, args, options) => {
50+
if (args[0] === "check-ignore") {
51+
checkIgnoreCalls += 1;
52+
const chunkPaths = (options?.stdin ?? "").split("\0").filter((value) => value.length > 0);
53+
const chunkIgnored = chunkPaths.filter((value) => value.startsWith("ignored/"));
54+
return processResult({
55+
code: chunkIgnored.length > 0 ? 0 : 1,
56+
stdout: chunkIgnored.length > 0 ? `${chunkIgnored.join("\0")}\0` : "",
57+
});
58+
}
59+
60+
throw new Error(`Unexpected command: git ${args.join(" ")}`);
61+
});
62+
63+
const { filterGitIgnoredPaths } = await import("./gitIgnore");
64+
const result = await filterGitIgnoredPaths("/virtual/workspace", relativePaths);
65+
66+
assert.isAbove(checkIgnoreCalls, 1);
67+
assert.deepEqual(result, keptPaths);
68+
});
69+
70+
it("fails open when git check-ignore cannot complete", async () => {
71+
const relativePaths = ["src/keep.ts", "ignored.txt"];
72+
73+
runProcessMock.mockRejectedValueOnce(new Error("spawn failed"));
74+
75+
const { filterGitIgnoredPaths } = await import("./gitIgnore");
76+
const result = await filterGitIgnoredPaths("/virtual/workspace", relativePaths);
77+
78+
assert.deepEqual(result, relativePaths);
79+
});
80+
});

apps/server/src/gitIgnore.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,29 @@ import { runProcess } from "./processRunner";
22

33
const GIT_CHECK_IGNORE_MAX_STDIN_BYTES = 256 * 1024;
44

5+
/**
6+
* Shared git-ignore helpers for server-side workspace scans.
7+
*
8+
* Both callers use these helpers as an optimization and a consistency layer, not
9+
* as a hard dependency. If git is unavailable, slow, or returns an unexpected
10+
* result, we intentionally fail open so the UI keeps working and avoids hiding
11+
* files unpredictably.
12+
*/
13+
514
function splitNullSeparatedPaths(input: string, truncated: boolean): string[] {
615
const parts = input.split("\0");
7-
if (parts.length === 0) return [];
816
if (truncated && parts[parts.length - 1]?.length) {
917
parts.pop();
1018
}
1119
return parts.filter((value) => value.length > 0);
1220
}
1321

22+
/**
23+
* Returns whether `cwd` is inside a git work tree.
24+
*
25+
* This is a cheap capability probe used to decide whether later git-aware
26+
* filtering is worth attempting.
27+
*/
1428
export async function isInsideGitWorkTree(cwd: string): Promise<boolean> {
1529
const insideWorkTree = await runProcess("git", ["rev-parse", "--is-inside-work-tree"], {
1630
cwd,
@@ -24,6 +38,14 @@ export async function isInsideGitWorkTree(cwd: string): Promise<boolean> {
2438
);
2539
}
2640

41+
/**
42+
* Filters repo-relative paths that match git ignore rules for `cwd`.
43+
*
44+
* We use `git check-ignore --no-index` so both tracked and untracked candidates
45+
* respect the current ignore rules. Input is chunked to keep stdin bounded, and
46+
* unexpected git failures return the original paths unchanged so callers fail
47+
* open instead of dropping potentially valid files.
48+
*/
2749
export async function filterGitIgnoredPaths(
2850
cwd: string,
2951
relativePaths: readonly string[],

0 commit comments

Comments
 (0)