a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
175 lines
5.5 KiB
Go
175 lines
5.5 KiB
Go
package excludes
|
|
|
|
import (
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
ignore "github.com/sabhiram/go-gitignore"
|
|
|
|
"github.com/zzet/gortex/internal/pathkey"
|
|
)
|
|
|
|
// Matcher tests whether a path should be excluded from indexing/watching.
|
|
// It is safe for concurrent reads after construction.
|
|
type Matcher struct {
|
|
ign *ignore.GitIgnore
|
|
patterns []string
|
|
}
|
|
|
|
// New compiles the given patterns into a Matcher. A nil/empty list is
|
|
// valid and will match nothing.
|
|
//
|
|
// Patterns are folded to Unicode NFC so a pattern naming a non-ASCII
|
|
// directory matches paths regardless of which Unicode form the
|
|
// filesystem walk produced — MatchRel folds the candidate path to the
|
|
// same form before testing it.
|
|
func New(patterns []string) *Matcher {
|
|
cleaned := make([]string, 0, len(patterns))
|
|
for _, p := range patterns {
|
|
p = strings.TrimSpace(p)
|
|
if p == "" || strings.HasPrefix(p, "#") {
|
|
continue
|
|
}
|
|
cleaned = append(cleaned, pathkey.Normalize(p))
|
|
}
|
|
return &Matcher{
|
|
ign: ignore.CompileIgnoreLines(cleaned...),
|
|
patterns: cleaned,
|
|
}
|
|
}
|
|
|
|
// Patterns returns the cleaned pattern list (empties and comments removed).
|
|
func (m *Matcher) Patterns() []string {
|
|
if m == nil {
|
|
return nil
|
|
}
|
|
out := make([]string, len(m.patterns))
|
|
copy(out, m.patterns)
|
|
return out
|
|
}
|
|
|
|
// MatchRel reports whether a repo-root-relative path is excluded.
|
|
// Path separators are normalised to forward slashes and the path is
|
|
// folded to Unicode NFC — matching how New normalised the patterns —
|
|
// before matching, so a non-ASCII path component compares equal to its
|
|
// pattern whether the OS supplied it decomposed (macOS NFD) or
|
|
// precomposed (Linux / git NFC).
|
|
func (m *Matcher) MatchRel(relPath string) bool {
|
|
if m == nil || m.ign == nil {
|
|
return false
|
|
}
|
|
rel := pathkey.Normalize(filepath.ToSlash(relPath))
|
|
rel = strings.TrimPrefix(rel, "./")
|
|
if rel == "" || rel == "." {
|
|
return false
|
|
}
|
|
return m.ign.MatchesPath(rel)
|
|
}
|
|
|
|
// MatchAbs reports whether an absolute path under root is excluded.
|
|
// Returns false if path is not under root.
|
|
func (m *Matcher) MatchAbs(absPath, root string) bool {
|
|
return m.MatchAbsDir(absPath, root, false)
|
|
}
|
|
|
|
// MatchAbsDir reports whether an absolute path under root is excluded.
|
|
// When isDir is true the path is treated as a directory, so a pattern
|
|
// written with a trailing slash (e.g. "build/") matches the directory
|
|
// itself — letting the caller prune the whole subtree instead of
|
|
// descending it and re-testing every file. Returns false if path is
|
|
// not under root.
|
|
func (m *Matcher) MatchAbsDir(absPath, root string, isDir bool) bool {
|
|
if m == nil || m.ign == nil {
|
|
return false
|
|
}
|
|
rel, err := filepath.Rel(root, absPath)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
if isDir {
|
|
rel += "/"
|
|
}
|
|
return m.MatchRel(rel)
|
|
}
|
|
|
|
// HasNegatedDescendant reports whether any re-include ("!") pattern in
|
|
// the matcher could match a path strictly beneath relDir.
|
|
//
|
|
// The index walk prunes an excluded directory with filepath.SkipDir so
|
|
// it never descends a subtree it would only throw away. But go-gitignore
|
|
// treats "*" as matching across "/", so a blanket like "a/b/*" reports
|
|
// the directory "a/b" itself as excluded — pruning it would skip a later
|
|
// "!a/b/keep/" re-include before the walk ever reaches the child. This
|
|
// lets the walk ask "could a negation resurrect something under here?"
|
|
// and keep descending when the answer is yes, mirroring git, which never
|
|
// prunes a directory a negation could re-include a child from.
|
|
//
|
|
// relDir is a repo-root-relative, forward-slash directory path (a
|
|
// trailing slash and a leading "./" are tolerated). The check is
|
|
// deliberately conservative: an unanchored or wildcard-leading negation
|
|
// can match at varying depths, so it is treated as "could be under
|
|
// anything" and the directory is kept rather than pruned.
|
|
func (m *Matcher) HasNegatedDescendant(relDir string) bool {
|
|
if m == nil {
|
|
return false
|
|
}
|
|
relDir = pathkey.Normalize(filepath.ToSlash(relDir))
|
|
relDir = strings.TrimPrefix(relDir, "./")
|
|
relDir = strings.TrimSuffix(relDir, "/")
|
|
if relDir == "." {
|
|
relDir = ""
|
|
}
|
|
for _, p := range m.patterns {
|
|
if !strings.HasPrefix(p, "!") {
|
|
continue
|
|
}
|
|
np := strings.TrimSpace(p[1:])
|
|
np = strings.TrimPrefix(np, "/")
|
|
np = strings.TrimSuffix(np, "/")
|
|
if np == "" {
|
|
continue
|
|
}
|
|
// A negation with no internal slash is unanchored: gitignore
|
|
// matches it at any depth, so it can re-include something under
|
|
// any directory. Keep descending.
|
|
if !strings.Contains(np, "/") {
|
|
return true
|
|
}
|
|
anchor := literalAnchor(np)
|
|
if anchor == "" {
|
|
// First segment is itself a wildcard ("*/...", "**/..."): it
|
|
// can match at varying depths, so stay conservative.
|
|
return true
|
|
}
|
|
// At the root, every anchored negation lives somewhere beneath us.
|
|
if relDir == "" {
|
|
return true
|
|
}
|
|
// The negation's match-set intersects relDir's subtree when its
|
|
// literal anchor sits at or under relDir, or relDir sits under the
|
|
// anchor (a wildcard tail can then still reach into relDir).
|
|
if anchor == relDir ||
|
|
strings.HasPrefix(anchor, relDir+"/") ||
|
|
strings.HasPrefix(relDir, anchor+"/") {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// literalAnchor returns the leading path segments of a slash-bearing
|
|
// gitignore pattern up to (but excluding) the first segment that holds a
|
|
// wildcard meta-character. It returns "" when the first segment is
|
|
// itself a wildcard ("*", "**", "?foo", ...).
|
|
func literalAnchor(pattern string) string {
|
|
segs := strings.Split(pattern, "/")
|
|
lit := make([]string, 0, len(segs))
|
|
for _, s := range segs {
|
|
if strings.ContainsAny(s, "*?[") {
|
|
break
|
|
}
|
|
lit = append(lit, s)
|
|
}
|
|
return strings.Join(lit, "/")
|
|
}
|