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

     1  package gitignore
     2  
     3  import (
     4  	"os"
     5  	"path/filepath"
     6  	"strings"
     7  )
     8  
     9  const File = ".gitignore"
    10  
    11  // repository is the implementation of the set of .gitignore files within a
    12  // repository hierarchy
    13  type repository struct {
    14  	ignore
    15  	_errors  func(e Error) bool
    16  	_cache   Cache
    17  	_file    string
    18  	_exclude GitIgnore
    19  } // repository{}
    20  
    21  // NewRepository returns a GitIgnore instance representing a git repository
    22  // with root directory base. If base is not a directory, or base cannot be
    23  // read, NewRepository will return an error.
    24  //
    25  // Internally, NewRepository uses NewRepositoryWithFile.
    26  func NewRepository(base string) (GitIgnore, error) {
    27  	return NewRepositoryWithFile(base, File)
    28  } // NewRepository()
    29  
    30  // NewRepositoryWithFile returns a GitIgnore instance representing a git
    31  // repository with root directory base. The repository will use file as
    32  // the name of the files within the repository from which to load the
    33  // .gitignore patterns. If file is the empty string, NewRepositoryWithFile
    34  // uses ".gitignore". If the ignore file name is ".gitignore", the returned
    35  // GitIgnore instance will also consider patterns listed in
    36  // $GIT_DIR/info/exclude when performing repository matching.
    37  //
    38  // Internally, NewRepositoryWithFile uses NewRepositoryWithErrors.
    39  func NewRepositoryWithFile(base, file string) (GitIgnore, error) {
    40  	// define an error handler to catch any file access errors
    41  	//		- record the first encountered error
    42  	var _error Error
    43  	_errors := func(e Error) bool {
    44  		if _error == nil {
    45  			_error = e
    46  		}
    47  		return true
    48  	}
    49  
    50  	// attempt to retrieve the repository represented by this file
    51  	_repository := NewRepositoryWithErrors(base, file, _errors)
    52  
    53  	// did we encounter an error?
    54  	//		- if the error has a zero Position then it was encountered
    55  	//		  before parsing was attempted, so we return that error
    56  	if _error != nil {
    57  		if _error.Position().Zero() {
    58  			return nil, _error.Underlying()
    59  		}
    60  	}
    61  
    62  	// otherwise, we ignore the parser errors
    63  	return _repository, nil
    64  } // NewRepositoryWithFile()
    65  
    66  // NewRepositoryWithErrors returns a GitIgnore instance representing a git
    67  // repository with a root directory base. As with NewRepositoryWithFile, file
    68  // specifies the name of the files within the repository containing the
    69  // .gitignore patterns, and defaults to ".gitignore" if file is not specified.
    70  // If the ignore file name is ".gitignore", the returned GitIgnore instance
    71  // will also consider patterns listed in $GIT_DIR/info/exclude when performing
    72  // repository matching.
    73  //
    74  // If errors is given, it will be invoked for each error encountered while
    75  // matching a path against the repository GitIgnore (such as file permission
    76  // denied, or errors during .gitignore parsing). See Match below.
    77  //
    78  // Internally, NewRepositoryWithErrors uses NewRepositoryWithCache.
    79  func NewRepositoryWithErrors(base, file string, errors func(e Error) bool) GitIgnore {
    80  	return NewRepositoryWithCache(base, file, NewCache(), errors)
    81  } // NewRepositoryWithErrors()
    82  
    83  // NewRepositoryWithCache returns a GitIgnore instance representing a git
    84  // repository with a root directory base. As with NewRepositoryWithErrors,
    85  // file specifies the name of the files within the repository containing the
    86  // .gitignore patterns, and defaults to ".gitignore" if file is not specified.
    87  // If the ignore file name is ".gitignore", the returned GitIgnore instance
    88  // will also consider patterns listed in $GIT_DIR/info/exclude when performing
    89  // repository matching.
    90  //
    91  // NewRepositoryWithCache will attempt to load each .gitignore within the
    92  // repository only once, using NewWithCache to store the corresponding
    93  // GitIgnore instance in cache. If cache is given as nil,
    94  // NewRepositoryWithCache will create a Cache instance for this repository.
    95  //
    96  // If errors is given, it will be invoked for each error encountered while
    97  // matching a path against the repository GitIgnore (such as file permission
    98  // denied, or errors during .gitignore parsing). See Match below.
    99  func NewRepositoryWithCache(base, file string, cache Cache, errors func(e Error) bool) GitIgnore {
   100  	// do we have an error handler?
   101  	_errors := errors
   102  	if _errors == nil {
   103  		_errors = func(e Error) bool { return true }
   104  	}
   105  
   106  	// extract the absolute path of the base directory
   107  	_base, _err := filepath.Abs(base)
   108  	if _err != nil {
   109  		_errors(NewError(_err, Position{}))
   110  		return nil
   111  	}
   112  
   113  	// ensure the given base is a directory
   114  	_info, _err := os.Stat(_base)
   115  	if _info != nil {
   116  		if !_info.IsDir() {
   117  			_err = InvalidDirectoryError
   118  		}
   119  	}
   120  	if _err != nil {
   121  		_errors(NewError(_err, Position{}))
   122  		return nil
   123  	}
   124  
   125  	// if we haven't been given a base file name, use the default
   126  	if file == "" {
   127  		file = File
   128  	}
   129  
   130  	// are we matching .gitignore files?
   131  	//		- if we are, we also consider $GIT_DIR/info/exclude
   132  	var _exclude GitIgnore
   133  	if file == File {
   134  		_exclude, _err = exclude(_base)
   135  		if _err != nil {
   136  			_errors(NewError(_err, Position{}))
   137  			return nil
   138  		}
   139  	}
   140  
   141  	// create the repository instance
   142  	_ignore := ignore{_base: _base}
   143  	_repository := &repository{
   144  		ignore:   _ignore,
   145  		_errors:  _errors,
   146  		_exclude: _exclude,
   147  		_cache:   cache,
   148  		_file:    file,
   149  	}
   150  
   151  	return _repository
   152  } // NewRepositoryWithCache()
   153  
   154  // Match attempts to match the path against this repository. Matching proceeds
   155  // according to normal gitignore rules, where .gtignore files in the same
   156  // directory as path, take precedence over .gitignore files higher up the
   157  // path hierarchy, and child files and directories are ignored if the parent
   158  // is ignored. If the path is matched by a gitignore pattern in the repository,
   159  // a Match is returned detailing the matched pattern. The returned Match
   160  // can be used to determine if the path should be ignored or included according
   161  // to the repository.
   162  //
   163  // If an error is encountered during matching, the repository error handler
   164  // (if configured via NewRepositoryWithErrors or NewRepositoryWithCache), will
   165  // be called. If the error handler returns false, matching will terminate and
   166  // Match will return nil. If handler returns true, Match will continue
   167  // processing in an attempt to match path.
   168  //
   169  // Match will raise an error and return nil if the absolute path cannot be
   170  // determined, or if its not possible to determine if path represents a file
   171  // or a directory.
   172  //
   173  // If path is not located under the root of this repository, Match returns nil.
   174  func (r *repository) Match(path string) Match {
   175  	// ensure we have the absolute path for the given file
   176  	_path, _err := filepath.Abs(path)
   177  	if _err != nil {
   178  		r._errors(NewError(_err, Position{}))
   179  		return nil
   180  	}
   181  
   182  	// is the path a file or a directory?
   183  	_info, _err := os.Stat(_path)
   184  	if _err != nil {
   185  		r._errors(NewError(_err, Position{}))
   186  		return nil
   187  	}
   188  	_isdir := _info.IsDir()
   189  
   190  	// attempt to match the absolute path
   191  	return r.Absolute(_path, _isdir)
   192  } // Match()
   193  
   194  // Absolute attempts to match an absolute path against this repository. If the
   195  // path is not located under the base directory of this repository, or is not
   196  // matched by this repository, nil is returned.
   197  func (r *repository) Absolute(path string, isdir bool) Match {
   198  	// does the file share the same directory as this ignore file?
   199  	if !strings.HasPrefix(path, r.Base()) {
   200  		return nil
   201  	}
   202  
   203  	// extract the relative path of this file
   204  	_rel, err := filepath.Rel(r.Base(), path)
   205  	if err != nil {
   206  		return nil
   207  	}
   208  
   209  	return r.Relative(_rel, isdir)
   210  } // Absolute()
   211  
   212  // Relative attempts to match a path relative to the repository base directory.
   213  // If the path is not matched by the repository, nil is returned.
   214  func (r *repository) Relative(path string, isdir bool) Match {
   215  	// if there's no path, then there's nothing to match
   216  	_path := filepath.Clean(path)
   217  	if _path == "." {
   218  		return nil
   219  	}
   220  
   221  	// repository matching:
   222  	//		- a child path cannot be considered if its parent is ignored
   223  	//		- a .gitignore in a lower directory overrides a .gitignore in a
   224  	//		  higher directory
   225  
   226  	// first, is the parent directory ignored?
   227  	//		- extract the parent directory from the current path
   228  	_parent, _local := filepath.Split(_path)
   229  	_match := r.Relative(_parent, true)
   230  	if _match != nil {
   231  		if _match.Ignore() {
   232  			return _match
   233  		}
   234  	}
   235  	_parent = filepath.Clean(_parent)
   236  
   237  	// the parent directory isn't ignored, so we now look at the original path
   238  	//		- we consider .gitignore files in the current directory first, then
   239  	//		  move up the path hierarchy
   240  	var _last string
   241  	for {
   242  		_file := filepath.Join(r._base, _parent, r._file)
   243  		_ignore := NewWithCache(_file, r._cache, r._errors)
   244  		if _ignore != nil {
   245  			_match := _ignore.Relative(_local, isdir)
   246  			if _match != nil {
   247  				return _match
   248  			}
   249  		}
   250  
   251  		// if there's no parent, then we're done
   252  		//		- since we use filepath.Clean() we look for "."
   253  		if _parent == "." {
   254  			break
   255  		}
   256  
   257  		// we don't have a match for this file, so we progress up the
   258  		// path hierarchy
   259  		//		- we are manually building _local using the .gitignore
   260  		//		  separator "/", which is how we handle operating system
   261  		//		  file system differences
   262  		_parent, _last = filepath.Split(_parent)
   263  		_parent = filepath.Clean(_parent)
   264  		_local = _last + string(_SEPARATOR) + _local
   265  	}
   266  
   267  	// do we have a global exclude file? (i.e. GIT_DIR/info/exclude)
   268  	if r._exclude != nil {
   269  		return r._exclude.Relative(path, isdir)
   270  	}
   271  
   272  	// we have no match
   273  	return nil
   274  } // Relative()
   275  
   276  // ensure repository satisfies the GitIgnore interface
   277  var _ GitIgnore = &repository{}