##// END OF EJS Templates
match: simplify the regexps created for glob patterns...
Valentin Gatien-Baron -
r43130:cf165e06 default draft
parent child Browse files
Show More
@@ -1,1526 +1,1531
1 1 # match.py - filename matching
2 2 #
3 3 # Copyright 2008, 2009 Matt Mackall <mpm@selenic.com> and others
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import, print_function
9 9
10 10 import copy
11 11 import itertools
12 12 import os
13 13 import re
14 14
15 15 from .i18n import _
16 16 from . import (
17 17 encoding,
18 18 error,
19 19 pathutil,
20 20 policy,
21 21 pycompat,
22 22 util,
23 23 )
24 24 from .utils import (
25 25 stringutil,
26 26 )
27 27
28 28 rustmod = policy.importrust(r'filepatterns')
29 29
30 30 allpatternkinds = ('re', 'glob', 'path', 'relglob', 'relpath', 'relre',
31 31 'rootglob',
32 32 'listfile', 'listfile0', 'set', 'include', 'subinclude',
33 33 'rootfilesin')
34 34 cwdrelativepatternkinds = ('relpath', 'glob')
35 35
36 36 propertycache = util.propertycache
37 37
38 38 def _rematcher(regex):
39 39 '''compile the regexp with the best available regexp engine and return a
40 40 matcher function'''
41 41 m = util.re.compile(regex)
42 42 try:
43 43 # slightly faster, provided by facebook's re2 bindings
44 44 return m.test_match
45 45 except AttributeError:
46 46 return m.match
47 47
48 48 def _expandsets(kindpats, ctx=None, listsubrepos=False, badfn=None):
49 49 '''Returns the kindpats list with the 'set' patterns expanded to matchers'''
50 50 matchers = []
51 51 other = []
52 52
53 53 for kind, pat, source in kindpats:
54 54 if kind == 'set':
55 55 if ctx is None:
56 56 raise error.ProgrammingError("fileset expression with no "
57 57 "context")
58 58 matchers.append(ctx.matchfileset(pat, badfn=badfn))
59 59
60 60 if listsubrepos:
61 61 for subpath in ctx.substate:
62 62 sm = ctx.sub(subpath).matchfileset(pat, badfn=badfn)
63 63 pm = prefixdirmatcher(subpath, sm, badfn=badfn)
64 64 matchers.append(pm)
65 65
66 66 continue
67 67 other.append((kind, pat, source))
68 68 return matchers, other
69 69
70 70 def _expandsubinclude(kindpats, root):
71 71 '''Returns the list of subinclude matcher args and the kindpats without the
72 72 subincludes in it.'''
73 73 relmatchers = []
74 74 other = []
75 75
76 76 for kind, pat, source in kindpats:
77 77 if kind == 'subinclude':
78 78 sourceroot = pathutil.dirname(util.normpath(source))
79 79 pat = util.pconvert(pat)
80 80 path = pathutil.join(sourceroot, pat)
81 81
82 82 newroot = pathutil.dirname(path)
83 83 matcherargs = (newroot, '', [], ['include:%s' % path])
84 84
85 85 prefix = pathutil.canonpath(root, root, newroot)
86 86 if prefix:
87 87 prefix += '/'
88 88 relmatchers.append((prefix, matcherargs))
89 89 else:
90 90 other.append((kind, pat, source))
91 91
92 92 return relmatchers, other
93 93
94 94 def _kindpatsalwaysmatch(kindpats):
95 95 """"Checks whether the kindspats match everything, as e.g.
96 96 'relpath:.' does.
97 97 """
98 98 for kind, pat, source in kindpats:
99 99 if pat != '' or kind not in ['relpath', 'glob']:
100 100 return False
101 101 return True
102 102
103 103 def _buildkindpatsmatcher(matchercls, root, kindpats, ctx=None,
104 104 listsubrepos=False, badfn=None):
105 105 matchers = []
106 106 fms, kindpats = _expandsets(kindpats, ctx=ctx,
107 107 listsubrepos=listsubrepos, badfn=badfn)
108 108 if kindpats:
109 109 m = matchercls(root, kindpats, badfn=badfn)
110 110 matchers.append(m)
111 111 if fms:
112 112 matchers.extend(fms)
113 113 if not matchers:
114 114 return nevermatcher(badfn=badfn)
115 115 if len(matchers) == 1:
116 116 return matchers[0]
117 117 return unionmatcher(matchers)
118 118
119 119 def match(root, cwd, patterns=None, include=None, exclude=None, default='glob',
120 120 auditor=None, ctx=None, listsubrepos=False, warn=None,
121 121 badfn=None, icasefs=False):
122 122 r"""build an object to match a set of file patterns
123 123
124 124 arguments:
125 125 root - the canonical root of the tree you're matching against
126 126 cwd - the current working directory, if relevant
127 127 patterns - patterns to find
128 128 include - patterns to include (unless they are excluded)
129 129 exclude - patterns to exclude (even if they are included)
130 130 default - if a pattern in patterns has no explicit type, assume this one
131 131 auditor - optional path auditor
132 132 ctx - optional changecontext
133 133 listsubrepos - if True, recurse into subrepositories
134 134 warn - optional function used for printing warnings
135 135 badfn - optional bad() callback for this matcher instead of the default
136 136 icasefs - make a matcher for wdir on case insensitive filesystems, which
137 137 normalizes the given patterns to the case in the filesystem
138 138
139 139 a pattern is one of:
140 140 'glob:<glob>' - a glob relative to cwd
141 141 're:<regexp>' - a regular expression
142 142 'path:<path>' - a path relative to repository root, which is matched
143 143 recursively
144 144 'rootfilesin:<path>' - a path relative to repository root, which is
145 145 matched non-recursively (will not match subdirectories)
146 146 'relglob:<glob>' - an unrooted glob (*.c matches C files in all dirs)
147 147 'relpath:<path>' - a path relative to cwd
148 148 'relre:<regexp>' - a regexp that needn't match the start of a name
149 149 'set:<fileset>' - a fileset expression
150 150 'include:<path>' - a file of patterns to read and include
151 151 'subinclude:<path>' - a file of patterns to match against files under
152 152 the same directory
153 153 '<something>' - a pattern of the specified default type
154 154
155 155 Usually a patternmatcher is returned:
156 156 >>> match(b'foo', b'.', [b're:.*\.c$', b'path:foo/a', b'*.py'])
157 157 <patternmatcher patterns='.*\\.c$|foo/a(?:/|$)|[^/]*\\.py$'>
158 158
159 159 Combining 'patterns' with 'include' (resp. 'exclude') gives an
160 160 intersectionmatcher (resp. a differencematcher):
161 161 >>> type(match(b'foo', b'.', [b're:.*\.c$'], include=[b'path:lib']))
162 162 <class 'mercurial.match.intersectionmatcher'>
163 163 >>> type(match(b'foo', b'.', [b're:.*\.c$'], exclude=[b'path:build']))
164 164 <class 'mercurial.match.differencematcher'>
165 165
166 166 Notice that, if 'patterns' is empty, an alwaysmatcher is returned:
167 167 >>> match(b'foo', b'.', [])
168 168 <alwaysmatcher>
169 169
170 170 The 'default' argument determines which kind of pattern is assumed if a
171 171 pattern has no prefix:
172 172 >>> match(b'foo', b'.', [b'.*\.c$'], default=b're')
173 173 <patternmatcher patterns='.*\\.c$'>
174 174 >>> match(b'foo', b'.', [b'main.py'], default=b'relpath')
175 175 <patternmatcher patterns='main\\.py(?:/|$)'>
176 176 >>> match(b'foo', b'.', [b'main.py'], default=b're')
177 177 <patternmatcher patterns='main.py'>
178 178
179 179 The primary use of matchers is to check whether a value (usually a file
180 180 name) matches againset one of the patterns given at initialization. There
181 181 are two ways of doing this check.
182 182
183 183 >>> m = match(b'foo', b'', [b're:.*\.c$', b'relpath:a'])
184 184
185 185 1. Calling the matcher with a file name returns True if any pattern
186 186 matches that file name:
187 187 >>> m(b'a')
188 188 True
189 189 >>> m(b'main.c')
190 190 True
191 191 >>> m(b'test.py')
192 192 False
193 193
194 194 2. Using the exact() method only returns True if the file name matches one
195 195 of the exact patterns (i.e. not re: or glob: patterns):
196 196 >>> m.exact(b'a')
197 197 True
198 198 >>> m.exact(b'main.c')
199 199 False
200 200 """
201 201 normalize = _donormalize
202 202 if icasefs:
203 203 dirstate = ctx.repo().dirstate
204 204 dsnormalize = dirstate.normalize
205 205
206 206 def normalize(patterns, default, root, cwd, auditor, warn):
207 207 kp = _donormalize(patterns, default, root, cwd, auditor, warn)
208 208 kindpats = []
209 209 for kind, pats, source in kp:
210 210 if kind not in ('re', 'relre'): # regex can't be normalized
211 211 p = pats
212 212 pats = dsnormalize(pats)
213 213
214 214 # Preserve the original to handle a case only rename.
215 215 if p != pats and p in dirstate:
216 216 kindpats.append((kind, p, source))
217 217
218 218 kindpats.append((kind, pats, source))
219 219 return kindpats
220 220
221 221 if patterns:
222 222 kindpats = normalize(patterns, default, root, cwd, auditor, warn)
223 223 if _kindpatsalwaysmatch(kindpats):
224 224 m = alwaysmatcher(badfn)
225 225 else:
226 226 m = _buildkindpatsmatcher(patternmatcher, root, kindpats, ctx=ctx,
227 227 listsubrepos=listsubrepos, badfn=badfn)
228 228 else:
229 229 # It's a little strange that no patterns means to match everything.
230 230 # Consider changing this to match nothing (probably using nevermatcher).
231 231 m = alwaysmatcher(badfn)
232 232
233 233 if include:
234 234 kindpats = normalize(include, 'glob', root, cwd, auditor, warn)
235 235 im = _buildkindpatsmatcher(includematcher, root, kindpats, ctx=ctx,
236 236 listsubrepos=listsubrepos, badfn=None)
237 237 m = intersectmatchers(m, im)
238 238 if exclude:
239 239 kindpats = normalize(exclude, 'glob', root, cwd, auditor, warn)
240 240 em = _buildkindpatsmatcher(includematcher, root, kindpats, ctx=ctx,
241 241 listsubrepos=listsubrepos, badfn=None)
242 242 m = differencematcher(m, em)
243 243 return m
244 244
245 245 def exact(files, badfn=None):
246 246 return exactmatcher(files, badfn=badfn)
247 247
248 248 def always(badfn=None):
249 249 return alwaysmatcher(badfn)
250 250
251 251 def never(badfn=None):
252 252 return nevermatcher(badfn)
253 253
254 254 def badmatch(match, badfn):
255 255 """Make a copy of the given matcher, replacing its bad method with the given
256 256 one.
257 257 """
258 258 m = copy.copy(match)
259 259 m.bad = badfn
260 260 return m
261 261
262 262 def _donormalize(patterns, default, root, cwd, auditor=None, warn=None):
263 263 '''Convert 'kind:pat' from the patterns list to tuples with kind and
264 264 normalized and rooted patterns and with listfiles expanded.'''
265 265 kindpats = []
266 266 for kind, pat in [_patsplit(p, default) for p in patterns]:
267 267 if kind in cwdrelativepatternkinds:
268 268 pat = pathutil.canonpath(root, cwd, pat, auditor=auditor)
269 269 elif kind in ('relglob', 'path', 'rootfilesin', 'rootglob'):
270 270 pat = util.normpath(pat)
271 271 elif kind in ('listfile', 'listfile0'):
272 272 try:
273 273 files = util.readfile(pat)
274 274 if kind == 'listfile0':
275 275 files = files.split('\0')
276 276 else:
277 277 files = files.splitlines()
278 278 files = [f for f in files if f]
279 279 except EnvironmentError:
280 280 raise error.Abort(_("unable to read file list (%s)") % pat)
281 281 for k, p, source in _donormalize(files, default, root, cwd,
282 282 auditor, warn):
283 283 kindpats.append((k, p, pat))
284 284 continue
285 285 elif kind == 'include':
286 286 try:
287 287 fullpath = os.path.join(root, util.localpath(pat))
288 288 includepats = readpatternfile(fullpath, warn)
289 289 for k, p, source in _donormalize(includepats, default,
290 290 root, cwd, auditor, warn):
291 291 kindpats.append((k, p, source or pat))
292 292 except error.Abort as inst:
293 293 raise error.Abort('%s: %s' % (pat, inst[0]))
294 294 except IOError as inst:
295 295 if warn:
296 296 warn(_("skipping unreadable pattern file '%s': %s\n") %
297 297 (pat, stringutil.forcebytestr(inst.strerror)))
298 298 continue
299 299 # else: re or relre - which cannot be normalized
300 300 kindpats.append((kind, pat, ''))
301 301 return kindpats
302 302
303 303 class basematcher(object):
304 304
305 305 def __init__(self, badfn=None):
306 306 if badfn is not None:
307 307 self.bad = badfn
308 308
309 309 def __call__(self, fn):
310 310 return self.matchfn(fn)
311 311 # Callbacks related to how the matcher is used by dirstate.walk.
312 312 # Subscribers to these events must monkeypatch the matcher object.
313 313 def bad(self, f, msg):
314 314 '''Callback from dirstate.walk for each explicit file that can't be
315 315 found/accessed, with an error message.'''
316 316
317 317 # If an explicitdir is set, it will be called when an explicitly listed
318 318 # directory is visited.
319 319 explicitdir = None
320 320
321 321 # If an traversedir is set, it will be called when a directory discovered
322 322 # by recursive traversal is visited.
323 323 traversedir = None
324 324
325 325 @propertycache
326 326 def _files(self):
327 327 return []
328 328
329 329 def files(self):
330 330 '''Explicitly listed files or patterns or roots:
331 331 if no patterns or .always(): empty list,
332 332 if exact: list exact files,
333 333 if not .anypats(): list all files and dirs,
334 334 else: optimal roots'''
335 335 return self._files
336 336
337 337 @propertycache
338 338 def _fileset(self):
339 339 return set(self._files)
340 340
341 341 def exact(self, f):
342 342 '''Returns True if f is in .files().'''
343 343 return f in self._fileset
344 344
345 345 def matchfn(self, f):
346 346 return False
347 347
348 348 def visitdir(self, dir):
349 349 '''Decides whether a directory should be visited based on whether it
350 350 has potential matches in it or one of its subdirectories. This is
351 351 based on the match's primary, included, and excluded patterns.
352 352
353 353 Returns the string 'all' if the given directory and all subdirectories
354 354 should be visited. Otherwise returns True or False indicating whether
355 355 the given directory should be visited.
356 356 '''
357 357 return True
358 358
359 359 def visitchildrenset(self, dir):
360 360 '''Decides whether a directory should be visited based on whether it
361 361 has potential matches in it or one of its subdirectories, and
362 362 potentially lists which subdirectories of that directory should be
363 363 visited. This is based on the match's primary, included, and excluded
364 364 patterns.
365 365
366 366 This function is very similar to 'visitdir', and the following mapping
367 367 can be applied:
368 368
369 369 visitdir | visitchildrenlist
370 370 ----------+-------------------
371 371 False | set()
372 372 'all' | 'all'
373 373 True | 'this' OR non-empty set of subdirs -or files- to visit
374 374
375 375 Example:
376 376 Assume matchers ['path:foo/bar', 'rootfilesin:qux'], we would return
377 377 the following values (assuming the implementation of visitchildrenset
378 378 is capable of recognizing this; some implementations are not).
379 379
380 380 '' -> {'foo', 'qux'}
381 381 'baz' -> set()
382 382 'foo' -> {'bar'}
383 383 # Ideally this would be 'all', but since the prefix nature of matchers
384 384 # is applied to the entire matcher, we have to downgrade this to
385 385 # 'this' due to the non-prefix 'rootfilesin'-kind matcher being mixed
386 386 # in.
387 387 'foo/bar' -> 'this'
388 388 'qux' -> 'this'
389 389
390 390 Important:
391 391 Most matchers do not know if they're representing files or
392 392 directories. They see ['path:dir/f'] and don't know whether 'f' is a
393 393 file or a directory, so visitchildrenset('dir') for most matchers will
394 394 return {'f'}, but if the matcher knows it's a file (like exactmatcher
395 395 does), it may return 'this'. Do not rely on the return being a set
396 396 indicating that there are no files in this dir to investigate (or
397 397 equivalently that if there are files to investigate in 'dir' that it
398 398 will always return 'this').
399 399 '''
400 400 return 'this'
401 401
402 402 def always(self):
403 403 '''Matcher will match everything and .files() will be empty --
404 404 optimization might be possible.'''
405 405 return False
406 406
407 407 def isexact(self):
408 408 '''Matcher will match exactly the list of files in .files() --
409 409 optimization might be possible.'''
410 410 return False
411 411
412 412 def prefix(self):
413 413 '''Matcher will match the paths in .files() recursively --
414 414 optimization might be possible.'''
415 415 return False
416 416
417 417 def anypats(self):
418 418 '''None of .always(), .isexact(), and .prefix() is true --
419 419 optimizations will be difficult.'''
420 420 return not self.always() and not self.isexact() and not self.prefix()
421 421
422 422 class alwaysmatcher(basematcher):
423 423 '''Matches everything.'''
424 424
425 425 def __init__(self, badfn=None):
426 426 super(alwaysmatcher, self).__init__(badfn)
427 427
428 428 def always(self):
429 429 return True
430 430
431 431 def matchfn(self, f):
432 432 return True
433 433
434 434 def visitdir(self, dir):
435 435 return 'all'
436 436
437 437 def visitchildrenset(self, dir):
438 438 return 'all'
439 439
440 440 def __repr__(self):
441 441 return r'<alwaysmatcher>'
442 442
443 443 class nevermatcher(basematcher):
444 444 '''Matches nothing.'''
445 445
446 446 def __init__(self, badfn=None):
447 447 super(nevermatcher, self).__init__(badfn)
448 448
449 449 # It's a little weird to say that the nevermatcher is an exact matcher
450 450 # or a prefix matcher, but it seems to make sense to let callers take
451 451 # fast paths based on either. There will be no exact matches, nor any
452 452 # prefixes (files() returns []), so fast paths iterating over them should
453 453 # be efficient (and correct).
454 454 def isexact(self):
455 455 return True
456 456
457 457 def prefix(self):
458 458 return True
459 459
460 460 def visitdir(self, dir):
461 461 return False
462 462
463 463 def visitchildrenset(self, dir):
464 464 return set()
465 465
466 466 def __repr__(self):
467 467 return r'<nevermatcher>'
468 468
469 469 class predicatematcher(basematcher):
470 470 """A matcher adapter for a simple boolean function"""
471 471
472 472 def __init__(self, predfn, predrepr=None, badfn=None):
473 473 super(predicatematcher, self).__init__(badfn)
474 474 self.matchfn = predfn
475 475 self._predrepr = predrepr
476 476
477 477 @encoding.strmethod
478 478 def __repr__(self):
479 479 s = (stringutil.buildrepr(self._predrepr)
480 480 or pycompat.byterepr(self.matchfn))
481 481 return '<predicatenmatcher pred=%s>' % s
482 482
483 483 def normalizerootdir(dir, funcname):
484 484 if dir == '.':
485 485 util.nouideprecwarn("match.%s() no longer accepts "
486 486 "'.', use '' instead." % funcname, '5.1')
487 487 return ''
488 488 return dir
489 489
490 490
491 491 class patternmatcher(basematcher):
492 492 """Matches a set of (kind, pat, source) against a 'root' directory.
493 493
494 494 >>> kindpats = [
495 495 ... (b're', br'.*\.c$', b''),
496 496 ... (b'path', b'foo/a', b''),
497 497 ... (b'relpath', b'b', b''),
498 498 ... (b'glob', b'*.h', b''),
499 499 ... ]
500 500 >>> m = patternmatcher(b'foo', kindpats)
501 501 >>> m(b'main.c') # matches re:.*\.c$
502 502 True
503 503 >>> m(b'b.txt')
504 504 False
505 505 >>> m(b'foo/a') # matches path:foo/a
506 506 True
507 507 >>> m(b'a') # does not match path:b, since 'root' is 'foo'
508 508 False
509 509 >>> m(b'b') # matches relpath:b, since 'root' is 'foo'
510 510 True
511 511 >>> m(b'lib.h') # matches glob:*.h
512 512 True
513 513
514 514 >>> m.files()
515 515 ['', 'foo/a', 'b', '']
516 516 >>> m.exact(b'foo/a')
517 517 True
518 518 >>> m.exact(b'b')
519 519 True
520 520 >>> m.exact(b'lib.h') # exact matches are for (rel)path kinds
521 521 False
522 522 """
523 523
524 524 def __init__(self, root, kindpats, badfn=None):
525 525 super(patternmatcher, self).__init__(badfn)
526 526
527 527 self._files = _explicitfiles(kindpats)
528 528 self._prefix = _prefix(kindpats)
529 529 self._pats, self.matchfn = _buildmatch(kindpats, '$', root)
530 530
531 531 @propertycache
532 532 def _dirs(self):
533 533 return set(util.dirs(self._fileset))
534 534
535 535 def visitdir(self, dir):
536 536 dir = normalizerootdir(dir, 'visitdir')
537 537 if self._prefix and dir in self._fileset:
538 538 return 'all'
539 539 return (dir in self._fileset or
540 540 dir in self._dirs or
541 541 any(parentdir in self._fileset
542 542 for parentdir in util.finddirs(dir)))
543 543
544 544 def visitchildrenset(self, dir):
545 545 ret = self.visitdir(dir)
546 546 if ret is True:
547 547 return 'this'
548 548 elif not ret:
549 549 return set()
550 550 assert ret == 'all'
551 551 return 'all'
552 552
553 553 def prefix(self):
554 554 return self._prefix
555 555
556 556 @encoding.strmethod
557 557 def __repr__(self):
558 558 return ('<patternmatcher patterns=%r>' % pycompat.bytestr(self._pats))
559 559
560 560 # This is basically a reimplementation of util.dirs that stores the children
561 561 # instead of just a count of them, plus a small optional optimization to avoid
562 562 # some directories we don't need.
563 563 class _dirchildren(object):
564 564 def __init__(self, paths, onlyinclude=None):
565 565 self._dirs = {}
566 566 self._onlyinclude = onlyinclude or []
567 567 addpath = self.addpath
568 568 for f in paths:
569 569 addpath(f)
570 570
571 571 def addpath(self, path):
572 572 if path == '':
573 573 return
574 574 dirs = self._dirs
575 575 findsplitdirs = _dirchildren._findsplitdirs
576 576 for d, b in findsplitdirs(path):
577 577 if d not in self._onlyinclude:
578 578 continue
579 579 dirs.setdefault(d, set()).add(b)
580 580
581 581 @staticmethod
582 582 def _findsplitdirs(path):
583 583 # yields (dirname, basename) tuples, walking back to the root. This is
584 584 # very similar to util.finddirs, except:
585 585 # - produces a (dirname, basename) tuple, not just 'dirname'
586 586 # Unlike manifest._splittopdir, this does not suffix `dirname` with a
587 587 # slash.
588 588 oldpos = len(path)
589 589 pos = path.rfind('/')
590 590 while pos != -1:
591 591 yield path[:pos], path[pos + 1:oldpos]
592 592 oldpos = pos
593 593 pos = path.rfind('/', 0, pos)
594 594 yield '', path[:oldpos]
595 595
596 596 def get(self, path):
597 597 return self._dirs.get(path, set())
598 598
599 599 class includematcher(basematcher):
600 600
601 601 def __init__(self, root, kindpats, badfn=None):
602 602 super(includematcher, self).__init__(badfn)
603 603
604 604 self._pats, self.matchfn = _buildmatch(kindpats, '(?:/|$)', root)
605 605 self._prefix = _prefix(kindpats)
606 606 roots, dirs, parents = _rootsdirsandparents(kindpats)
607 607 # roots are directories which are recursively included.
608 608 self._roots = set(roots)
609 609 # dirs are directories which are non-recursively included.
610 610 self._dirs = set(dirs)
611 611 # parents are directories which are non-recursively included because
612 612 # they are needed to get to items in _dirs or _roots.
613 613 self._parents = parents
614 614
615 615 def visitdir(self, dir):
616 616 dir = normalizerootdir(dir, 'visitdir')
617 617 if self._prefix and dir in self._roots:
618 618 return 'all'
619 619 return (dir in self._roots or
620 620 dir in self._dirs or
621 621 dir in self._parents or
622 622 any(parentdir in self._roots
623 623 for parentdir in util.finddirs(dir)))
624 624
625 625 @propertycache
626 626 def _allparentschildren(self):
627 627 # It may seem odd that we add dirs, roots, and parents, and then
628 628 # restrict to only parents. This is to catch the case of:
629 629 # dirs = ['foo/bar']
630 630 # parents = ['foo']
631 631 # if we asked for the children of 'foo', but had only added
632 632 # self._parents, we wouldn't be able to respond ['bar'].
633 633 return _dirchildren(
634 634 itertools.chain(self._dirs, self._roots, self._parents),
635 635 onlyinclude=self._parents)
636 636
637 637 def visitchildrenset(self, dir):
638 638 if self._prefix and dir in self._roots:
639 639 return 'all'
640 640 # Note: this does *not* include the 'dir in self._parents' case from
641 641 # visitdir, that's handled below.
642 642 if ('' in self._roots or
643 643 dir in self._roots or
644 644 dir in self._dirs or
645 645 any(parentdir in self._roots
646 646 for parentdir in util.finddirs(dir))):
647 647 return 'this'
648 648
649 649 if dir in self._parents:
650 650 return self._allparentschildren.get(dir) or set()
651 651 return set()
652 652
653 653 @encoding.strmethod
654 654 def __repr__(self):
655 655 return ('<includematcher includes=%r>' % pycompat.bytestr(self._pats))
656 656
657 657 class exactmatcher(basematcher):
658 658 r'''Matches the input files exactly. They are interpreted as paths, not
659 659 patterns (so no kind-prefixes).
660 660
661 661 >>> m = exactmatcher([b'a.txt', br're:.*\.c$'])
662 662 >>> m(b'a.txt')
663 663 True
664 664 >>> m(b'b.txt')
665 665 False
666 666
667 667 Input files that would be matched are exactly those returned by .files()
668 668 >>> m.files()
669 669 ['a.txt', 're:.*\\.c$']
670 670
671 671 So pattern 're:.*\.c$' is not considered as a regex, but as a file name
672 672 >>> m(b'main.c')
673 673 False
674 674 >>> m(br're:.*\.c$')
675 675 True
676 676 '''
677 677
678 678 def __init__(self, files, badfn=None):
679 679 super(exactmatcher, self).__init__(badfn)
680 680
681 681 if isinstance(files, list):
682 682 self._files = files
683 683 else:
684 684 self._files = list(files)
685 685
686 686 matchfn = basematcher.exact
687 687
688 688 @propertycache
689 689 def _dirs(self):
690 690 return set(util.dirs(self._fileset))
691 691
692 692 def visitdir(self, dir):
693 693 dir = normalizerootdir(dir, 'visitdir')
694 694 return dir in self._dirs
695 695
696 696 def visitchildrenset(self, dir):
697 697 dir = normalizerootdir(dir, 'visitchildrenset')
698 698
699 699 if not self._fileset or dir not in self._dirs:
700 700 return set()
701 701
702 702 candidates = self._fileset | self._dirs - {''}
703 703 if dir != '':
704 704 d = dir + '/'
705 705 candidates = set(c[len(d):] for c in candidates if
706 706 c.startswith(d))
707 707 # self._dirs includes all of the directories, recursively, so if
708 708 # we're attempting to match foo/bar/baz.txt, it'll have '', 'foo',
709 709 # 'foo/bar' in it. Thus we can safely ignore a candidate that has a
710 710 # '/' in it, indicating a it's for a subdir-of-a-subdir; the
711 711 # immediate subdir will be in there without a slash.
712 712 ret = {c for c in candidates if '/' not in c}
713 713 # We really do not expect ret to be empty, since that would imply that
714 714 # there's something in _dirs that didn't have a file in _fileset.
715 715 assert ret
716 716 return ret
717 717
718 718 def isexact(self):
719 719 return True
720 720
721 721 @encoding.strmethod
722 722 def __repr__(self):
723 723 return ('<exactmatcher files=%r>' % self._files)
724 724
725 725 class differencematcher(basematcher):
726 726 '''Composes two matchers by matching if the first matches and the second
727 727 does not.
728 728
729 729 The second matcher's non-matching-attributes (bad, explicitdir,
730 730 traversedir) are ignored.
731 731 '''
732 732 def __init__(self, m1, m2):
733 733 super(differencematcher, self).__init__()
734 734 self._m1 = m1
735 735 self._m2 = m2
736 736 self.bad = m1.bad
737 737 self.explicitdir = m1.explicitdir
738 738 self.traversedir = m1.traversedir
739 739
740 740 def matchfn(self, f):
741 741 return self._m1(f) and not self._m2(f)
742 742
743 743 @propertycache
744 744 def _files(self):
745 745 if self.isexact():
746 746 return [f for f in self._m1.files() if self(f)]
747 747 # If m1 is not an exact matcher, we can't easily figure out the set of
748 748 # files, because its files() are not always files. For example, if
749 749 # m1 is "path:dir" and m2 is "rootfileins:.", we don't
750 750 # want to remove "dir" from the set even though it would match m2,
751 751 # because the "dir" in m1 may not be a file.
752 752 return self._m1.files()
753 753
754 754 def visitdir(self, dir):
755 755 if self._m2.visitdir(dir) == 'all':
756 756 return False
757 757 elif not self._m2.visitdir(dir):
758 758 # m2 does not match dir, we can return 'all' here if possible
759 759 return self._m1.visitdir(dir)
760 760 return bool(self._m1.visitdir(dir))
761 761
762 762 def visitchildrenset(self, dir):
763 763 m2_set = self._m2.visitchildrenset(dir)
764 764 if m2_set == 'all':
765 765 return set()
766 766 m1_set = self._m1.visitchildrenset(dir)
767 767 # Possible values for m1: 'all', 'this', set(...), set()
768 768 # Possible values for m2: 'this', set(...), set()
769 769 # If m2 has nothing under here that we care about, return m1, even if
770 770 # it's 'all'. This is a change in behavior from visitdir, which would
771 771 # return True, not 'all', for some reason.
772 772 if not m2_set:
773 773 return m1_set
774 774 if m1_set in ['all', 'this']:
775 775 # Never return 'all' here if m2_set is any kind of non-empty (either
776 776 # 'this' or set(foo)), since m2 might return set() for a
777 777 # subdirectory.
778 778 return 'this'
779 779 # Possible values for m1: set(...), set()
780 780 # Possible values for m2: 'this', set(...)
781 781 # We ignore m2's set results. They're possibly incorrect:
782 782 # m1 = path:dir/subdir, m2=rootfilesin:dir, visitchildrenset(''):
783 783 # m1 returns {'dir'}, m2 returns {'dir'}, if we subtracted we'd
784 784 # return set(), which is *not* correct, we still need to visit 'dir'!
785 785 return m1_set
786 786
787 787 def isexact(self):
788 788 return self._m1.isexact()
789 789
790 790 @encoding.strmethod
791 791 def __repr__(self):
792 792 return ('<differencematcher m1=%r, m2=%r>' % (self._m1, self._m2))
793 793
794 794 def intersectmatchers(m1, m2):
795 795 '''Composes two matchers by matching if both of them match.
796 796
797 797 The second matcher's non-matching-attributes (bad, explicitdir,
798 798 traversedir) are ignored.
799 799 '''
800 800 if m1 is None or m2 is None:
801 801 return m1 or m2
802 802 if m1.always():
803 803 m = copy.copy(m2)
804 804 # TODO: Consider encapsulating these things in a class so there's only
805 805 # one thing to copy from m1.
806 806 m.bad = m1.bad
807 807 m.explicitdir = m1.explicitdir
808 808 m.traversedir = m1.traversedir
809 809 return m
810 810 if m2.always():
811 811 m = copy.copy(m1)
812 812 return m
813 813 return intersectionmatcher(m1, m2)
814 814
815 815 class intersectionmatcher(basematcher):
816 816 def __init__(self, m1, m2):
817 817 super(intersectionmatcher, self).__init__()
818 818 self._m1 = m1
819 819 self._m2 = m2
820 820 self.bad = m1.bad
821 821 self.explicitdir = m1.explicitdir
822 822 self.traversedir = m1.traversedir
823 823
824 824 @propertycache
825 825 def _files(self):
826 826 if self.isexact():
827 827 m1, m2 = self._m1, self._m2
828 828 if not m1.isexact():
829 829 m1, m2 = m2, m1
830 830 return [f for f in m1.files() if m2(f)]
831 831 # It neither m1 nor m2 is an exact matcher, we can't easily intersect
832 832 # the set of files, because their files() are not always files. For
833 833 # example, if intersecting a matcher "-I glob:foo.txt" with matcher of
834 834 # "path:dir2", we don't want to remove "dir2" from the set.
835 835 return self._m1.files() + self._m2.files()
836 836
837 837 def matchfn(self, f):
838 838 return self._m1(f) and self._m2(f)
839 839
840 840 def visitdir(self, dir):
841 841 visit1 = self._m1.visitdir(dir)
842 842 if visit1 == 'all':
843 843 return self._m2.visitdir(dir)
844 844 # bool() because visit1=True + visit2='all' should not be 'all'
845 845 return bool(visit1 and self._m2.visitdir(dir))
846 846
847 847 def visitchildrenset(self, dir):
848 848 m1_set = self._m1.visitchildrenset(dir)
849 849 if not m1_set:
850 850 return set()
851 851 m2_set = self._m2.visitchildrenset(dir)
852 852 if not m2_set:
853 853 return set()
854 854
855 855 if m1_set == 'all':
856 856 return m2_set
857 857 elif m2_set == 'all':
858 858 return m1_set
859 859
860 860 if m1_set == 'this' or m2_set == 'this':
861 861 return 'this'
862 862
863 863 assert isinstance(m1_set, set) and isinstance(m2_set, set)
864 864 return m1_set.intersection(m2_set)
865 865
866 866 def always(self):
867 867 return self._m1.always() and self._m2.always()
868 868
869 869 def isexact(self):
870 870 return self._m1.isexact() or self._m2.isexact()
871 871
872 872 @encoding.strmethod
873 873 def __repr__(self):
874 874 return ('<intersectionmatcher m1=%r, m2=%r>' % (self._m1, self._m2))
875 875
876 876 class subdirmatcher(basematcher):
877 877 """Adapt a matcher to work on a subdirectory only.
878 878
879 879 The paths are remapped to remove/insert the path as needed:
880 880
881 881 >>> from . import pycompat
882 882 >>> m1 = match(b'root', b'', [b'a.txt', b'sub/b.txt'])
883 883 >>> m2 = subdirmatcher(b'sub', m1)
884 884 >>> m2(b'a.txt')
885 885 False
886 886 >>> m2(b'b.txt')
887 887 True
888 888 >>> m2.matchfn(b'a.txt')
889 889 False
890 890 >>> m2.matchfn(b'b.txt')
891 891 True
892 892 >>> m2.files()
893 893 ['b.txt']
894 894 >>> m2.exact(b'b.txt')
895 895 True
896 896 >>> def bad(f, msg):
897 897 ... print(pycompat.sysstr(b"%s: %s" % (f, msg)))
898 898 >>> m1.bad = bad
899 899 >>> m2.bad(b'x.txt', b'No such file')
900 900 sub/x.txt: No such file
901 901 """
902 902
903 903 def __init__(self, path, matcher):
904 904 super(subdirmatcher, self).__init__()
905 905 self._path = path
906 906 self._matcher = matcher
907 907 self._always = matcher.always()
908 908
909 909 self._files = [f[len(path) + 1:] for f in matcher._files
910 910 if f.startswith(path + "/")]
911 911
912 912 # If the parent repo had a path to this subrepo and the matcher is
913 913 # a prefix matcher, this submatcher always matches.
914 914 if matcher.prefix():
915 915 self._always = any(f == path for f in matcher._files)
916 916
917 917 def bad(self, f, msg):
918 918 self._matcher.bad(self._path + "/" + f, msg)
919 919
920 920 def matchfn(self, f):
921 921 # Some information is lost in the superclass's constructor, so we
922 922 # can not accurately create the matching function for the subdirectory
923 923 # from the inputs. Instead, we override matchfn() and visitdir() to
924 924 # call the original matcher with the subdirectory path prepended.
925 925 return self._matcher.matchfn(self._path + "/" + f)
926 926
927 927 def visitdir(self, dir):
928 928 dir = normalizerootdir(dir, 'visitdir')
929 929 if dir == '':
930 930 dir = self._path
931 931 else:
932 932 dir = self._path + "/" + dir
933 933 return self._matcher.visitdir(dir)
934 934
935 935 def visitchildrenset(self, dir):
936 936 dir = normalizerootdir(dir, 'visitchildrenset')
937 937 if dir == '':
938 938 dir = self._path
939 939 else:
940 940 dir = self._path + "/" + dir
941 941 return self._matcher.visitchildrenset(dir)
942 942
943 943 def always(self):
944 944 return self._always
945 945
946 946 def prefix(self):
947 947 return self._matcher.prefix() and not self._always
948 948
949 949 @encoding.strmethod
950 950 def __repr__(self):
951 951 return ('<subdirmatcher path=%r, matcher=%r>' %
952 952 (self._path, self._matcher))
953 953
954 954 class prefixdirmatcher(basematcher):
955 955 """Adapt a matcher to work on a parent directory.
956 956
957 957 The matcher's non-matching-attributes (bad, explicitdir, traversedir) are
958 958 ignored.
959 959
960 960 The prefix path should usually be the relative path from the root of
961 961 this matcher to the root of the wrapped matcher.
962 962
963 963 >>> m1 = match(util.localpath(b'root/d/e'), b'f', [b'../a.txt', b'b.txt'])
964 964 >>> m2 = prefixdirmatcher(b'd/e', m1)
965 965 >>> m2(b'a.txt')
966 966 False
967 967 >>> m2(b'd/e/a.txt')
968 968 True
969 969 >>> m2(b'd/e/b.txt')
970 970 False
971 971 >>> m2.files()
972 972 ['d/e/a.txt', 'd/e/f/b.txt']
973 973 >>> m2.exact(b'd/e/a.txt')
974 974 True
975 975 >>> m2.visitdir(b'd')
976 976 True
977 977 >>> m2.visitdir(b'd/e')
978 978 True
979 979 >>> m2.visitdir(b'd/e/f')
980 980 True
981 981 >>> m2.visitdir(b'd/e/g')
982 982 False
983 983 >>> m2.visitdir(b'd/ef')
984 984 False
985 985 """
986 986
987 987 def __init__(self, path, matcher, badfn=None):
988 988 super(prefixdirmatcher, self).__init__(badfn)
989 989 if not path:
990 990 raise error.ProgrammingError('prefix path must not be empty')
991 991 self._path = path
992 992 self._pathprefix = path + '/'
993 993 self._matcher = matcher
994 994
995 995 @propertycache
996 996 def _files(self):
997 997 return [self._pathprefix + f for f in self._matcher._files]
998 998
999 999 def matchfn(self, f):
1000 1000 if not f.startswith(self._pathprefix):
1001 1001 return False
1002 1002 return self._matcher.matchfn(f[len(self._pathprefix):])
1003 1003
1004 1004 @propertycache
1005 1005 def _pathdirs(self):
1006 1006 return set(util.finddirs(self._path))
1007 1007
1008 1008 def visitdir(self, dir):
1009 1009 if dir == self._path:
1010 1010 return self._matcher.visitdir('')
1011 1011 if dir.startswith(self._pathprefix):
1012 1012 return self._matcher.visitdir(dir[len(self._pathprefix):])
1013 1013 return dir in self._pathdirs
1014 1014
1015 1015 def visitchildrenset(self, dir):
1016 1016 if dir == self._path:
1017 1017 return self._matcher.visitchildrenset('')
1018 1018 if dir.startswith(self._pathprefix):
1019 1019 return self._matcher.visitchildrenset(dir[len(self._pathprefix):])
1020 1020 if dir in self._pathdirs:
1021 1021 return 'this'
1022 1022 return set()
1023 1023
1024 1024 def isexact(self):
1025 1025 return self._matcher.isexact()
1026 1026
1027 1027 def prefix(self):
1028 1028 return self._matcher.prefix()
1029 1029
1030 1030 @encoding.strmethod
1031 1031 def __repr__(self):
1032 1032 return ('<prefixdirmatcher path=%r, matcher=%r>'
1033 1033 % (pycompat.bytestr(self._path), self._matcher))
1034 1034
1035 1035 class unionmatcher(basematcher):
1036 1036 """A matcher that is the union of several matchers.
1037 1037
1038 1038 The non-matching-attributes (bad, explicitdir, traversedir) are taken from
1039 1039 the first matcher.
1040 1040 """
1041 1041
1042 1042 def __init__(self, matchers):
1043 1043 m1 = matchers[0]
1044 1044 super(unionmatcher, self).__init__()
1045 1045 self.explicitdir = m1.explicitdir
1046 1046 self.traversedir = m1.traversedir
1047 1047 self._matchers = matchers
1048 1048
1049 1049 def matchfn(self, f):
1050 1050 for match in self._matchers:
1051 1051 if match(f):
1052 1052 return True
1053 1053 return False
1054 1054
1055 1055 def visitdir(self, dir):
1056 1056 r = False
1057 1057 for m in self._matchers:
1058 1058 v = m.visitdir(dir)
1059 1059 if v == 'all':
1060 1060 return v
1061 1061 r |= v
1062 1062 return r
1063 1063
1064 1064 def visitchildrenset(self, dir):
1065 1065 r = set()
1066 1066 this = False
1067 1067 for m in self._matchers:
1068 1068 v = m.visitchildrenset(dir)
1069 1069 if not v:
1070 1070 continue
1071 1071 if v == 'all':
1072 1072 return v
1073 1073 if this or v == 'this':
1074 1074 this = True
1075 1075 # don't break, we might have an 'all' in here.
1076 1076 continue
1077 1077 assert isinstance(v, set)
1078 1078 r = r.union(v)
1079 1079 if this:
1080 1080 return 'this'
1081 1081 return r
1082 1082
1083 1083 @encoding.strmethod
1084 1084 def __repr__(self):
1085 1085 return ('<unionmatcher matchers=%r>' % self._matchers)
1086 1086
1087 1087 def patkind(pattern, default=None):
1088 1088 '''If pattern is 'kind:pat' with a known kind, return kind.
1089 1089
1090 1090 >>> patkind(br're:.*\.c$')
1091 1091 're'
1092 1092 >>> patkind(b'glob:*.c')
1093 1093 'glob'
1094 1094 >>> patkind(b'relpath:test.py')
1095 1095 'relpath'
1096 1096 >>> patkind(b'main.py')
1097 1097 >>> patkind(b'main.py', default=b're')
1098 1098 're'
1099 1099 '''
1100 1100 return _patsplit(pattern, default)[0]
1101 1101
1102 1102 def _patsplit(pattern, default):
1103 1103 """Split a string into the optional pattern kind prefix and the actual
1104 1104 pattern."""
1105 1105 if ':' in pattern:
1106 1106 kind, pat = pattern.split(':', 1)
1107 1107 if kind in allpatternkinds:
1108 1108 return kind, pat
1109 1109 return default, pattern
1110 1110
1111 1111 def _globre(pat):
1112 1112 r'''Convert an extended glob string to a regexp string.
1113 1113
1114 1114 >>> from . import pycompat
1115 1115 >>> def bprint(s):
1116 1116 ... print(pycompat.sysstr(s))
1117 1117 >>> bprint(_globre(br'?'))
1118 1118 .
1119 1119 >>> bprint(_globre(br'*'))
1120 1120 [^/]*
1121 1121 >>> bprint(_globre(br'**'))
1122 1122 .*
1123 1123 >>> bprint(_globre(br'**/a'))
1124 1124 (?:.*/)?a
1125 1125 >>> bprint(_globre(br'a/**/b'))
1126 1126 a/(?:.*/)?b
1127 1127 >>> bprint(_globre(br'[a*?!^][^b][!c]'))
1128 1128 [a*?!^][\^b][^c]
1129 1129 >>> bprint(_globre(br'{a,b}'))
1130 1130 (?:a|b)
1131 1131 >>> bprint(_globre(br'.\*\?'))
1132 1132 \.\*\?
1133 1133 '''
1134 1134 i, n = 0, len(pat)
1135 1135 res = ''
1136 1136 group = 0
1137 1137 escape = util.stringutil.regexbytesescapemap.get
1138 1138 def peek():
1139 1139 return i < n and pat[i:i + 1]
1140 1140 while i < n:
1141 1141 c = pat[i:i + 1]
1142 1142 i += 1
1143 1143 if c not in '*?[{},\\':
1144 1144 res += escape(c, c)
1145 1145 elif c == '*':
1146 1146 if peek() == '*':
1147 1147 i += 1
1148 1148 if peek() == '/':
1149 1149 i += 1
1150 1150 res += '(?:.*/)?'
1151 1151 else:
1152 1152 res += '.*'
1153 1153 else:
1154 1154 res += '[^/]*'
1155 1155 elif c == '?':
1156 1156 res += '.'
1157 1157 elif c == '[':
1158 1158 j = i
1159 1159 if j < n and pat[j:j + 1] in '!]':
1160 1160 j += 1
1161 1161 while j < n and pat[j:j + 1] != ']':
1162 1162 j += 1
1163 1163 if j >= n:
1164 1164 res += '\\['
1165 1165 else:
1166 1166 stuff = pat[i:j].replace('\\','\\\\')
1167 1167 i = j + 1
1168 1168 if stuff[0:1] == '!':
1169 1169 stuff = '^' + stuff[1:]
1170 1170 elif stuff[0:1] == '^':
1171 1171 stuff = '\\' + stuff
1172 1172 res = '%s[%s]' % (res, stuff)
1173 1173 elif c == '{':
1174 1174 group += 1
1175 1175 res += '(?:'
1176 1176 elif c == '}' and group:
1177 1177 res += ')'
1178 1178 group -= 1
1179 1179 elif c == ',' and group:
1180 1180 res += '|'
1181 1181 elif c == '\\':
1182 1182 p = peek()
1183 1183 if p:
1184 1184 i += 1
1185 1185 res += escape(p, p)
1186 1186 else:
1187 1187 res += escape(c, c)
1188 1188 else:
1189 1189 res += escape(c, c)
1190 1190 return res
1191 1191
1192 1192 def _regex(kind, pat, globsuffix):
1193 1193 '''Convert a (normalized) pattern of any kind into a
1194 1194 regular expression.
1195 1195 globsuffix is appended to the regexp of globs.'''
1196 1196
1197 1197 if rustmod is not None:
1198 1198 try:
1199 1199 return rustmod.build_single_regex(
1200 1200 kind,
1201 1201 pat,
1202 1202 globsuffix
1203 1203 )
1204 1204 except rustmod.PatternError:
1205 1205 raise error.ProgrammingError(
1206 1206 'not a regex pattern: %s:%s' % (kind, pat)
1207 1207 )
1208 1208
1209 1209 if not pat and kind in ('glob', 'relpath'):
1210 1210 return ''
1211 1211 if kind == 're':
1212 1212 return pat
1213 1213 if kind in ('path', 'relpath'):
1214 1214 if pat == '.':
1215 1215 return ''
1216 1216 return util.stringutil.reescape(pat) + '(?:/|$)'
1217 1217 if kind == 'rootfilesin':
1218 1218 if pat == '.':
1219 1219 escaped = ''
1220 1220 else:
1221 1221 # Pattern is a directory name.
1222 1222 escaped = util.stringutil.reescape(pat) + '/'
1223 1223 # Anything after the pattern must be a non-directory.
1224 1224 return escaped + '[^/]+$'
1225 1225 if kind == 'relglob':
1226 return '(?:|.*/)' + _globre(pat) + globsuffix
1226 globre = _globre(pat)
1227 if globre.startswith('[^/]*'):
1228 # When pat has the form *XYZ (common), make the returned regex more
1229 # legible by returning the regex for **XYZ instead of **/*XYZ.
1230 return '.*' + globre[len('[^/]*'):] + globsuffix
1231 return '(?:|.*/)' + globre + globsuffix
1227 1232 if kind == 'relre':
1228 1233 if pat.startswith('^'):
1229 1234 return pat
1230 1235 return '.*' + pat
1231 1236 if kind in ('glob', 'rootglob'):
1232 1237 return _globre(pat) + globsuffix
1233 1238 raise error.ProgrammingError('not a regex pattern: %s:%s' % (kind, pat))
1234 1239
1235 1240 def _buildmatch(kindpats, globsuffix, root):
1236 1241 '''Return regexp string and a matcher function for kindpats.
1237 1242 globsuffix is appended to the regexp of globs.'''
1238 1243 matchfuncs = []
1239 1244
1240 1245 subincludes, kindpats = _expandsubinclude(kindpats, root)
1241 1246 if subincludes:
1242 1247 submatchers = {}
1243 1248 def matchsubinclude(f):
1244 1249 for prefix, matcherargs in subincludes:
1245 1250 if f.startswith(prefix):
1246 1251 mf = submatchers.get(prefix)
1247 1252 if mf is None:
1248 1253 mf = match(*matcherargs)
1249 1254 submatchers[prefix] = mf
1250 1255
1251 1256 if mf(f[len(prefix):]):
1252 1257 return True
1253 1258 return False
1254 1259 matchfuncs.append(matchsubinclude)
1255 1260
1256 1261 regex = ''
1257 1262 if kindpats:
1258 1263 if all(k == 'rootfilesin' for k, p, s in kindpats):
1259 1264 dirs = {p for k, p, s in kindpats}
1260 1265 def mf(f):
1261 1266 i = f.rfind('/')
1262 1267 if i >= 0:
1263 1268 dir = f[:i]
1264 1269 else:
1265 1270 dir = '.'
1266 1271 return dir in dirs
1267 1272 regex = b'rootfilesin: %s' % stringutil.pprint(list(sorted(dirs)))
1268 1273 matchfuncs.append(mf)
1269 1274 else:
1270 1275 regex, mf = _buildregexmatch(kindpats, globsuffix)
1271 1276 matchfuncs.append(mf)
1272 1277
1273 1278 if len(matchfuncs) == 1:
1274 1279 return regex, matchfuncs[0]
1275 1280 else:
1276 1281 return regex, lambda f: any(mf(f) for mf in matchfuncs)
1277 1282
1278 1283 MAX_RE_SIZE = 20000
1279 1284
1280 1285 def _joinregexes(regexps):
1281 1286 """gather multiple regular expressions into a single one"""
1282 1287 return '|'.join(regexps)
1283 1288
1284 1289 def _buildregexmatch(kindpats, globsuffix):
1285 1290 """Build a match function from a list of kinds and kindpats,
1286 1291 return regexp string and a matcher function.
1287 1292
1288 1293 Test too large input
1289 1294 >>> _buildregexmatch([
1290 1295 ... (b'relglob', b'?' * MAX_RE_SIZE, b'')
1291 1296 ... ], b'$')
1292 1297 Traceback (most recent call last):
1293 1298 ...
1294 1299 Abort: matcher pattern is too long (20009 bytes)
1295 1300 """
1296 1301 try:
1297 1302 allgroups = []
1298 1303 regexps = [_regex(k, p, globsuffix) for (k, p, s) in kindpats]
1299 1304 fullregexp = _joinregexes(regexps)
1300 1305
1301 1306 startidx = 0
1302 1307 groupsize = 0
1303 1308 for idx, r in enumerate(regexps):
1304 1309 piecesize = len(r)
1305 1310 if piecesize > MAX_RE_SIZE:
1306 1311 msg = _("matcher pattern is too long (%d bytes)") % piecesize
1307 1312 raise error.Abort(msg)
1308 1313 elif (groupsize + piecesize) > MAX_RE_SIZE:
1309 1314 group = regexps[startidx:idx]
1310 1315 allgroups.append(_joinregexes(group))
1311 1316 startidx = idx
1312 1317 groupsize = 0
1313 1318 groupsize += piecesize + 1
1314 1319
1315 1320 if startidx == 0:
1316 1321 matcher = _rematcher(fullregexp)
1317 1322 func = lambda s: bool(matcher(s))
1318 1323 else:
1319 1324 group = regexps[startidx:]
1320 1325 allgroups.append(_joinregexes(group))
1321 1326 allmatchers = [_rematcher(g) for g in allgroups]
1322 1327 func = lambda s: any(m(s) for m in allmatchers)
1323 1328 return fullregexp, func
1324 1329 except re.error:
1325 1330 for k, p, s in kindpats:
1326 1331 try:
1327 1332 _rematcher(_regex(k, p, globsuffix))
1328 1333 except re.error:
1329 1334 if s:
1330 1335 raise error.Abort(_("%s: invalid pattern (%s): %s") %
1331 1336 (s, k, p))
1332 1337 else:
1333 1338 raise error.Abort(_("invalid pattern (%s): %s") % (k, p))
1334 1339 raise error.Abort(_("invalid pattern"))
1335 1340
1336 1341 def _patternrootsanddirs(kindpats):
1337 1342 '''Returns roots and directories corresponding to each pattern.
1338 1343
1339 1344 This calculates the roots and directories exactly matching the patterns and
1340 1345 returns a tuple of (roots, dirs) for each. It does not return other
1341 1346 directories which may also need to be considered, like the parent
1342 1347 directories.
1343 1348 '''
1344 1349 r = []
1345 1350 d = []
1346 1351 for kind, pat, source in kindpats:
1347 1352 if kind in ('glob', 'rootglob'): # find the non-glob prefix
1348 1353 root = []
1349 1354 for p in pat.split('/'):
1350 1355 if '[' in p or '{' in p or '*' in p or '?' in p:
1351 1356 break
1352 1357 root.append(p)
1353 1358 r.append('/'.join(root))
1354 1359 elif kind in ('relpath', 'path'):
1355 1360 if pat == '.':
1356 1361 pat = ''
1357 1362 r.append(pat)
1358 1363 elif kind in ('rootfilesin',):
1359 1364 if pat == '.':
1360 1365 pat = ''
1361 1366 d.append(pat)
1362 1367 else: # relglob, re, relre
1363 1368 r.append('')
1364 1369 return r, d
1365 1370
1366 1371 def _roots(kindpats):
1367 1372 '''Returns root directories to match recursively from the given patterns.'''
1368 1373 roots, dirs = _patternrootsanddirs(kindpats)
1369 1374 return roots
1370 1375
1371 1376 def _rootsdirsandparents(kindpats):
1372 1377 '''Returns roots and exact directories from patterns.
1373 1378
1374 1379 `roots` are directories to match recursively, `dirs` should
1375 1380 be matched non-recursively, and `parents` are the implicitly required
1376 1381 directories to walk to items in either roots or dirs.
1377 1382
1378 1383 Returns a tuple of (roots, dirs, parents).
1379 1384
1380 1385 >>> r = _rootsdirsandparents(
1381 1386 ... [(b'glob', b'g/h/*', b''), (b'glob', b'g/h', b''),
1382 1387 ... (b'glob', b'g*', b'')])
1383 1388 >>> print(r[0:2], sorted(r[2])) # the set has an unstable output
1384 1389 (['g/h', 'g/h', ''], []) ['', 'g']
1385 1390 >>> r = _rootsdirsandparents(
1386 1391 ... [(b'rootfilesin', b'g/h', b''), (b'rootfilesin', b'', b'')])
1387 1392 >>> print(r[0:2], sorted(r[2])) # the set has an unstable output
1388 1393 ([], ['g/h', '']) ['', 'g']
1389 1394 >>> r = _rootsdirsandparents(
1390 1395 ... [(b'relpath', b'r', b''), (b'path', b'p/p', b''),
1391 1396 ... (b'path', b'', b'')])
1392 1397 >>> print(r[0:2], sorted(r[2])) # the set has an unstable output
1393 1398 (['r', 'p/p', ''], []) ['', 'p']
1394 1399 >>> r = _rootsdirsandparents(
1395 1400 ... [(b'relglob', b'rg*', b''), (b're', b're/', b''),
1396 1401 ... (b'relre', b'rr', b'')])
1397 1402 >>> print(r[0:2], sorted(r[2])) # the set has an unstable output
1398 1403 (['', '', ''], []) ['']
1399 1404 '''
1400 1405 r, d = _patternrootsanddirs(kindpats)
1401 1406
1402 1407 p = set()
1403 1408 # Add the parents as non-recursive/exact directories, since they must be
1404 1409 # scanned to get to either the roots or the other exact directories.
1405 1410 p.update(util.dirs(d))
1406 1411 p.update(util.dirs(r))
1407 1412
1408 1413 # FIXME: all uses of this function convert these to sets, do so before
1409 1414 # returning.
1410 1415 # FIXME: all uses of this function do not need anything in 'roots' and
1411 1416 # 'dirs' to also be in 'parents', consider removing them before returning.
1412 1417 return r, d, p
1413 1418
1414 1419 def _explicitfiles(kindpats):
1415 1420 '''Returns the potential explicit filenames from the patterns.
1416 1421
1417 1422 >>> _explicitfiles([(b'path', b'foo/bar', b'')])
1418 1423 ['foo/bar']
1419 1424 >>> _explicitfiles([(b'rootfilesin', b'foo/bar', b'')])
1420 1425 []
1421 1426 '''
1422 1427 # Keep only the pattern kinds where one can specify filenames (vs only
1423 1428 # directory names).
1424 1429 filable = [kp for kp in kindpats if kp[0] not in ('rootfilesin',)]
1425 1430 return _roots(filable)
1426 1431
1427 1432 def _prefix(kindpats):
1428 1433 '''Whether all the patterns match a prefix (i.e. recursively)'''
1429 1434 for kind, pat, source in kindpats:
1430 1435 if kind not in ('path', 'relpath'):
1431 1436 return False
1432 1437 return True
1433 1438
1434 1439 _commentre = None
1435 1440
1436 1441 def readpatternfile(filepath, warn, sourceinfo=False):
1437 1442 '''parse a pattern file, returning a list of
1438 1443 patterns. These patterns should be given to compile()
1439 1444 to be validated and converted into a match function.
1440 1445
1441 1446 trailing white space is dropped.
1442 1447 the escape character is backslash.
1443 1448 comments start with #.
1444 1449 empty lines are skipped.
1445 1450
1446 1451 lines can be of the following formats:
1447 1452
1448 1453 syntax: regexp # defaults following lines to non-rooted regexps
1449 1454 syntax: glob # defaults following lines to non-rooted globs
1450 1455 re:pattern # non-rooted regular expression
1451 1456 glob:pattern # non-rooted glob
1452 1457 rootglob:pat # rooted glob (same root as ^ in regexps)
1453 1458 pattern # pattern of the current default type
1454 1459
1455 1460 if sourceinfo is set, returns a list of tuples:
1456 1461 (pattern, lineno, originalline).
1457 1462 This is useful to debug ignore patterns.
1458 1463 '''
1459 1464
1460 1465 if rustmod is not None:
1461 1466 result, warnings = rustmod.read_pattern_file(
1462 1467 filepath,
1463 1468 bool(warn),
1464 1469 sourceinfo,
1465 1470 )
1466 1471
1467 1472 for warning_params in warnings:
1468 1473 # Can't be easily emitted from Rust, because it would require
1469 1474 # a mechanism for both gettext and calling the `warn` function.
1470 1475 warn(_("%s: ignoring invalid syntax '%s'\n") % warning_params)
1471 1476
1472 1477 return result
1473 1478
1474 1479 syntaxes = {
1475 1480 're': 'relre:',
1476 1481 'regexp': 'relre:',
1477 1482 'glob': 'relglob:',
1478 1483 'rootglob': 'rootglob:',
1479 1484 'include': 'include',
1480 1485 'subinclude': 'subinclude',
1481 1486 }
1482 1487 syntax = 'relre:'
1483 1488 patterns = []
1484 1489
1485 1490 fp = open(filepath, 'rb')
1486 1491 for lineno, line in enumerate(util.iterfile(fp), start=1):
1487 1492 if "#" in line:
1488 1493 global _commentre
1489 1494 if not _commentre:
1490 1495 _commentre = util.re.compile(br'((?:^|[^\\])(?:\\\\)*)#.*')
1491 1496 # remove comments prefixed by an even number of escapes
1492 1497 m = _commentre.search(line)
1493 1498 if m:
1494 1499 line = line[:m.end(1)]
1495 1500 # fixup properly escaped comments that survived the above
1496 1501 line = line.replace("\\#", "#")
1497 1502 line = line.rstrip()
1498 1503 if not line:
1499 1504 continue
1500 1505
1501 1506 if line.startswith('syntax:'):
1502 1507 s = line[7:].strip()
1503 1508 try:
1504 1509 syntax = syntaxes[s]
1505 1510 except KeyError:
1506 1511 if warn:
1507 1512 warn(_("%s: ignoring invalid syntax '%s'\n") %
1508 1513 (filepath, s))
1509 1514 continue
1510 1515
1511 1516 linesyntax = syntax
1512 1517 for s, rels in syntaxes.iteritems():
1513 1518 if line.startswith(rels):
1514 1519 linesyntax = rels
1515 1520 line = line[len(rels):]
1516 1521 break
1517 1522 elif line.startswith(s+':'):
1518 1523 linesyntax = rels
1519 1524 line = line[len(s) + 1:]
1520 1525 break
1521 1526 if sourceinfo:
1522 1527 patterns.append((linesyntax + line, lineno, line))
1523 1528 else:
1524 1529 patterns.append(linesyntax + line)
1525 1530 fp.close()
1526 1531 return patterns
@@ -1,384 +1,392
1 1 // filepatterns.rs
2 2 //
3 3 // Copyright 2019 Raphaël Gomès <rgomes@octobus.net>
4 4 //
5 5 // This software may be used and distributed according to the terms of the
6 6 // GNU General Public License version 2 or any later version.
7 7
8 8 //! Handling of Mercurial-specific patterns.
9 9
10 10 use crate::{
11 11 utils::{files::get_path_from_bytes, SliceExt},
12 12 LineNumber, PatternError, PatternFileError,
13 13 };
14 14 use lazy_static::lazy_static;
15 15 use regex::bytes::{NoExpand, Regex};
16 16 use std::collections::HashMap;
17 17 use std::fs::File;
18 18 use std::io::Read;
19 19 use std::vec::Vec;
20 20
21 21 lazy_static! {
22 22 static ref RE_ESCAPE: Vec<Vec<u8>> = {
23 23 let mut v: Vec<Vec<u8>> = (0..=255).map(|byte| vec![byte]).collect();
24 24 let to_escape = b"()[]{}?*+-|^$\\.&~# \t\n\r\x0b\x0c";
25 25 for byte in to_escape {
26 26 v[*byte as usize].insert(0, b'\\');
27 27 }
28 28 v
29 29 };
30 30 }
31 31
32 32 /// These are matched in order
33 33 const GLOB_REPLACEMENTS: &[(&[u8], &[u8])] =
34 34 &[(b"*/", b"(?:.*/)?"), (b"*", b".*"), (b"", b"[^/]*")];
35 35
36 36 #[derive(Debug, Copy, Clone, PartialEq, Eq)]
37 37 pub enum PatternSyntax {
38 38 Regexp,
39 39 /// Glob that matches at the front of the path
40 40 RootGlob,
41 41 /// Glob that matches at any suffix of the path (still anchored at
42 42 /// slashes)
43 43 Glob,
44 44 Path,
45 45 RelPath,
46 46 RelGlob,
47 47 RelRegexp,
48 48 RootFiles,
49 49 }
50 50
51 51 /// Transforms a glob pattern into a regex
52 52 fn glob_to_re(pat: &[u8]) -> Vec<u8> {
53 53 let mut input = pat;
54 54 let mut res: Vec<u8> = vec![];
55 55 let mut group_depth = 0;
56 56
57 57 while let Some((c, rest)) = input.split_first() {
58 58 input = rest;
59 59
60 60 match c {
61 61 b'*' => {
62 62 for (source, repl) in GLOB_REPLACEMENTS {
63 63 if let Some(rest) = input.drop_prefix(source) {
64 64 input = rest;
65 65 res.extend(*repl);
66 66 break;
67 67 }
68 68 }
69 69 }
70 70 b'?' => res.extend(b"."),
71 71 b'[' => {
72 72 match input.iter().skip(1).position(|b| *b == b']') {
73 73 None => res.extend(b"\\["),
74 74 Some(end) => {
75 75 // Account for the one we skipped
76 76 let end = end + 1;
77 77
78 78 res.extend(b"[");
79 79
80 80 for (i, b) in input[..end].iter().enumerate() {
81 81 if *b == b'!' && i == 0 {
82 82 res.extend(b"^")
83 83 } else if *b == b'^' && i == 0 {
84 84 res.extend(b"\\^")
85 85 } else if *b == b'\\' {
86 86 res.extend(b"\\\\")
87 87 } else {
88 88 res.push(*b)
89 89 }
90 90 }
91 91 res.extend(b"]");
92 92 input = &input[end + 1..];
93 93 }
94 94 }
95 95 }
96 96 b'{' => {
97 97 group_depth += 1;
98 98 res.extend(b"(?:")
99 99 }
100 100 b'}' if group_depth > 0 => {
101 101 group_depth -= 1;
102 102 res.extend(b")");
103 103 }
104 104 b',' if group_depth > 0 => res.extend(b"|"),
105 105 b'\\' => {
106 106 let c = {
107 107 if let Some((c, rest)) = input.split_first() {
108 108 input = rest;
109 109 c
110 110 } else {
111 111 c
112 112 }
113 113 };
114 114 res.extend(&RE_ESCAPE[*c as usize])
115 115 }
116 116 _ => res.extend(&RE_ESCAPE[*c as usize]),
117 117 }
118 118 }
119 119 res
120 120 }
121 121
122 122 fn escape_pattern(pattern: &[u8]) -> Vec<u8> {
123 123 pattern
124 124 .iter()
125 125 .flat_map(|c| RE_ESCAPE[*c as usize].clone())
126 126 .collect()
127 127 }
128 128
129 129 fn parse_pattern_syntax(kind: &[u8]) -> Result<PatternSyntax, PatternError> {
130 130 match kind {
131 131 b"re" => Ok(PatternSyntax::Regexp),
132 132 b"path" => Ok(PatternSyntax::Path),
133 133 b"relpath" => Ok(PatternSyntax::RelPath),
134 134 b"rootfilesin" => Ok(PatternSyntax::RootFiles),
135 135 b"relglob" => Ok(PatternSyntax::RelGlob),
136 136 b"relre" => Ok(PatternSyntax::RelRegexp),
137 137 b"glob" => Ok(PatternSyntax::Glob),
138 138 b"rootglob" => Ok(PatternSyntax::RootGlob),
139 139 _ => Err(PatternError::UnsupportedSyntax(
140 140 String::from_utf8_lossy(kind).to_string(),
141 141 )),
142 142 }
143 143 }
144 144
145 145 /// Builds the regex that corresponds to the given pattern.
146 146 /// If within a `syntax: regexp` context, returns the pattern,
147 147 /// otherwise, returns the corresponding regex.
148 148 fn _build_single_regex(
149 149 syntax: PatternSyntax,
150 150 pattern: &[u8],
151 151 globsuffix: &[u8],
152 152 ) -> Vec<u8> {
153 153 if pattern.is_empty() {
154 154 return vec![];
155 155 }
156 156 match syntax {
157 157 PatternSyntax::Regexp => pattern.to_owned(),
158 158 PatternSyntax::RelRegexp => {
159 159 if pattern[0] == b'^' {
160 160 return pattern.to_owned();
161 161 }
162 162 let mut res = b".*".to_vec();
163 163 res.extend(pattern);
164 164 res
165 165 }
166 166 PatternSyntax::Path | PatternSyntax::RelPath => {
167 167 if pattern == b"." {
168 168 return vec![];
169 169 }
170 170 let mut pattern = escape_pattern(pattern);
171 171 pattern.extend(b"(?:/|$)");
172 172 pattern
173 173 }
174 174 PatternSyntax::RootFiles => {
175 175 let mut res = if pattern == b"." {
176 176 vec![]
177 177 } else {
178 178 // Pattern is a directory name.
179 179 let mut as_vec: Vec<u8> = escape_pattern(pattern);
180 180 as_vec.push(b'/');
181 181 as_vec
182 182 };
183 183
184 184 // Anything after the pattern must be a non-directory.
185 185 res.extend(b"[^/]+$");
186 186 res
187 187 }
188 PatternSyntax::RelGlob => {
189 let mut res: Vec<u8> = vec![];
190 let glob_re = glob_to_re(pattern);
191 if let Some(rest) = glob_re.drop_prefix(b"[^/]*") {
192 res.extend(b".*");
193 res.extend(rest);
194 } else {
195 res.extend(b"(?:|.*/)");
196 res.extend(glob_re);
197 }
198 res.extend(globsuffix.iter());
199 res
200 }
188 201 PatternSyntax::Glob
189 | PatternSyntax::RelGlob
190 202 | PatternSyntax::RootGlob => {
191 203 let mut res: Vec<u8> = vec![];
192 if syntax == PatternSyntax::RelGlob {
193 res.extend(b"(?:|.*/)");
194 }
195
196 204 res.extend(glob_to_re(pattern));
197 205 res.extend(globsuffix.iter());
198 206 res
199 207 }
200 208 }
201 209 }
202 210
203 211 const GLOB_SPECIAL_CHARACTERS: [u8; 7] =
204 212 [b'*', b'?', b'[', b']', b'{', b'}', b'\\'];
205 213
206 214 /// Wrapper function to `_build_single_regex` that short-circuits 'exact' globs
207 215 /// that don't need to be transformed into a regex.
208 216 pub fn build_single_regex(
209 217 kind: &[u8],
210 218 pat: &[u8],
211 219 globsuffix: &[u8],
212 220 ) -> Result<Vec<u8>, PatternError> {
213 221 let enum_kind = parse_pattern_syntax(kind)?;
214 222 if enum_kind == PatternSyntax::RootGlob
215 223 && !pat.iter().any(|b| GLOB_SPECIAL_CHARACTERS.contains(b))
216 224 {
217 225 let mut escaped = escape_pattern(pat);
218 226 escaped.extend(b"(?:/|$)");
219 227 Ok(escaped)
220 228 } else {
221 229 Ok(_build_single_regex(enum_kind, pat, globsuffix))
222 230 }
223 231 }
224 232
225 233 lazy_static! {
226 234 static ref SYNTAXES: HashMap<&'static [u8], &'static [u8]> = {
227 235 let mut m = HashMap::new();
228 236
229 237 m.insert(b"re".as_ref(), b"relre:".as_ref());
230 238 m.insert(b"regexp".as_ref(), b"relre:".as_ref());
231 239 m.insert(b"glob".as_ref(), b"relglob:".as_ref());
232 240 m.insert(b"rootglob".as_ref(), b"rootglob:".as_ref());
233 241 m.insert(b"include".as_ref(), b"include".as_ref());
234 242 m.insert(b"subinclude".as_ref(), b"subinclude".as_ref());
235 243 m
236 244 };
237 245 }
238 246
239 247 pub type PatternTuple = (Vec<u8>, LineNumber, Vec<u8>);
240 248 type WarningTuple = (Vec<u8>, Vec<u8>);
241 249
242 250 pub fn parse_pattern_file_contents(
243 251 lines: &[u8],
244 252 file_path: &[u8],
245 253 warn: bool,
246 254 ) -> (Vec<PatternTuple>, Vec<WarningTuple>) {
247 255 let comment_regex = Regex::new(r"((?:^|[^\\])(?:\\\\)*)#.*").unwrap();
248 256 let comment_escape_regex = Regex::new(r"\\#").unwrap();
249 257 let mut inputs: Vec<PatternTuple> = vec![];
250 258 let mut warnings: Vec<WarningTuple> = vec![];
251 259
252 260 let mut current_syntax = b"relre:".as_ref();
253 261
254 262 for (line_number, mut line) in lines.split(|c| *c == b'\n').enumerate() {
255 263 let line_number = line_number + 1;
256 264
257 265 let line_buf;
258 266 if line.contains(&b'#') {
259 267 if let Some(cap) = comment_regex.captures(line) {
260 268 line = &line[..cap.get(1).unwrap().end()]
261 269 }
262 270 line_buf = comment_escape_regex.replace_all(line, NoExpand(b"#"));
263 271 line = &line_buf;
264 272 }
265 273
266 274 let mut line = line.trim_end();
267 275
268 276 if line.is_empty() {
269 277 continue;
270 278 }
271 279
272 280 if let Some(syntax) = line.drop_prefix(b"syntax:") {
273 281 let syntax = syntax.trim();
274 282
275 283 if let Some(rel_syntax) = SYNTAXES.get(syntax) {
276 284 current_syntax = rel_syntax;
277 285 } else if warn {
278 286 warnings.push((file_path.to_owned(), syntax.to_owned()));
279 287 }
280 288 continue;
281 289 }
282 290
283 291 let mut line_syntax: &[u8] = &current_syntax;
284 292
285 293 for (s, rels) in SYNTAXES.iter() {
286 294 if let Some(rest) = line.drop_prefix(rels) {
287 295 line_syntax = rels;
288 296 line = rest;
289 297 break;
290 298 }
291 299 if let Some(rest) = line.drop_prefix(&[s, &b":"[..]].concat()) {
292 300 line_syntax = rels;
293 301 line = rest;
294 302 break;
295 303 }
296 304 }
297 305
298 306 inputs.push((
299 307 [line_syntax, line].concat(),
300 308 line_number,
301 309 line.to_owned(),
302 310 ));
303 311 }
304 312 (inputs, warnings)
305 313 }
306 314
307 315 pub fn read_pattern_file(
308 316 file_path: &[u8],
309 317 warn: bool,
310 318 ) -> Result<(Vec<PatternTuple>, Vec<WarningTuple>), PatternFileError> {
311 319 let mut f = File::open(get_path_from_bytes(file_path))?;
312 320 let mut contents = Vec::new();
313 321
314 322 f.read_to_end(&mut contents)?;
315 323
316 324 Ok(parse_pattern_file_contents(&contents, file_path, warn))
317 325 }
318 326
319 327 #[cfg(test)]
320 328 mod tests {
321 329 use super::*;
322 330
323 331 #[test]
324 332 fn escape_pattern_test() {
325 333 let untouched = br#"!"%',/0123456789:;<=>@ABCDEFGHIJKLMNOPQRSTUVWXYZ_`abcdefghijklmnopqrstuvwxyz"#;
326 334 assert_eq!(escape_pattern(untouched), untouched.to_vec());
327 335 // All escape codes
328 336 assert_eq!(
329 337 escape_pattern(br#"()[]{}?*+-|^$\\.&~# \t\n\r\v\f"#),
330 338 br#"\(\)\[\]\{\}\?\*\+\-\|\^\$\\\\\.\&\~\#\ \\t\\n\\r\\v\\f"#
331 339 .to_vec()
332 340 );
333 341 }
334 342
335 343 #[test]
336 344 fn glob_test() {
337 345 assert_eq!(glob_to_re(br#"?"#), br#"."#);
338 346 assert_eq!(glob_to_re(br#"*"#), br#"[^/]*"#);
339 347 assert_eq!(glob_to_re(br#"**"#), br#".*"#);
340 348 assert_eq!(glob_to_re(br#"**/a"#), br#"(?:.*/)?a"#);
341 349 assert_eq!(glob_to_re(br#"a/**/b"#), br#"a/(?:.*/)?b"#);
342 350 assert_eq!(glob_to_re(br#"[a*?!^][^b][!c]"#), br#"[a*?!^][\^b][^c]"#);
343 351 assert_eq!(glob_to_re(br#"{a,b}"#), br#"(?:a|b)"#);
344 352 assert_eq!(glob_to_re(br#".\*\?"#), br#"\.\*\?"#);
345 353 }
346 354
347 355 #[test]
348 356 fn test_parse_pattern_file_contents() {
349 357 let lines = b"syntax: glob\n*.elc";
350 358
351 359 assert_eq!(
352 360 vec![(b"relglob:*.elc".to_vec(), 2, b"*.elc".to_vec())],
353 361 parse_pattern_file_contents(lines, b"file_path", false).0,
354 362 );
355 363
356 364 let lines = b"syntax: include\nsyntax: glob";
357 365
358 366 assert_eq!(
359 367 parse_pattern_file_contents(lines, b"file_path", false).0,
360 368 vec![]
361 369 );
362 370 let lines = b"glob:**.o";
363 371 assert_eq!(
364 372 parse_pattern_file_contents(lines, b"file_path", false).0,
365 373 vec![(b"relglob:**.o".to_vec(), 1, b"**.o".to_vec())]
366 374 );
367 375 }
368 376
369 377 #[test]
370 378 fn test_build_single_regex_shortcut() {
371 379 assert_eq!(
372 380 br"(?:/|$)".to_vec(),
373 381 build_single_regex(b"rootglob", b"", b"").unwrap()
374 382 );
375 383 assert_eq!(
376 384 br"whatever(?:/|$)".to_vec(),
377 385 build_single_regex(b"rootglob", b"whatever", b"").unwrap()
378 386 );
379 387 assert_eq!(
380 388 br"[^/]*\.o".to_vec(),
381 389 build_single_regex(b"rootglob", b"*.o", b"").unwrap()
382 390 );
383 391 }
384 392 }
@@ -1,390 +1,390
1 1 $ hg init ignorerepo
2 2 $ cd ignorerepo
3 3
4 4 debugignore with no hgignore should be deterministic:
5 5 $ hg debugignore
6 6 <nevermatcher>
7 7
8 8 Issue562: .hgignore requires newline at end:
9 9
10 10 $ touch foo
11 11 $ touch bar
12 12 $ touch baz
13 13 $ cat > makeignore.py <<EOF
14 14 > f = open(".hgignore", "w")
15 15 > f.write("ignore\n")
16 16 > f.write("foo\n")
17 17 > # No EOL here
18 18 > f.write("bar")
19 19 > f.close()
20 20 > EOF
21 21
22 22 $ "$PYTHON" makeignore.py
23 23
24 24 Should display baz only:
25 25
26 26 $ hg status
27 27 ? baz
28 28
29 29 $ rm foo bar baz .hgignore makeignore.py
30 30
31 31 $ touch a.o
32 32 $ touch a.c
33 33 $ touch syntax
34 34 $ mkdir dir
35 35 $ touch dir/a.o
36 36 $ touch dir/b.o
37 37 $ touch dir/c.o
38 38
39 39 $ hg add dir/a.o
40 40 $ hg commit -m 0
41 41 $ hg add dir/b.o
42 42
43 43 $ hg status
44 44 A dir/b.o
45 45 ? a.c
46 46 ? a.o
47 47 ? dir/c.o
48 48 ? syntax
49 49
50 50 $ echo "*.o" > .hgignore
51 51 $ hg status
52 52 abort: $TESTTMP/ignorerepo/.hgignore: invalid pattern (relre): *.o (glob)
53 53 [255]
54 54
55 55 Ensure given files are relative to cwd
56 56
57 57 $ echo "dir/.*\.o" > .hgignore
58 58 $ hg status -i
59 59 I dir/c.o
60 60
61 61 $ hg debugignore dir/c.o dir/missing.o
62 62 dir/c.o is ignored
63 63 (ignore rule in $TESTTMP/ignorerepo/.hgignore, line 1: 'dir/.*\.o') (glob)
64 64 dir/missing.o is ignored
65 65 (ignore rule in $TESTTMP/ignorerepo/.hgignore, line 1: 'dir/.*\.o') (glob)
66 66 $ cd dir
67 67 $ hg debugignore c.o missing.o
68 68 c.o is ignored
69 69 (ignore rule in $TESTTMP/ignorerepo/.hgignore, line 1: 'dir/.*\.o') (glob)
70 70 missing.o is ignored
71 71 (ignore rule in $TESTTMP/ignorerepo/.hgignore, line 1: 'dir/.*\.o') (glob)
72 72
73 73 For icasefs, inexact matches also work, except for missing files
74 74
75 75 #if icasefs
76 76 $ hg debugignore c.O missing.O
77 77 c.o is ignored
78 78 (ignore rule in $TESTTMP/ignorerepo/.hgignore, line 1: 'dir/.*\.o') (glob)
79 79 missing.O is not ignored
80 80 #endif
81 81
82 82 $ cd ..
83 83
84 84 $ echo ".*\.o" > .hgignore
85 85 $ hg status
86 86 A dir/b.o
87 87 ? .hgignore
88 88 ? a.c
89 89 ? syntax
90 90
91 91 Ensure that comments work:
92 92
93 93 $ touch 'foo#bar' 'quux#' 'quu0#'
94 94 #if no-windows
95 95 $ touch 'baz\' 'baz\wat' 'ba0\#wat' 'ba1\\' 'ba1\\wat' 'quu0\'
96 96 #endif
97 97
98 98 $ cat <<'EOF' >> .hgignore
99 99 > # full-line comment
100 100 > # whitespace-only comment line
101 101 > syntax# pattern, no whitespace, then comment
102 102 > a.c # pattern, then whitespace, then comment
103 103 > baz\\# # (escaped) backslash, then comment
104 104 > ba0\\\#w # (escaped) backslash, escaped comment character, then comment
105 105 > ba1\\\\# # (escaped) backslashes, then comment
106 106 > foo\#b # escaped comment character
107 107 > quux\## escaped comment character at end of name
108 108 > EOF
109 109 $ hg status
110 110 A dir/b.o
111 111 ? .hgignore
112 112 ? quu0#
113 113 ? quu0\ (no-windows !)
114 114
115 115 $ cat <<'EOF' > .hgignore
116 116 > .*\.o
117 117 > syntax: glob
118 118 > syntax# pattern, no whitespace, then comment
119 119 > a.c # pattern, then whitespace, then comment
120 120 > baz\\#* # (escaped) backslash, then comment
121 121 > ba0\\\#w* # (escaped) backslash, escaped comment character, then comment
122 122 > ba1\\\\#* # (escaped) backslashes, then comment
123 123 > foo\#b* # escaped comment character
124 124 > quux\## escaped comment character at end of name
125 125 > quu0[\#]# escaped comment character inside [...]
126 126 > EOF
127 127 $ hg status
128 128 A dir/b.o
129 129 ? .hgignore
130 130 ? ba1\\wat (no-windows !)
131 131 ? baz\wat (no-windows !)
132 132 ? quu0\ (no-windows !)
133 133
134 134 $ rm 'foo#bar' 'quux#' 'quu0#'
135 135 #if no-windows
136 136 $ rm 'baz\' 'baz\wat' 'ba0\#wat' 'ba1\\' 'ba1\\wat' 'quu0\'
137 137 #endif
138 138
139 139 Check that '^\.' does not ignore the root directory:
140 140
141 141 $ echo "^\." > .hgignore
142 142 $ hg status
143 143 A dir/b.o
144 144 ? a.c
145 145 ? a.o
146 146 ? dir/c.o
147 147 ? syntax
148 148
149 149 Test that patterns from ui.ignore options are read:
150 150
151 151 $ echo > .hgignore
152 152 $ cat >> $HGRCPATH << EOF
153 153 > [ui]
154 154 > ignore.other = $TESTTMP/ignorerepo/.hg/testhgignore
155 155 > EOF
156 156 $ echo "glob:**.o" > .hg/testhgignore
157 157 $ hg status
158 158 A dir/b.o
159 159 ? .hgignore
160 160 ? a.c
161 161 ? syntax
162 162
163 163 empty out testhgignore
164 164 $ echo > .hg/testhgignore
165 165
166 166 Test relative ignore path (issue4473):
167 167
168 168 $ cat >> $HGRCPATH << EOF
169 169 > [ui]
170 170 > ignore.relative = .hg/testhgignorerel
171 171 > EOF
172 172 $ echo "glob:*.o" > .hg/testhgignorerel
173 173 $ cd dir
174 174 $ hg status
175 175 A dir/b.o
176 176 ? .hgignore
177 177 ? a.c
178 178 ? syntax
179 179 $ hg debugignore
180 <includematcher includes='(?:|.*/)[^/]*\\.o(?:/|$)'>
180 <includematcher includes='.*\\.o(?:/|$)'>
181 181
182 182 $ cd ..
183 183 $ echo > .hg/testhgignorerel
184 184 $ echo "syntax: glob" > .hgignore
185 185 $ echo "re:.*\.o" >> .hgignore
186 186 $ hg status
187 187 A dir/b.o
188 188 ? .hgignore
189 189 ? a.c
190 190 ? syntax
191 191
192 192 $ echo "syntax: invalid" > .hgignore
193 193 $ hg status
194 194 $TESTTMP/ignorerepo/.hgignore: ignoring invalid syntax 'invalid'
195 195 A dir/b.o
196 196 ? .hgignore
197 197 ? a.c
198 198 ? a.o
199 199 ? dir/c.o
200 200 ? syntax
201 201
202 202 $ echo "syntax: glob" > .hgignore
203 203 $ echo "*.o" >> .hgignore
204 204 $ hg status
205 205 A dir/b.o
206 206 ? .hgignore
207 207 ? a.c
208 208 ? syntax
209 209
210 210 $ echo "relglob:syntax*" > .hgignore
211 211 $ hg status
212 212 A dir/b.o
213 213 ? .hgignore
214 214 ? a.c
215 215 ? a.o
216 216 ? dir/c.o
217 217
218 218 $ echo "relglob:*" > .hgignore
219 219 $ hg status
220 220 A dir/b.o
221 221
222 222 $ cd dir
223 223 $ hg status .
224 224 A b.o
225 225
226 226 $ hg debugignore
227 <includematcher includes='(?:|.*/)[^/]*(?:/|$)'>
227 <includematcher includes='.*(?:/|$)'>
228 228
229 229 $ hg debugignore b.o
230 230 b.o is ignored
231 231 (ignore rule in $TESTTMP/ignorerepo/.hgignore, line 1: '*') (glob)
232 232
233 233 $ cd ..
234 234
235 235 Check patterns that match only the directory
236 236
237 237 "(fsmonitor !)" below assumes that fsmonitor is enabled with
238 238 "walk_on_invalidate = false" (default), which doesn't involve
239 239 re-walking whole repository at detection of .hgignore change.
240 240
241 241 $ echo "^dir\$" > .hgignore
242 242 $ hg status
243 243 A dir/b.o
244 244 ? .hgignore
245 245 ? a.c
246 246 ? a.o
247 247 ? dir/c.o (fsmonitor !)
248 248 ? syntax
249 249
250 250 Check recursive glob pattern matches no directories (dir/**/c.o matches dir/c.o)
251 251
252 252 $ echo "syntax: glob" > .hgignore
253 253 $ echo "dir/**/c.o" >> .hgignore
254 254 $ touch dir/c.o
255 255 $ mkdir dir/subdir
256 256 $ touch dir/subdir/c.o
257 257 $ hg status
258 258 A dir/b.o
259 259 ? .hgignore
260 260 ? a.c
261 261 ? a.o
262 262 ? syntax
263 263 $ hg debugignore a.c
264 264 a.c is not ignored
265 265 $ hg debugignore dir/c.o
266 266 dir/c.o is ignored
267 267 (ignore rule in $TESTTMP/ignorerepo/.hgignore, line 2: 'dir/**/c.o') (glob)
268 268
269 269 Check rooted globs
270 270
271 271 $ hg purge --all --config extensions.purge=
272 272 $ echo "syntax: rootglob" > .hgignore
273 273 $ echo "a/*.ext" >> .hgignore
274 274 $ for p in a b/a aa; do mkdir -p $p; touch $p/b.ext; done
275 275 $ hg status -A 'set:**.ext'
276 276 ? aa/b.ext
277 277 ? b/a/b.ext
278 278 I a/b.ext
279 279
280 280 Check using 'include:' in ignore file
281 281
282 282 $ hg purge --all --config extensions.purge=
283 283 $ touch foo.included
284 284
285 285 $ echo ".*.included" > otherignore
286 286 $ hg status -I "include:otherignore"
287 287 ? foo.included
288 288
289 289 $ echo "include:otherignore" >> .hgignore
290 290 $ hg status
291 291 A dir/b.o
292 292 ? .hgignore
293 293 ? otherignore
294 294
295 295 Check recursive uses of 'include:'
296 296
297 297 $ echo "include:nested/ignore" >> otherignore
298 298 $ mkdir nested nested/more
299 299 $ echo "glob:*ignore" > nested/ignore
300 300 $ echo "rootglob:a" >> nested/ignore
301 301 $ touch a nested/a nested/more/a
302 302 $ hg status
303 303 A dir/b.o
304 304 ? nested/a
305 305 ? nested/more/a
306 306 $ rm a nested/a nested/more/a
307 307
308 308 $ cp otherignore goodignore
309 309 $ echo "include:badignore" >> otherignore
310 310 $ hg status
311 311 skipping unreadable pattern file 'badignore': $ENOENT$
312 312 A dir/b.o
313 313
314 314 $ mv goodignore otherignore
315 315
316 316 Check using 'include:' while in a non-root directory
317 317
318 318 $ cd ..
319 319 $ hg -R ignorerepo status
320 320 A dir/b.o
321 321 $ cd ignorerepo
322 322
323 323 Check including subincludes
324 324
325 325 $ hg revert -q --all
326 326 $ hg purge --all --config extensions.purge=
327 327 $ echo ".hgignore" > .hgignore
328 328 $ mkdir dir1 dir2
329 329 $ touch dir1/file1 dir1/file2 dir2/file1 dir2/file2
330 330 $ echo "subinclude:dir2/.hgignore" >> .hgignore
331 331 $ echo "glob:file*2" > dir2/.hgignore
332 332 $ hg status
333 333 ? dir1/file1
334 334 ? dir1/file2
335 335 ? dir2/file1
336 336
337 337 Check including subincludes with other patterns
338 338
339 339 $ echo "subinclude:dir1/.hgignore" >> .hgignore
340 340
341 341 $ mkdir dir1/subdir
342 342 $ touch dir1/subdir/file1
343 343 $ echo "rootglob:f?le1" > dir1/.hgignore
344 344 $ hg status
345 345 ? dir1/file2
346 346 ? dir1/subdir/file1
347 347 ? dir2/file1
348 348 $ rm dir1/subdir/file1
349 349
350 350 $ echo "regexp:f.le1" > dir1/.hgignore
351 351 $ hg status
352 352 ? dir1/file2
353 353 ? dir2/file1
354 354
355 355 Check multiple levels of sub-ignores
356 356
357 357 $ touch dir1/subdir/subfile1 dir1/subdir/subfile3 dir1/subdir/subfile4
358 358 $ echo "subinclude:subdir/.hgignore" >> dir1/.hgignore
359 359 $ echo "glob:subfil*3" >> dir1/subdir/.hgignore
360 360
361 361 $ hg status
362 362 ? dir1/file2
363 363 ? dir1/subdir/subfile4
364 364 ? dir2/file1
365 365
366 366 Check include subignore at the same level
367 367
368 368 $ mv dir1/subdir/.hgignore dir1/.hgignoretwo
369 369 $ echo "regexp:f.le1" > dir1/.hgignore
370 370 $ echo "subinclude:.hgignoretwo" >> dir1/.hgignore
371 371 $ echo "glob:file*2" > dir1/.hgignoretwo
372 372
373 373 $ hg status | grep file2
374 374 [1]
375 375 $ hg debugignore dir1/file2
376 376 dir1/file2 is ignored
377 377 (ignore rule in dir2/.hgignore, line 1: 'file*2')
378 378
379 379 #if windows
380 380
381 381 Windows paths are accepted on input
382 382
383 383 $ rm dir1/.hgignore
384 384 $ echo "dir1/file*" >> .hgignore
385 385 $ hg debugignore "dir1\file2"
386 386 dir1/file2 is ignored
387 387 (ignore rule in $TESTTMP\ignorerepo\.hgignore, line 4: 'dir1/file*')
388 388 $ hg up -qC .
389 389
390 390 #endif
@@ -1,652 +1,652
1 1 $ hg init t
2 2 $ cd t
3 3 $ mkdir -p beans
4 4 $ for b in kidney navy turtle borlotti black pinto; do
5 5 > echo $b > beans/$b
6 6 > done
7 7 $ mkdir -p mammals/Procyonidae
8 8 $ for m in cacomistle coatimundi raccoon; do
9 9 > echo $m > mammals/Procyonidae/$m
10 10 > done
11 11 $ echo skunk > mammals/skunk
12 12 $ echo fennel > fennel
13 13 $ echo fenugreek > fenugreek
14 14 $ echo fiddlehead > fiddlehead
15 15 $ hg addremove
16 16 adding beans/black
17 17 adding beans/borlotti
18 18 adding beans/kidney
19 19 adding beans/navy
20 20 adding beans/pinto
21 21 adding beans/turtle
22 22 adding fennel
23 23 adding fenugreek
24 24 adding fiddlehead
25 25 adding mammals/Procyonidae/cacomistle
26 26 adding mammals/Procyonidae/coatimundi
27 27 adding mammals/Procyonidae/raccoon
28 28 adding mammals/skunk
29 29 $ hg commit -m "commit #0"
30 30
31 31 $ hg debugwalk -v
32 32 * matcher:
33 33 <alwaysmatcher>
34 34 f beans/black beans/black
35 35 f beans/borlotti beans/borlotti
36 36 f beans/kidney beans/kidney
37 37 f beans/navy beans/navy
38 38 f beans/pinto beans/pinto
39 39 f beans/turtle beans/turtle
40 40 f fennel fennel
41 41 f fenugreek fenugreek
42 42 f fiddlehead fiddlehead
43 43 f mammals/Procyonidae/cacomistle mammals/Procyonidae/cacomistle
44 44 f mammals/Procyonidae/coatimundi mammals/Procyonidae/coatimundi
45 45 f mammals/Procyonidae/raccoon mammals/Procyonidae/raccoon
46 46 f mammals/skunk mammals/skunk
47 47 $ hg debugwalk -v -I.
48 48 * matcher:
49 49 <includematcher includes=''>
50 50 f beans/black beans/black
51 51 f beans/borlotti beans/borlotti
52 52 f beans/kidney beans/kidney
53 53 f beans/navy beans/navy
54 54 f beans/pinto beans/pinto
55 55 f beans/turtle beans/turtle
56 56 f fennel fennel
57 57 f fenugreek fenugreek
58 58 f fiddlehead fiddlehead
59 59 f mammals/Procyonidae/cacomistle mammals/Procyonidae/cacomistle
60 60 f mammals/Procyonidae/coatimundi mammals/Procyonidae/coatimundi
61 61 f mammals/Procyonidae/raccoon mammals/Procyonidae/raccoon
62 62 f mammals/skunk mammals/skunk
63 63
64 64 $ cd mammals
65 65 $ hg debugwalk -v
66 66 * matcher:
67 67 <alwaysmatcher>
68 68 f beans/black ../beans/black
69 69 f beans/borlotti ../beans/borlotti
70 70 f beans/kidney ../beans/kidney
71 71 f beans/navy ../beans/navy
72 72 f beans/pinto ../beans/pinto
73 73 f beans/turtle ../beans/turtle
74 74 f fennel ../fennel
75 75 f fenugreek ../fenugreek
76 76 f fiddlehead ../fiddlehead
77 77 f mammals/Procyonidae/cacomistle Procyonidae/cacomistle
78 78 f mammals/Procyonidae/coatimundi Procyonidae/coatimundi
79 79 f mammals/Procyonidae/raccoon Procyonidae/raccoon
80 80 f mammals/skunk skunk
81 81 $ hg debugwalk -v -X ../beans
82 82 * matcher:
83 83 <differencematcher
84 84 m1=<alwaysmatcher>,
85 85 m2=<includematcher includes='beans(?:/|$)'>>
86 86 f fennel ../fennel
87 87 f fenugreek ../fenugreek
88 88 f fiddlehead ../fiddlehead
89 89 f mammals/Procyonidae/cacomistle Procyonidae/cacomistle
90 90 f mammals/Procyonidae/coatimundi Procyonidae/coatimundi
91 91 f mammals/Procyonidae/raccoon Procyonidae/raccoon
92 92 f mammals/skunk skunk
93 93 $ hg debugwalk -v -I '*k'
94 94 * matcher:
95 95 <includematcher includes='mammals/[^/]*k(?:/|$)'>
96 96 f mammals/skunk skunk
97 97 $ hg debugwalk -v -I 'glob:*k'
98 98 * matcher:
99 99 <includematcher includes='mammals/[^/]*k(?:/|$)'>
100 100 f mammals/skunk skunk
101 101 $ hg debugwalk -v -I 'relglob:*k'
102 102 * matcher:
103 <includematcher includes='(?:|.*/)[^/]*k(?:/|$)'>
103 <includematcher includes='.*k(?:/|$)'>
104 104 f beans/black ../beans/black
105 105 f fenugreek ../fenugreek
106 106 f mammals/skunk skunk
107 107 $ hg debugwalk -v -I 'relglob:*k' .
108 108 * matcher:
109 109 <intersectionmatcher
110 110 m1=<patternmatcher patterns='mammals(?:/|$)'>,
111 m2=<includematcher includes='(?:|.*/)[^/]*k(?:/|$)'>>
111 m2=<includematcher includes='.*k(?:/|$)'>>
112 112 f mammals/skunk skunk
113 113 $ hg debugwalk -v -I 're:.*k$'
114 114 * matcher:
115 115 <includematcher includes='.*k$'>
116 116 f beans/black ../beans/black
117 117 f fenugreek ../fenugreek
118 118 f mammals/skunk skunk
119 119 $ hg debugwalk -v -I 'relre:.*k$'
120 120 * matcher:
121 121 <includematcher includes='.*.*k$'>
122 122 f beans/black ../beans/black
123 123 f fenugreek ../fenugreek
124 124 f mammals/skunk skunk
125 125 $ hg debugwalk -v -I 'path:beans'
126 126 * matcher:
127 127 <includematcher includes='beans(?:/|$)'>
128 128 f beans/black ../beans/black
129 129 f beans/borlotti ../beans/borlotti
130 130 f beans/kidney ../beans/kidney
131 131 f beans/navy ../beans/navy
132 132 f beans/pinto ../beans/pinto
133 133 f beans/turtle ../beans/turtle
134 134 $ hg debugwalk -v -I 'relpath:detour/../../beans'
135 135 * matcher:
136 136 <includematcher includes='beans(?:/|$)'>
137 137 f beans/black ../beans/black
138 138 f beans/borlotti ../beans/borlotti
139 139 f beans/kidney ../beans/kidney
140 140 f beans/navy ../beans/navy
141 141 f beans/pinto ../beans/pinto
142 142 f beans/turtle ../beans/turtle
143 143
144 144 $ hg debugwalk -v 'rootfilesin:'
145 145 * matcher:
146 146 <patternmatcher patterns="rootfilesin: ['.']">
147 147 f fennel ../fennel
148 148 f fenugreek ../fenugreek
149 149 f fiddlehead ../fiddlehead
150 150 $ hg debugwalk -v -I 'rootfilesin:'
151 151 * matcher:
152 152 <includematcher includes="rootfilesin: ['.']">
153 153 f fennel ../fennel
154 154 f fenugreek ../fenugreek
155 155 f fiddlehead ../fiddlehead
156 156 $ hg debugwalk -v 'rootfilesin:.'
157 157 * matcher:
158 158 <patternmatcher patterns="rootfilesin: ['.']">
159 159 f fennel ../fennel
160 160 f fenugreek ../fenugreek
161 161 f fiddlehead ../fiddlehead
162 162 $ hg debugwalk -v -I 'rootfilesin:.'
163 163 * matcher:
164 164 <includematcher includes="rootfilesin: ['.']">
165 165 f fennel ../fennel
166 166 f fenugreek ../fenugreek
167 167 f fiddlehead ../fiddlehead
168 168 $ hg debugwalk -v -X 'rootfilesin:'
169 169 * matcher:
170 170 <differencematcher
171 171 m1=<alwaysmatcher>,
172 172 m2=<includematcher includes="rootfilesin: ['.']">>
173 173 f beans/black ../beans/black
174 174 f beans/borlotti ../beans/borlotti
175 175 f beans/kidney ../beans/kidney
176 176 f beans/navy ../beans/navy
177 177 f beans/pinto ../beans/pinto
178 178 f beans/turtle ../beans/turtle
179 179 f mammals/Procyonidae/cacomistle Procyonidae/cacomistle
180 180 f mammals/Procyonidae/coatimundi Procyonidae/coatimundi
181 181 f mammals/Procyonidae/raccoon Procyonidae/raccoon
182 182 f mammals/skunk skunk
183 183 $ hg debugwalk -v 'rootfilesin:fennel'
184 184 * matcher:
185 185 <patternmatcher patterns="rootfilesin: ['fennel']">
186 186 $ hg debugwalk -v -I 'rootfilesin:fennel'
187 187 * matcher:
188 188 <includematcher includes="rootfilesin: ['fennel']">
189 189 $ hg debugwalk -v 'rootfilesin:skunk'
190 190 * matcher:
191 191 <patternmatcher patterns="rootfilesin: ['skunk']">
192 192 $ hg debugwalk -v -I 'rootfilesin:skunk'
193 193 * matcher:
194 194 <includematcher includes="rootfilesin: ['skunk']">
195 195 $ hg debugwalk -v 'rootfilesin:beans'
196 196 * matcher:
197 197 <patternmatcher patterns="rootfilesin: ['beans']">
198 198 f beans/black ../beans/black
199 199 f beans/borlotti ../beans/borlotti
200 200 f beans/kidney ../beans/kidney
201 201 f beans/navy ../beans/navy
202 202 f beans/pinto ../beans/pinto
203 203 f beans/turtle ../beans/turtle
204 204 $ hg debugwalk -v -I 'rootfilesin:beans'
205 205 * matcher:
206 206 <includematcher includes="rootfilesin: ['beans']">
207 207 f beans/black ../beans/black
208 208 f beans/borlotti ../beans/borlotti
209 209 f beans/kidney ../beans/kidney
210 210 f beans/navy ../beans/navy
211 211 f beans/pinto ../beans/pinto
212 212 f beans/turtle ../beans/turtle
213 213 $ hg debugwalk -v 'rootfilesin:mammals'
214 214 * matcher:
215 215 <patternmatcher patterns="rootfilesin: ['mammals']">
216 216 f mammals/skunk skunk
217 217 $ hg debugwalk -v -I 'rootfilesin:mammals'
218 218 * matcher:
219 219 <includematcher includes="rootfilesin: ['mammals']">
220 220 f mammals/skunk skunk
221 221 $ hg debugwalk -v 'rootfilesin:mammals/'
222 222 * matcher:
223 223 <patternmatcher patterns="rootfilesin: ['mammals']">
224 224 f mammals/skunk skunk
225 225 $ hg debugwalk -v -I 'rootfilesin:mammals/'
226 226 * matcher:
227 227 <includematcher includes="rootfilesin: ['mammals']">
228 228 f mammals/skunk skunk
229 229 $ hg debugwalk -v -X 'rootfilesin:mammals'
230 230 * matcher:
231 231 <differencematcher
232 232 m1=<alwaysmatcher>,
233 233 m2=<includematcher includes="rootfilesin: ['mammals']">>
234 234 f beans/black ../beans/black
235 235 f beans/borlotti ../beans/borlotti
236 236 f beans/kidney ../beans/kidney
237 237 f beans/navy ../beans/navy
238 238 f beans/pinto ../beans/pinto
239 239 f beans/turtle ../beans/turtle
240 240 f fennel ../fennel
241 241 f fenugreek ../fenugreek
242 242 f fiddlehead ../fiddlehead
243 243 f mammals/Procyonidae/cacomistle Procyonidae/cacomistle
244 244 f mammals/Procyonidae/coatimundi Procyonidae/coatimundi
245 245 f mammals/Procyonidae/raccoon Procyonidae/raccoon
246 246
247 247 $ hg debugwalk -v .
248 248 * matcher:
249 249 <patternmatcher patterns='mammals(?:/|$)'>
250 250 f mammals/Procyonidae/cacomistle Procyonidae/cacomistle
251 251 f mammals/Procyonidae/coatimundi Procyonidae/coatimundi
252 252 f mammals/Procyonidae/raccoon Procyonidae/raccoon
253 253 f mammals/skunk skunk
254 254 $ hg debugwalk -v -I.
255 255 * matcher:
256 256 <includematcher includes='mammals(?:/|$)'>
257 257 f mammals/Procyonidae/cacomistle Procyonidae/cacomistle
258 258 f mammals/Procyonidae/coatimundi Procyonidae/coatimundi
259 259 f mammals/Procyonidae/raccoon Procyonidae/raccoon
260 260 f mammals/skunk skunk
261 261 $ hg debugwalk -v Procyonidae
262 262 * matcher:
263 263 <patternmatcher patterns='mammals/Procyonidae(?:/|$)'>
264 264 f mammals/Procyonidae/cacomistle Procyonidae/cacomistle
265 265 f mammals/Procyonidae/coatimundi Procyonidae/coatimundi
266 266 f mammals/Procyonidae/raccoon Procyonidae/raccoon
267 267
268 268 $ cd Procyonidae
269 269 $ hg debugwalk -v .
270 270 * matcher:
271 271 <patternmatcher patterns='mammals/Procyonidae(?:/|$)'>
272 272 f mammals/Procyonidae/cacomistle cacomistle
273 273 f mammals/Procyonidae/coatimundi coatimundi
274 274 f mammals/Procyonidae/raccoon raccoon
275 275 $ hg debugwalk -v ..
276 276 * matcher:
277 277 <patternmatcher patterns='mammals(?:/|$)'>
278 278 f mammals/Procyonidae/cacomistle cacomistle
279 279 f mammals/Procyonidae/coatimundi coatimundi
280 280 f mammals/Procyonidae/raccoon raccoon
281 281 f mammals/skunk ../skunk
282 282 $ cd ..
283 283
284 284 $ hg debugwalk -v ../beans
285 285 * matcher:
286 286 <patternmatcher patterns='beans(?:/|$)'>
287 287 f beans/black ../beans/black
288 288 f beans/borlotti ../beans/borlotti
289 289 f beans/kidney ../beans/kidney
290 290 f beans/navy ../beans/navy
291 291 f beans/pinto ../beans/pinto
292 292 f beans/turtle ../beans/turtle
293 293 $ hg debugwalk -v .
294 294 * matcher:
295 295 <patternmatcher patterns='mammals(?:/|$)'>
296 296 f mammals/Procyonidae/cacomistle Procyonidae/cacomistle
297 297 f mammals/Procyonidae/coatimundi Procyonidae/coatimundi
298 298 f mammals/Procyonidae/raccoon Procyonidae/raccoon
299 299 f mammals/skunk skunk
300 300 $ hg debugwalk -v .hg
301 301 abort: path 'mammals/.hg' is inside nested repo 'mammals'
302 302 [255]
303 303 $ hg debugwalk -v ../.hg
304 304 abort: path contains illegal component: .hg
305 305 [255]
306 306 $ cd ..
307 307
308 308 $ hg debugwalk -v -Ibeans
309 309 * matcher:
310 310 <includematcher includes='beans(?:/|$)'>
311 311 f beans/black beans/black
312 312 f beans/borlotti beans/borlotti
313 313 f beans/kidney beans/kidney
314 314 f beans/navy beans/navy
315 315 f beans/pinto beans/pinto
316 316 f beans/turtle beans/turtle
317 317 $ hg debugwalk -v -I '{*,{b,m}*/*}k'
318 318 * matcher:
319 319 <includematcher includes='(?:[^/]*|(?:b|m)[^/]*/[^/]*)k(?:/|$)'>
320 320 f beans/black beans/black
321 321 f fenugreek fenugreek
322 322 f mammals/skunk mammals/skunk
323 323 $ hg debugwalk -v -Ibeans mammals
324 324 * matcher:
325 325 <intersectionmatcher
326 326 m1=<patternmatcher patterns='mammals(?:/|$)'>,
327 327 m2=<includematcher includes='beans(?:/|$)'>>
328 328 $ hg debugwalk -v -Inon-existent
329 329 * matcher:
330 330 <includematcher includes='non\\-existent(?:/|$)'>
331 331 $ hg debugwalk -v -Inon-existent -Ibeans/black
332 332 * matcher:
333 333 <includematcher includes='non\\-existent(?:/|$)|beans/black(?:/|$)'>
334 334 f beans/black beans/black
335 335 $ hg debugwalk -v -Ibeans beans/black
336 336 * matcher:
337 337 <intersectionmatcher
338 338 m1=<patternmatcher patterns='beans/black(?:/|$)'>,
339 339 m2=<includematcher includes='beans(?:/|$)'>>
340 340 f beans/black beans/black exact
341 341 $ hg debugwalk -v -Ibeans/black beans
342 342 * matcher:
343 343 <intersectionmatcher
344 344 m1=<patternmatcher patterns='beans(?:/|$)'>,
345 345 m2=<includematcher includes='beans/black(?:/|$)'>>
346 346 f beans/black beans/black
347 347 $ hg debugwalk -v -Xbeans/black beans
348 348 * matcher:
349 349 <differencematcher
350 350 m1=<patternmatcher patterns='beans(?:/|$)'>,
351 351 m2=<includematcher includes='beans/black(?:/|$)'>>
352 352 f beans/borlotti beans/borlotti
353 353 f beans/kidney beans/kidney
354 354 f beans/navy beans/navy
355 355 f beans/pinto beans/pinto
356 356 f beans/turtle beans/turtle
357 357 $ hg debugwalk -v -Xbeans/black -Ibeans
358 358 * matcher:
359 359 <differencematcher
360 360 m1=<includematcher includes='beans(?:/|$)'>,
361 361 m2=<includematcher includes='beans/black(?:/|$)'>>
362 362 f beans/borlotti beans/borlotti
363 363 f beans/kidney beans/kidney
364 364 f beans/navy beans/navy
365 365 f beans/pinto beans/pinto
366 366 f beans/turtle beans/turtle
367 367 $ hg debugwalk -v -Xbeans/black beans/black
368 368 * matcher:
369 369 <differencematcher
370 370 m1=<patternmatcher patterns='beans/black(?:/|$)'>,
371 371 m2=<includematcher includes='beans/black(?:/|$)'>>
372 372 $ hg debugwalk -v -Xbeans/black -Ibeans/black
373 373 * matcher:
374 374 <differencematcher
375 375 m1=<includematcher includes='beans/black(?:/|$)'>,
376 376 m2=<includematcher includes='beans/black(?:/|$)'>>
377 377 $ hg debugwalk -v -Xbeans beans/black
378 378 * matcher:
379 379 <differencematcher
380 380 m1=<patternmatcher patterns='beans/black(?:/|$)'>,
381 381 m2=<includematcher includes='beans(?:/|$)'>>
382 382 $ hg debugwalk -v -Xbeans -Ibeans/black
383 383 * matcher:
384 384 <differencematcher
385 385 m1=<includematcher includes='beans/black(?:/|$)'>,
386 386 m2=<includematcher includes='beans(?:/|$)'>>
387 387 $ hg debugwalk -v 'glob:mammals/../beans/b*'
388 388 * matcher:
389 389 <patternmatcher patterns='beans/b[^/]*$'>
390 390 f beans/black beans/black
391 391 f beans/borlotti beans/borlotti
392 392 $ hg debugwalk -v '-X*/Procyonidae' mammals
393 393 * matcher:
394 394 <differencematcher
395 395 m1=<patternmatcher patterns='mammals(?:/|$)'>,
396 396 m2=<includematcher includes='[^/]*/Procyonidae(?:/|$)'>>
397 397 f mammals/skunk mammals/skunk
398 398 $ hg debugwalk -v path:mammals
399 399 * matcher:
400 400 <patternmatcher patterns='mammals(?:/|$)'>
401 401 f mammals/Procyonidae/cacomistle mammals/Procyonidae/cacomistle
402 402 f mammals/Procyonidae/coatimundi mammals/Procyonidae/coatimundi
403 403 f mammals/Procyonidae/raccoon mammals/Procyonidae/raccoon
404 404 f mammals/skunk mammals/skunk
405 405 $ hg debugwalk -v ..
406 406 abort: .. not under root '$TESTTMP/t'
407 407 [255]
408 408 $ hg debugwalk -v beans/../..
409 409 abort: beans/../.. not under root '$TESTTMP/t'
410 410 [255]
411 411 $ hg debugwalk -v .hg
412 412 abort: path contains illegal component: .hg
413 413 [255]
414 414 $ hg debugwalk -v beans/../.hg
415 415 abort: path contains illegal component: .hg
416 416 [255]
417 417 $ hg debugwalk -v beans/../.hg/data
418 418 abort: path contains illegal component: .hg/data
419 419 [255]
420 420 $ hg debugwalk -v beans/.hg
421 421 abort: path 'beans/.hg' is inside nested repo 'beans'
422 422 [255]
423 423
424 424 Test explicit paths and excludes:
425 425
426 426 $ hg debugwalk -v fennel -X fennel
427 427 * matcher:
428 428 <differencematcher
429 429 m1=<patternmatcher patterns='fennel(?:/|$)'>,
430 430 m2=<includematcher includes='fennel(?:/|$)'>>
431 431 $ hg debugwalk -v fennel -X 'f*'
432 432 * matcher:
433 433 <differencematcher
434 434 m1=<patternmatcher patterns='fennel(?:/|$)'>,
435 435 m2=<includematcher includes='f[^/]*(?:/|$)'>>
436 436 $ hg debugwalk -v beans/black -X 'path:beans'
437 437 * matcher:
438 438 <differencematcher
439 439 m1=<patternmatcher patterns='beans/black(?:/|$)'>,
440 440 m2=<includematcher includes='beans(?:/|$)'>>
441 441 $ hg debugwalk -v -I 'path:beans/black' -X 'path:beans'
442 442 * matcher:
443 443 <differencematcher
444 444 m1=<includematcher includes='beans/black(?:/|$)'>,
445 445 m2=<includematcher includes='beans(?:/|$)'>>
446 446
447 447 Test absolute paths:
448 448
449 449 $ hg debugwalk -v `pwd`/beans
450 450 * matcher:
451 451 <patternmatcher patterns='beans(?:/|$)'>
452 452 f beans/black beans/black
453 453 f beans/borlotti beans/borlotti
454 454 f beans/kidney beans/kidney
455 455 f beans/navy beans/navy
456 456 f beans/pinto beans/pinto
457 457 f beans/turtle beans/turtle
458 458 $ hg debugwalk -v `pwd`/..
459 459 abort: $TESTTMP/t/.. not under root '$TESTTMP/t'
460 460 [255]
461 461
462 462 Test patterns:
463 463
464 464 $ hg debugwalk -v glob:\*
465 465 * matcher:
466 466 <patternmatcher patterns='[^/]*$'>
467 467 f fennel fennel
468 468 f fenugreek fenugreek
469 469 f fiddlehead fiddlehead
470 470 #if eol-in-paths
471 471 $ echo glob:glob > glob:glob
472 472 $ hg addremove
473 473 adding glob:glob
474 474 warning: filename contains ':', which is reserved on Windows: 'glob:glob'
475 475 $ hg debugwalk -v glob:\*
476 476 * matcher:
477 477 <patternmatcher patterns='[^/]*$'>
478 478 f fennel fennel
479 479 f fenugreek fenugreek
480 480 f fiddlehead fiddlehead
481 481 f glob:glob glob:glob
482 482 $ hg debugwalk -v glob:glob
483 483 * matcher:
484 484 <patternmatcher patterns='glob$'>
485 485 glob: $ENOENT$
486 486 $ hg debugwalk -v glob:glob:glob
487 487 * matcher:
488 488 <patternmatcher patterns='glob:glob$'>
489 489 f glob:glob glob:glob exact
490 490 $ hg debugwalk -v path:glob:glob
491 491 * matcher:
492 492 <patternmatcher patterns='glob:glob(?:/|$)'>
493 493 f glob:glob glob:glob exact
494 494 $ rm glob:glob
495 495 $ hg addremove
496 496 removing glob:glob
497 497 #endif
498 498
499 499 $ hg debugwalk -v 'glob:**e'
500 500 * matcher:
501 501 <patternmatcher patterns='.*e$'>
502 502 f beans/turtle beans/turtle
503 503 f mammals/Procyonidae/cacomistle mammals/Procyonidae/cacomistle
504 504
505 505 $ hg debugwalk -v 're:.*[kb]$'
506 506 * matcher:
507 507 <patternmatcher patterns='.*[kb]$'>
508 508 f beans/black beans/black
509 509 f fenugreek fenugreek
510 510 f mammals/skunk mammals/skunk
511 511
512 512 $ hg debugwalk -v path:beans/black
513 513 * matcher:
514 514 <patternmatcher patterns='beans/black(?:/|$)'>
515 515 f beans/black beans/black exact
516 516 $ hg debugwalk -v path:beans//black
517 517 * matcher:
518 518 <patternmatcher patterns='beans/black(?:/|$)'>
519 519 f beans/black beans/black exact
520 520
521 521 $ hg debugwalk -v relglob:Procyonidae
522 522 * matcher:
523 523 <patternmatcher patterns='(?:|.*/)Procyonidae$'>
524 524 $ hg debugwalk -v 'relglob:Procyonidae/**'
525 525 * matcher:
526 526 <patternmatcher patterns='(?:|.*/)Procyonidae/.*$'>
527 527 f mammals/Procyonidae/cacomistle mammals/Procyonidae/cacomistle
528 528 f mammals/Procyonidae/coatimundi mammals/Procyonidae/coatimundi
529 529 f mammals/Procyonidae/raccoon mammals/Procyonidae/raccoon
530 530 $ hg debugwalk -v 'relglob:Procyonidae/**' fennel
531 531 * matcher:
532 532 <patternmatcher patterns='(?:|.*/)Procyonidae/.*$|fennel(?:/|$)'>
533 533 f fennel fennel exact
534 534 f mammals/Procyonidae/cacomistle mammals/Procyonidae/cacomistle
535 535 f mammals/Procyonidae/coatimundi mammals/Procyonidae/coatimundi
536 536 f mammals/Procyonidae/raccoon mammals/Procyonidae/raccoon
537 537 $ hg debugwalk -v beans 'glob:beans/*'
538 538 * matcher:
539 539 <patternmatcher patterns='beans(?:/|$)|beans/[^/]*$'>
540 540 f beans/black beans/black
541 541 f beans/borlotti beans/borlotti
542 542 f beans/kidney beans/kidney
543 543 f beans/navy beans/navy
544 544 f beans/pinto beans/pinto
545 545 f beans/turtle beans/turtle
546 546 $ hg debugwalk -v 'glob:mamm**'
547 547 * matcher:
548 548 <patternmatcher patterns='mamm.*$'>
549 549 f mammals/Procyonidae/cacomistle mammals/Procyonidae/cacomistle
550 550 f mammals/Procyonidae/coatimundi mammals/Procyonidae/coatimundi
551 551 f mammals/Procyonidae/raccoon mammals/Procyonidae/raccoon
552 552 f mammals/skunk mammals/skunk
553 553 $ hg debugwalk -v 'glob:mamm**' fennel
554 554 * matcher:
555 555 <patternmatcher patterns='mamm.*$|fennel(?:/|$)'>
556 556 f fennel fennel exact
557 557 f mammals/Procyonidae/cacomistle mammals/Procyonidae/cacomistle
558 558 f mammals/Procyonidae/coatimundi mammals/Procyonidae/coatimundi
559 559 f mammals/Procyonidae/raccoon mammals/Procyonidae/raccoon
560 560 f mammals/skunk mammals/skunk
561 561 $ hg debugwalk -v 'glob:j*'
562 562 * matcher:
563 563 <patternmatcher patterns='j[^/]*$'>
564 564 $ hg debugwalk -v NOEXIST
565 565 * matcher:
566 566 <patternmatcher patterns='NOEXIST(?:/|$)'>
567 567 NOEXIST: * (glob)
568 568
569 569 #if fifo
570 570 $ mkfifo fifo
571 571 $ hg debugwalk -v fifo
572 572 * matcher:
573 573 <patternmatcher patterns='fifo(?:/|$)'>
574 574 fifo: unsupported file type (type is fifo)
575 575 #endif
576 576
577 577 $ rm fenugreek
578 578 $ hg debugwalk -v fenugreek
579 579 * matcher:
580 580 <patternmatcher patterns='fenugreek(?:/|$)'>
581 581 f fenugreek fenugreek exact
582 582 $ hg rm fenugreek
583 583 $ hg debugwalk -v fenugreek
584 584 * matcher:
585 585 <patternmatcher patterns='fenugreek(?:/|$)'>
586 586 f fenugreek fenugreek exact
587 587 $ touch new
588 588 $ hg debugwalk -v new
589 589 * matcher:
590 590 <patternmatcher patterns='new(?:/|$)'>
591 591 f new new exact
592 592
593 593 $ mkdir ignored
594 594 $ touch ignored/file
595 595 $ echo '^ignored$' > .hgignore
596 596 $ hg debugwalk -v ignored
597 597 * matcher:
598 598 <patternmatcher patterns='ignored(?:/|$)'>
599 599 $ hg debugwalk -v ignored/file
600 600 * matcher:
601 601 <patternmatcher patterns='ignored/file(?:/|$)'>
602 602 f ignored/file ignored/file exact
603 603
604 604 Test listfile and listfile0
605 605
606 606 $ "$PYTHON" -c "open('listfile0', 'wb').write(b'fenugreek\0new\0')"
607 607 $ hg debugwalk -v -I 'listfile0:listfile0'
608 608 * matcher:
609 609 <includematcher includes='fenugreek(?:/|$)|new(?:/|$)'>
610 610 f fenugreek fenugreek
611 611 f new new
612 612 $ "$PYTHON" -c "open('listfile', 'wb').write(b'fenugreek\nnew\r\nmammals/skunk\n')"
613 613 $ hg debugwalk -v -I 'listfile:listfile'
614 614 * matcher:
615 615 <includematcher includes='fenugreek(?:/|$)|new(?:/|$)|mammals/skunk(?:/|$)'>
616 616 f fenugreek fenugreek
617 617 f mammals/skunk mammals/skunk
618 618 f new new
619 619
620 620 $ cd ..
621 621 $ hg debugwalk -v -R t t/mammals/skunk
622 622 * matcher:
623 623 <patternmatcher patterns='mammals/skunk(?:/|$)'>
624 624 f mammals/skunk t/mammals/skunk exact
625 625 $ mkdir t2
626 626 $ cd t2
627 627 $ hg debugwalk -v -R ../t ../t/mammals/skunk
628 628 * matcher:
629 629 <patternmatcher patterns='mammals/skunk(?:/|$)'>
630 630 f mammals/skunk ../t/mammals/skunk exact
631 631 $ hg debugwalk -v --cwd ../t mammals/skunk
632 632 * matcher:
633 633 <patternmatcher patterns='mammals/skunk(?:/|$)'>
634 634 f mammals/skunk mammals/skunk exact
635 635
636 636 $ cd ..
637 637
638 638 Test split patterns on overflow
639 639
640 640 $ cd t
641 641 $ echo fennel > overflow.list
642 642 $ cat >> printnum.py <<EOF
643 643 > from __future__ import print_function
644 644 > for i in range(20000 // 100):
645 645 > print('x' * 100)
646 646 > EOF
647 647 $ "$PYTHON" printnum.py >> overflow.list
648 648 $ echo fenugreek >> overflow.list
649 649 $ hg debugwalk 'listfile:overflow.list' 2>&1 | egrep -v '^xxx'
650 650 f fennel fennel exact
651 651 f fenugreek fenugreek exact
652 652 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now