@@ -965,7 +965,15 @@ def _git_info_exclude(vcs_root: Path) -> Path | None:
965965 return exclude if exclude .is_file () else None
966966
967967
968- def _load_dir_own_ignore (d : Path , * , gitignore : bool = True ) -> list [tuple [Path , str ]]:
968+ _IGNORE_SOURCE_GITIGNORE = "gitignore"
969+ _IGNORE_SOURCE_GRAPHIFYIGNORE = "graphifyignore"
970+ _IGNORE_SOURCE_INFO_EXCLUDE = "info_exclude"
971+ _IGNORE_SOURCE_CLI_EXCLUDE = "cli_exclude"
972+
973+
974+ def _load_dir_own_ignore (
975+ d : Path , * , gitignore : bool = True
976+ ) -> list [tuple [Path , str , str ]]:
969977 """Read .gitignore/.graphifyignore directly inside *d* (not its ancestors).
970978
971979 Merges .gitignore and .graphifyignore for this one directory (#1363):
@@ -975,25 +983,36 @@ def _load_dir_own_ignore(d: Path, *, gitignore: bool = True) -> list[tuple[Path,
975983 .gitignore-excluded file (#945 kept: a dir with only a .gitignore still
976984 gets sensible defaults).
977985
986+ Each pattern is tagged with its source so callers can apply only a subset
987+ of origins — e.g. memory-tree files should respect ``.graphifyignore``
988+ but NOT ``.gitignore`` (generated output like ``graphify-out/`` is
989+ typically gitignored, and honouring that there would silently drop memory
990+ files).
991+
978992 Shared by `_load_graphifyignore` (ancestor chain, loaded once before the
979993 scan) and the live os.walk loop in `detect()` (called per-directory as
980994 each descendant is visited), so nested ignore files *below* the scan
981995 root are honored too — previously only the scan root and its ancestors
982996 were read, so e.g. `vendor/sub/.gitignore` was silently ignored (#1206).
983997 """
984- patterns : list [tuple [Path , str ]] = []
985- for fname in ((".gitignore" , ".graphifyignore" ) if gitignore else (".graphifyignore" ,)):
998+ patterns : list [tuple [Path , str , str ]] = []
999+ for fname , source in (
1000+ ((".gitignore" , _IGNORE_SOURCE_GITIGNORE ),
1001+ (".graphifyignore" , _IGNORE_SOURCE_GRAPHIFYIGNORE ))
1002+ if gitignore
1003+ else ((".graphifyignore" , _IGNORE_SOURCE_GRAPHIFYIGNORE ),)
1004+ ):
9861005 ignore_file = d / fname
9871006 if ignore_file .exists ():
9881007 for raw in ignore_file .read_text (encoding = "utf-8-sig" , errors = "ignore" ).splitlines ():
9891008 line = _parse_gitignore_line (raw )
9901009 if line :
991- patterns .append ((d , line ))
1010+ patterns .append ((d , line , source ))
9921011 return patterns
9931012
9941013
995- def _load_graphifyignore (root : Path , * , gitignore : bool = True ) -> list [tuple [Path , str ]]:
996- """Read .graphifyignore files and return (anchor_dir, pattern) pairs .
1014+ def _load_graphifyignore (root : Path , * , gitignore : bool = True ) -> list [tuple [Path , str , str ]]:
1015+ """Read .graphifyignore files and return (anchor_dir, pattern, source) triples .
9971016
9981017 Patterns are returned outer-first so that inner (closer) rules are
9991018 appended last and win via last-match-wins semantics — matching gitignore
@@ -1005,6 +1024,9 @@ def _load_graphifyignore(root: Path, *, gitignore: bool = True) -> list[tuple[Pa
10051024 Covers the scan root and its ancestors only — directories *below* the
10061025 scan root are picked up live during the os.walk in `detect()` instead,
10071026 since they aren't known until the walk reaches them (#1206).
1027+
1028+ The third element of each tuple records provenance so callers can apply
1029+ only patterns from a specific origin (see ``_IGNORE_SOURCE_*`` constants).
10081030 """
10091031 root = root .resolve ()
10101032 ceiling = _find_vcs_root (root ) or root
@@ -1019,7 +1041,7 @@ def _load_graphifyignore(root: Path, *, gitignore: bool = True) -> list[tuple[Pa
10191041 current = current .parent
10201042 dirs .reverse () # ceiling first, scan root last
10211043
1022- patterns : list [tuple [Path , str ]] = []
1044+ patterns : list [tuple [Path , str , str ]] = []
10231045
10241046 # $GIT_DIR/info/exclude is repo-root-scoped and, per git, ranks below every
10251047 # per-directory .gitignore/.graphifyignore — so load it first (lowest priority
@@ -1030,7 +1052,7 @@ def _load_graphifyignore(root: Path, *, gitignore: bool = True) -> list[tuple[Pa
10301052 for raw in info_exclude .read_text (encoding = "utf-8-sig" , errors = "ignore" ).splitlines ():
10311053 line = _parse_gitignore_line (raw )
10321054 if line :
1033- patterns .append ((ceiling , line ))
1055+ patterns .append ((ceiling , line , _IGNORE_SOURCE_INFO_EXCLUDE ))
10341056
10351057 for d in dirs :
10361058 patterns .extend (_load_dir_own_ignore (d , gitignore = gitignore ))
@@ -1068,11 +1090,12 @@ def _matches(path_idx: int, pattern_idx: int) -> bool:
10681090def _is_ignored (
10691091 path : Path ,
10701092 root : Path ,
1071- patterns : list [tuple [Path , str ]],
1093+ patterns : list [tuple [Path , str , str ]],
10721094 * ,
10731095 _cache : dict [Path , bool ] | None = None ,
1096+ sources : set [str ] | None = None ,
10741097) -> bool :
1075- """Return True if the path should be ignored per .graphifyignore patterns.
1098+ """Return True if the path should be ignored per ignore patterns.
10761099
10771100 Uses gitignore last-match-wins semantics: all patterns are evaluated in
10781101 order; the final matching pattern determines the result. Negation patterns
@@ -1084,6 +1107,12 @@ def _is_ignored(
10841107 _cache: optional dict shared across calls within the same scan. Ancestor
10851108 directory results are memoised so files under the same subtree don't
10861109 re-evaluate the same patterns repeatedly.
1110+
1111+ sources: optional set of provenance tags to filter by. When given, only
1112+ patterns whose third element is in ``sources`` are evaluated; all others
1113+ are skipped. Used to apply ONLY ``.graphifyignore``-sourced patterns to
1114+ the memory tree (so a gitignored ``graphify-out/`` doesn't silently drop
1115+ memory files) while keeping full ``.gitignore`` respect for the code tree.
10871116 """
10881117 if not patterns :
10891118 return False
@@ -1108,7 +1137,12 @@ def _matches(rel: str, p: str, path_relative: bool) -> bool:
11081137 return False
11091138
11101139 result = False
1111- for anchor , pattern in patterns :
1140+ for entry in patterns :
1141+ anchor , pattern = entry [0 ], entry [1 ]
1142+ if sources is not None :
1143+ source = entry [2 ] if len (entry ) > 2 else None
1144+ if source not in sources :
1145+ continue
11121146 negated = pattern .startswith ("!" )
11131147 raw = pattern [1 :] if negated else pattern
11141148 directory_only = raw .endswith ("/" )
@@ -1231,7 +1265,7 @@ def _wc(path: Path) -> int:
12311265 for pat in extra_excludes :
12321266 line = _parse_gitignore_line (pat )
12331267 if line :
1234- ignore_patterns .append ((root , line ))
1268+ ignore_patterns .append ((root , line , _IGNORE_SOURCE_CLI_EXCLUDE ))
12351269
12361270 # Always include graphify-out/memory/ - query results filed back into the graph
12371271 memory_dir = root / GRAPHIFY_OUT / "memory"
@@ -1334,9 +1368,21 @@ def _on_walk_error(err: OSError) -> None:
13341368 # Skip files inside our own converted/ dir (avoid re-processing sidecars)
13351369 if str (p ).startswith (str (converted_dir )):
13361370 continue
1337- if _is_ignored (p , root , ignore_patterns , _cache = ignore_cache ):
1338- ignored .append (str (p ))
1339- continue
1371+ # Memory-tree files respect ONLY .graphifyignore patterns — not
1372+ # .gitignore or $GIT_DIR/info/exclude. ``graphify-out/`` is generated
1373+ # output and is commonly gitignored; applying .gitignore to memory
1374+ # would silently drop every memory file (#2267).
1375+ if in_memory :
1376+ if _is_ignored (
1377+ p , root , ignore_patterns , _cache = ignore_cache ,
1378+ sources = {_IGNORE_SOURCE_GRAPHIFYIGNORE },
1379+ ):
1380+ ignored .append (str (p ))
1381+ continue
1382+ else :
1383+ if _is_ignored (p , root , ignore_patterns , _cache = ignore_cache ):
1384+ ignored .append (str (p ))
1385+ continue
13401386 if not _resolves_under_root (p , root ):
13411387 skipped_sensitive .append (str (p ) + " [symlink target outside scan root]" )
13421388 continue
0 commit comments