go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/lucicfg/tracked.go (about)

     1  // Copyright 2019 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package lucicfg
    16  
    17  import (
    18  	"os"
    19  	"path"
    20  	"path/filepath"
    21  	"sort"
    22  	"strings"
    23  
    24  	"go.chromium.org/luci/common/errors"
    25  )
    26  
    27  // TrackedSet returns a predicate that classifies whether a slash-separated path
    28  // belongs to a tracked set or not.
    29  //
    30  // Each entry in `patterns` is either `<glob pattern>` (a "positive" glob) or
    31  // `!<glob pattern>` (a "negative" glob). A path is considered tracked if its
    32  // base name matches any of the positive globs and none of the negative globs.
    33  // If `patterns` is empty, no paths are considered tracked. If all patterns
    34  // are negative, single `**/*` positive pattern is implied as well.
    35  //
    36  // The predicate returns an error if some pattern is malformed.
    37  func TrackedSet(patterns []string) func(string) (bool, error) {
    38  	if len(patterns) == 0 {
    39  		return func(string) (bool, error) { return false, nil }
    40  	}
    41  
    42  	var pos, neg []string
    43  	for _, pat := range patterns {
    44  		if strings.HasPrefix(pat, "!") {
    45  			neg = append(neg, pat[1:])
    46  		} else {
    47  			pos = append(pos, pat)
    48  		}
    49  	}
    50  
    51  	if len(pos) == 0 {
    52  		pos = []string{"**/*"}
    53  	}
    54  
    55  	return func(p string) (bool, error) {
    56  		if isPos, err := matchesAny(p, pos); !isPos || err != nil {
    57  			return false, err
    58  		}
    59  		if isNeg, err := matchesAny(p, neg); isNeg || err != nil {
    60  			return false, err
    61  		}
    62  		return true, nil
    63  	}
    64  }
    65  
    66  // FindTrackedFiles recursively discovers all regular files in the given
    67  // directory whose names match given patterns.
    68  //
    69  // See TrackedSet for the format of `patterns`. If the directory doesn't exist,
    70  // returns empty slice.
    71  //
    72  // Returned file names are sorted, slash-separated and relative to `dir`.
    73  func FindTrackedFiles(dir string, patterns []string) ([]string, error) {
    74  	// Avoid scanning the directory if the tracked set is known to be empty.
    75  	if len(patterns) == 0 {
    76  		return nil, nil
    77  	}
    78  
    79  	// Missing directory is considered empty.
    80  	if _, err := os.Stat(dir); os.IsNotExist(err) {
    81  		return nil, nil
    82  	}
    83  
    84  	isTracked := TrackedSet(patterns)
    85  
    86  	var tracked []string
    87  	err := filepath.Walk(dir, func(p string, info os.FileInfo, err error) error {
    88  		if err != nil || !info.Mode().IsRegular() {
    89  			return err
    90  		}
    91  		rel, err := filepath.Rel(dir, p)
    92  		if err != nil {
    93  			return err
    94  		}
    95  		rel = filepath.ToSlash(rel)
    96  		yes, err := isTracked(rel)
    97  		if yes {
    98  			tracked = append(tracked, rel)
    99  		}
   100  		return err
   101  	})
   102  	if err != nil {
   103  		return nil, errors.Annotate(err, "failed to scan the directory for tracked files").Err()
   104  	}
   105  
   106  	sort.Strings(tracked)
   107  	return tracked, nil
   108  }
   109  
   110  func matchesAny(name string, pats []string) (yes bool, err error) {
   111  	for _, pat := range pats {
   112  		subject := name
   113  		if strings.HasPrefix(pat, "**/") {
   114  			pat = pat[3:]
   115  			subject = path.Base(name)
   116  		}
   117  		switch match, err := path.Match(pat, subject); {
   118  		case err != nil:
   119  			return false, errors.Annotate(err, "bad pattern %q", pat).Err()
   120  		case match:
   121  			return true, nil
   122  		}
   123  	}
   124  	return false, nil
   125  }