diff --git a/gitnexus/src/config/ignore-service.ts b/gitnexus/src/config/ignore-service.ts index affd83578f..b7fb5a3971 100644 --- a/gitnexus/src/config/ignore-service.ts +++ b/gitnexus/src/config/ignore-service.ts @@ -186,13 +186,65 @@ const IGNORED_FILES = new Set([ +// User-defined ignore patterns loaded from .gitnexusignore +let userIgnorePatterns: string[] = []; +let userIgnoreLoaded = false; + +/** + * Load .gitnexusignore from the repo root. Called once per analysis. + * Format: one pattern per line, # comments, blank lines ignored. + * Patterns are path prefixes (e.g., "app/engine/source/") or + * directory names (e.g., "workspace") matched against path segments. + */ +export const loadIgnoreFile = async (repoPath: string): Promise => { + if (userIgnoreLoaded) return; + userIgnoreLoaded = true; + + const fs = await import('fs/promises'); + const path = await import('path'); + const ignorePath = path.join(repoPath, '.gitnexusignore'); + + try { + const content = await fs.readFile(ignorePath, 'utf-8'); + userIgnorePatterns = content + .split('\n') + .map(line => line.trim()) + .filter(line => line.length > 0 && !line.startsWith('#')); + } catch { + // No .gitnexusignore file — that's fine + userIgnorePatterns = []; + } +}; + +/** Reset loaded state (for testing or re-indexing) */ +export const resetIgnoreFile = (): void => { + userIgnorePatterns = []; + userIgnoreLoaded = false; +}; + export const shouldIgnorePath = (filePath: string): boolean => { const normalizedPath = filePath.replace(/\\/g, '/'); const parts = normalizedPath.split('/'); const fileName = parts[parts.length - 1]; const fileNameLower = fileName.toLowerCase(); - // Check if any path segment is in ignore list + // Check user-defined .gitnexusignore patterns first + for (const pattern of userIgnorePatterns) { + // Path prefix match: "app/engine/source/" matches "app/engine/source/foo.ts" + if (pattern.includes('/')) { + const cleanPattern = pattern.replace(/\/+$/, ''); + if (normalizedPath.startsWith(cleanPattern + '/') || normalizedPath === cleanPattern) { + return true; + } + } else { + // Directory name match: "workspace" matches any path segment + if (parts.includes(pattern)) { + return true; + } + } + } + + // Check if any path segment is in default ignore list for (const part of parts) { if (DEFAULT_IGNORE_LIST.has(part)) { return true; diff --git a/gitnexus/src/core/ingestion/filesystem-walker.ts b/gitnexus/src/core/ingestion/filesystem-walker.ts index 7074593a0a..82845352c6 100644 --- a/gitnexus/src/core/ingestion/filesystem-walker.ts +++ b/gitnexus/src/core/ingestion/filesystem-walker.ts @@ -1,7 +1,7 @@ import fs from 'fs/promises'; import path from 'path'; import { glob } from 'glob'; -import { shouldIgnorePath } from '../../config/ignore-service.js'; +import { shouldIgnorePath, loadIgnoreFile } from '../../config/ignore-service.js'; export interface FileEntry { path: string; @@ -32,6 +32,9 @@ export const walkRepositoryPaths = async ( repoPath: string, onProgress?: (current: number, total: number, filePath: string) => void ): Promise => { + // Load .gitnexusignore before filtering + await loadIgnoreFile(repoPath); + const files = await glob('**/*', { cwd: repoPath, nodir: true,