github.com/munnerz/test-infra@v0.0.0-20190108210205-ce3d181dc989/prow/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  	"k8s.io/test-infra/prow/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: %v", 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: %v", 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  }