github.com/xaverkapeller/go-gitignore@v0.0.0-20201129201858-74ef740b8b77/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 return r.Relative(_rel, isdir) 209 } // Absolute() 210 211 // Relative attempts to match a path relative to the repository base directory. 212 // If the path is not matched by the repository, nil is returned. 213 func (r *repository) Relative(path string, isdir bool) Match { 214 // if there's no path, then there's nothing to match 215 _path := filepath.Clean(path) 216 if _path == "." { 217 return nil 218 } 219 220 // repository matching: 221 // - a child path cannot be considered if its parent is ignored 222 // - a .gitignore in a lower directory overrides a .gitignore in a 223 // higher directory 224 225 // first, is the parent directory ignored? 226 // - extract the parent directory from the current path 227 _parent, _local := filepath.Split(_path) 228 _match := r.Relative(_parent, true) 229 if _match != nil { 230 if _match.Ignore() { 231 return _match 232 } 233 } 234 _parent = filepath.Clean(_parent) 235 236 // the parent directory isn't ignored, so we now look at the original path 237 // - we consider .gitignore files in the current directory first, then 238 // move up the path hierarchy 239 var _last string 240 for { 241 _file := filepath.Join(r._base, _parent, r._file) 242 _ignore := NewWithCache(_file, r._cache, r._errors) 243 if _ignore != nil { 244 _match := _ignore.Relative(_local, isdir) 245 if _match != nil { 246 return _match 247 } 248 } 249 250 // if there's no parent, then we're done 251 // - since we use filepath.Clean() we look for "." 252 if _parent == "." { 253 break 254 } 255 256 // we don't have a match for this file, so we progress up the 257 // path hierarchy 258 // - we are manually building _local using the .gitignore 259 // separator "/", which is how we handle operating system 260 // file system differences 261 _parent, _last = filepath.Split(_parent) 262 _parent = filepath.Clean(_parent) 263 _local = _last + string(_SEPARATOR) + _local 264 } 265 266 // do we have a global exclude file? (i.e. GIT_DIR/info/exclude) 267 if r._exclude != nil { 268 return r._exclude.Relative(path, isdir) 269 } 270 271 // we have no match 272 return nil 273 } // Relative() 274 275 // ensure repository satisfies the GitIgnore interface 276 var _ GitIgnore = &repository{}