sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/genfiles/genfiles.go (about) 1 /* 2 Copyright 2016 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 // Package genfiles understands the .generated_files config file. 18 // The ".generated_files" config lives in the repo's root. 19 // 20 // The config is a series of newline-delimited statements. Statements which 21 // begin with a `#` are ignored. A statement is a white-space delimited 22 // key-value tuple. 23 // 24 // statement = key val 25 // 26 // where whitespace is ignored, and: 27 // 28 // key = "path" | "file-name" | "path-prefix" | 29 // "file-prefix" | "paths-from-repo" 30 // 31 // For example: 32 // 33 // # Simple generated files config 34 // file-prefix zz_generated. 35 // file-name generated.pb.go 36 // 37 // The statement's `key` specifies the type of the corresponding value: 38 // - "path": exact path to a single file 39 // - "file-name": exact leaf file name, regardless of path 40 // - "path-prefix": prefix match on the file path 41 // - "file-prefix": prefix match of the leaf filename (no path) 42 // - "paths-from-repo": load file paths from a file in repo 43 package genfiles 44 45 import ( 46 "bufio" 47 "bytes" 48 "fmt" 49 "io" 50 "path/filepath" 51 "strings" 52 53 "sigs.k8s.io/prow/pkg/github" 54 ) 55 56 const genConfigFile = ".generated_files" 57 58 // ghFileClient scopes to the only relevant functionality we require of a github client. 59 type ghFileClient interface { 60 GetFile(org, repo, filepath, commit string) ([]byte, error) 61 } 62 63 // Group is a logical collection of files. Check for a file's 64 // inclusion in the group using the Match method. 65 type Group struct { 66 Paths, FileNames, PathPrefixes, FilePrefixes map[string]bool 67 } 68 69 // NewGroup reads the .generated_files file in the root of the repository 70 // and any referenced path files (from "path-from-repo" commands). 71 func NewGroup(gc ghFileClient, owner, repo, sha string) (*Group, error) { 72 g := &Group{ 73 Paths: make(map[string]bool), 74 FileNames: make(map[string]bool), 75 PathPrefixes: make(map[string]bool), 76 FilePrefixes: make(map[string]bool), 77 } 78 79 bs, err := gc.GetFile(owner, repo, genConfigFile, sha) 80 if err != nil { 81 switch err.(type) { 82 case *github.FileNotFound: 83 return g, nil 84 default: 85 return nil, fmt.Errorf("could not get .generated_files: %w", err) 86 } 87 } 88 89 repoFiles, err := g.load(bytes.NewBuffer(bs)) 90 if err != nil { 91 return nil, err 92 } 93 for _, f := range repoFiles { 94 bs, err = gc.GetFile(owner, repo, f, sha) 95 if err != nil { 96 return nil, err 97 } 98 if err = g.loadPaths(bytes.NewBuffer(bs)); err != nil { 99 return nil, err 100 } 101 } 102 103 return g, nil 104 } 105 106 // Use load to read a generated files config file, and populate g with the commands. 107 // "paths-from-repo" commands are aggregated into repoPaths. It is the caller's 108 // responsibility to fetch these and load them via g.loadPaths. 109 func (g *Group) load(r io.Reader) ([]string, error) { 110 var repoPaths []string 111 s := bufio.NewScanner(r) 112 for s.Scan() { 113 l := strings.TrimSpace(s.Text()) 114 if l == "" || l[0] == '#' { 115 // Ignore comments and empty lines. 116 continue 117 } 118 119 fs := strings.Fields(l) 120 if len(fs) != 2 { 121 return repoPaths, &ParseError{line: l} 122 } 123 124 switch fs[0] { 125 case "prefix", "path-prefix": 126 g.PathPrefixes[fs[1]] = true 127 case "file-prefix": 128 g.FilePrefixes[fs[1]] = true 129 case "file-name": 130 g.FileNames[fs[1]] = true 131 case "path": 132 g.FileNames[fs[1]] = true 133 case "paths-from-repo": 134 // Despite the name, this command actually requires a file 135 // of paths from the _same_ repo in which the .generated_files 136 // config lives. 137 repoPaths = append(repoPaths, fs[1]) 138 default: 139 return repoPaths, &ParseError{line: l} 140 } 141 } 142 143 if err := s.Err(); err != nil { 144 return repoPaths, err 145 } 146 147 return repoPaths, nil 148 } 149 150 // Use loadPaths to load a file of new-line delimited paths, such as 151 // resolving file data referenced in a "paths-from-repo" command. 152 func (g *Group) loadPaths(r io.Reader) error { 153 s := bufio.NewScanner(r) 154 155 for s.Scan() { 156 l := strings.TrimSpace(s.Text()) 157 if l == "" || l[0] == '#' { 158 // Ignore comments and empty lines. 159 continue 160 } 161 162 g.Paths[l] = true 163 } 164 165 if err := s.Err(); err != nil { 166 return fmt.Errorf("scan error: %w", err) 167 } 168 169 return nil 170 } 171 172 // Match determines whether a file, given here by its full path 173 // is included in the generated files group. 174 func (g *Group) Match(path string) bool { 175 if g.Paths[path] { 176 return true 177 } 178 179 for prefix := range g.PathPrefixes { 180 if strings.HasPrefix(path, prefix) { 181 return true 182 } 183 } 184 185 base := filepath.Base(path) 186 187 if g.FileNames[base] { 188 return true 189 } 190 191 for prefix := range g.FilePrefixes { 192 if strings.HasPrefix(base, prefix) { 193 return true 194 } 195 } 196 197 return false 198 } 199 200 // ParseError is an invalid line in a .generated_files config. 201 type ParseError struct { 202 line string 203 } 204 205 func (pe *ParseError) Error() string { 206 return fmt.Sprintf("invalid config line: %q", pe.line) 207 }