From 9ca07982e19e52ba5989619b15ccfadb7d8da47f Mon Sep 17 00:00:00 2001 From: Andrew Gazelka Date: Thu, 2 Apr 2026 04:19:36 -0700 Subject: [PATCH] fix(watch): skip ignored directories when watching --- src/commands/watch.ts | 167 +++++++++++++++++++++++++++++++++++------- 1 file changed, 140 insertions(+), 27 deletions(-) diff --git a/src/commands/watch.ts b/src/commands/watch.ts index b334bb6..50e9d3d 100644 --- a/src/commands/watch.ts +++ b/src/commands/watch.ts @@ -139,44 +139,157 @@ export async function startWatch(options: WatchOptions): Promise { console.log("Watching for file changes in", watchRoot); fileSystem.loadMgrepignore(watchRoot); - fs.watch(watchRoot, { recursive: true }, (eventType, rawFilename) => { - const filename = rawFilename?.toString(); - if (!filename) { + + // Use per-directory non-recursive watchers to avoid watching ignored + // directories like .git/, node_modules/, target/, etc. fs.watch with + // recursive: true allocates inotify watches on ALL subdirectories before + // the callback filter runs, which exhausts the kernel watcher limit on + // large repos. + const watchers = new Map(); + + function isMissingPathError(error: unknown): boolean { + return ( + error instanceof Error && + "code" in error && + (error.code === "ENOENT" || error.code === "ENOTDIR") + ); + } + + function getPathStats(filePath: string): fs.Stats | null | undefined { + try { + return fs.statSync(filePath); + } catch (error) { + if (isMissingPathError(error)) { + return null; + } + + console.warn(`Warning: failed to inspect path ${filePath}:`, error); + return undefined; + } + } + + function closeWatcher(dirPath: string): void { + const watcher = watchers.get(dirPath); + if (!watcher) { return; } - const filePath = path.join(watchRoot, filename); + + watcher.close(); + watchers.delete(dirPath); + } + + function closeWatcherSubtree(dirPath: string): void { + const prefix = `${dirPath}${path.sep}`; + + for (const watchedPath of Array.from(watchers.keys())) { + if (watchedPath === dirPath || watchedPath.startsWith(prefix)) { + closeWatcher(watchedPath); + } + } + } + + function handleDeletion(filePath: string): void { + closeWatcherSubtree(filePath); + + void deleteFile(store, options.store, filePath) + .then(() => { + console.log(`delete: ${filePath}`); + }) + .catch((error) => { + console.error("Failed to delete file:", filePath, error); + }); + } + + function handleFileEvent( + eventType: fs.WatchEventType, + dirPath: string, + name: string, + ): void { + const filePath = path.join(dirPath, name); if (fileSystem.isIgnored(filePath, watchRoot)) { return; } + const stats = getPathStats(filePath); + if (stats === undefined) { + return; + } + + if (stats === null) { + handleDeletion(filePath); + return; + } + + if (stats.isDirectory()) { + watchDirectory(filePath); + return; + } + + if (!stats.isFile()) { + return; + } + + void uploadFile( + store, + options.store, + filePath, + path.basename(filePath), + config, + ) + .then((didUpload) => { + if (didUpload) { + console.log(`${eventType}: ${filePath}`); + } + }) + .catch((error) => { + console.error("Failed to upload changed file:", filePath, error); + }); + } + + function watchDirectory(dirPath: string): void { + if (watchers.has(dirPath)) { + return; + } + + if (dirPath !== watchRoot && fileSystem.isIgnored(dirPath, watchRoot)) { + return; + } + + const stats = getPathStats(dirPath); + if (!stats?.isDirectory()) { + return; + } + try { - const stat = fs.statSync(filePath); - if (!stat.isFile()) { - return; - } + const watcher = fs.watch(dirPath, (eventType, rawFilename) => { + const name = rawFilename?.toString(); + if (!name) { + return; + } - uploadFile(store, options.store, filePath, filename, config) - .then((didUpload) => { - if (didUpload) { - console.log(`${eventType}: ${filePath}`); - } - }) - .catch((err) => { - console.error("Failed to upload changed file:", filePath, err); - }); - } catch { - if (filePath.startsWith(watchRoot) && !fs.existsSync(filePath)) { - deleteFile(store, options.store, filePath) - .then(() => { - console.log(`delete: ${filePath}`); - }) - .catch((err) => { - console.error("Failed to delete file:", filePath, err); - }); + handleFileEvent(eventType, dirPath, name); + }); + watchers.set(dirPath, watcher); + } catch (error) { + console.warn(`Warning: failed to watch ${dirPath}:`, error); + return; + } + + try { + for (const entry of fs.readdirSync(dirPath, { withFileTypes: true })) { + if (!entry.isDirectory()) { + continue; + } + + watchDirectory(path.join(dirPath, entry.name)); } + } catch (error) { + console.warn(`Warning: failed to read directory ${dirPath}:`, error); } - }); + } + + watchDirectory(watchRoot); } catch (error) { if (refreshInterval) { clearInterval(refreshInterval);