##// END OF EJS Templates
match: allow passing a context object to match core
Matt Mackall -
r14674:1c151b96 default
parent child Browse files
Show More
@@ -1,1115 +1,1116 b''
1 1 # context.py - changeset and file context objects for mercurial
2 2 #
3 3 # Copyright 2006, 2007 Matt Mackall <mpm@selenic.com>
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 node import nullid, nullrev, short, hex
9 9 from i18n import _
10 10 import ancestor, bdiff, error, util, scmutil, subrepo, patch, encoding
11 11 import match as matchmod
12 12 import os, errno, stat
13 13
14 14 propertycache = util.propertycache
15 15
16 16 class changectx(object):
17 17 """A changecontext object makes access to data related to a particular
18 18 changeset convenient."""
19 19 def __init__(self, repo, changeid=''):
20 20 """changeid is a revision number, node, or tag"""
21 21 if changeid == '':
22 22 changeid = '.'
23 23 self._repo = repo
24 24 if isinstance(changeid, (long, int)):
25 25 self._rev = changeid
26 26 self._node = self._repo.changelog.node(changeid)
27 27 else:
28 28 self._node = self._repo.lookup(changeid)
29 29 self._rev = self._repo.changelog.rev(self._node)
30 30
31 31 def __str__(self):
32 32 return short(self.node())
33 33
34 34 def __int__(self):
35 35 return self.rev()
36 36
37 37 def __repr__(self):
38 38 return "<changectx %s>" % str(self)
39 39
40 40 def __hash__(self):
41 41 try:
42 42 return hash(self._rev)
43 43 except AttributeError:
44 44 return id(self)
45 45
46 46 def __eq__(self, other):
47 47 try:
48 48 return self._rev == other._rev
49 49 except AttributeError:
50 50 return False
51 51
52 52 def __ne__(self, other):
53 53 return not (self == other)
54 54
55 55 def __nonzero__(self):
56 56 return self._rev != nullrev
57 57
58 58 @propertycache
59 59 def _changeset(self):
60 60 return self._repo.changelog.read(self.node())
61 61
62 62 @propertycache
63 63 def _manifest(self):
64 64 return self._repo.manifest.read(self._changeset[0])
65 65
66 66 @propertycache
67 67 def _manifestdelta(self):
68 68 return self._repo.manifest.readdelta(self._changeset[0])
69 69
70 70 @propertycache
71 71 def _parents(self):
72 72 p = self._repo.changelog.parentrevs(self._rev)
73 73 if p[1] == nullrev:
74 74 p = p[:-1]
75 75 return [changectx(self._repo, x) for x in p]
76 76
77 77 @propertycache
78 78 def substate(self):
79 79 return subrepo.state(self, self._repo.ui)
80 80
81 81 def __contains__(self, key):
82 82 return key in self._manifest
83 83
84 84 def __getitem__(self, key):
85 85 return self.filectx(key)
86 86
87 87 def __iter__(self):
88 88 for f in sorted(self._manifest):
89 89 yield f
90 90
91 91 def changeset(self):
92 92 return self._changeset
93 93 def manifest(self):
94 94 return self._manifest
95 95 def manifestnode(self):
96 96 return self._changeset[0]
97 97
98 98 def rev(self):
99 99 return self._rev
100 100 def node(self):
101 101 return self._node
102 102 def hex(self):
103 103 return hex(self._node)
104 104 def user(self):
105 105 return self._changeset[1]
106 106 def date(self):
107 107 return self._changeset[2]
108 108 def files(self):
109 109 return self._changeset[3]
110 110 def description(self):
111 111 return self._changeset[4]
112 112 def branch(self):
113 113 return encoding.tolocal(self._changeset[5].get("branch"))
114 114 def extra(self):
115 115 return self._changeset[5]
116 116 def tags(self):
117 117 return self._repo.nodetags(self._node)
118 118 def bookmarks(self):
119 119 return self._repo.nodebookmarks(self._node)
120 120 def hidden(self):
121 121 return self._rev in self._repo.changelog.hiddenrevs
122 122
123 123 def parents(self):
124 124 """return contexts for each parent changeset"""
125 125 return self._parents
126 126
127 127 def p1(self):
128 128 return self._parents[0]
129 129
130 130 def p2(self):
131 131 if len(self._parents) == 2:
132 132 return self._parents[1]
133 133 return changectx(self._repo, -1)
134 134
135 135 def children(self):
136 136 """return contexts for each child changeset"""
137 137 c = self._repo.changelog.children(self._node)
138 138 return [changectx(self._repo, x) for x in c]
139 139
140 140 def ancestors(self):
141 141 for a in self._repo.changelog.ancestors(self._rev):
142 142 yield changectx(self._repo, a)
143 143
144 144 def descendants(self):
145 145 for d in self._repo.changelog.descendants(self._rev):
146 146 yield changectx(self._repo, d)
147 147
148 148 def _fileinfo(self, path):
149 149 if '_manifest' in self.__dict__:
150 150 try:
151 151 return self._manifest[path], self._manifest.flags(path)
152 152 except KeyError:
153 153 raise error.LookupError(self._node, path,
154 154 _('not found in manifest'))
155 155 if '_manifestdelta' in self.__dict__ or path in self.files():
156 156 if path in self._manifestdelta:
157 157 return self._manifestdelta[path], self._manifestdelta.flags(path)
158 158 node, flag = self._repo.manifest.find(self._changeset[0], path)
159 159 if not node:
160 160 raise error.LookupError(self._node, path,
161 161 _('not found in manifest'))
162 162
163 163 return node, flag
164 164
165 165 def filenode(self, path):
166 166 return self._fileinfo(path)[0]
167 167
168 168 def flags(self, path):
169 169 try:
170 170 return self._fileinfo(path)[1]
171 171 except error.LookupError:
172 172 return ''
173 173
174 174 def filectx(self, path, fileid=None, filelog=None):
175 175 """get a file context from this changeset"""
176 176 if fileid is None:
177 177 fileid = self.filenode(path)
178 178 return filectx(self._repo, path, fileid=fileid,
179 179 changectx=self, filelog=filelog)
180 180
181 181 def ancestor(self, c2):
182 182 """
183 183 return the ancestor context of self and c2
184 184 """
185 185 # deal with workingctxs
186 186 n2 = c2._node
187 187 if n2 is None:
188 188 n2 = c2._parents[0]._node
189 189 n = self._repo.changelog.ancestor(self._node, n2)
190 190 return changectx(self._repo, n)
191 191
192 192 def walk(self, match):
193 193 fset = set(match.files())
194 194 # for dirstate.walk, files=['.'] means "walk the whole tree".
195 195 # follow that here, too
196 196 fset.discard('.')
197 197 for fn in self:
198 198 for ffn in fset:
199 199 # match if the file is the exact name or a directory
200 200 if ffn == fn or fn.startswith("%s/" % ffn):
201 201 fset.remove(ffn)
202 202 break
203 203 if match(fn):
204 204 yield fn
205 205 for fn in sorted(fset):
206 206 if match.bad(fn, _('no such file in rev %s') % self) and match(fn):
207 207 yield fn
208 208
209 209 def sub(self, path):
210 210 return subrepo.subrepo(self, path)
211 211
212 212 def match(self, pats=[], include=None, exclude=None, default='glob'):
213 213 r = self._repo
214 214 return matchmod.match(r.root, r.getcwd(), pats,
215 include, exclude, default, auditor=r.auditor)
215 include, exclude, default,
216 auditor=r.auditor, ctx=self)
216 217
217 218 def diff(self, ctx2=None, match=None, **opts):
218 219 """Returns a diff generator for the given contexts and matcher"""
219 220 if ctx2 is None:
220 221 ctx2 = self.p1()
221 222 if ctx2 is not None and not isinstance(ctx2, changectx):
222 223 ctx2 = self._repo[ctx2]
223 224 diffopts = patch.diffopts(self._repo.ui, opts)
224 225 return patch.diff(self._repo, ctx2.node(), self.node(),
225 226 match=match, opts=diffopts)
226 227
227 228 class filectx(object):
228 229 """A filecontext object makes access to data related to a particular
229 230 filerevision convenient."""
230 231 def __init__(self, repo, path, changeid=None, fileid=None,
231 232 filelog=None, changectx=None):
232 233 """changeid can be a changeset revision, node, or tag.
233 234 fileid can be a file revision or node."""
234 235 self._repo = repo
235 236 self._path = path
236 237
237 238 assert (changeid is not None
238 239 or fileid is not None
239 240 or changectx is not None), \
240 241 ("bad args: changeid=%r, fileid=%r, changectx=%r"
241 242 % (changeid, fileid, changectx))
242 243
243 244 if filelog:
244 245 self._filelog = filelog
245 246
246 247 if changeid is not None:
247 248 self._changeid = changeid
248 249 if changectx is not None:
249 250 self._changectx = changectx
250 251 if fileid is not None:
251 252 self._fileid = fileid
252 253
253 254 @propertycache
254 255 def _changectx(self):
255 256 return changectx(self._repo, self._changeid)
256 257
257 258 @propertycache
258 259 def _filelog(self):
259 260 return self._repo.file(self._path)
260 261
261 262 @propertycache
262 263 def _changeid(self):
263 264 if '_changectx' in self.__dict__:
264 265 return self._changectx.rev()
265 266 else:
266 267 return self._filelog.linkrev(self._filerev)
267 268
268 269 @propertycache
269 270 def _filenode(self):
270 271 if '_fileid' in self.__dict__:
271 272 return self._filelog.lookup(self._fileid)
272 273 else:
273 274 return self._changectx.filenode(self._path)
274 275
275 276 @propertycache
276 277 def _filerev(self):
277 278 return self._filelog.rev(self._filenode)
278 279
279 280 @propertycache
280 281 def _repopath(self):
281 282 return self._path
282 283
283 284 def __nonzero__(self):
284 285 try:
285 286 self._filenode
286 287 return True
287 288 except error.LookupError:
288 289 # file is missing
289 290 return False
290 291
291 292 def __str__(self):
292 293 return "%s@%s" % (self.path(), short(self.node()))
293 294
294 295 def __repr__(self):
295 296 return "<filectx %s>" % str(self)
296 297
297 298 def __hash__(self):
298 299 try:
299 300 return hash((self._path, self._filenode))
300 301 except AttributeError:
301 302 return id(self)
302 303
303 304 def __eq__(self, other):
304 305 try:
305 306 return (self._path == other._path
306 307 and self._filenode == other._filenode)
307 308 except AttributeError:
308 309 return False
309 310
310 311 def __ne__(self, other):
311 312 return not (self == other)
312 313
313 314 def filectx(self, fileid):
314 315 '''opens an arbitrary revision of the file without
315 316 opening a new filelog'''
316 317 return filectx(self._repo, self._path, fileid=fileid,
317 318 filelog=self._filelog)
318 319
319 320 def filerev(self):
320 321 return self._filerev
321 322 def filenode(self):
322 323 return self._filenode
323 324 def flags(self):
324 325 return self._changectx.flags(self._path)
325 326 def filelog(self):
326 327 return self._filelog
327 328
328 329 def rev(self):
329 330 if '_changectx' in self.__dict__:
330 331 return self._changectx.rev()
331 332 if '_changeid' in self.__dict__:
332 333 return self._changectx.rev()
333 334 return self._filelog.linkrev(self._filerev)
334 335
335 336 def linkrev(self):
336 337 return self._filelog.linkrev(self._filerev)
337 338 def node(self):
338 339 return self._changectx.node()
339 340 def hex(self):
340 341 return hex(self.node())
341 342 def user(self):
342 343 return self._changectx.user()
343 344 def date(self):
344 345 return self._changectx.date()
345 346 def files(self):
346 347 return self._changectx.files()
347 348 def description(self):
348 349 return self._changectx.description()
349 350 def branch(self):
350 351 return self._changectx.branch()
351 352 def extra(self):
352 353 return self._changectx.extra()
353 354 def manifest(self):
354 355 return self._changectx.manifest()
355 356 def changectx(self):
356 357 return self._changectx
357 358
358 359 def data(self):
359 360 return self._filelog.read(self._filenode)
360 361 def path(self):
361 362 return self._path
362 363 def size(self):
363 364 return self._filelog.size(self._filerev)
364 365
365 366 def cmp(self, fctx):
366 367 """compare with other file context
367 368
368 369 returns True if different than fctx.
369 370 """
370 371 if (fctx._filerev is None and self._repo._encodefilterpats
371 372 or self.size() == fctx.size()):
372 373 return self._filelog.cmp(self._filenode, fctx.data())
373 374
374 375 return True
375 376
376 377 def renamed(self):
377 378 """check if file was actually renamed in this changeset revision
378 379
379 380 If rename logged in file revision, we report copy for changeset only
380 381 if file revisions linkrev points back to the changeset in question
381 382 or both changeset parents contain different file revisions.
382 383 """
383 384
384 385 renamed = self._filelog.renamed(self._filenode)
385 386 if not renamed:
386 387 return renamed
387 388
388 389 if self.rev() == self.linkrev():
389 390 return renamed
390 391
391 392 name = self.path()
392 393 fnode = self._filenode
393 394 for p in self._changectx.parents():
394 395 try:
395 396 if fnode == p.filenode(name):
396 397 return None
397 398 except error.LookupError:
398 399 pass
399 400 return renamed
400 401
401 402 def parents(self):
402 403 p = self._path
403 404 fl = self._filelog
404 405 pl = [(p, n, fl) for n in self._filelog.parents(self._filenode)]
405 406
406 407 r = self._filelog.renamed(self._filenode)
407 408 if r:
408 409 pl[0] = (r[0], r[1], None)
409 410
410 411 return [filectx(self._repo, p, fileid=n, filelog=l)
411 412 for p, n, l in pl if n != nullid]
412 413
413 414 def p1(self):
414 415 return self.parents()[0]
415 416
416 417 def p2(self):
417 418 p = self.parents()
418 419 if len(p) == 2:
419 420 return p[1]
420 421 return filectx(self._repo, self._path, fileid=-1, filelog=self._filelog)
421 422
422 423 def children(self):
423 424 # hard for renames
424 425 c = self._filelog.children(self._filenode)
425 426 return [filectx(self._repo, self._path, fileid=x,
426 427 filelog=self._filelog) for x in c]
427 428
428 429 def annotate(self, follow=False, linenumber=None):
429 430 '''returns a list of tuples of (ctx, line) for each line
430 431 in the file, where ctx is the filectx of the node where
431 432 that line was last changed.
432 433 This returns tuples of ((ctx, linenumber), line) for each line,
433 434 if "linenumber" parameter is NOT "None".
434 435 In such tuples, linenumber means one at the first appearance
435 436 in the managed file.
436 437 To reduce annotation cost,
437 438 this returns fixed value(False is used) as linenumber,
438 439 if "linenumber" parameter is "False".'''
439 440
440 441 def decorate_compat(text, rev):
441 442 return ([rev] * len(text.splitlines()), text)
442 443
443 444 def without_linenumber(text, rev):
444 445 return ([(rev, False)] * len(text.splitlines()), text)
445 446
446 447 def with_linenumber(text, rev):
447 448 size = len(text.splitlines())
448 449 return ([(rev, i) for i in xrange(1, size + 1)], text)
449 450
450 451 decorate = (((linenumber is None) and decorate_compat) or
451 452 (linenumber and with_linenumber) or
452 453 without_linenumber)
453 454
454 455 def pair(parent, child):
455 456 for a1, a2, b1, b2 in bdiff.blocks(parent[1], child[1]):
456 457 child[0][b1:b2] = parent[0][a1:a2]
457 458 return child
458 459
459 460 getlog = util.lrucachefunc(lambda x: self._repo.file(x))
460 461 def getctx(path, fileid):
461 462 log = path == self._path and self._filelog or getlog(path)
462 463 return filectx(self._repo, path, fileid=fileid, filelog=log)
463 464 getctx = util.lrucachefunc(getctx)
464 465
465 466 def parents(f):
466 467 # we want to reuse filectx objects as much as possible
467 468 p = f._path
468 469 if f._filerev is None: # working dir
469 470 pl = [(n.path(), n.filerev()) for n in f.parents()]
470 471 else:
471 472 pl = [(p, n) for n in f._filelog.parentrevs(f._filerev)]
472 473
473 474 if follow:
474 475 r = f.renamed()
475 476 if r:
476 477 pl[0] = (r[0], getlog(r[0]).rev(r[1]))
477 478
478 479 return [getctx(p, n) for p, n in pl if n != nullrev]
479 480
480 481 # use linkrev to find the first changeset where self appeared
481 482 if self.rev() != self.linkrev():
482 483 base = self.filectx(self.filerev())
483 484 else:
484 485 base = self
485 486
486 487 # This algorithm would prefer to be recursive, but Python is a
487 488 # bit recursion-hostile. Instead we do an iterative
488 489 # depth-first search.
489 490
490 491 visit = [base]
491 492 hist = {}
492 493 pcache = {}
493 494 needed = {base: 1}
494 495 while visit:
495 496 f = visit[-1]
496 497 if f not in pcache:
497 498 pcache[f] = parents(f)
498 499
499 500 ready = True
500 501 pl = pcache[f]
501 502 for p in pl:
502 503 if p not in hist:
503 504 ready = False
504 505 visit.append(p)
505 506 needed[p] = needed.get(p, 0) + 1
506 507 if ready:
507 508 visit.pop()
508 509 curr = decorate(f.data(), f)
509 510 for p in pl:
510 511 curr = pair(hist[p], curr)
511 512 if needed[p] == 1:
512 513 del hist[p]
513 514 else:
514 515 needed[p] -= 1
515 516
516 517 hist[f] = curr
517 518 pcache[f] = []
518 519
519 520 return zip(hist[base][0], hist[base][1].splitlines(True))
520 521
521 522 def ancestor(self, fc2, actx=None):
522 523 """
523 524 find the common ancestor file context, if any, of self, and fc2
524 525
525 526 If actx is given, it must be the changectx of the common ancestor
526 527 of self's and fc2's respective changesets.
527 528 """
528 529
529 530 if actx is None:
530 531 actx = self.changectx().ancestor(fc2.changectx())
531 532
532 533 # the trivial case: changesets are unrelated, files must be too
533 534 if not actx:
534 535 return None
535 536
536 537 # the easy case: no (relevant) renames
537 538 if fc2.path() == self.path() and self.path() in actx:
538 539 return actx[self.path()]
539 540 acache = {}
540 541
541 542 # prime the ancestor cache for the working directory
542 543 for c in (self, fc2):
543 544 if c._filerev is None:
544 545 pl = [(n.path(), n.filenode()) for n in c.parents()]
545 546 acache[(c._path, None)] = pl
546 547
547 548 flcache = {self._repopath:self._filelog, fc2._repopath:fc2._filelog}
548 549 def parents(vertex):
549 550 if vertex in acache:
550 551 return acache[vertex]
551 552 f, n = vertex
552 553 if f not in flcache:
553 554 flcache[f] = self._repo.file(f)
554 555 fl = flcache[f]
555 556 pl = [(f, p) for p in fl.parents(n) if p != nullid]
556 557 re = fl.renamed(n)
557 558 if re:
558 559 pl.append(re)
559 560 acache[vertex] = pl
560 561 return pl
561 562
562 563 a, b = (self._path, self._filenode), (fc2._path, fc2._filenode)
563 564 v = ancestor.ancestor(a, b, parents)
564 565 if v:
565 566 f, n = v
566 567 return filectx(self._repo, f, fileid=n, filelog=flcache[f])
567 568
568 569 return None
569 570
570 571 def ancestors(self):
571 572 visit = {}
572 573 c = self
573 574 while True:
574 575 for parent in c.parents():
575 576 visit[(parent.rev(), parent.node())] = parent
576 577 if not visit:
577 578 break
578 579 c = visit.pop(max(visit))
579 580 yield c
580 581
581 582 class workingctx(changectx):
582 583 """A workingctx object makes access to data related to
583 584 the current working directory convenient.
584 585 date - any valid date string or (unixtime, offset), or None.
585 586 user - username string, or None.
586 587 extra - a dictionary of extra values, or None.
587 588 changes - a list of file lists as returned by localrepo.status()
588 589 or None to use the repository status.
589 590 """
590 591 def __init__(self, repo, text="", user=None, date=None, extra=None,
591 592 changes=None):
592 593 self._repo = repo
593 594 self._rev = None
594 595 self._node = None
595 596 self._text = text
596 597 if date:
597 598 self._date = util.parsedate(date)
598 599 if user:
599 600 self._user = user
600 601 if changes:
601 602 self._status = list(changes[:4])
602 603 self._unknown = changes[4]
603 604 self._ignored = changes[5]
604 605 self._clean = changes[6]
605 606 else:
606 607 self._unknown = None
607 608 self._ignored = None
608 609 self._clean = None
609 610
610 611 self._extra = {}
611 612 if extra:
612 613 self._extra = extra.copy()
613 614 if 'branch' not in self._extra:
614 615 try:
615 616 branch = encoding.fromlocal(self._repo.dirstate.branch())
616 617 except UnicodeDecodeError:
617 618 raise util.Abort(_('branch name not in UTF-8!'))
618 619 self._extra['branch'] = branch
619 620 if self._extra['branch'] == '':
620 621 self._extra['branch'] = 'default'
621 622
622 623 def __str__(self):
623 624 return str(self._parents[0]) + "+"
624 625
625 626 def __repr__(self):
626 627 return "<workingctx %s>" % str(self)
627 628
628 629 def __nonzero__(self):
629 630 return True
630 631
631 632 def __contains__(self, key):
632 633 return self._repo.dirstate[key] not in "?r"
633 634
634 635 @propertycache
635 636 def _manifest(self):
636 637 """generate a manifest corresponding to the working directory"""
637 638
638 639 if self._unknown is None:
639 640 self.status(unknown=True)
640 641
641 642 man = self._parents[0].manifest().copy()
642 643 copied = self._repo.dirstate.copies()
643 644 if len(self._parents) > 1:
644 645 man2 = self.p2().manifest()
645 646 def getman(f):
646 647 if f in man:
647 648 return man
648 649 return man2
649 650 else:
650 651 getman = lambda f: man
651 652 def cf(f):
652 653 f = copied.get(f, f)
653 654 return getman(f).flags(f)
654 655 ff = self._repo.dirstate.flagfunc(cf)
655 656 modified, added, removed, deleted = self._status
656 657 unknown = self._unknown
657 658 for i, l in (("a", added), ("m", modified), ("u", unknown)):
658 659 for f in l:
659 660 orig = copied.get(f, f)
660 661 man[f] = getman(orig).get(orig, nullid) + i
661 662 try:
662 663 man.set(f, ff(f))
663 664 except OSError:
664 665 pass
665 666
666 667 for f in deleted + removed:
667 668 if f in man:
668 669 del man[f]
669 670
670 671 return man
671 672
672 673 def __iter__(self):
673 674 d = self._repo.dirstate
674 675 for f in d:
675 676 if d[f] != 'r':
676 677 yield f
677 678
678 679 @propertycache
679 680 def _status(self):
680 681 return self._repo.status()[:4]
681 682
682 683 @propertycache
683 684 def _user(self):
684 685 return self._repo.ui.username()
685 686
686 687 @propertycache
687 688 def _date(self):
688 689 return util.makedate()
689 690
690 691 @propertycache
691 692 def _parents(self):
692 693 p = self._repo.dirstate.parents()
693 694 if p[1] == nullid:
694 695 p = p[:-1]
695 696 self._parents = [changectx(self._repo, x) for x in p]
696 697 return self._parents
697 698
698 699 def status(self, ignored=False, clean=False, unknown=False):
699 700 """Explicit status query
700 701 Unless this method is used to query the working copy status, the
701 702 _status property will implicitly read the status using its default
702 703 arguments."""
703 704 stat = self._repo.status(ignored=ignored, clean=clean, unknown=unknown)
704 705 self._unknown = self._ignored = self._clean = None
705 706 if unknown:
706 707 self._unknown = stat[4]
707 708 if ignored:
708 709 self._ignored = stat[5]
709 710 if clean:
710 711 self._clean = stat[6]
711 712 self._status = stat[:4]
712 713 return stat
713 714
714 715 def manifest(self):
715 716 return self._manifest
716 717 def user(self):
717 718 return self._user or self._repo.ui.username()
718 719 def date(self):
719 720 return self._date
720 721 def description(self):
721 722 return self._text
722 723 def files(self):
723 724 return sorted(self._status[0] + self._status[1] + self._status[2])
724 725
725 726 def modified(self):
726 727 return self._status[0]
727 728 def added(self):
728 729 return self._status[1]
729 730 def removed(self):
730 731 return self._status[2]
731 732 def deleted(self):
732 733 return self._status[3]
733 734 def unknown(self):
734 735 assert self._unknown is not None # must call status first
735 736 return self._unknown
736 737 def ignored(self):
737 738 assert self._ignored is not None # must call status first
738 739 return self._ignored
739 740 def clean(self):
740 741 assert self._clean is not None # must call status first
741 742 return self._clean
742 743 def branch(self):
743 744 return encoding.tolocal(self._extra['branch'])
744 745 def extra(self):
745 746 return self._extra
746 747
747 748 def tags(self):
748 749 t = []
749 750 for p in self.parents():
750 751 t.extend(p.tags())
751 752 return t
752 753
753 754 def bookmarks(self):
754 755 b = []
755 756 for p in self.parents():
756 757 b.extend(p.bookmarks())
757 758 return b
758 759
759 760 def children(self):
760 761 return []
761 762
762 763 def flags(self, path):
763 764 if '_manifest' in self.__dict__:
764 765 try:
765 766 return self._manifest.flags(path)
766 767 except KeyError:
767 768 return ''
768 769
769 770 orig = self._repo.dirstate.copies().get(path, path)
770 771
771 772 def findflag(ctx):
772 773 mnode = ctx.changeset()[0]
773 774 node, flag = self._repo.manifest.find(mnode, orig)
774 775 ff = self._repo.dirstate.flagfunc(lambda x: flag or '')
775 776 try:
776 777 return ff(path)
777 778 except OSError:
778 779 pass
779 780
780 781 flag = findflag(self._parents[0])
781 782 if flag is None and len(self.parents()) > 1:
782 783 flag = findflag(self._parents[1])
783 784 if flag is None or self._repo.dirstate[path] == 'r':
784 785 return ''
785 786 return flag
786 787
787 788 def filectx(self, path, filelog=None):
788 789 """get a file context from the working directory"""
789 790 return workingfilectx(self._repo, path, workingctx=self,
790 791 filelog=filelog)
791 792
792 793 def ancestor(self, c2):
793 794 """return the ancestor context of self and c2"""
794 795 return self._parents[0].ancestor(c2) # punt on two parents for now
795 796
796 797 def walk(self, match):
797 798 return sorted(self._repo.dirstate.walk(match, self.substate.keys(),
798 799 True, False))
799 800
800 801 def dirty(self, missing=False):
801 802 "check whether a working directory is modified"
802 803 # check subrepos first
803 804 for s in self.substate:
804 805 if self.sub(s).dirty():
805 806 return True
806 807 # check current working dir
807 808 return (self.p2() or self.branch() != self.p1().branch() or
808 809 self.modified() or self.added() or self.removed() or
809 810 (missing and self.deleted()))
810 811
811 812 def add(self, list, prefix=""):
812 813 join = lambda f: os.path.join(prefix, f)
813 814 wlock = self._repo.wlock()
814 815 ui, ds = self._repo.ui, self._repo.dirstate
815 816 try:
816 817 rejected = []
817 818 for f in list:
818 819 scmutil.checkportable(ui, join(f))
819 820 p = self._repo.wjoin(f)
820 821 try:
821 822 st = os.lstat(p)
822 823 except OSError:
823 824 ui.warn(_("%s does not exist!\n") % join(f))
824 825 rejected.append(f)
825 826 continue
826 827 if st.st_size > 10000000:
827 828 ui.warn(_("%s: up to %d MB of RAM may be required "
828 829 "to manage this file\n"
829 830 "(use 'hg revert %s' to cancel the "
830 831 "pending addition)\n")
831 832 % (f, 3 * st.st_size // 1000000, join(f)))
832 833 if not (stat.S_ISREG(st.st_mode) or stat.S_ISLNK(st.st_mode)):
833 834 ui.warn(_("%s not added: only files and symlinks "
834 835 "supported currently\n") % join(f))
835 836 rejected.append(p)
836 837 elif ds[f] in 'amn':
837 838 ui.warn(_("%s already tracked!\n") % join(f))
838 839 elif ds[f] == 'r':
839 840 ds.normallookup(f)
840 841 else:
841 842 ds.add(f)
842 843 return rejected
843 844 finally:
844 845 wlock.release()
845 846
846 847 def forget(self, files):
847 848 wlock = self._repo.wlock()
848 849 try:
849 850 for f in files:
850 851 if self._repo.dirstate[f] != 'a':
851 852 self._repo.dirstate.remove(f)
852 853 elif f not in self._repo.dirstate:
853 854 self._repo.ui.warn(_("%s not tracked!\n") % f)
854 855 else:
855 856 self._repo.dirstate.drop(f)
856 857 finally:
857 858 wlock.release()
858 859
859 860 def ancestors(self):
860 861 for a in self._repo.changelog.ancestors(
861 862 *[p.rev() for p in self._parents]):
862 863 yield changectx(self._repo, a)
863 864
864 865 def undelete(self, list):
865 866 pctxs = self.parents()
866 867 wlock = self._repo.wlock()
867 868 try:
868 869 for f in list:
869 870 if self._repo.dirstate[f] != 'r':
870 871 self._repo.ui.warn(_("%s not removed!\n") % f)
871 872 else:
872 873 fctx = f in pctxs[0] and pctxs[0][f] or pctxs[1][f]
873 874 t = fctx.data()
874 875 self._repo.wwrite(f, t, fctx.flags())
875 876 self._repo.dirstate.normal(f)
876 877 finally:
877 878 wlock.release()
878 879
879 880 def copy(self, source, dest):
880 881 p = self._repo.wjoin(dest)
881 882 if not os.path.lexists(p):
882 883 self._repo.ui.warn(_("%s does not exist!\n") % dest)
883 884 elif not (os.path.isfile(p) or os.path.islink(p)):
884 885 self._repo.ui.warn(_("copy failed: %s is not a file or a "
885 886 "symbolic link\n") % dest)
886 887 else:
887 888 wlock = self._repo.wlock()
888 889 try:
889 890 if self._repo.dirstate[dest] in '?r':
890 891 self._repo.dirstate.add(dest)
891 892 self._repo.dirstate.copy(source, dest)
892 893 finally:
893 894 wlock.release()
894 895
895 896 class workingfilectx(filectx):
896 897 """A workingfilectx object makes access to data related to a particular
897 898 file in the working directory convenient."""
898 899 def __init__(self, repo, path, filelog=None, workingctx=None):
899 900 """changeid can be a changeset revision, node, or tag.
900 901 fileid can be a file revision or node."""
901 902 self._repo = repo
902 903 self._path = path
903 904 self._changeid = None
904 905 self._filerev = self._filenode = None
905 906
906 907 if filelog:
907 908 self._filelog = filelog
908 909 if workingctx:
909 910 self._changectx = workingctx
910 911
911 912 @propertycache
912 913 def _changectx(self):
913 914 return workingctx(self._repo)
914 915
915 916 def __nonzero__(self):
916 917 return True
917 918
918 919 def __str__(self):
919 920 return "%s@%s" % (self.path(), self._changectx)
920 921
921 922 def __repr__(self):
922 923 return "<workingfilectx %s>" % str(self)
923 924
924 925 def data(self):
925 926 return self._repo.wread(self._path)
926 927 def renamed(self):
927 928 rp = self._repo.dirstate.copied(self._path)
928 929 if not rp:
929 930 return None
930 931 return rp, self._changectx._parents[0]._manifest.get(rp, nullid)
931 932
932 933 def parents(self):
933 934 '''return parent filectxs, following copies if necessary'''
934 935 def filenode(ctx, path):
935 936 return ctx._manifest.get(path, nullid)
936 937
937 938 path = self._path
938 939 fl = self._filelog
939 940 pcl = self._changectx._parents
940 941 renamed = self.renamed()
941 942
942 943 if renamed:
943 944 pl = [renamed + (None,)]
944 945 else:
945 946 pl = [(path, filenode(pcl[0], path), fl)]
946 947
947 948 for pc in pcl[1:]:
948 949 pl.append((path, filenode(pc, path), fl))
949 950
950 951 return [filectx(self._repo, p, fileid=n, filelog=l)
951 952 for p, n, l in pl if n != nullid]
952 953
953 954 def children(self):
954 955 return []
955 956
956 957 def size(self):
957 958 return os.lstat(self._repo.wjoin(self._path)).st_size
958 959 def date(self):
959 960 t, tz = self._changectx.date()
960 961 try:
961 962 return (int(os.lstat(self._repo.wjoin(self._path)).st_mtime), tz)
962 963 except OSError, err:
963 964 if err.errno != errno.ENOENT:
964 965 raise
965 966 return (t, tz)
966 967
967 968 def cmp(self, fctx):
968 969 """compare with other file context
969 970
970 971 returns True if different than fctx.
971 972 """
972 973 # fctx should be a filectx (not a wfctx)
973 974 # invert comparison to reuse the same code path
974 975 return fctx.cmp(self)
975 976
976 977 class memctx(object):
977 978 """Use memctx to perform in-memory commits via localrepo.commitctx().
978 979
979 980 Revision information is supplied at initialization time while
980 981 related files data and is made available through a callback
981 982 mechanism. 'repo' is the current localrepo, 'parents' is a
982 983 sequence of two parent revisions identifiers (pass None for every
983 984 missing parent), 'text' is the commit message and 'files' lists
984 985 names of files touched by the revision (normalized and relative to
985 986 repository root).
986 987
987 988 filectxfn(repo, memctx, path) is a callable receiving the
988 989 repository, the current memctx object and the normalized path of
989 990 requested file, relative to repository root. It is fired by the
990 991 commit function for every file in 'files', but calls order is
991 992 undefined. If the file is available in the revision being
992 993 committed (updated or added), filectxfn returns a memfilectx
993 994 object. If the file was removed, filectxfn raises an
994 995 IOError. Moved files are represented by marking the source file
995 996 removed and the new file added with copy information (see
996 997 memfilectx).
997 998
998 999 user receives the committer name and defaults to current
999 1000 repository username, date is the commit date in any format
1000 1001 supported by util.parsedate() and defaults to current date, extra
1001 1002 is a dictionary of metadata or is left empty.
1002 1003 """
1003 1004 def __init__(self, repo, parents, text, files, filectxfn, user=None,
1004 1005 date=None, extra=None):
1005 1006 self._repo = repo
1006 1007 self._rev = None
1007 1008 self._node = None
1008 1009 self._text = text
1009 1010 self._date = date and util.parsedate(date) or util.makedate()
1010 1011 self._user = user
1011 1012 parents = [(p or nullid) for p in parents]
1012 1013 p1, p2 = parents
1013 1014 self._parents = [changectx(self._repo, p) for p in (p1, p2)]
1014 1015 files = sorted(set(files))
1015 1016 self._status = [files, [], [], [], []]
1016 1017 self._filectxfn = filectxfn
1017 1018
1018 1019 self._extra = extra and extra.copy() or {}
1019 1020 if self._extra.get('branch', '') == '':
1020 1021 self._extra['branch'] = 'default'
1021 1022
1022 1023 def __str__(self):
1023 1024 return str(self._parents[0]) + "+"
1024 1025
1025 1026 def __int__(self):
1026 1027 return self._rev
1027 1028
1028 1029 def __nonzero__(self):
1029 1030 return True
1030 1031
1031 1032 def __getitem__(self, key):
1032 1033 return self.filectx(key)
1033 1034
1034 1035 def p1(self):
1035 1036 return self._parents[0]
1036 1037 def p2(self):
1037 1038 return self._parents[1]
1038 1039
1039 1040 def user(self):
1040 1041 return self._user or self._repo.ui.username()
1041 1042 def date(self):
1042 1043 return self._date
1043 1044 def description(self):
1044 1045 return self._text
1045 1046 def files(self):
1046 1047 return self.modified()
1047 1048 def modified(self):
1048 1049 return self._status[0]
1049 1050 def added(self):
1050 1051 return self._status[1]
1051 1052 def removed(self):
1052 1053 return self._status[2]
1053 1054 def deleted(self):
1054 1055 return self._status[3]
1055 1056 def unknown(self):
1056 1057 return self._status[4]
1057 1058 def ignored(self):
1058 1059 return self._status[5]
1059 1060 def clean(self):
1060 1061 return self._status[6]
1061 1062 def branch(self):
1062 1063 return encoding.tolocal(self._extra['branch'])
1063 1064 def extra(self):
1064 1065 return self._extra
1065 1066 def flags(self, f):
1066 1067 return self[f].flags()
1067 1068
1068 1069 def parents(self):
1069 1070 """return contexts for each parent changeset"""
1070 1071 return self._parents
1071 1072
1072 1073 def filectx(self, path, filelog=None):
1073 1074 """get a file context from the working directory"""
1074 1075 return self._filectxfn(self._repo, self, path)
1075 1076
1076 1077 def commit(self):
1077 1078 """commit context to the repo"""
1078 1079 return self._repo.commitctx(self)
1079 1080
1080 1081 class memfilectx(object):
1081 1082 """memfilectx represents an in-memory file to commit.
1082 1083
1083 1084 See memctx for more details.
1084 1085 """
1085 1086 def __init__(self, path, data, islink=False, isexec=False, copied=None):
1086 1087 """
1087 1088 path is the normalized file path relative to repository root.
1088 1089 data is the file content as a string.
1089 1090 islink is True if the file is a symbolic link.
1090 1091 isexec is True if the file is executable.
1091 1092 copied is the source file path if current file was copied in the
1092 1093 revision being committed, or None."""
1093 1094 self._path = path
1094 1095 self._data = data
1095 1096 self._flags = (islink and 'l' or '') + (isexec and 'x' or '')
1096 1097 self._copied = None
1097 1098 if copied:
1098 1099 self._copied = (copied, nullid)
1099 1100
1100 1101 def __nonzero__(self):
1101 1102 return True
1102 1103 def __str__(self):
1103 1104 return "%s@%s" % (self.path(), self._changectx)
1104 1105 def path(self):
1105 1106 return self._path
1106 1107 def data(self):
1107 1108 return self._data
1108 1109 def flags(self):
1109 1110 return self._flags
1110 1111 def isexec(self):
1111 1112 return 'x' in self._flags
1112 1113 def islink(self):
1113 1114 return 'l' in self._flags
1114 1115 def renamed(self):
1115 1116 return self._copied
@@ -1,310 +1,311 b''
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 import re
9 9 import scmutil, util
10 10 from i18n import _
11 11
12 12 class match(object):
13 13 def __init__(self, root, cwd, patterns, include=[], exclude=[],
14 default='glob', exact=False, auditor=None):
14 default='glob', exact=False, auditor=None, ctx=None):
15 15 """build an object to match a set of file patterns
16 16
17 17 arguments:
18 18 root - the canonical root of the tree you're matching against
19 19 cwd - the current working directory, if relevant
20 20 patterns - patterns to find
21 21 include - patterns to include
22 22 exclude - patterns to exclude
23 23 default - if a pattern in names has no explicit type, assume this one
24 24 exact - patterns are actually literals
25 25
26 26 a pattern is one of:
27 27 'glob:<glob>' - a glob relative to cwd
28 28 're:<regexp>' - a regular expression
29 29 'path:<path>' - a path relative to canonroot
30 30 'relglob:<glob>' - an unrooted glob (*.c matches C files in all dirs)
31 31 'relpath:<path>' - a path relative to cwd
32 32 'relre:<regexp>' - a regexp that needn't match the start of a name
33 33 '<something>' - a pattern of the specified default type
34 34 """
35 35
36 36 self._root = root
37 37 self._cwd = cwd
38 38 self._files = []
39 39 self._anypats = bool(include or exclude)
40 self._ctx = ctx
40 41
41 42 if include:
42 43 pats = _normalize(include, 'glob', root, cwd, auditor)
43 44 self.includepat, im = _buildmatch(pats, '(?:/|$)')
44 45 if exclude:
45 46 pats = _normalize(exclude, 'glob', root, cwd, auditor)
46 47 self.excludepat, em = _buildmatch(pats, '(?:/|$)')
47 48 if exact:
48 49 self._files = patterns
49 50 pm = self.exact
50 51 elif patterns:
51 52 pats = _normalize(patterns, default, root, cwd, auditor)
52 53 self._files = _roots(pats)
53 54 self._anypats = self._anypats or _anypats(pats)
54 55 self.patternspat, pm = _buildmatch(pats, '$')
55 56
56 57 if patterns or exact:
57 58 if include:
58 59 if exclude:
59 60 m = lambda f: im(f) and not em(f) and pm(f)
60 61 else:
61 62 m = lambda f: im(f) and pm(f)
62 63 else:
63 64 if exclude:
64 65 m = lambda f: not em(f) and pm(f)
65 66 else:
66 67 m = pm
67 68 else:
68 69 if include:
69 70 if exclude:
70 71 m = lambda f: im(f) and not em(f)
71 72 else:
72 73 m = im
73 74 else:
74 75 if exclude:
75 76 m = lambda f: not em(f)
76 77 else:
77 78 m = lambda f: True
78 79
79 80 self.matchfn = m
80 81 self._fmap = set(self._files)
81 82
82 83 def __call__(self, fn):
83 84 return self.matchfn(fn)
84 85 def __iter__(self):
85 86 for f in self._files:
86 87 yield f
87 88 def bad(self, f, msg):
88 89 '''callback for each explicit file that can't be
89 90 found/accessed, with an error message
90 91 '''
91 92 pass
92 93 def dir(self, f):
93 94 pass
94 95 def missing(self, f):
95 96 pass
96 97 def exact(self, f):
97 98 return f in self._fmap
98 99 def rel(self, f):
99 100 return util.pathto(self._root, self._cwd, f)
100 101 def files(self):
101 102 return self._files
102 103 def anypats(self):
103 104 return self._anypats
104 105
105 106 class exact(match):
106 107 def __init__(self, root, cwd, files):
107 108 match.__init__(self, root, cwd, files, exact = True)
108 109
109 110 class always(match):
110 111 def __init__(self, root, cwd):
111 112 match.__init__(self, root, cwd, [])
112 113
113 114 class narrowmatcher(match):
114 115 """Adapt a matcher to work on a subdirectory only.
115 116
116 117 The paths are remapped to remove/insert the path as needed:
117 118
118 119 >>> m1 = match('root', '', ['a.txt', 'sub/b.txt'])
119 120 >>> m2 = narrowmatcher('sub', m1)
120 121 >>> bool(m2('a.txt'))
121 122 False
122 123 >>> bool(m2('b.txt'))
123 124 True
124 125 >>> bool(m2.matchfn('a.txt'))
125 126 False
126 127 >>> bool(m2.matchfn('b.txt'))
127 128 True
128 129 >>> m2.files()
129 130 ['b.txt']
130 131 >>> m2.exact('b.txt')
131 132 True
132 133 >>> m2.rel('b.txt')
133 134 'b.txt'
134 135 >>> def bad(f, msg):
135 136 ... print "%s: %s" % (f, msg)
136 137 >>> m1.bad = bad
137 138 >>> m2.bad('x.txt', 'No such file')
138 139 sub/x.txt: No such file
139 140 """
140 141
141 142 def __init__(self, path, matcher):
142 143 self._root = matcher._root
143 144 self._cwd = matcher._cwd
144 145 self._path = path
145 146 self._matcher = matcher
146 147
147 148 self._files = [f[len(path) + 1:] for f in matcher._files
148 149 if f.startswith(path + "/")]
149 150 self._anypats = matcher._anypats
150 151 self.matchfn = lambda fn: matcher.matchfn(self._path + "/" + fn)
151 152 self._fmap = set(self._files)
152 153
153 154 def bad(self, f, msg):
154 155 self._matcher.bad(self._path + "/" + f, msg)
155 156
156 157 def patkind(pat):
157 158 return _patsplit(pat, None)[0]
158 159
159 160 def _patsplit(pat, default):
160 161 """Split a string into an optional pattern kind prefix and the
161 162 actual pattern."""
162 163 if ':' in pat:
163 164 kind, val = pat.split(':', 1)
164 165 if kind in ('re', 'glob', 'path', 'relglob', 'relpath', 'relre',
165 166 'listfile', 'listfile0'):
166 167 return kind, val
167 168 return default, pat
168 169
169 170 def _globre(pat):
170 171 "convert a glob pattern into a regexp"
171 172 i, n = 0, len(pat)
172 173 res = ''
173 174 group = 0
174 175 escape = re.escape
175 176 def peek():
176 177 return i < n and pat[i]
177 178 while i < n:
178 179 c = pat[i]
179 180 i += 1
180 181 if c not in '*?[{},\\':
181 182 res += escape(c)
182 183 elif c == '*':
183 184 if peek() == '*':
184 185 i += 1
185 186 res += '.*'
186 187 else:
187 188 res += '[^/]*'
188 189 elif c == '?':
189 190 res += '.'
190 191 elif c == '[':
191 192 j = i
192 193 if j < n and pat[j] in '!]':
193 194 j += 1
194 195 while j < n and pat[j] != ']':
195 196 j += 1
196 197 if j >= n:
197 198 res += '\\['
198 199 else:
199 200 stuff = pat[i:j].replace('\\','\\\\')
200 201 i = j + 1
201 202 if stuff[0] == '!':
202 203 stuff = '^' + stuff[1:]
203 204 elif stuff[0] == '^':
204 205 stuff = '\\' + stuff
205 206 res = '%s[%s]' % (res, stuff)
206 207 elif c == '{':
207 208 group += 1
208 209 res += '(?:'
209 210 elif c == '}' and group:
210 211 res += ')'
211 212 group -= 1
212 213 elif c == ',' and group:
213 214 res += '|'
214 215 elif c == '\\':
215 216 p = peek()
216 217 if p:
217 218 i += 1
218 219 res += escape(p)
219 220 else:
220 221 res += escape(c)
221 222 else:
222 223 res += escape(c)
223 224 return res
224 225
225 226 def _regex(kind, name, tail):
226 227 '''convert a pattern into a regular expression'''
227 228 if not name:
228 229 return ''
229 230 if kind == 're':
230 231 return name
231 232 elif kind == 'path':
232 233 return '^' + re.escape(name) + '(?:/|$)'
233 234 elif kind == 'relglob':
234 235 return '(?:|.*/)' + _globre(name) + tail
235 236 elif kind == 'relpath':
236 237 return re.escape(name) + '(?:/|$)'
237 238 elif kind == 'relre':
238 239 if name.startswith('^'):
239 240 return name
240 241 return '.*' + name
241 242 return _globre(name) + tail
242 243
243 244 def _buildmatch(pats, tail):
244 245 """build a matching function from a set of patterns"""
245 246 try:
246 247 pat = '(?:%s)' % '|'.join([_regex(k, p, tail) for (k, p) in pats])
247 248 if len(pat) > 20000:
248 249 raise OverflowError()
249 250 return pat, re.compile(pat).match
250 251 except OverflowError:
251 252 # We're using a Python with a tiny regex engine and we
252 253 # made it explode, so we'll divide the pattern list in two
253 254 # until it works
254 255 l = len(pats)
255 256 if l < 2:
256 257 raise
257 258 pata, a = _buildmatch(pats[:l//2], tail)
258 259 patb, b = _buildmatch(pats[l//2:], tail)
259 260 return pat, lambda s: a(s) or b(s)
260 261 except re.error:
261 262 for k, p in pats:
262 263 try:
263 264 re.compile('(?:%s)' % _regex(k, p, tail))
264 265 except re.error:
265 266 raise util.Abort(_("invalid pattern (%s): %s") % (k, p))
266 267 raise util.Abort(_("invalid pattern"))
267 268
268 269 def _normalize(names, default, root, cwd, auditor):
269 270 pats = []
270 271 for kind, name in [_patsplit(p, default) for p in names]:
271 272 if kind in ('glob', 'relpath'):
272 273 name = scmutil.canonpath(root, cwd, name, auditor)
273 274 elif kind in ('relglob', 'path'):
274 275 name = util.normpath(name)
275 276 elif kind in ('listfile', 'listfile0'):
276 277 try:
277 278 files = util.readfile(name)
278 279 if kind == 'listfile0':
279 280 files = files.split('\0')
280 281 else:
281 282 files = files.splitlines()
282 283 files = [f for f in files if f]
283 284 except EnvironmentError:
284 285 raise util.Abort(_("unable to read file list (%s)") % name)
285 286 pats += _normalize(files, default, root, cwd, auditor)
286 287 continue
287 288
288 289 pats.append((kind, name))
289 290 return pats
290 291
291 292 def _roots(patterns):
292 293 r = []
293 294 for kind, name in patterns:
294 295 if kind == 'glob': # find the non-glob prefix
295 296 root = []
296 297 for p in name.split('/'):
297 298 if '[' in p or '{' in p or '*' in p or '?' in p:
298 299 break
299 300 root.append(p)
300 301 r.append('/'.join(root) or '.')
301 302 elif kind in ('relpath', 'path'):
302 303 r.append(name or '.')
303 304 elif kind == 'relglob':
304 305 r.append('.')
305 306 return r
306 307
307 308 def _anypats(patterns):
308 309 for kind, name in patterns:
309 310 if kind in ('glob', 're', 'relglob', 'relre'):
310 311 return True
General Comments 0
You need to be logged in to leave comments. Login now