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{}