github.com/Wrdlbrnft/go-gitignore@v0.0.0-20201129201858-74ef740b8b77/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{}