Skip to content

.graphifyignore anchored negation (!/dir/) doesn't re-include nested files — 'exclude-all, re-include subtree' collapses to zero files #1975

Description

@jmsingleton-tw

Bug: .graphifyignore anchored negation (!/dir/) doesn't re-include nested files — "exclude-all, re-include a subtree" collapses to zero files

Version: graphifyy 0.9.17 (also observed on 0.9.16)
Platform: macOS (Darwin), Python 3.13

Summary

The standard gitignore idiom for scoping a build to a subtree —

/*
!/src/

("ignore everything at the root, then re-include src/") — does not behave like gitignore under graphify. The re-include rescues the bare src directory entry but not any file nested beneath it, so detect() returns zero files. A /dir/** companion is required to make it work.

Under real .gitignore semantics, !/src/ re-includes the directory and its descendants are then eligible (git's rule is subtler — a ! can't rescue a file under a still-excluded dir — but !/src/ does un-exclude the directory itself so its contents are walked). Users reasonably expect .graphifyignore to match .gitignore semantics, especially since graphify reads and merges .gitignore files.

Minimal reproduction

import tempfile
from pathlib import Path
from graphify.detect import detect

base = Path(tempfile.mkdtemp()).resolve()   # .resolve() matters on macOS (/var -> /private/var)
for rel in ["src/app/main.py", "src/lib/util.py", "docs/guide.md", "README.md"]:
    p = base / rel; p.parent.mkdir(parents=True, exist_ok=True); p.write_text("x\n")

(base / ".graphifyignore").write_text("/*\n!/src/\n")
print("exclude-all + !/src/       :", sum(len(v) for v in detect(base)["files"].values()), "files")
# -> 0   (expected: the 2 files under src/)

(base / ".graphifyignore").write_text("/*\n!/src/\n!/src/**\n")
print("exclude-all + !/src/ + **  :", sum(len(v) for v in detect(base)["files"].values()), "files")
# -> 2   (correct)

Output (deterministic across runs and PYTHONHASHSEED values):

exclude-all + !/src/       : 0 files
exclude-all + !/src/ + **  : 2 files

Root cause (as far as I traced it)

In graphify/detect.py, _is_ignored matches an anchored pattern by stripping the surrounding slashes and using fnmatch. For !/src/ the effective pattern becomes the literal string src, which matches only the exact path src — never src/app/main.py. Meanwhile /* (stripped to *) matches at every depth because fnmatch's * crosses /. Net effect: /* ignores everything including nested files, and !/src/ un-ignores only the directory node, so every file stays ignored.

Impact

Anyone using the documented "scope to a subtree" pattern silently gets an empty graph. It's especially sharp because:

  • The bare directory is re-included, so a superficial check ("is src ignored? no") looks fine.
  • graphify merges .gitignore too, so repos that use dir/* + !dir/keep/ idioms in their existing .gitignore may be mis-scoped without the author realizing.

Workaround (works today)

Pair every anchored directory re-include with a ** companion:

/*
!/src/
!/src/**

Suggested fix

Make anchored-directory negations (!/dir/) re-include the directory's descendants, matching gitignore semantics — i.e. treat !/dir/ as also covering dir/** for the purpose of un-ignoring, or translate anchored patterns to a form whose * does not cross /.


Note on a red herring: when the reproduction is run with an unresolved tempfile.mkdtemp() path on macOS (/var/... rather than the real /private/var/...), _is_ignored's path relativization mismatches and nothing matches, which can make it look like the negation "works" (files not ignored). Using a resolved absolute path — as any real checkout has — shows the true, deterministic collapse. Mentioning in case it saves triage time.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions