fix(config): honor parts negation on Windows (#2720)
Some checks failed
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / ci (push) Blocked by required conditions
Skill copy sync / shipped skills drift guard (push) Has been cancelled

Normalize repository-relative paths before applying ignore-package rules so `.gitnexusignore` negation can override hardcoded `parts` exclusions during Windows traversal.

Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
This commit is contained in:
azizur100389 2026-07-28 20:04:34 +01:00 committed by GitHub
parent 7be6d29ca0
commit 79ff44dfa9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 87 additions and 4 deletions

View file

@ -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

View file

@ -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;

View file

@ -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);