github.com/lambdatest/go-gitignore@v0.0.0-20230214141342-7fe15342e580/gitignore.go (about)

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