Skip to content

fix(ignore): respect negation patterns in .gitnexusignore#654

Merged
magyargergo merged 3 commits into
abhigyanpatwari:mainfrom
ivkond:fix/gitnexusignore-negation-patterns
Apr 6, 2026
Merged

fix(ignore): respect negation patterns in .gitnexusignore#654
magyargergo merged 3 commits into
abhigyanpatwari:mainfrom
ivkond:fix/gitnexusignore-negation-patterns

Conversation

@ivkond

@ivkond ivkond commented Apr 4, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Fix: childrenIgnored() in ignore-service.ts checked ig.ignores(rel) || ig.ignores(rel + '/') — the bare-path check short-circuited on true because directory-only negation patterns (e.g. !iOS/) don't apply to paths without a trailing slash
  • Root cause: With an exclude-all + whitelist .gitnexusignore (*!iOS/!iOS/**), ALL directories were pruned during glob traversal, producing 0 nodes
  • Fix: Only check ig.ignores(rel + '/') since childrenIgnored is exclusively called for directories. Bare-name patterns (e.g. local) still match local/ per gitignore spec

Fixes #596
Related to #231 (PR that introduced .gitnexusignore support)

Test plan

  • Added unit test reproducing the exact issue v1.4.10: .gitnexusignore negation patterns silently produce 0 nodes #596 scenario (exclude-all + whitelist pattern)
  • Added unit test verifying ignored() respects negation for files under whitelisted dirs
  • All 142 ignore-service tests pass (140 existing + 2 new)
  • Full test suite passes (4811 tests, 0 regressions)
  • Manual verification: create repo with .gitnexusignore using * + !src/ + !src/** pattern and run gitnexus analyze

🤖 Generated with Claude Code

…ored

childrenIgnored checked `ig.ignores(rel) || ig.ignores(rel + '/')` which
short-circuited on the bare path — directory-only negation patterns like
`!iOS/` were missed because `ig.ignores('iOS')` treats the path as a file.
Now only checks with trailing slash since childrenIgnored is only called
for directories. Bare-name patterns (e.g. `local`) still match per gitignore spec.

Fixes #596

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Apr 4, 2026

Copy link
Copy Markdown

@ivkond is attempting to deploy a commit to the NexusCore Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions

github-actions Bot commented Apr 4, 2026

Copy link
Copy Markdown
Contributor

CI Report

All checks passed

Pipeline Status

Stage Status Details
✅ Typecheck success tsc --noEmit
✅ Tests success unit tests, 3 platforms
✅ E2E success gitnexus-web changes only

Test Results

Tests Passed Failed Skipped Duration
5530 5438 0 92 204s

✅ All 5438 tests passed

92 test(s) skipped — expand for details
  • Swift MethodExtractor > isTypeDeclaration > recognizes class_declaration
  • Swift MethodExtractor > isTypeDeclaration > recognizes protocol_declaration
  • Swift MethodExtractor > isTypeDeclaration > rejects import_declaration
  • Swift MethodExtractor > visibility > extracts public method
  • Swift MethodExtractor > visibility > extracts private method
  • Swift MethodExtractor > visibility > defaults to internal when no modifier
  • Swift MethodExtractor > protocol methods > marks protocol method as abstract
  • Swift MethodExtractor > static and class methods > detects static func as isStatic
  • Swift MethodExtractor > static and class methods > detects class func as isStatic
  • Swift MethodExtractor > parameters > extracts parameters with types and default values
  • Swift MethodExtractor > return type > extracts return type from -> annotation
  • Swift MethodExtractor > annotations > extracts @objc attribute
  • Swift MethodExtractor > isFinal > detects final func
  • Swift MethodExtractor > isFinal > is false when not final
  • Swift MethodExtractor > isAsync > detects async func
  • Swift MethodExtractor > isOverride > detects override method
  • buildTypeEnv > known limitations (documented skip tests) > Ruby block parameter: users.each { |user| } — closure param inference, different feature
  • Swift constructor-inferred type resolution > detects User and Repo classes, both with save methods
  • Swift constructor-inferred type resolution > resolves user.save() to Models/User.swift via constructor-inferred type
  • Swift constructor-inferred type resolution > resolves repo.save() to Models/Repo.swift via constructor-inferred type
  • Swift constructor-inferred type resolution > emits exactly 2 save() CALLS edges (one per receiver type)
  • Swift self resolution > detects User and Repo classes, each with a save function
  • Swift self resolution > resolves self.save() inside User.process to User.save, not Repo.save
  • Swift parent resolution > detects BaseModel and User classes plus Serializable protocol
  • Swift parent resolution > emits EXTENDS edge: User → BaseModel
  • Swift parent resolution > emits IMPLEMENTS edge: User → Serializable (protocol conformance)
  • Swift cross-file User.init() inference > resolves user.save() via User.init(name:) inference
  • Swift cross-file User.init() inference > resolves user.greet() via User.init(name:) inference
  • Swift return type inference > detects User class and getUser function
  • Swift return type inference > detects save function on User (Swift class methods are Function nodes)
  • Swift return type inference > resolves user.save() to User#save via return type of getUser() -> User
  • Swift return-type inference via function return type > resolves user.save() to User#save via return type of getUser()
  • Swift return-type inference via function return type > user.save() does NOT resolve to Repo#save
  • Swift return-type inference via function return type > resolves repo.save() to Repo#save via return type of getRepo()
  • Swift implicit imports (cross-file visibility) > detects UserService class in Models.swift
  • Swift implicit imports (cross-file visibility) > resolves UserService() constructor call across files (no explicit import)
  • Swift implicit imports (cross-file visibility) > resolves service.fetchUser() member call across files
  • Swift implicit imports (cross-file visibility) > creates IMPORTS edges between files in the same module
  • Swift extension deduplication > detects Product class
  • Swift extension deduplication > resolves Product() constructor despite extension creating duplicate class node
  • Swift extension deduplication > resolves product.save() to Product.swift (primary definition)
  • Swift constructor call fallback (no new keyword) > resolves OCRService() as constructor call across files
  • Swift constructor call fallback (no new keyword) > resolves ocr.recognize() member call via constructor-inferred type
  • Swift export visibility (internal vs private) > resolves PublicService() constructor across files
  • Swift export visibility (internal vs private) > resolves internalHelper() across files (internal = module-scoped)
  • Swift if let / guard let binding resolution > detects User and Repo classes
  • Swift if let / guard let binding resolution > resolves user.save() inside if-let to User#save
  • Swift if let / guard let binding resolution > resolves repo.save() inside guard-let to Repo#save
  • Swift if let / guard let binding resolution > user.save() in if-let does NOT resolve to Repo#save
  • Swift await / try expression unwrapping > resolves user.save() via await fetchUser() return type
  • Swift await / try expression unwrapping > resolves repo.save() via try parseRepo() return type
  • Swift await / try expression unwrapping > detects fetchUser and parseRepo as functions
  • Swift for-in loop element type inference > detects User and Repo classes
  • Swift for-in loop element type inference > creates implicit import edges between files
  • Swift field-type resolution > detects classes and their properties
  • Swift field-type resolution > emits HAS_PROPERTY edges from class to field
  • Swift field-type resolution > resolves field-chain call user.address.save() → Address#save
  • Swift field-type resolution > emits ACCESSES edges for field reads in chains
  • Swift field-type resolution > populates field metadata (visibility, declaredType) on Property nodes
  • Swift call-result binding > resolves call-result-bound method call user.save() → User#save
  • Swift call-result binding > getUser() is present as a defined function
  • Swift call-result binding > emits processUser -> getUser CALLS edge for let-assigned free function call
  • Swift method enrichment > detects Animal protocol and Dog class
  • Swift method enrichment > emits IMPLEMENTS edge Dog -> Animal
  • Swift method enrichment > emits HAS_METHOD edges for Dog methods
  • Swift method enrichment > marks protocol Animal.speak as isAbstract
  • Swift method enrichment > marks Dog.speak as NOT isAbstract
  • Swift method enrichment > marks breathe as isFinal
  • Swift method enrichment > marks classify as isStatic
  • Swift method enrichment > captures @objc annotation on breathe
  • Swift method enrichment > populates parameterTypes for classify(_ name: String)
  • Swift method enrichment > records parameterCount for classify
  • Swift method enrichment > records returnType for speak
  • Swift method enrichment > resolves dog.speak() CALLS edge
  • Swift method enrichment > resolves Dog.classify("dog") CALLS edge
  • Swift abstract dispatch > detects Repository protocol and SqlRepository class
  • Swift abstract dispatch > emits IMPLEMENTS edge SqlRepository -> Repository
  • Swift abstract dispatch > emits HAS_METHOD edges for Repository.find and Repository.save
  • Swift abstract dispatch > emits HAS_METHOD edges for SqlRepository.find and SqlRepository.save
  • Swift abstract dispatch > marks base Repository.find as isAbstract
  • Swift abstract dispatch > marks base Repository.save as isAbstract
  • Swift abstract dispatch > marks concrete SqlRepository.find as NOT isAbstract
  • Swift abstract dispatch > resolves repo.find(id: 42) CALLS edge
  • Swift abstract dispatch > resolves repo.save(entity: user) CALLS edge
  • Swift abstract dispatch > populates parameterTypes for Repository.find
  • Swift abstract dispatch > populates parameterTypes for Repository.save
  • Swift abstract dispatch > records returnType for SqlRepository.find
  • Swift abstract dispatch > emits METHOD_IMPLEMENTS edges from SqlRepository methods → Repository protocol methods
  • Swift overloaded method disambiguation > detects 2 distinct find Method nodes on SqlRepository
  • Swift overloaded method disambiguation > emits METHOD_IMPLEMENTS edges for both find overloads
  • Swift overloaded method disambiguation > emits METHOD_IMPLEMENTS edge for save
  • Swift overloaded method disambiguation > emits exactly 3 METHOD_IMPLEMENTS edges total

Code Coverage

Tests

Metric Coverage Covered Base Delta Status
Statements 71.59% 14997/20948 71.59% = 0.0 🟢 ██████████████░░░░░░
Branches 60.84% 9805/16116 60.84% = 0.0 🟢 ████████████░░░░░░░░
Functions 75.98% 1370/1803 75.98% = 0.0 🟢 ███████████████░░░░░
Lines 73.75% 13589/18424 73.75% = 0.0 🟢 ██████████████░░░░░░

📋 View full run · Generated by CI

@xkonjin xkonjin left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review: Fix negation patterns in .gitnexusignore

Summary

Clean fix for issue #596. The change correctly addresses the directory-only negation pattern handling by ensuring paths are tested with trailing slashes in .

Observations

1. Correctness
The logic change from to just is correct for the directory-only context. The gitignore spec does treat bare patterns as matching both files and directories, and the trailing slash forces directory-only matching.

2. Performance consideration
The original dual-check was slightly safer for mixed file/dir patterns but unnecessary overhead here. Since is only called for directories, the simplified single-check is appropriate.

3. Test coverage
Excellent test additions covering:

  • Whitelist patterns ( + )
  • Nested whitelists ()
  • File-level negation under whitelisted directories

Suggestion (non-blocking)**

Consider adding a test case for edge case: without trailing slash in the negation pattern itself, to ensure the ignore library normalizes these consistently.

Approval

Approved. Fixes the reported bug with clear tests and minimal, targeted changes.

@xkonjin xkonjin left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review: Fix negation patterns in .gitnexusignore

Summary

Clean fix for issue #596. The change correctly addresses the directory-only negation pattern handling by ensuring paths are tested with trailing slashes in childrenIgnored.

Observations

1. Correctness
The logic change from ig.ignores(rel) || ig.ignores(rel + '/') to just ig.ignores(rel + '/') is correct for the directory-only context. The gitignore spec does treat bare patterns as matching both files and directories, and the trailing slash forces directory-only matching.

2. Performance consideration
The original dual-check was slightly safer for mixed file/dir patterns but unnecessary overhead here. Since childrenIgnored is only called for directories, the simplified single-check is appropriate.

3. Test coverage
Excellent test additions covering:

  • Whitelist patterns (* + !iOS/)
  • Nested whitelists (!backend/living_plan/**)
  • File-level negation under whitelisted directories

Suggestion (non-blocking)**

Consider adding a test case for edge case: !dir without trailing slash in the negation pattern itself, to ensure the ignore library normalizes these consistently.

Approval

Approved. Fixes the reported bug with clear tests and minimal, targeted changes.

Verifies that `!iOS` (without trailing slash) also un-ignores the iOS/
directory — confirms the `ignore` package normalizes both `!dir` and
`!dir/` forms consistently when tested with a trailing-slash path.

Addresses non-blocking review suggestion on #654.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@xkonjin xkonjin left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review: Negation Pattern Fix

Summary

Clean, minimal fix for directory-only negation patterns in .gitnexusignore. Addresses issue #596.

Correctness

The logic change from dual-check (ig.ignores(rel) || ig.ignores(rel + /)) to single-check (ig.ignores(rel + /)) is correct for the directory-only context. Since childrenIgnored is only invoked for directories, testing with trailing slash ensures negation patterns like !iOS/ are properly respected.

Test Coverage

Excellent test additions:

  • Whitelist patterns (* + !iOS/)
  • Nested whitelists (!backend/living_plan/**)
  • File-level negation under whitelisted directories
  • Bare negation without trailing slash (!iOS vs !iOS/)

The tests directly validate the reported bug scenario and edge cases.

Suggestions (non-blocking)

  1. Performance consideration: The comment notes the dual-check was removed for performance. Consider quantifying this — is the overhead measurable in large repos? The clarity of correctness may outweigh micro-optimizations.

  2. Edge case documentation: The test for bare negation !iOS relies on the ignore library normalizing patterns. A brief inline comment linking to the ignore package docs would help future maintainers.

Nit

Line 404 comment has a trailing space after "applied correctly —".

Verdict

Approved. Fixes the reported bug with clear tests and minimal, targeted changes.

Adds references to the `ignore` package documentation in both the
childrenIgnored comment and the bare-negation test, explaining why
`!iOS` (without trailing slash) also re-includes the iOS/ directory.

Addresses non-blocking review suggestion on #654.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@ivkond

ivkond commented Apr 5, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review! Addressed the suggestions in 17560ec:

#2 (edge case documentation) — applied. Added inline links to the ignore package docs in both places:

#1 (trailing space nit) — not reproduced. Checked the committed line with cat -A; the space after "applied correctly —" is the em-dash spacing ( — without), no trailing whitespace at EOL. Happy to fix if I'm missing something.

#3 (performance justification) — already reflects the semantic reasoning. The code comment reads "Since childrenIgnored is only called for directories, always test with a trailing slash" — the dual-check was removed because it's semantically redundant in directory-only context, not for perf. No micro-benchmark needed.

@magyargergo magyargergo merged commit 10f8815 into abhigyanpatwari:main Apr 6, 2026
11 of 12 checks passed
@magyargergo

Copy link
Copy Markdown
Collaborator

@ivkond Thank you for your contribution!

@ivkond ivkond deleted the fix/gitnexusignore-negation-patterns branch April 6, 2026 07:34
motolese pushed a commit to motolese/datamoto-gitnexus that referenced this pull request Apr 23, 2026
…twari#654)

* fix(ignore): respect negation patterns in .gitnexusignore childrenIgnored

childrenIgnored checked `ig.ignores(rel) || ig.ignores(rel + '/')` which
short-circuited on the bare path — directory-only negation patterns like
`!iOS/` were missed because `ig.ignores('iOS')` treats the path as a file.
Now only checks with trailing slash since childrenIgnored is only called
for directories. Bare-name patterns (e.g. `local`) still match per gitignore spec.

Fixes abhigyanpatwari#596

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test(ignore): add edge-case for bare `!dir` negation pattern

Verifies that `!iOS` (without trailing slash) also un-ignores the iOS/
directory — confirms the `ignore` package normalizes both `!dir` and
`!dir/` forms consistently when tested with a trailing-slash path.

Addresses non-blocking review suggestion on abhigyanpatwari#654.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs(ignore): link ignore package docs for bare-name normalization

Adds references to the `ignore` package documentation in both the
childrenIgnored comment and the bare-negation test, explaining why
`!iOS` (without trailing slash) also re-includes the iOS/ directory.

Addresses non-blocking review suggestion on abhigyanpatwari#654.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

v1.4.10: .gitnexusignore negation patterns silently produce 0 nodes

3 participants