-
Notifications
You must be signed in to change notification settings - Fork 59
/
jujuignore.go
285 lines (237 loc) · 7.16 KB
/
jujuignore.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
278
279
280
281
282
283
284
285
// Copyright 2019 Canonical Ltd.
// Licensed under the LGPLv3, see LICENCE file for details.
package charm
import (
"bufio"
"io"
"strings"
"unicode"
"github.com/juju/errors"
"gopkg.in/gobwas/glob.v0"
)
var (
ignorePatternReplacer = strings.NewReplacer(
"\\#", "#",
"\\!", "!",
"\\ ", " ",
)
)
type ruleResult uint8
const (
// ruleResultKeep indicates that a file did not match an ignore
// rule and should be copied.
ruleResultKeep ruleResult = iota
// ruleResultSkip indicates that a file matched an ignore rule and
// should not be copied.
ruleResultSkip
// ruleResultKeep indicates that a file matched an inverted ignore rule
// and should be copied.
ruleResultForceKeep
)
type ignoreRuleEvalFn func(path string, isDir bool) ruleResult
// ignoreOnlyDirs constructs a ignoreRuleEvalFn that always returns
// ruleResultKeep for input paths that are not directories or the result of
// evaluating r for directory paths.
func ignoreOnlyDirs(r ignoreRuleEvalFn) ignoreRuleEvalFn {
return func(path string, isDir bool) ruleResult {
if !isDir {
return ruleResultKeep
}
return r(path, isDir)
}
}
// negateIgnoreRule constructs a ignoreRuleEvalFn that returns
// ruleResultForceKeep if r evaluates to ruleResultSkip. This function enables
// the construction of negateed ignore rules that force-include a file even
// though it was previously excluded by another rule.
func negateIgnoreRule(r ignoreRuleEvalFn) ignoreRuleEvalFn {
return func(path string, isDir bool) ruleResult {
if res := r(path, isDir); res == ruleResultSkip {
return ruleResultForceKeep
}
return ruleResultKeep
}
}
// ignoreGlobMatch constructs a ignoreRuleEvalFn that returns ruleResultSkip
// when the input matches any of the provided glob patterns. If an invalid glob
// pattern is provided then ignoreGlobMatch returns an error.
func ignoreGlobMatch(pattern string) (ignoreRuleEvalFn, error) {
var (
err error
expandedPatterns = genIgnorePatternPermutations(pattern)
globPats = make([]glob.Glob, len(expandedPatterns))
)
for i, pat := range expandedPatterns {
globPats[i], err = glob.Compile(pat, '/')
if err != nil {
return nil, err
}
}
return func(path string, isDir bool) ruleResult {
for _, globPat := range globPats {
if globPat.Match(path) {
return ruleResultSkip
}
}
return ruleResultKeep
}, nil
}
type ignoreRuleset []ignoreRuleEvalFn
// newIgnoreRuleset reads the contents of a .jujuignore file from r and returns
// back an ignoreRuleset that can be used to match files against the set of
// exclusion rules.
//
// .jujuignore files use the same syntax as .gitignore files. For more details
// see: https://git-scm.com/docs/gitignore#_pattern_format
func newIgnoreRuleset(r io.Reader) (ignoreRuleset, error) {
var (
lineNo int
rs ignoreRuleset
s = bufio.NewScanner(r)
)
for s.Scan() {
lineNo++
// Cleanup leading whitespace; ignore empty and comment lines
rule := strings.TrimLeftFunc(s.Text(), unicode.IsSpace)
if len(rule) == 0 || rule[0] == '#' {
continue
}
r, err := compileIgnoreRule(rule)
if err != nil {
return nil, errors.Annotatef(err, "[line %d]", lineNo)
}
rs = append(rs, r)
}
if err := s.Err(); err != nil {
return nil, err
}
return rs, nil
}
// Match returns true if path matches any of the ignore rules in the set.
func (rs ignoreRuleset) Match(path string, isDir bool) bool {
// To properly support start-of-pathname patterns all paths must
// begin with a /
if len(path) > 0 && path[0] != '/' {
path = "/" + path
}
var keep = true
for _, r := range rs {
switch r(path, isDir) {
case ruleResultKeep:
// Keep file unless already excluded
if !keep {
continue
}
keep = true
case ruleResultSkip:
keep = false
case ruleResultForceKeep:
// Keep file even if already excluded (inverted rule)
keep = true
}
}
return !keep
}
// compileIgnoreRule returns an ignoreRuleEvalFn for the provided rule.
func compileIgnoreRule(rule string) (ignoreRuleEvalFn, error) {
var (
negateRule bool
applyToDirOnly bool
)
// If the rule begins with a '!' then the pattern is negated; any
// matching file excluded by a previous pattern will become included
// again.
if strings.HasPrefix(rule, "!") {
rule = strings.TrimPrefix(rule, "!")
negateRule = true
}
rule = unescapeIgnorePattern(rule)
// If the rule ends in a '/' then the slash is stripped off but the
// rule will only apply to directories.
if strings.HasSuffix(rule, "/") {
rule = strings.TrimSuffix(rule, "/")
applyToDirOnly = true
}
// A leading "**" followed by a slash means match in all directories.
// "**/foo" is equivalent to "foo/bar" so we can actually trim it.
if strings.HasPrefix(rule, "**/") {
rule = strings.TrimPrefix(rule, "**/")
}
// A leading slash matches the beginning of the pathname. For example,
// "/*.go" matches "foo.go" but not "bar/foo.go". In all other cases
// the pattern applies at any location (substring pattern) and we need
// to prefix it with "**/" (** behaves like * but also matches path
// separators)
if !strings.HasPrefix(rule, "/") {
rule = "**/" + rule
}
fn, err := ignoreGlobMatch(rule)
if err != nil {
return nil, err
}
if applyToDirOnly {
fn = ignoreOnlyDirs(fn)
}
if negateRule {
fn = negateIgnoreRule(fn)
}
return fn, nil
}
// unescapeIgnorePattern removes unescaped trailing spaces and unescapes spaces,
// hashes and bang characters in pattern.
func unescapeIgnorePattern(pattern string) string {
// Trim trailing spaces, unless they are escaped with a backslash
for index := len(pattern) - 1; index > 0 && pattern[index] == ' '; index-- {
if pattern[index-1] != '\\' {
pattern = pattern[:index]
}
}
// Unescape supported characters
return ignorePatternReplacer.Replace(pattern)
}
// genIgnorePatternPermutations receives as input a string possibly containing
// one or more double-star separator patterns (/**/) and generates a list of
// additional glob patterns that allow matching zero-or-more items at the
// location of each double-star separator.
//
// For example, given "foo/**/bar/**/baz" as input, this function returns:
// - foo/**/bar/**/baz
// - foo/bar/**/baz
// - foo/**/bar/baz
// - foo/bar/baz
func genIgnorePatternPermutations(in string) []string {
var (
out []string
remaining = []string{in}
addedPatternWithoutStars bool
)
for len(remaining) != 0 {
next := remaining[0]
remaining = remaining[1:]
// Split on the double-star separator; stop if no the pattern
// does not contain any more double-star separators.
parts := strings.Split(next, "/**/")
if len(parts) == 1 {
if !addedPatternWithoutStars {
out = append(out, next)
addedPatternWithoutStars = true
}
continue
}
// Push next to the the out list and append a list of patterns
// to the remain list for the next run by sequentially
// substituting each double star pattern with a slash. For
// example if next is "a/**/b/**c" the generated patterns will
// be:
// - a/b/**/c
// - a/**/b/c
out = append(out, next)
for i := 1; i < len(parts); i++ {
remaining = append(
remaining,
strings.Join(parts[:i], "/**/")+"/"+strings.Join(parts[i:], "/**/"),
)
}
}
return out
}