161
166
Patterns are translated to regular expressions to expidite matching.
163
The regular expressions for multiple patterns are aggregated into
164
a super-regex containing groups of up to 99 patterns.
168
The regular expressions for multiple patterns are aggregated into
169
a super-regex containing groups of up to 99 patterns.
165
170
The 99 limitation is due to the grouping limit of the Python re module.
166
171
The resulting super-regex and associated patterns are stored as a list of
167
172
(regex,[patterns]) in _regex_patterns.
169
174
For performance reasons the patterns are categorised as extension patterns
170
175
(those that match against a file extension), basename patterns
171
176
(those that match against the basename of the filename),
172
177
and fullpath patterns (those that match against the full path).
173
The translations used for extensions and basenames are relatively simpler
178
The translations used for extensions and basenames are relatively simpler
174
179
and therefore faster to perform than the fullpath patterns.
176
Also, the extension patterns are more likely to find a match and
181
Also, the extension patterns are more likely to find a match and
177
182
so are matched first, then the basename patterns, then the fullpath
185
# We want to _add_patterns in a specific order (as per type_list below)
186
# starting with the shortest and going to the longest.
187
# As some Python version don't support ordered dicts the list below is
188
# used to select inputs for _add_pattern in a specific order.
189
pattern_types = [ "extension", "basename", "fullpath" ]
193
"translator" : _sub_extension,
194
"prefix" : r'(?:.*/)?(?!.*/)(?:.*\.)'
197
"translator" : _sub_basename,
198
"prefix" : r'(?:.*/)?(?!.*/)'
201
"translator" : _sub_fullpath,
180
206
def __init__(self, patterns):
181
207
self._regex_patterns = []
185
213
for pat in patterns:
186
214
pat = normalize_pattern(pat)
187
if pat.startswith(u'RE:') or u'/' in pat:
188
path_patterns.append(pat)
189
elif pat.startswith(u'*.'):
190
ext_patterns.append(pat)
192
base_patterns.append(pat)
193
self._add_patterns(ext_patterns,_sub_extension,
194
prefix=r'(?:.*/)?(?!.*/)(?:.*\.)')
195
self._add_patterns(base_patterns,_sub_basename,
196
prefix=r'(?:.*/)?(?!.*/)')
197
self._add_patterns(path_patterns,_sub_fullpath)
215
pattern_lists[Globster.identify(pat)].append(pat)
216
pi = Globster.pattern_info
217
for t in Globster.pattern_types:
218
self._add_patterns(pattern_lists[t], pi[t]["translator"],
199
221
def _add_patterns(self, patterns, translator, prefix=''):
201
grouped_rules = ['(%s)' % translator(pat) for pat in patterns[:99]]
224
'(%s)' % translator(pat) for pat in patterns[:99]]
202
225
joined_rule = '%s(?:%s)$' % (prefix, '|'.join(grouped_rules))
203
self._regex_patterns.append((re.compile(joined_rule, re.UNICODE),
226
# Explicitly use lazy_compile here, because we count on its
227
# nicer error reporting.
228
self._regex_patterns.append((
229
lazy_regex.lazy_compile(joined_rule, re.UNICODE),
205
231
patterns = patterns[99:]
207
233
def match(self, filename):
208
234
"""Searches for a pattern that matches the given filename.
236
:return A matching pattern or None if there is no matching pattern.
239
for regex, patterns in self._regex_patterns:
240
match = regex.match(filename)
242
return patterns[match.lastindex -1]
243
except errors.InvalidPattern, e:
244
# We can't show the default e.msg to the user as thats for
245
# the combined pattern we sent to regex. Instead we indicate to
246
# the user that an ignore file needs fixing.
247
mutter('Invalid pattern found in regex: %s.', e.msg)
248
e.msg = "File ~/.bazaar/ignore or .bzrignore contains error(s)."
250
for _, patterns in self._regex_patterns:
252
if not Globster.is_pattern_valid(p):
253
bad_patterns += ('\n %s' % p)
254
e.msg += bad_patterns
259
def identify(pattern):
260
"""Returns pattern category.
262
:param pattern: normalized pattern.
263
Identify if a pattern is fullpath, basename or extension
264
and returns the appropriate type.
266
if pattern.startswith(u'RE:') or u'/' in pattern:
268
elif pattern.startswith(u'*.'):
274
def is_pattern_valid(pattern):
275
"""Returns True if pattern is valid.
277
:param pattern: Normalized pattern.
278
is_pattern_valid() assumes pattern to be normalized.
279
see: globbing.normalize_pattern
282
translator = Globster.pattern_info[Globster.identify(pattern)]["translator"]
283
tpattern = '(%s)' % translator(pattern)
285
re_obj = lazy_regex.lazy_compile(tpattern, re.UNICODE)
286
re_obj.search("") # force compile
287
except errors.InvalidPattern, e:
292
class ExceptionGlobster(object):
293
"""A Globster that supports exception patterns.
295
Exceptions are ignore patterns prefixed with '!'. Exception
296
patterns take precedence over regular patterns and cause a
297
matching filename to return None from the match() function.
298
Patterns using a '!!' prefix are highest precedence, and act
299
as regular ignores. '!!' patterns are useful to establish ignores
300
that apply under paths specified by '!' exception patterns.
303
def __init__(self,patterns):
304
ignores = [[], [], []]
306
if p.startswith(u'!!'):
307
ignores[2].append(p[2:])
308
elif p.startswith(u'!'):
309
ignores[1].append(p[1:])
312
self._ignores = [Globster(i) for i in ignores]
314
def match(self, filename):
315
"""Searches for a pattern that matches the given filename.
210
317
:return A matching pattern or None if there is no matching pattern.
212
for regex, patterns in self._regex_patterns:
213
match = regex.match(filename)
215
return patterns[match.lastindex -1]
319
double_neg = self._ignores[2].match(filename)
321
return "!!%s" % double_neg
322
elif self._ignores[1].match(filename):
325
return self._ignores[0].match(filename)
219
327
class _OrderedGlobster(Globster):
220
328
"""A Globster that keeps pattern order."""
228
336
self._regex_patterns = []
229
337
for pat in patterns:
230
338
pat = normalize_pattern(pat)
231
if pat.startswith(u'RE:') or u'/' in pat:
232
self._add_patterns([pat], _sub_fullpath)
233
elif pat.startswith(u'*.'):
234
self._add_patterns([pat], _sub_extension,
235
prefix=r'(?:.*/)?(?!.*/)(?:.*\.)')
237
self._add_patterns([pat], _sub_basename,
238
prefix=r'(?:.*/)?(?!.*/)')
339
t = Globster.identify(pat)
340
self._add_patterns([pat], Globster.pattern_info[t]["translator"],
341
Globster.pattern_info[t]["prefix"])
344
_slashes = lazy_regex.lazy_compile(r'[\\/]+')
241
345
def normalize_pattern(pattern):
242
346
"""Converts backslashes in path patterns to forward slashes.
244
348
Doesn't normalize regular expressions - they may contain escapes.
246
if not pattern.startswith('RE:'):
247
pattern = pattern.replace('\\','/')
248
return pattern.rstrip('/')
350
if not (pattern.startswith('RE:') or pattern.startswith('!RE:')):
351
pattern = _slashes.sub('/', pattern)
353
pattern = pattern.rstrip('/')