diff --git a/gitnexus/src/config/ignore-service.ts b/gitnexus/src/config/ignore-service.ts index a5d0e05bb..163998a8e 100644 --- a/gitnexus/src/config/ignore-service.ts +++ b/gitnexus/src/config/ignore-service.ts @@ -466,9 +466,9 @@ export const createIgnoreFilter = async (repoPath: string, options?: IgnoreOptio return { ignored(p: Path): boolean { - // path-scurry's Path.relative() returns POSIX paths on all platforms, - // which is what the `ignore` package expects. No explicit normalization needed. - const rel = p.relative(); + // The `ignore` package expects POSIX separators; path-scurry can surface + // native separators on Windows when called through glob. + const rel = p.relative().replace(/\\/g, '/'); if (!rel) return false; // User's .gitnexusignore negation takes precedence over hardcoded // rules (#771). If any ancestor or the path itself was explicitly @@ -488,7 +488,7 @@ export const createIgnoreFilter = async (repoPath: string, options?: IgnoreOptio // glob's `dot: false` option in filesystem-walker.ts. The hardcoded // list check below is defense-in-depth — do not remove `dot: false` // assuming this covers it. - const rel = p.relative(); + const rel = p.relative().replace(/\\/g, '/'); // User's .gitnexusignore negation takes precedence (#771) — if the // user explicitly unignored this directory or any ancestor via a // !pattern rule, allow descent even if the directory name is in diff --git a/gitnexus/test/integration/filesystem-walker.test.ts b/gitnexus/test/integration/filesystem-walker.test.ts index 8f934573c..031f446dd 100644 --- a/gitnexus/test/integration/filesystem-walker.test.ts +++ b/gitnexus/test/integration/filesystem-walker.test.ts @@ -219,6 +219,73 @@ describe('filesystem-walker', () => { }); }); + describe('.gitnexusignore negation of hardcoded directories', () => { + let partsDir: string; + + beforeAll(async () => { + partsDir = await fs.mkdtemp(path.join(os.tmpdir(), 'gn-walker-parts-')); + + await fs.mkdir(path.join(partsDir, 'parts', 'src', 'main', 'java', 'com', 'example'), { + recursive: true, + }); + await fs.mkdir( + path.join( + partsDir, + 'admin', + 'src', + 'main', + 'java', + 'com', + 'example', + 'controller', + 'parts', + ), + { recursive: true }, + ); + await fs.mkdir(path.join(partsDir, 'node_modules', 'pkg'), { recursive: true }); + + await fs.writeFile(path.join(partsDir, '.gitnexusignore'), '!parts/\n'); + await fs.writeFile( + path.join(partsDir, 'parts', 'src', 'main', 'java', 'com', 'example', 'Part.java'), + 'package com.example; class Part {}', + ); + await fs.writeFile( + path.join( + partsDir, + 'admin', + 'src', + 'main', + 'java', + 'com', + 'example', + 'controller', + 'parts', + 'PartsController.java', + ), + 'package com.example.controller.parts; class PartsController {}', + ); + await fs.writeFile( + path.join(partsDir, 'node_modules', 'pkg', 'index.js'), + 'module.exports = {}', + ); + }); + + afterAll(async () => { + await fs.rm(partsDir, { recursive: true, force: true }); + }); + + it('traverses top-level and nested parts directories when explicitly unignored (#2673)', async () => { + const files = await walkRepositoryPaths(partsDir); + const paths = files.map((f) => f.path.replace(/\\/g, '/')); + + expect(paths).toContain('parts/src/main/java/com/example/Part.java'); + expect(paths).toContain( + 'admin/src/main/java/com/example/controller/parts/PartsController.java', + ); + expect(paths.every((p) => !p.includes('node_modules/'))).toBe(true); + }); + }); + describe('combined .gitignore + .gitnexusignore', () => { let combinedDir: string; diff --git a/gitnexus/test/unit/ignore-service.test.ts b/gitnexus/test/unit/ignore-service.test.ts index 23cb9abbc..42e1986df 100644 --- a/gitnexus/test/unit/ignore-service.test.ts +++ b/gitnexus/test/unit/ignore-service.test.ts @@ -335,6 +335,22 @@ describe('.gitnexusignore negation overrides hardcoded DEFAULT_IGNORE_LIST (#771 expect(filter.ignored(mkPath('node_modules/express/index.js'))).toBe(false); }); + it('`!parts/` unlocks Java modules and nested parts directories (#2673)', async () => { + await fs.writeFile(path.join(tmpDir, '.gitnexusignore'), '!parts/\n'); + const filter = await createIgnoreFilter(tmpDir); + + expect(filter.childrenIgnored(mkPath('parts'))).toBe(false); + expect(filter.ignored(mkPath('parts/src/main/java/com/example/Part.java'))).toBe(false); + expect(filter.childrenIgnored(mkPath('admin/src/main/java/com/example/controller/parts'))).toBe( + false, + ); + expect( + filter.ignored( + mkPath('admin/src/main/java/com/example/controller/parts/PartsController.java'), + ), + ).toBe(false); + }); + it('negation of one hardcoded entry does not leak to others', async () => { await fs.writeFile(path.join(tmpDir, '.gitnexusignore'), '!__tests__/\n'); const filter = await createIgnoreFilter(tmpDir);