WIP Phase 4 ingestion: platform guides, exclusion gate, gitea/vault/graph harvest, payload provenance
This commit is contained in:
89
corpus/globs.py
Normal file
89
corpus/globs.py
Normal file
@@ -0,0 +1,89 @@
|
||||
"""Path globs, with the one extension the guides need: `**/` matches nothing.
|
||||
|
||||
`fnmatch` is close but wrong for this job in two ways: it lets `*` cross a `/`,
|
||||
and it has no `**`. Platform guides are read by a human and written as ordinary
|
||||
gitignore-style globs (`**/node_modules/**`, `200 projects/**`), so this module
|
||||
implements exactly that little language and nothing else.
|
||||
|
||||
Matching is always against a normalised, repository-relative POSIX path.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
_CACHE: dict = {}
|
||||
|
||||
|
||||
def normalise(path: str) -> str:
|
||||
"""Repository-relative, POSIX separators, no leading `./` or `/`."""
|
||||
path = str(path).replace("\\", "/").strip()
|
||||
while path.startswith("./"):
|
||||
path = path[2:]
|
||||
return path.lstrip("/")
|
||||
|
||||
|
||||
def compile_glob(pattern: str):
|
||||
"""Compile one glob to a regular expression. Cached."""
|
||||
cached = _CACHE.get(pattern)
|
||||
if cached is not None:
|
||||
return cached
|
||||
|
||||
pattern = normalise(pattern)
|
||||
out = ["^"]
|
||||
index = 0
|
||||
while index < len(pattern):
|
||||
char = pattern[index]
|
||||
if char == "*":
|
||||
if pattern[index : index + 2] == "**":
|
||||
index += 2
|
||||
if pattern[index : index + 1] == "/":
|
||||
# `**/` — any number of whole directories, including none.
|
||||
out.append("(?:[^/]+/)*")
|
||||
index += 1
|
||||
else:
|
||||
out.append(".*")
|
||||
continue
|
||||
out.append("[^/]*")
|
||||
elif char == "?":
|
||||
out.append("[^/]")
|
||||
elif char == "[":
|
||||
close = pattern.find("]", index + 1)
|
||||
if close == -1:
|
||||
out.append(re.escape(char))
|
||||
else:
|
||||
body = pattern[index + 1 : close]
|
||||
if body.startswith("!"):
|
||||
body = "^" + body[1:]
|
||||
out.append("[" + body + "]")
|
||||
index = close
|
||||
else:
|
||||
out.append(re.escape(char))
|
||||
index += 1
|
||||
out.append("$")
|
||||
compiled = re.compile("".join(out))
|
||||
_CACHE[pattern] = compiled
|
||||
return compiled
|
||||
|
||||
|
||||
def matches(path: str, pattern: str) -> bool:
|
||||
return bool(compile_glob(pattern).match(normalise(path)))
|
||||
|
||||
|
||||
def matches_any(path: str, patterns) -> bool:
|
||||
return any(matches(path, pattern) for pattern in (patterns or ()))
|
||||
|
||||
|
||||
def under_any(path: str, patterns) -> bool:
|
||||
"""True when `path` is a pattern's directory itself or inside it.
|
||||
|
||||
`200 projects/**` should also exclude the bare directory entry `200 projects`
|
||||
that a tree listing returns, not only the files underneath it.
|
||||
"""
|
||||
path = normalise(path)
|
||||
for pattern in patterns or ():
|
||||
if matches(path, pattern):
|
||||
return True
|
||||
if pattern.endswith("/**") and path == normalise(pattern[:-3]):
|
||||
return True
|
||||
return False
|
||||
Reference in New Issue
Block a user