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