Skip to content

gitignore rules containing "/" never match (0.11.0) — ignored directories still get indexed #4

Open

Description

@tkromsa

Summary

In code-memory 0.11.0, any .gitignore rule that contains a / is compiled into a regex that can never match. Only single-segment patterns (no slash) work. As a result, ignored directories are still walked and indexed, and they can dominate retrieval results.

Affected version

flurryx-code-memory 0.11.0 (PyPI), Python 3.14, macOS arm64.

Root cause

In code_memory/extractor/gitignore.py, _compile() (lines 35-53):

tr = fnmatch.translate(seg)
# python's translate yields "(?s:...)\\Z"; extract inner
m = re.match(r"\\(\\?s:(.*)\\\\\\Z", tr)
inner = m.group(1) if m else tr
out.append(inner)

fnmatch.translate() emits (?s:...)\Z — a single backslash before Z. The pattern above is a raw string containing \\Z, which matches a literal backslash followed by Z. So m is always None and inner falls back to the whole translated pattern, anchors included.

For pattern a/b:

parts   = ["a", "b"]
out     = ["(?s:a)\\Z", "/", "(?s:b)\\Z"]
body    = "(?s:a)\\Z/(?s:b)\\Z"
compiled= ^(?s:a)\Z/(?s:b)\Z$

Each inner segment carries \Z (end-of-string), so a/b can never match: after a is consumed the engine is at end of string, but / still has to be consumed.

Reproduction

from pathlib import Path
from code_memory.extractor.gitignore import GitignoreMatcher

m = GitignoreMatcher.from_root("/tmp/repo")
# /tmp/repo/.gitignore contains:  a/b
print(m.match(Path("a/b"), is_dir=True))   # -> False   (expected: True)

Patterns without a slash are unaffected, because they produce a single segment with no internal \Z.

Impact

In a repo I maintain, the rule .claude/plugins/marketplaces (80 MB of vendored third-party plugin code) was correctly ignored by git, but code-memory still ingested it: 500+ files indexed and retrieval results dominated by the vendored files instead of the repo's own code. The workaround was adding a single-segment rule (marketplaces) to the root .gitignore — semantically different from the intended path rule and easy to get wrong.

Suggested fix

Match the actual output of fnmatch.translate and strip the anchors:

m = re.match(r"\\(\\?s:(.*?)\\)\\Z", tr)
inner = m.group(1) if m else tr

A non-greedy (.*?) with \Z matches the real (?s:...)\Z form. Alternatively, delegate to a maintained gitignore matcher.

Notes

  • The source comment already states the intent ("python's translate yields (?s:...)\\Z; extract inner"), so this looks like a double-escaping slip rather than a design decision.
  • I only tested single-segment vs multi-segment matching; the ** handling, negation (!) and directory-only (/) rule paths may have related issues in the same function and are worth a look.

Environment

  • flurryx-code-memory 0.11.0
  • Python 3.14
  • macOS arm64

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions