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