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 }