github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/pkg/model/matcher.go (about)

     1  package model
     2  
     3  import (
     4  	"path/filepath"
     5  
     6  	"github.com/tilt-dev/tilt/internal/ospath"
     7  )
     8  
     9  type PathMatcher interface {
    10  	Matches(f string) (bool, error)
    11  
    12  	// If this matches the entire dir, we can often optimize filetree walks a bit
    13  	MatchesEntireDir(file string) (bool, error)
    14  }
    15  
    16  // A Matcher that matches nothing.
    17  type emptyMatcher struct{}
    18  
    19  func (m emptyMatcher) Matches(f string) (bool, error) {
    20  	return false, nil
    21  }
    22  func (emptyMatcher) MatchesEntireDir(p string) (bool, error) { return false, nil }
    23  
    24  var EmptyMatcher PathMatcher = emptyMatcher{}
    25  
    26  // This matcher will match a path if it is:
    27  // A. an exact match for one of matcher.paths, or
    28  // B. the child of a path in matcher.paths
    29  // e.g. if paths = {"foo.bar", "baz/"}, will match both
    30  // A. "foo.bar" (exact match), and
    31  // B. "baz/qux" (child of one of the paths)
    32  type fileOrChildMatcher struct {
    33  	paths map[string]bool
    34  }
    35  
    36  func (m fileOrChildMatcher) Matches(f string) (bool, error) {
    37  	// (A) Exact match
    38  	if m.paths[f] {
    39  		return true, nil
    40  	}
    41  
    42  	// (B) f is child of any of m.paths
    43  	for path := range m.paths {
    44  		if ospath.IsChild(path, f) {
    45  			return true, nil
    46  		}
    47  	}
    48  
    49  	return false, nil
    50  }
    51  
    52  func (m fileOrChildMatcher) MatchesEntireDir(f string) (bool, error) {
    53  	return m.Matches(f)
    54  }
    55  
    56  // NewRelativeFileOrChildMatcher returns a matcher for the given paths (with any
    57  // relative paths converted to absolute, relative to the given baseDir).
    58  func NewRelativeFileOrChildMatcher(baseDir string, paths ...string) fileOrChildMatcher {
    59  	pathMap := make(map[string]bool, len(paths))
    60  	for _, path := range paths {
    61  		if !filepath.IsAbs(path) {
    62  			path = filepath.Join(baseDir, path)
    63  		}
    64  		pathMap[path] = true
    65  	}
    66  	return fileOrChildMatcher{paths: pathMap}
    67  }
    68  
    69  // A PathSet stores one or more filepaths, along with the directory that any
    70  // relative paths are relative to
    71  // NOTE(maia): in its current usage (for LiveUpdate.Run.Triggers, LiveUpdate.FallBackOnFiles())
    72  // this isn't strictly necessary, could just as easily convert paths to Abs when specified in
    73  // the Tiltfile--but leaving this code in place for now because it was already written and
    74  // may help with complicated future cases (glob support, etc.)
    75  type PathSet struct {
    76  	Paths         []string
    77  	BaseDirectory string
    78  }
    79  
    80  func NewPathSet(paths []string, baseDir string) PathSet {
    81  	return PathSet{
    82  		Paths:         paths,
    83  		BaseDirectory: baseDir,
    84  	}
    85  }
    86  
    87  func (ps PathSet) Empty() bool { return len(ps.Paths) == 0 }
    88  
    89  // AnyMatch returns true if any of the given filepaths match any paths contained in the pathset
    90  // (along with the first path that matched).
    91  func (ps PathSet) AnyMatch(paths []string) (bool, string, error) {
    92  	matcher := NewRelativeFileOrChildMatcher(ps.BaseDirectory, ps.Paths...)
    93  
    94  	for _, path := range paths {
    95  		match, err := matcher.Matches(path)
    96  		if err != nil {
    97  			return false, "", err
    98  		}
    99  		if match {
   100  			return true, path, nil
   101  		}
   102  	}
   103  	return false, "", nil
   104  }
   105  
   106  // Intersection returns the set of paths that are in both the PathSet and the passed set of paths.
   107  func (ps PathSet) Intersection(paths []string) ([]string, error) {
   108  	var matches []string
   109  	matcher := NewRelativeFileOrChildMatcher(ps.BaseDirectory, ps.Paths...)
   110  	for _, path := range paths {
   111  		match, err := matcher.Matches(path)
   112  		if err != nil {
   113  			return nil, err
   114  		}
   115  		if match {
   116  			matches = append(matches, path)
   117  		}
   118  	}
   119  	return matches, nil
   120  }
   121  
   122  type CompositePathMatcher struct {
   123  	Matchers []PathMatcher
   124  }
   125  
   126  func NewCompositeMatcher(matchers []PathMatcher) PathMatcher {
   127  	if len(matchers) == 0 {
   128  		return EmptyMatcher
   129  	}
   130  	return CompositePathMatcher{Matchers: matchers}
   131  }
   132  
   133  func (c CompositePathMatcher) Matches(f string) (bool, error) {
   134  	for _, t := range c.Matchers {
   135  		ret, err := t.Matches(f)
   136  		if err != nil {
   137  			return false, err
   138  		}
   139  		if ret {
   140  			return true, nil
   141  		}
   142  	}
   143  	return false, nil
   144  }
   145  
   146  func (c CompositePathMatcher) MatchesEntireDir(f string) (bool, error) {
   147  	for _, t := range c.Matchers {
   148  		matches, err := t.MatchesEntireDir(f)
   149  		if matches || err != nil {
   150  			return matches, err
   151  		}
   152  	}
   153  	return false, nil
   154  }
   155  
   156  var _ PathMatcher = CompositePathMatcher{}