github.com/keleustes/helm@v3.0.0-beta.3+incompatible/internal/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  // Ignore evalutes the file at the given path, and returns true if it should be ignored.
    77  //
    78  // Ignore evaluates path against the rules in order. Evaluation stops when a match
    79  // is found. Matching a negative rule will stop evaluation.
    80  func (r *Rules) Ignore(path string, fi os.FileInfo) bool {
    81  	// Don't match on empty dirs.
    82  	if path == "" {
    83  		return false
    84  	}
    85  
    86  	// Disallow ignoring the current working directory.
    87  	// See issue:
    88  	// 1776 (New York City) Hamilton: "Pardon me, are you Aaron Burr, sir?"
    89  	if path == "." || path == "./" {
    90  		return false
    91  	}
    92  	for _, p := range r.patterns {
    93  		if p.match == nil {
    94  			log.Printf("ignore: no matcher supplied for %q", p.raw)
    95  			return false
    96  		}
    97  
    98  		// For negative rules, we need to capture and return non-matches,
    99  		// and continue for matches.
   100  		if p.negate {
   101  			if p.mustDir && !fi.IsDir() {
   102  				return true
   103  			}
   104  			if !p.match(path, fi) {
   105  				return true
   106  			}
   107  			continue
   108  		}
   109  
   110  		// If the rule is looking for directories, and this is not a directory,
   111  		// skip it.
   112  		if p.mustDir && !fi.IsDir() {
   113  			continue
   114  		}
   115  		if p.match(path, fi) {
   116  			return true
   117  		}
   118  	}
   119  	return false
   120  }
   121  
   122  // parseRule parses a rule string and creates a pattern, which is then stored in the Rules object.
   123  func (r *Rules) parseRule(rule string) error {
   124  	rule = strings.TrimSpace(rule)
   125  
   126  	// Ignore blank lines
   127  	if rule == "" {
   128  		return nil
   129  	}
   130  	// Comment
   131  	if strings.HasPrefix(rule, "#") {
   132  		return nil
   133  	}
   134  
   135  	// Fail any rules that contain **
   136  	if strings.Contains(rule, "**") {
   137  		return errors.New("double-star (**) syntax is not supported")
   138  	}
   139  
   140  	// Fail any patterns that can't compile. A non-empty string must be
   141  	// given to Match() to avoid optimization that skips rule evaluation.
   142  	if _, err := filepath.Match(rule, "abc"); err != nil {
   143  		return err
   144  	}
   145  
   146  	p := &pattern{raw: rule}
   147  
   148  	// Negation is handled at a higher level, so strip the leading ! from the
   149  	// string.
   150  	if strings.HasPrefix(rule, "!") {
   151  		p.negate = true
   152  		rule = rule[1:]
   153  	}
   154  
   155  	// Directory verification is handled by a higher level, so the trailing /
   156  	// is removed from the rule. That way, a directory named "foo" matches,
   157  	// even if the supplied string does not contain a literal slash character.
   158  	if strings.HasSuffix(rule, "/") {
   159  		p.mustDir = true
   160  		rule = strings.TrimSuffix(rule, "/")
   161  	}
   162  
   163  	if strings.HasPrefix(rule, "/") {
   164  		// Require path matches the root path.
   165  		p.match = func(n string, fi os.FileInfo) bool {
   166  			rule = strings.TrimPrefix(rule, "/")
   167  			ok, err := filepath.Match(rule, n)
   168  			if err != nil {
   169  				log.Printf("Failed to compile %q: %s", rule, err)
   170  				return false
   171  			}
   172  			return ok
   173  		}
   174  	} else if strings.Contains(rule, "/") {
   175  		// require structural match.
   176  		p.match = func(n string, fi os.FileInfo) bool {
   177  			ok, err := filepath.Match(rule, n)
   178  			if err != nil {
   179  				log.Printf("Failed to compile %q: %s", rule, err)
   180  				return false
   181  			}
   182  			return ok
   183  		}
   184  	} else {
   185  		p.match = func(n string, fi os.FileInfo) bool {
   186  			// When there is no slash in the pattern, we evaluate ONLY the
   187  			// filename.
   188  			n = filepath.Base(n)
   189  			ok, err := filepath.Match(rule, n)
   190  			if err != nil {
   191  				log.Printf("Failed to compile %q: %s", rule, err)
   192  				return false
   193  			}
   194  			return ok
   195  		}
   196  	}
   197  
   198  	r.patterns = append(r.patterns, p)
   199  	return nil
   200  }
   201  
   202  // matcher is a function capable of computing a match.
   203  //
   204  // It returns true if the rule matches.
   205  type matcher func(name string, fi os.FileInfo) bool
   206  
   207  // pattern describes a pattern to be matched in a rule set.
   208  type pattern struct {
   209  	// raw is the unparsed string, with nothing stripped.
   210  	raw string
   211  	// match is the matcher function.
   212  	match matcher
   213  	// negate indicates that the rule's outcome should be negated.
   214  	negate bool
   215  	// mustDir indicates that the matched file must be a directory.
   216  	mustDir bool
   217  }