-
Notifications
You must be signed in to change notification settings - Fork 2k
Expand file tree
/
Copy pathgitIgnore.ts
More file actions
123 lines (105 loc) · 3.44 KB
/
gitIgnore.ts
File metadata and controls
123 lines (105 loc) · 3.44 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
import { runProcess } from "./processRunner";
const GIT_CHECK_IGNORE_MAX_STDIN_BYTES = 256 * 1024;
/**
* Shared git-ignore helpers for server-side workspace scans.
*
* Both callers use these helpers as an optimization and a consistency layer, not
* as a hard dependency. If git is unavailable, slow, or returns an unexpected
* result, we intentionally fail open so the UI keeps working and avoids hiding
* files unpredictably.
*/
function splitNullSeparatedPaths(input: string, truncated: boolean): string[] {
const parts = input.split("\0");
if (truncated && parts[parts.length - 1]?.length) {
parts.pop();
}
return parts.filter((value) => value.length > 0);
}
/**
* Returns whether `cwd` is inside a git work tree.
*
* This is a cheap capability probe used to decide whether later git-aware
* filtering is worth attempting.
*/
export async function isInsideGitWorkTree(cwd: string): Promise<boolean> {
const insideWorkTree = await runProcess("git", ["rev-parse", "--is-inside-work-tree"], {
cwd,
allowNonZeroExit: true,
timeoutMs: 5_000,
maxBufferBytes: 4_096,
}).catch(() => null);
return Boolean(
insideWorkTree && insideWorkTree.code === 0 && insideWorkTree.stdout.trim() === "true",
);
}
/**
* Filters repo-relative paths that match git ignore rules for `cwd`.
*
* We use `git check-ignore --no-index` so both tracked and untracked candidates
* respect the current ignore rules. Input is chunked to keep stdin bounded, and
* unexpected git failures return the original paths unchanged so callers fail
* open instead of dropping potentially valid files.
*/
export async function filterGitIgnoredPaths(
cwd: string,
relativePaths: readonly string[],
): Promise<string[]> {
if (relativePaths.length === 0) {
return [...relativePaths];
}
const ignoredPaths = new Set<string>();
let chunk: string[] = [];
let chunkBytes = 0;
const flushChunk = async (): Promise<boolean> => {
if (chunk.length === 0) {
return true;
}
const checkIgnore = await runProcess("git", ["check-ignore", "--no-index", "-z", "--stdin"], {
cwd,
allowNonZeroExit: true,
timeoutMs: 20_000,
maxBufferBytes: 16 * 1024 * 1024,
outputMode: "truncate",
stdin: `${chunk.join("\0")}\0`,
}).catch(() => null);
chunk = [];
chunkBytes = 0;
if (!checkIgnore) {
return false;
}
// git-check-ignore exits with 1 when no paths match.
if (checkIgnore.code !== 0 && checkIgnore.code !== 1) {
return false;
}
const matchedIgnoredPaths = splitNullSeparatedPaths(
checkIgnore.stdout,
Boolean(checkIgnore.stdoutTruncated),
);
for (const ignoredPath of matchedIgnoredPaths) {
ignoredPaths.add(ignoredPath);
}
return true;
};
for (const relativePath of relativePaths) {
const relativePathBytes = Buffer.byteLength(relativePath) + 1;
if (
chunk.length > 0 &&
chunkBytes + relativePathBytes > GIT_CHECK_IGNORE_MAX_STDIN_BYTES &&
!(await flushChunk())
) {
return [...relativePaths];
}
chunk.push(relativePath);
chunkBytes += relativePathBytes;
if (chunkBytes >= GIT_CHECK_IGNORE_MAX_STDIN_BYTES && !(await flushChunk())) {
return [...relativePaths];
}
}
if (!(await flushChunk())) {
return [...relativePaths];
}
if (ignoredPaths.size === 0) {
return [...relativePaths];
}
return relativePaths.filter((relativePath) => !ignoredPaths.has(relativePath));
}