Skip to content

index_repository: root-anchored .gitignore patterns are applied to nested paths, silently excluding tracked source #1205

Open

Description

@simkimsia

Version

v0.8.1-282-g1cca858 — built from source at commit 1cca858

Platform

macOS 15 (Darwin 24.6.0), arm64

Install channel

Built from source: make -f Makefile.cbm cbm

Binary variant

Standard (no UI). Built without libgit2 (pkg-config libgit2 absent, so HAVE_LIBGIT2 was not set) — I don't believe that's relevant to discovery/ignore handling, but flagging it in case.

What happened, and what did you expect?

A root-anchored .gitignore pattern is applied to nested paths, so index_repository silently excludes tracked source directories that git itself does not ignore.

Under gitignore semantics, a leading / anchors the pattern to the directory containing the .gitignore. So /coverage must match <root>/coverage and must not match <root>/src/routes/coverage. git check-ignore agrees. CBM excludes both.

The effect is silent: the excluded directory appears in excluded.dirs (good), but index_repository still returns status: "indexed", so a consumer can't distinguish "this directory has no symbols" from "this directory was dropped". Every symbol in it is simply absent from the graph.

I hit this on a real SvelteKit app where /coverage (the test-coverage output) is gitignored at the root, and a legitimate route directory happens to be named coverage. That route vanished from the graph with no error.

Reproduction

mkdir -p repro/coverage repro/src/routes/coverage && cd repro
git init -q .

printf '/coverage\n' > .gitignore                                    # ROOT-anchored

printf 'def root_helper():\n    return 1\n'    > coverage/root_report.py        # should be ignored
printf 'def nested_handler():\n    return 2\n' > src/routes/coverage/page.py    # should NOT be ignored
printf 'def app_main():\n    return 3\n'       > src/app.py

git add -A && git -c user.email=t@t -c user.name=t commit -qm init

Git's verdict:

$ git check-ignore -q coverage/root_report.py       && echo IGNORED
IGNORED
$ git check-ignore -q src/routes/coverage/page.py   && echo IGNORED || echo "not ignored"
not ignored

$ git ls-files
.gitignore
src/app.py
src/routes/coverage/page.py     # <- tracked

CBM's verdict:

$ codebase-memory-mcp cli index_repository --repo-path . --persistence false
{"excluded":{"dirs":[".git","coverage","src/routes/coverage"], ...}, "nodes":18, ...}
                                          ^^^^^^^^^^^^^^^^^^^^^^ should not be here

Resulting graph:

$ sqlite3 -readonly ~/.cache/codebase-memory-mcp/<project>.db \
    "SELECT file_path FROM nodes WHERE label='File';"
src/app.py

$ sqlite3 -readonly ... "SELECT name, file_path FROM nodes WHERE label='Function';"
len|<python-builtins>
print|<python-builtins>
app_main|src/app.py

nested_handler is gone. src/routes/coverage/page.py has no File node at all.

Expected: excluded.dirs contains .git and coverage only; src/routes/coverage/page.py is indexed and nested_handler appears as a Function.

Logs

CBM_LOG_LEVEL=info shows nothing beyond the normal pipeline lines — the exclusion isn't logged as a decision, which is part of why it's easy to miss.

Project scale (if relevant)

Trivial for the repro above. Originally found on a ~170-file SvelteKit repo.


One more thing, possibly separate

On that same real repo, 4 of 32 tracked .svelte files produced no nodes. Exactly one is explained by the bug above. The other three are not gitignored, not in excluded.dirs, and not size-related — the largest .svelte in the repo (114,675 bytes) indexes fine while an 8,657-byte file is dropped. Bracketed path segments aren't universally fatal either, since src/routes/[[tab=dashboardTab]]/+page.svelte indexes correctly.

I haven't isolated that one into a minimal repro yet, so I've left it out of the title. Happy to open it separately if you'd prefer — or it may share a root cause with the discovery path above.

Confirmations

  • Searched existing issues; didn't find this one.
  • Reproduction above is minimal and self-contained.

Activity

  1. added
    bugSomething isn't working
    parsing/qualityGraph extraction bugs, false positives, missing edges
    priority/highNeeds near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker.
    on Jul 22, 2026
  2. added this to the 0.9.1-rc milestone on Jul 22, 2026
  3. DeusData commented on Jul 25, 2026

    @DeusData
    Owner

    Thank you for this — the reproduction is excellent, and the bug is real and confirmed. I want to give you a correction on the root cause, though, because it turns out to be a bigger problem than the one you described, in a different subsystem.

    The symptom is confirmed

    src/routes/coverage is silently dropped while git tracks it. Reproduced on current main.

    But .gitignore anchoring is not the cause

    The gitignore engine actually handles your /coverage pattern correctly:

    • gi_add_pattern() (src/discover/gitignore.c:211-215) sets rooted = true and strips the leading /.
    • Rooted patterns match via glob_match(p->pattern, rel_path) (gitignore.c:362), and rel_path is the full repo-relative path, built at src/discover/discover.c:837.
    • glob_match() is anchored at both ends (gitignore.c:157-162), so glob_match("coverage", "src/routes/coverage") fails on the very first character.

    The decisive test — your repro with no .gitignore at all:

    $ git check-ignore -q src/routes/coverage/page.py && echo IGNORED || echo "not ignored"
    not ignored
    
    $ codebase-memory-mcp cli index_repository --repo-path . --persistence false
    {"excluded":{"dirs":[".git","coverage","src/routes/coverage"],"count":3,...},"status":"indexed"}

    Both directories are still excluded with no ignore file present anywhere. So .gitignore is not involved.

    The actual cause: the built-in skip list is basename-matched at any depth

    ALWAYS_SKIP_DIRS (src/discover/discover.c:31-51) contains "coverage" on line 40, and cbm_should_skip_dir() (discover.c:339-345) takes only the basename — discover.c:570 passes entry->name, never the relative path. So any directory named coverage, at any depth, is dropped before ignore rules are ever consulted.

    That list also contains env, deploy, deployed, target, temp, tmp, dist, obj, vendor, vendored, and public-adjacent names — all plausible route or module names. Ten tracked, non-ignored sibling directories under src/routes/, in --mode full:

    "excluded":{"dirs":[".git","src/routes/obj","src/routes/temp","src/routes/dist",
     "src/routes/target","src/routes/deploy","src/routes/env","src/routes/tmp",
     "src/routes/coverage","src/routes/vendor"],"count":10}

    Nine of ten silently dropped. Only src/routes/normal survived. In non-FULL modes FAST_SKIP_DIRS (discover.c:53+) adds docs, scripts, tools, examples, static, assets, migrations, integration and more on the same basename-at-any-depth rule — so the exposure is wider still outside full mode.

    Your SvelteKit case is the general form of this: a framework where route directories are named after ordinary English words collides with a skip list designed for repo-root build artifacts.

    Your two secondary points

    The silent status: "indexed" — agreed that this is what makes it dangerous rather than merely annoying, and that excluded.dirs being technically-correct-but-unread is not much of a defence. How the status contract should change is a design decision I don't want to settle inside triage, so I'm recording it rather than answering it here.

    The 3 unexplained .svelte files — not explained by this bug, and not by size given your 114 KB/8.6 KB counterexample. Please do open that separately; with the coverage directory now accounted for, the remaining three are a clean signal and I'd rather track them on their own than have them absorbed into this one.

    Thanks again — filing this with a full git check-ignore comparison and a minimal repro is what made the real cause findable in one pass.

  4. DeusData commented on Oct 19, 2026

    @DeusData
    Owner

    Thank you for the clarification and examples. The earlier triage corrected this to built-in basename exclusions, rather than root anchoring in .gitignore. I have no new change to that exclusion/status behavior to report.

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

    bugSomething isn't workingparsing/qualityGraph extraction bugs, false positives, missing edgespriority/highNeeds near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker.ux/behaviorDisplay bugs, docs, adoption UX

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions