github.com/boyter/gocodewalker@v1.3.2/go-gitignore/gitignore.go (about)

     1  // SPDX-License-Identifier: MIT
     2  
     3  package gitignore
     4  
     5  import (
     6  	"io"
     7  	"os"
     8  	"path/filepath"
     9  	"runtime"
    10  	"strings"
    11  )
    12  
    13  // use an empty GitIgnore for cached lookups
    14  var empty = &ignore{}
    15  
    16  // GitIgnore is the interface to .gitignore files and repositories. It defines
    17  // methods for testing files for matching the .gitignore file, and then
    18  // determining whether a file should be ignored or included.
    19  type GitIgnore interface {
    20  	// Base returns the directory containing the .gitignore file.
    21  	Base() string
    22  
    23  	// Match attempts to match the path against this GitIgnore, and will
    24  	// return its Match if successful. Match will invoke the GitIgnore error
    25  	// handler (if defined) if it is not possible to determine the absolute
    26  	// path of the given path, or if its not possible to determine if the
    27  	// path represents a file or a directory. If an error occurs, Match
    28  	// returns nil and the error handler (if defined via New, NewWithErrors
    29  	// or NewWithCache) will be invoked.
    30  	Match(path string) Match
    31  
    32  	MatchIsDir(path string, _isdir bool) Match
    33  
    34  	// Absolute attempts to match an absolute path against this GitIgnore. If
    35  	// the path is not located under the base directory of this GitIgnore, or
    36  	// is not matched by this GitIgnore, nil is returned.
    37  	Absolute(string, bool) Match
    38  
    39  	// Relative attempts to match a path relative to the GitIgnore base
    40  	// directory. isdir is used to indicate whether the path represents a file
    41  	// or a directory. If the path is not matched by the GitIgnore, nil is
    42  	// returned.
    43  	Relative(path string, isdir bool) Match
    44  
    45  	// Ignore returns true if the path is ignored by this GitIgnore. Paths
    46  	// that are not matched by this GitIgnore are not ignored. Internally,
    47  	// Ignore uses Match, and will return false if Match() returns nil for path.
    48  	Ignore(path string) bool
    49  
    50  	// Include returns true if the path is included by this GitIgnore. Paths
    51  	// that are not matched by this GitIgnore are always included. Internally,
    52  	// Include uses Match, and will return true if Match() returns nil for path.
    53  	Include(path string) bool
    54  }
    55  
    56  // ignore is the implementation of a .gitignore file.
    57  type ignore struct {
    58  	_base    string
    59  	_pattern []Pattern
    60  	_errors  func(Error) bool
    61  }
    62  
    63  // NewGitIgnore creates a new GitIgnore instance from the patterns listed in t,
    64  // representing a .gitignore file in the base directory. If errors is given, it
    65  // will be invoked for every error encountered when parsing the .gitignore
    66  // patterns. Parsing will terminate if errors is called and returns false,
    67  // otherwise, parsing will continue until end of file has been reached.
    68  func New(r io.Reader, base string, errors func(Error) bool) GitIgnore {
    69  	// do we have an error handler?
    70  	_errors := errors
    71  	if _errors == nil {
    72  		_errors = func(e Error) bool { return true }
    73  	}
    74  
    75  	// extract the patterns from the reader
    76  	_parser := NewParser(r, _errors)
    77  	_patterns := _parser.Parse()
    78  
    79  	return &ignore{_base: base, _pattern: _patterns, _errors: _errors}
    80  } // New()
    81  
    82  // NewFromFile creates a GitIgnore instance from the given file. An error
    83  // will be returned if file cannot be opened or its absolute path determined.
    84  func NewFromFile(file string) (GitIgnore, error) {
    85  	// define an error handler to catch any file access errors
    86  	//		- record the first encountered error
    87  	var _error Error
    88  	_errors := func(e Error) bool {
    89  		if _error == nil {
    90  			_error = e
    91  		}
    92  		return true
    93  	}
    94  
    95  	// attempt to retrieve the GitIgnore represented by this file
    96  	_ignore := NewWithErrors(file, _errors)
    97  
    98  	// did we encounter an error?
    99  	//		- if the error has a zero Position then it was encountered
   100  	//		  before parsing was attempted, so we return that error
   101  	if _error != nil {
   102  		if _error.Position().Zero() {
   103  			return nil, _error.Underlying()
   104  		}
   105  	}
   106  
   107  	// otherwise, we ignore the parser errors
   108  	return _ignore, nil
   109  } // NewFromFile()
   110  
   111  // NewWithErrors creates a GitIgnore instance from the given file.
   112  // If errors is given, it will be invoked for every error encountered when
   113  // parsing the .gitignore patterns. Parsing will terminate if errors is called
   114  // and returns false, otherwise, parsing will continue until end of file has
   115  // been reached. NewWithErrors returns nil if the .gitignore could not be read.
   116  func NewWithErrors(file string, errors func(Error) bool) GitIgnore {
   117  	var _err error
   118  
   119  	// do we have an error handler?
   120  	_file := file
   121  	_errors := errors
   122  	if _errors == nil {
   123  		_errors = func(e Error) bool { return true }
   124  	} else {
   125  		// augment the error handler to include the .gitignore file name
   126  		//		- we do this here since the parser and lexer interfaces are
   127  		//		  not aware of file names
   128  		_errors = func(e Error) bool {
   129  			// augment the position with the file name
   130  			_position := e.Position()
   131  			_position.File = _file
   132  
   133  			// create a new error with the updated Position
   134  			_error := NewError(e.Underlying(), _position)
   135  
   136  			// invoke the original error handler
   137  			return errors(_error)
   138  		}
   139  	}
   140  
   141  	// we need the absolute path for the GitIgnore base
   142  	_file, _err = filepath.Abs(file)
   143  	if _err != nil {
   144  		_errors(NewError(_err, Position{}))
   145  		return nil
   146  	}
   147  	_base := filepath.Dir(_file)
   148  
   149  	// attempt to open the ignore file to create the io.Reader
   150  	_fh, _err := os.Open(_file)
   151  	if _err != nil {
   152  		_errors(NewError(_err, Position{}))
   153  		return nil
   154  	}
   155  
   156  	// return the GitIgnore instance
   157  	return New(_fh, _base, _errors)
   158  } // NewWithErrors()
   159  
   160  // NewWithCache returns a GitIgnore instance (using NewWithErrors)
   161  // for the given file. If the file has been loaded before, its GitIgnore
   162  // instance will be returned from the cache rather than being reloaded. If
   163  // cache is not defined, NewWithCache will behave as NewWithErrors
   164  //
   165  // If NewWithErrors returns nil, NewWithCache will store an empty
   166  // GitIgnore (i.e. no patterns) against the file to prevent repeated parse
   167  // attempts on subsequent requests for the same file. Subsequent calls to
   168  // NewWithCache for a file that could not be loaded due to an error will
   169  // return nil.
   170  //
   171  // If errors is given, it will be invoked for every error encountered when
   172  // parsing the .gitignore patterns. Parsing will terminate if errors is called
   173  // and returns false, otherwise, parsing will continue until end of file has
   174  // been reached.
   175  func NewWithCache(file string, cache Cache, errors func(Error) bool) GitIgnore {
   176  	// do we have an error handler?
   177  	_errors := errors
   178  	if _errors == nil {
   179  		_errors = func(e Error) bool { return true }
   180  	}
   181  
   182  	// use the file absolute path as its key into the cache
   183  	_abs, _err := filepath.Abs(file)
   184  	if _err != nil {
   185  		_errors(NewError(_err, Position{}))
   186  		return nil
   187  	}
   188  
   189  	var _ignore GitIgnore
   190  	if cache != nil {
   191  		_ignore = cache.Get(_abs)
   192  	}
   193  	if _ignore == nil {
   194  		_ignore = NewWithErrors(file, _errors)
   195  		if _ignore == nil {
   196  			// if the load failed, cache an empty GitIgnore to prevent
   197  			// further attempts to load this file
   198  			_ignore = empty
   199  		}
   200  		if cache != nil {
   201  			cache.Set(_abs, _ignore)
   202  		}
   203  	}
   204  
   205  	// return the ignore (if we have it)
   206  	if _ignore == empty {
   207  		return nil
   208  	} else {
   209  		return _ignore
   210  	}
   211  } // NewWithCache()
   212  
   213  // Base returns the directory containing the .gitignore file for this GitIgnore.
   214  func (i *ignore) Base() string {
   215  	return i._base
   216  } // Base()
   217  
   218  // Match attempts to match the path against this GitIgnore, and will
   219  // return its Match if successful. Match will invoke the GitIgnore error
   220  // handler (if defined) if it is not possible to determine the absolute
   221  // path of the given path, or if its not possible to determine if the
   222  // path represents a file or a directory. If an error occurs, Match
   223  // returns nil and the error handler (if defined via New, NewWithErrors
   224  // or NewWithCache) will be invoked.
   225  func (i *ignore) Match(path string) Match {
   226  	// ensure we have the absolute path for the given file
   227  	_path, _err := filepath.Abs(path)
   228  	if _err != nil {
   229  		i._errors(NewError(_err, Position{}))
   230  		return nil
   231  	}
   232  
   233  	// is the path a file or a directory?
   234  	_info, _err := os.Stat(_path)
   235  	if _err != nil {
   236  		i._errors(NewError(_err, Position{}))
   237  		return nil
   238  	}
   239  	_isdir := _info.IsDir()
   240  
   241  	// attempt to match the absolute path
   242  	return i.Absolute(_path, _isdir)
   243  } // Match()
   244  
   245  func (i *ignore) MatchIsDir(path string, _isdir bool) Match {
   246  	// ensure we have the absolute path for the given file
   247  	_path, _err := filepath.Abs(path)
   248  	if _err != nil {
   249  		i._errors(NewError(_err, Position{}))
   250  		return nil
   251  	}
   252  
   253  	// attempt to match the absolute path
   254  	return i.Absolute(_path, _isdir)
   255  } // Match()
   256  
   257  // Absolute attempts to match an absolute path against this GitIgnore. If
   258  // the path is not located under the base directory of this GitIgnore, or
   259  // is not matched by this GitIgnore, nil is returned.
   260  func (i *ignore) Absolute(path string, isdir bool) Match {
   261  	// does the file share the same directory as this ignore file?
   262  	if !strings.HasPrefix(path, i._base) {
   263  		return nil
   264  	}
   265  
   266  	// extract the relative path of this file
   267  	_prefix := len(i._base) + 1 // BOYTERWASHERE
   268  	//_prefix := len(i._base)
   269  	_rel := string(path[_prefix:])
   270  	return i.Relative(_rel, isdir)
   271  } // Absolute()
   272  
   273  // Relative attempts to match a path relative to the GitIgnore base
   274  // directory. isdir is used to indicate whether the path represents a file
   275  // or a directory. If the path is not matched by the GitIgnore, nil is
   276  // returned.
   277  func (i *ignore) Relative(path string, isdir bool) Match {
   278  	// if we are on Windows, then translate the path to Unix form
   279  	_rel := path
   280  	if runtime.GOOS == "windows" {
   281  		_rel = filepath.ToSlash(_rel)
   282  	}
   283  
   284  	// iterate over the patterns for this ignore file
   285  	//      - iterate in reverse, since later patterns overwrite earlier
   286  	for _i := len(i._pattern) - 1; _i >= 0; _i-- {
   287  		_pattern := i._pattern[_i]
   288  		if _pattern.Match(_rel, isdir) {
   289  			return _pattern
   290  		}
   291  	}
   292  
   293  	// we don't match this file
   294  	return nil
   295  } // Relative()
   296  
   297  // Ignore returns true if the path is ignored by this GitIgnore. Paths
   298  // that are not matched by this GitIgnore are not ignored. Internally,
   299  // Ignore uses Match, and will return false if Match() returns nil for path.
   300  func (i *ignore) Ignore(path string) bool {
   301  	_match := i.Match(path)
   302  	if _match != nil {
   303  		return _match.Ignore()
   304  	}
   305  
   306  	// we didn't match this path, so we don't ignore it
   307  	return false
   308  } // Ignore()
   309  
   310  // Include returns true if the path is included by this GitIgnore. Paths
   311  // that are not matched by this GitIgnore are always included. Internally,
   312  // Include uses Match, and will return true if Match() returns nil for path.
   313  func (i *ignore) Include(path string) bool {
   314  	_match := i.Match(path)
   315  	if _match != nil {
   316  		return _match.Include()
   317  	}
   318  
   319  	// we didn't match this path, so we include it
   320  	return true
   321  } // Include()
   322  
   323  // ensure Ignore satisfies the GitIgnore interface
   324  var _ GitIgnore = &ignore{}