ftreeview 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +235 -0
- package/dist/cli.js +1619 -0
- package/package.json +58 -0
- package/src/App.jsx +243 -0
- package/src/cli.js +228 -0
- package/src/components/StatusBar.jsx +270 -0
- package/src/components/TreeLine.jsx +190 -0
- package/src/components/TreeView.jsx +129 -0
- package/src/hooks/useChangedFiles.js +82 -0
- package/src/hooks/useGitStatus.js +347 -0
- package/src/hooks/useIgnore.js +182 -0
- package/src/hooks/useNavigation.js +247 -0
- package/src/hooks/useTree.js +508 -0
- package/src/hooks/useWatcher.js +129 -0
- package/src/index.js +22 -0
- package/src/lib/changeStatus.js +79 -0
- package/src/lib/connectors.js +233 -0
- package/src/lib/constants.js +64 -0
- package/src/lib/icons.js +658 -0
- package/src/lib/theme.js +102 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useGitStatus - Git Status Hook
|
|
3
|
+
*
|
|
4
|
+
* Provides git status information for files in a directory tree.
|
|
5
|
+
* Parses git status --porcelain=v1 -uall output to get file status.
|
|
6
|
+
*
|
|
7
|
+
* This hook detects non-git directories and handles errors gracefully.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { execFile } from 'node:child_process';
|
|
11
|
+
import { useEffect, useState, useCallback, useRef } from 'react';
|
|
12
|
+
import { DEFAULT_CONFIG } from '../lib/constants.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Parse git status --porcelain=v1 -uall output into a status map
|
|
16
|
+
*
|
|
17
|
+
* Porcelain format: XY filename
|
|
18
|
+
* X = staged status, Y = unstaged status
|
|
19
|
+
*
|
|
20
|
+
* Status codes:
|
|
21
|
+
* - "M " (modified, staged) → "M" green
|
|
22
|
+
* - " M" (modified, unstaged) → "M" yellow
|
|
23
|
+
* - "MM" (modified both) → "M" yellow
|
|
24
|
+
* - "A " (added, staged) → "A" green
|
|
25
|
+
* - "?? " (untracked) → "U" green
|
|
26
|
+
* - "D " (deleted, staged) → "D" red
|
|
27
|
+
* - " D" (deleted, unstaged) → "D" red
|
|
28
|
+
* - "R " (renamed, staged) → "R" cyan
|
|
29
|
+
* - "!!" (ignored) → skip
|
|
30
|
+
*
|
|
31
|
+
* @param {string} output - Raw git status --porcelain=v1 -uall output
|
|
32
|
+
* @returns {Map<string, string>} Map of relative paths to status codes
|
|
33
|
+
*/
|
|
34
|
+
export function parsePorcelain(output) {
|
|
35
|
+
const statusMap = new Map();
|
|
36
|
+
|
|
37
|
+
// Split output into lines and process each
|
|
38
|
+
const lines = output.split('\n').filter(Boolean);
|
|
39
|
+
|
|
40
|
+
for (const line of lines) {
|
|
41
|
+
// Skip ignored files
|
|
42
|
+
if (line.startsWith('!!')) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Extract status codes (first two characters)
|
|
47
|
+
const stagedStatus = line[0];
|
|
48
|
+
const unstagedStatus = line[1];
|
|
49
|
+
|
|
50
|
+
// Extract filename (after status codes and space)
|
|
51
|
+
// Handle quoted filenames with spaces (format: XY "filename")
|
|
52
|
+
let filePath;
|
|
53
|
+
if (line[2] === '"') {
|
|
54
|
+
// Quoted filename - find closing quote
|
|
55
|
+
const endQuote = line.indexOf('"', 3);
|
|
56
|
+
filePath = line.slice(3, endQuote);
|
|
57
|
+
} else {
|
|
58
|
+
// Simple filename - split by space
|
|
59
|
+
filePath = line.slice(3);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Determine status based on porcelain codes
|
|
63
|
+
let statusCode = '';
|
|
64
|
+
|
|
65
|
+
// Check for rename (R in staged position, possibly followed by original filename)
|
|
66
|
+
// Format: R oldname -> newname
|
|
67
|
+
if (stagedStatus === 'R') {
|
|
68
|
+
statusCode = 'R'; // Renamed (cyan)
|
|
69
|
+
// Handle "R old -> new" format - extract the new filename
|
|
70
|
+
if (filePath.includes(' -> ')) {
|
|
71
|
+
filePath = filePath.split(' -> ')[1];
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// Deleted (staged or unstaged)
|
|
75
|
+
else if (stagedStatus === 'D' || unstagedStatus === 'D') {
|
|
76
|
+
statusCode = 'D'; // Deleted (red)
|
|
77
|
+
}
|
|
78
|
+
// Added (staged)
|
|
79
|
+
else if (stagedStatus === 'A') {
|
|
80
|
+
statusCode = 'A'; // Added (green)
|
|
81
|
+
}
|
|
82
|
+
// Modified - check both staged and unstaged
|
|
83
|
+
else if (stagedStatus === 'M' || unstagedStatus === 'M') {
|
|
84
|
+
statusCode = 'M'; // Modified (yellow if unstaged, green if staged)
|
|
85
|
+
}
|
|
86
|
+
// Untracked
|
|
87
|
+
else if (stagedStatus === '?' && unstagedStatus === '?') {
|
|
88
|
+
statusCode = 'U'; // Untracked (green)
|
|
89
|
+
}
|
|
90
|
+
// Conflicted (both stage deleted, or merged but unresolved)
|
|
91
|
+
else if (
|
|
92
|
+
(stagedStatus === 'D' && unstagedStatus === 'D') ||
|
|
93
|
+
(stagedStatus === 'A' && unstagedStatus === 'A') ||
|
|
94
|
+
(stagedStatus === 'U' || unstagedStatus === 'U')
|
|
95
|
+
) {
|
|
96
|
+
statusCode = 'C'; // Conflicted
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Only add if we have a status code
|
|
100
|
+
if (statusCode) {
|
|
101
|
+
// Git reports untracked directories with a trailing slash (e.g. "?? newdir/").
|
|
102
|
+
// Strip it so it matches our FileNode.path (which never includes a trailing slash).
|
|
103
|
+
if (filePath.endsWith('/') || filePath.endsWith('\\')) {
|
|
104
|
+
filePath = filePath.slice(0, -1);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Normalize path separators to forward slashes (git uses forward slashes)
|
|
108
|
+
const normalizedPath = filePath.replace(/\\/g, '/');
|
|
109
|
+
statusMap.set(normalizedPath, statusCode);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return statusMap;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Compare two status maps for structural equality.
|
|
118
|
+
*
|
|
119
|
+
* @param {Map<string, string>} first - First status map
|
|
120
|
+
* @param {Map<string, string>} second - Second status map
|
|
121
|
+
* @returns {boolean} True when maps contain identical keys and values
|
|
122
|
+
*/
|
|
123
|
+
function areStatusMapsEqual(first, second) {
|
|
124
|
+
if (first === second) return true;
|
|
125
|
+
if (!first || !second) return false;
|
|
126
|
+
if (first.size !== second.size) return false;
|
|
127
|
+
|
|
128
|
+
for (const [path, status] of first.entries()) {
|
|
129
|
+
if (second.get(path) !== status) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Checks if the given path is within a git repository
|
|
139
|
+
*
|
|
140
|
+
* @param {string} rootPath - Path to check
|
|
141
|
+
* @returns {Promise<boolean>} True if path is in a git repository
|
|
142
|
+
*/
|
|
143
|
+
function isGitRepo(rootPath) {
|
|
144
|
+
return new Promise((resolve) => {
|
|
145
|
+
execFile(
|
|
146
|
+
'git',
|
|
147
|
+
['rev-parse', '--git-dir'],
|
|
148
|
+
{ cwd: rootPath, timeout: 5000 },
|
|
149
|
+
(error) => {
|
|
150
|
+
// If git command succeeds (no error), we're in a git repo
|
|
151
|
+
resolve(!error);
|
|
152
|
+
}
|
|
153
|
+
);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Executes git status --porcelain=v1 -uall and returns the output
|
|
159
|
+
*
|
|
160
|
+
* @param {string} rootPath - Root path of the repository
|
|
161
|
+
* @returns {Promise<string>} Git status output
|
|
162
|
+
*/
|
|
163
|
+
function getGitStatus(rootPath) {
|
|
164
|
+
return new Promise((resolve, reject) => {
|
|
165
|
+
execFile(
|
|
166
|
+
'git',
|
|
167
|
+
['status', '--porcelain=v1', '-uall'],
|
|
168
|
+
{ cwd: rootPath, timeout: 10000 },
|
|
169
|
+
(error, stdout) => {
|
|
170
|
+
if (error) {
|
|
171
|
+
reject(error);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
resolve(stdout);
|
|
175
|
+
}
|
|
176
|
+
);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* React hook for git status integration
|
|
182
|
+
*
|
|
183
|
+
* This hook runs git status on mount and provides:
|
|
184
|
+
* - statusMap: Map of relative paths to status codes
|
|
185
|
+
* - isGitRepo: Boolean indicating if in a git repository
|
|
186
|
+
* - refresh: Function to manually refresh git status
|
|
187
|
+
* - isLoading: Boolean indicating if git status is being fetched
|
|
188
|
+
* - error: Error object if git status failed
|
|
189
|
+
*
|
|
190
|
+
* @param {string} rootPath - Root directory path
|
|
191
|
+
* @param {boolean} enabled - Whether git status is enabled
|
|
192
|
+
* @returns {{statusMap: Map<string, string>, isGitRepo: boolean, refresh: function, isLoading: boolean, error: Error|null}}
|
|
193
|
+
*/
|
|
194
|
+
export function useGitStatus(rootPath, enabled = true) {
|
|
195
|
+
const minRefreshMs = Math.max(100, DEFAULT_CONFIG.debounceDelay * 2);
|
|
196
|
+
const [statusMap, setStatusMap] = useState(new Map());
|
|
197
|
+
const [isGitRepo, setIsGitRepo] = useState(false);
|
|
198
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
199
|
+
const [error, setError] = useState(null);
|
|
200
|
+
const mountedRef = useRef(true);
|
|
201
|
+
const inFlightRef = useRef(false);
|
|
202
|
+
const pendingRefreshRef = useRef(false);
|
|
203
|
+
const refreshTimerRef = useRef(null);
|
|
204
|
+
const lastFetchAtRef = useRef(0);
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Fetches and parses git status
|
|
208
|
+
*/
|
|
209
|
+
const fetchGitStatus = useCallback(async () => {
|
|
210
|
+
if (inFlightRef.current) {
|
|
211
|
+
pendingRefreshRef.current = true;
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
inFlightRef.current = true;
|
|
216
|
+
lastFetchAtRef.current = Date.now();
|
|
217
|
+
|
|
218
|
+
if (!enabled || !rootPath) {
|
|
219
|
+
setStatusMap((prev) => (prev.size === 0 ? prev : new Map()));
|
|
220
|
+
setIsGitRepo(false);
|
|
221
|
+
setError(null);
|
|
222
|
+
inFlightRef.current = false;
|
|
223
|
+
pendingRefreshRef.current = false;
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
setIsLoading(true);
|
|
228
|
+
setError(null);
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
// First check if we're in a git repository
|
|
232
|
+
const repoCheck = await isGitRepo(rootPath);
|
|
233
|
+
|
|
234
|
+
if (!mountedRef.current) return;
|
|
235
|
+
|
|
236
|
+
if (!repoCheck) {
|
|
237
|
+
// Not a git repository
|
|
238
|
+
setIsGitRepo(false);
|
|
239
|
+
setStatusMap((prev) => (prev.size === 0 ? prev : new Map()));
|
|
240
|
+
setIsLoading(false);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
setIsGitRepo(true);
|
|
245
|
+
|
|
246
|
+
// Fetch git status
|
|
247
|
+
const output = await getGitStatus(rootPath);
|
|
248
|
+
|
|
249
|
+
if (!mountedRef.current) return;
|
|
250
|
+
|
|
251
|
+
// Parse output into status map
|
|
252
|
+
const map = parsePorcelain(output);
|
|
253
|
+
setStatusMap((prev) => (areStatusMapsEqual(prev, map) ? prev : map));
|
|
254
|
+
} catch (err) {
|
|
255
|
+
if (!mountedRef.current) return;
|
|
256
|
+
|
|
257
|
+
// Handle errors gracefully
|
|
258
|
+
// Common errors: git not installed, not a git repo, permission denied
|
|
259
|
+
setError(err);
|
|
260
|
+
setIsGitRepo(false);
|
|
261
|
+
setStatusMap((prev) => (prev.size === 0 ? prev : new Map()));
|
|
262
|
+
} finally {
|
|
263
|
+
if (mountedRef.current) {
|
|
264
|
+
setIsLoading(false);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
inFlightRef.current = false;
|
|
268
|
+
|
|
269
|
+
if (pendingRefreshRef.current && mountedRef.current && enabled && rootPath) {
|
|
270
|
+
pendingRefreshRef.current = false;
|
|
271
|
+
const elapsedMs = Date.now() - lastFetchAtRef.current;
|
|
272
|
+
const delayMs = Math.max(0, minRefreshMs - elapsedMs);
|
|
273
|
+
|
|
274
|
+
if (refreshTimerRef.current) {
|
|
275
|
+
clearTimeout(refreshTimerRef.current);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
refreshTimerRef.current = setTimeout(() => {
|
|
279
|
+
refreshTimerRef.current = null;
|
|
280
|
+
fetchGitStatus();
|
|
281
|
+
}, delayMs);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}, [rootPath, enabled, minRefreshMs]);
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Manual refresh function with coalescing/throttling.
|
|
288
|
+
*/
|
|
289
|
+
const refresh = useCallback(() => {
|
|
290
|
+
if (!enabled || !rootPath) {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (inFlightRef.current) {
|
|
295
|
+
pendingRefreshRef.current = true;
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const elapsedMs = Date.now() - lastFetchAtRef.current;
|
|
300
|
+
if (elapsedMs < minRefreshMs) {
|
|
301
|
+
pendingRefreshRef.current = true;
|
|
302
|
+
|
|
303
|
+
if (refreshTimerRef.current) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const delayMs = minRefreshMs - elapsedMs;
|
|
308
|
+
refreshTimerRef.current = setTimeout(() => {
|
|
309
|
+
refreshTimerRef.current = null;
|
|
310
|
+
pendingRefreshRef.current = false;
|
|
311
|
+
fetchGitStatus();
|
|
312
|
+
}, delayMs);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
pendingRefreshRef.current = false;
|
|
317
|
+
if (refreshTimerRef.current) {
|
|
318
|
+
clearTimeout(refreshTimerRef.current);
|
|
319
|
+
refreshTimerRef.current = null;
|
|
320
|
+
}
|
|
321
|
+
fetchGitStatus();
|
|
322
|
+
}, [enabled, rootPath, minRefreshMs, fetchGitStatus]);
|
|
323
|
+
|
|
324
|
+
// Fetch on mount and when dependencies change
|
|
325
|
+
useEffect(() => {
|
|
326
|
+
mountedRef.current = true;
|
|
327
|
+
fetchGitStatus();
|
|
328
|
+
|
|
329
|
+
return () => {
|
|
330
|
+
mountedRef.current = false;
|
|
331
|
+
inFlightRef.current = false;
|
|
332
|
+
pendingRefreshRef.current = false;
|
|
333
|
+
if (refreshTimerRef.current) {
|
|
334
|
+
clearTimeout(refreshTimerRef.current);
|
|
335
|
+
refreshTimerRef.current = null;
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
}, [fetchGitStatus]);
|
|
339
|
+
|
|
340
|
+
return {
|
|
341
|
+
statusMap,
|
|
342
|
+
isGitRepo,
|
|
343
|
+
refresh,
|
|
344
|
+
isLoading,
|
|
345
|
+
error,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useIgnore - Gitignore Filtering Hook
|
|
3
|
+
*
|
|
4
|
+
* Provides gitignore-style filtering functionality using the 'ignore' package.
|
|
5
|
+
* Supports loading .gitignore files from the root and nested directories.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, readdirSync, statSync, readFileSync } from 'node:fs';
|
|
9
|
+
import { resolve, join, relative, sep } from 'node:path';
|
|
10
|
+
import ignore from 'ignore';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Default patterns that are always applied (unless --no-ignore is set)
|
|
14
|
+
*/
|
|
15
|
+
const DEFAULT_PATTERNS = [
|
|
16
|
+
'.git',
|
|
17
|
+
'.git/',
|
|
18
|
+
'.git/**',
|
|
19
|
+
'node_modules',
|
|
20
|
+
'node_modules/',
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Finds all .gitignore files in a directory tree
|
|
25
|
+
*
|
|
26
|
+
* @param {string} rootPath - Root directory path
|
|
27
|
+
* @returns {Map<string, string>} Map of directory paths to their .gitignore content
|
|
28
|
+
*/
|
|
29
|
+
function findGitignoreFiles(rootPath) {
|
|
30
|
+
const gitignoreMap = new Map();
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Recursively scan for .gitignore files
|
|
34
|
+
* @param {string} dirPath - Current directory to scan
|
|
35
|
+
*/
|
|
36
|
+
function scanDirectory(dirPath) {
|
|
37
|
+
try {
|
|
38
|
+
const entries = readdirSync(dirPath, { withFileTypes: true });
|
|
39
|
+
|
|
40
|
+
// Check if this directory has a .gitignore file
|
|
41
|
+
const gitignorePath = join(dirPath, '.gitignore');
|
|
42
|
+
if (existsSync(gitignorePath)) {
|
|
43
|
+
// We'll read these lazily when needed to avoid excessive I/O
|
|
44
|
+
gitignoreMap.set(dirPath, gitignorePath);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Recurse into subdirectories
|
|
48
|
+
for (const entry of entries) {
|
|
49
|
+
if (entry.isDirectory() && entry.name !== '.git') {
|
|
50
|
+
const subPath = join(dirPath, entry.name);
|
|
51
|
+
scanDirectory(subPath);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
} catch (error) {
|
|
55
|
+
// Skip directories we can't read (permission errors, etc.)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
scanDirectory(rootPath);
|
|
60
|
+
return gitignoreMap;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Creates an ignore filter function based on .gitignore files
|
|
65
|
+
*
|
|
66
|
+
* @param {string} rootPath - Root directory path
|
|
67
|
+
* @param {object} options - Configuration options
|
|
68
|
+
* @param {boolean} [options.showIgnored=false] - If true, don't filter anything (--no-ignore)
|
|
69
|
+
* @param {string[]} [options.extraIgnorePatterns] - Additional patterns to ignore
|
|
70
|
+
* @returns {object} Object with shouldIgnore method and addPattern method
|
|
71
|
+
*/
|
|
72
|
+
export function createIgnoreFilter(rootPath, options = {}) {
|
|
73
|
+
const { showIgnored = false, extraIgnorePatterns = [] } = options;
|
|
74
|
+
|
|
75
|
+
// If showIgnored is true, return a pass-through filter
|
|
76
|
+
if (showIgnored) {
|
|
77
|
+
return {
|
|
78
|
+
shouldIgnore: (relativePath) => false,
|
|
79
|
+
addPattern: () => {},
|
|
80
|
+
filter: (paths) => paths,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Initialize the ignore instance
|
|
85
|
+
const ig = ignore();
|
|
86
|
+
|
|
87
|
+
// Add default patterns (unless showIgnored is true)
|
|
88
|
+
for (const pattern of DEFAULT_PATTERNS) {
|
|
89
|
+
ig.add(pattern);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Add extra patterns from options
|
|
93
|
+
if (extraIgnorePatterns.length > 0) {
|
|
94
|
+
ig.add(extraIgnorePatterns);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Find all .gitignore files
|
|
98
|
+
const gitignoreMap = findGitignoreFiles(rootPath);
|
|
99
|
+
|
|
100
|
+
// Track which .gitignore files we've already loaded
|
|
101
|
+
const loadedIgnores = new Set();
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Ensures .gitignore patterns for a given path are loaded
|
|
105
|
+
* This implements lazy loading - we only load patterns when we encounter a directory
|
|
106
|
+
*
|
|
107
|
+
* @param {string} dirAbsPath - Absolute path to directory
|
|
108
|
+
*/
|
|
109
|
+
function loadPatternsForDirectory(dirAbsPath) {
|
|
110
|
+
if (loadedIgnores.has(dirAbsPath)) {
|
|
111
|
+
return; // Already loaded
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const gitignorePath = gitignoreMap.get(dirAbsPath);
|
|
115
|
+
if (gitignorePath && existsSync(gitignorePath)) {
|
|
116
|
+
try {
|
|
117
|
+
// Read and parse the .gitignore file
|
|
118
|
+
const content = readFileSync(gitignorePath, 'utf-8');
|
|
119
|
+
ig.add(content);
|
|
120
|
+
loadedIgnores.add(dirAbsPath);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
// Silently ignore read errors
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Checks if a relative path should be ignored
|
|
129
|
+
*
|
|
130
|
+
* @param {string} relativePath - Path relative to root (forward slashes)
|
|
131
|
+
* @returns {boolean} True if the path should be ignored
|
|
132
|
+
*/
|
|
133
|
+
function shouldIgnore(relativePath) {
|
|
134
|
+
// Empty string (root directory) is never ignored
|
|
135
|
+
if (relativePath === '') {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Normalize path to use forward slashes
|
|
140
|
+
const normalizedPath = relativePath.split(sep).join('/');
|
|
141
|
+
|
|
142
|
+
return ig.ignores(normalizedPath);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Filters an array of paths, keeping only non-ignored ones
|
|
147
|
+
*
|
|
148
|
+
* @param {string[]} paths - Array of relative paths
|
|
149
|
+
* @returns {string[]} Filtered array of paths
|
|
150
|
+
*/
|
|
151
|
+
function filter(paths) {
|
|
152
|
+
return ig.filter(paths);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Adds a pattern to the ignore filter
|
|
157
|
+
*
|
|
158
|
+
* @param {string|string[]} pattern - Pattern or array of patterns to add
|
|
159
|
+
*/
|
|
160
|
+
function addPattern(pattern) {
|
|
161
|
+
ig.add(pattern);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
shouldIgnore,
|
|
166
|
+
filter,
|
|
167
|
+
addPattern,
|
|
168
|
+
loadPatternsForDirectory,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* React hook for creating an ignore filter
|
|
174
|
+
* This is a convenience wrapper around createIgnoreFilter
|
|
175
|
+
*
|
|
176
|
+
* @param {string} rootPath - Root directory path
|
|
177
|
+
* @param {object} options - Configuration options
|
|
178
|
+
* @returns {object} Filter object with shouldIgnore method
|
|
179
|
+
*/
|
|
180
|
+
export function useIgnore(rootPath, options = {}) {
|
|
181
|
+
return createIgnoreFilter(rootPath, options);
|
|
182
|
+
}
|