github.com/appscode/helm@v3.0.0-alpha.1+incompatible/pkg/ignore/rules.go (about)

     1  /*
     2  Copyright The Helm 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 ignore
    18  
    19  import (
    20  	"bufio"
    21  	"io"
    22  	"log"
    23  	"os"
    24  	"path/filepath"
    25  	"strings"
    26  
    27  	"github.com/pkg/errors"
    28  )
    29  
    30  // HelmIgnore default name of an ignorefile.
    31  const HelmIgnore = ".helmignore"
    32  
    33  // Rules is a collection of path matching rules.
    34  //
    35  // Parse() and ParseFile() will construct and populate new Rules.
    36  // Empty() will create an immutable empty ruleset.
    37  type Rules struct {
    38  	patterns []*pattern
    39  }
    40  
    41  // Empty builds an empty ruleset.
    42  func Empty() *Rules {
    43  	return &Rules{patterns: []*pattern{}}
    44  }
    45  
    46  // AddDefaults adds default ignore patterns.
    47  //
    48  // Ignore all dotfiles in "templates/"
    49  func (r *Rules) AddDefaults() {
    50  	r.parseRule(`templates/.?*`)
    51  }
    52  
    53  // ParseFile parses a helmignore file and returns the *Rules.
    54  func ParseFile(file string) (*Rules, error) {
    55  	f, err := os.Open(file)
    56  	if err != nil {
    57  		return nil, err
    58  	}
    59  	defer f.Close()
    60  	return Parse(f)
    61  }
    62  
    63  // Parse parses a rules file
    64  func Parse(file io.Reader) (*Rules, error) {
    65  	r := &Rules{patterns: []*pattern{}}
    66  
    67  	s := bufio.NewScanner(file)
    68  	for s.Scan() {
    69  		if err := r.parseRule(s.Text()); err != nil {
    70  			return r, err
    71  		}
    72  	}
    73  	return r, s.Err()
    74  }
    75  
    76  // Len returns the number of patterns in this rule set.
    77  func (r *Rules) Len() int {
    78  	return len(r.patterns)
    79  }
    80  
    81  // Ignore evalutes the file at the given path, and returns true if it should be ignored.
    82  //
    83  // Ignore evaluates path against the rules in order. Evaluation stops when a match
    84  // is found. Matching a negative rule will stop evaluation.
    85  func (r *Rules) Ignore(path string, fi os.FileInfo) bool {
    86  	// Don't match on empty dirs.
    87  	if path == "" {
    88  		return false
    89  	}
    90  
    91  	// Disallow ignoring the current working directory.
    92  	// See issue:
    93  	// 1776 (New York City) Hamilton: "Pardon me, are you Aaron Burr, sir?"
    94  	if path == "." || path == "./" {
    95  		return false
    96  	}
    97  	for _, p := range r.patterns {
    98  		if p.match == nil {
    99  			log.Printf("ignore: no matcher supplied for %q", p.raw)
   100  			return false
   101  		}
   102  
   103  		// For negative rules, we need to capture and return non-matches,
   104  		// and continue for matches.
   105  		if p.negate {
   106  			if p.mustDir && !fi.IsDir() {
   107  				return true
   108  			}
   109  			if !p.match(path, fi) {
   110  				return true
   111  			}
   112  			continue
   113  		}
   114  
   115  		// If the rule is looking for directories, and this is not a directory,
   116  		// skip it.
   117  		if p.mustDir && !fi.IsDir() {
   118  			continue
   119  		}
   120  		if p.match(path, fi) {
   121  			return true
   122  		}
   123  	}
   124  	return false
   125  }
   126  
   127  // parseRule parses a rule string and creates a pattern, which is then stored in the Rules object.
   128  func (r *Rules) parseRule(rule string) error {
   129  	rule = strings.TrimSpace(rule)
   130  
   131  	// Ignore blank lines
   132  	if rule == "" {
   133  		return nil
   134  	}
   135  	// Comment
   136  	if strings.HasPrefix(rule, "#") {
   137  		return nil
   138  	}
   139  
   140  	// Fail any rules that contain **
   141  	if strings.Contains(rule, "**") {
   142  		return errors.New("double-star (**) syntax is not supported")
   143  	}
   144  
   145  	// Fail any patterns that can't compile. A non-empty string must be
   146  	// given to Match() to avoid optimization that skips rule evaluation.
   147  	if _, err := filepath.Match(rule, "abc"); err != nil {
   148  		return err
   149  	}
   150  
   151  	p := &pattern{raw: rule}
   152  
   153  	// Negation is handled at a higher level, so strip the leading ! from the
   154  	// string.
   155  	if strings.HasPrefix(rule, "!") {
   156  		p.negate = true
   157  		rule = rule[1:]
   158  	}
   159  
   160  	// Directory verification is handled by a higher level, so the trailing /
   161  	// is removed from the rule. That way, a directory named "foo" matches,
   162  	// even if the supplied string does not contain a literal slash character.
   163  	if strings.HasSuffix(rule, "/") {
   164  		p.mustDir = true
   165  		rule = strings.TrimSuffix(rule, "/")
   166  	}
   167  
   168  	if strings.HasPrefix(rule, "/") {
   169  		// Require path matches the root path.
   170  		p.match = func(n string, fi os.FileInfo) bool {
   171  			rule = strings.TrimPrefix(rule, "/")
   172  			ok, err := filepath.Match(rule, n)
   173  			if err != nil {
   174  				log.Printf("Failed to compile %q: %s", rule, err)
   175  				return false
   176  			}
   177  			return ok
   178  		}
   179  	} else if strings.Contains(rule, "/") {
   180  		// require structural match.
   181  		p.match = func(n string, fi os.FileInfo) bool {
   182  			ok, err := filepath.Match(rule, n)
   183  			if err != nil {
   184  				log.Printf("Failed to compile %q: %s", rule, err)
   185  				return false
   186  			}
   187  			return ok
   188  		}
   189  	} else {
   190  		p.match = func(n string, fi os.FileInfo) bool {
   191  			// When there is no slash in the pattern, we evaluate ONLY the
   192  			// filename.
   193  			n = filepath.Base(n)
   194  			ok, err := filepath.Match(rule, n)
   195  			if err != nil {
   196  				log.Printf("Failed to compile %q: %s", rule, err)
   197  				return false
   198  			}
   199  			return ok
   200  		}
   201  	}
   202  
   203  	r.patterns = append(r.patterns, p)
   204  	return nil
   205  }
   206  
   207  // matcher is a function capable of computing a match.
   208  //
   209  // It returns true if the rule matches.
   210  type matcher func(name string, fi os.FileInfo) bool
   211  
   212  // pattern describes a pattern to be matched in a rule set.
   213  type pattern struct {
   214  	// raw is the unparsed string, with nothing stripped.
   215  	raw string
   216  	// match is the matcher function.
   217  	match matcher
   218  	// negate indicates that the rule's outcome should be negated.
   219  	negate bool
   220  	// mustDir indicates that the matched file must be a directory.
   221  	mustDir bool
   222  }