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