##// END OF EJS Templates
revset-node: speedup by a few hundred fold...
Pierre-Yves David -
r23005:9bfe6835 default
parent child Browse files
Show More
@@ -1,2967 +1,2970 b''
1 1 # revset.py - revision set queries for mercurial
2 2 #
3 3 # Copyright 2010 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 import re
9 9 import parser, util, error, discovery, hbisect, phases
10 10 import node
11 11 import heapq
12 12 import match as matchmod
13 13 import ancestor as ancestormod
14 14 from i18n import _
15 15 import encoding
16 16 import obsolete as obsmod
17 17 import pathutil
18 18 import repoview
19 19
20 20 def _revancestors(repo, revs, followfirst):
21 21 """Like revlog.ancestors(), but supports followfirst."""
22 22 cut = followfirst and 1 or None
23 23 cl = repo.changelog
24 24
25 25 def iterate():
26 26 revqueue, revsnode = None, None
27 27 h = []
28 28
29 29 revs.sort(reverse=True)
30 30 revqueue = util.deque(revs)
31 31 if revqueue:
32 32 revsnode = revqueue.popleft()
33 33 heapq.heappush(h, -revsnode)
34 34
35 35 seen = set([node.nullrev])
36 36 while h:
37 37 current = -heapq.heappop(h)
38 38 if current not in seen:
39 39 if revsnode and current == revsnode:
40 40 if revqueue:
41 41 revsnode = revqueue.popleft()
42 42 heapq.heappush(h, -revsnode)
43 43 seen.add(current)
44 44 yield current
45 45 for parent in cl.parentrevs(current)[:cut]:
46 46 if parent != node.nullrev:
47 47 heapq.heappush(h, -parent)
48 48
49 49 return generatorset(iterate(), iterasc=False)
50 50
51 51 def _revdescendants(repo, revs, followfirst):
52 52 """Like revlog.descendants() but supports followfirst."""
53 53 cut = followfirst and 1 or None
54 54
55 55 def iterate():
56 56 cl = repo.changelog
57 57 first = min(revs)
58 58 nullrev = node.nullrev
59 59 if first == nullrev:
60 60 # Are there nodes with a null first parent and a non-null
61 61 # second one? Maybe. Do we care? Probably not.
62 62 for i in cl:
63 63 yield i
64 64 else:
65 65 seen = set(revs)
66 66 for i in cl.revs(first + 1):
67 67 for x in cl.parentrevs(i)[:cut]:
68 68 if x != nullrev and x in seen:
69 69 seen.add(i)
70 70 yield i
71 71 break
72 72
73 73 return generatorset(iterate(), iterasc=True)
74 74
75 75 def _revsbetween(repo, roots, heads):
76 76 """Return all paths between roots and heads, inclusive of both endpoint
77 77 sets."""
78 78 if not roots:
79 79 return baseset()
80 80 parentrevs = repo.changelog.parentrevs
81 81 visit = list(heads)
82 82 reachable = set()
83 83 seen = {}
84 84 minroot = min(roots)
85 85 roots = set(roots)
86 86 # open-code the post-order traversal due to the tiny size of
87 87 # sys.getrecursionlimit()
88 88 while visit:
89 89 rev = visit.pop()
90 90 if rev in roots:
91 91 reachable.add(rev)
92 92 parents = parentrevs(rev)
93 93 seen[rev] = parents
94 94 for parent in parents:
95 95 if parent >= minroot and parent not in seen:
96 96 visit.append(parent)
97 97 if not reachable:
98 98 return baseset()
99 99 for rev in sorted(seen):
100 100 for parent in seen[rev]:
101 101 if parent in reachable:
102 102 reachable.add(rev)
103 103 return baseset(sorted(reachable))
104 104
105 105 elements = {
106 106 "(": (20, ("group", 1, ")"), ("func", 1, ")")),
107 107 "~": (18, None, ("ancestor", 18)),
108 108 "^": (18, None, ("parent", 18), ("parentpost", 18)),
109 109 "-": (5, ("negate", 19), ("minus", 5)),
110 110 "::": (17, ("dagrangepre", 17), ("dagrange", 17),
111 111 ("dagrangepost", 17)),
112 112 "..": (17, ("dagrangepre", 17), ("dagrange", 17),
113 113 ("dagrangepost", 17)),
114 114 ":": (15, ("rangepre", 15), ("range", 15), ("rangepost", 15)),
115 115 "not": (10, ("not", 10)),
116 116 "!": (10, ("not", 10)),
117 117 "and": (5, None, ("and", 5)),
118 118 "&": (5, None, ("and", 5)),
119 119 "or": (4, None, ("or", 4)),
120 120 "|": (4, None, ("or", 4)),
121 121 "+": (4, None, ("or", 4)),
122 122 ",": (2, None, ("list", 2)),
123 123 ")": (0, None, None),
124 124 "symbol": (0, ("symbol",), None),
125 125 "string": (0, ("string",), None),
126 126 "end": (0, None, None),
127 127 }
128 128
129 129 keywords = set(['and', 'or', 'not'])
130 130
131 131 def tokenize(program, lookup=None):
132 132 '''
133 133 Parse a revset statement into a stream of tokens
134 134
135 135 Check that @ is a valid unquoted token character (issue3686):
136 136 >>> list(tokenize("@::"))
137 137 [('symbol', '@', 0), ('::', None, 1), ('end', None, 3)]
138 138
139 139 '''
140 140
141 141 pos, l = 0, len(program)
142 142 while pos < l:
143 143 c = program[pos]
144 144 if c.isspace(): # skip inter-token whitespace
145 145 pass
146 146 elif c == ':' and program[pos:pos + 2] == '::': # look ahead carefully
147 147 yield ('::', None, pos)
148 148 pos += 1 # skip ahead
149 149 elif c == '.' and program[pos:pos + 2] == '..': # look ahead carefully
150 150 yield ('..', None, pos)
151 151 pos += 1 # skip ahead
152 152 elif c in "():,-|&+!~^": # handle simple operators
153 153 yield (c, None, pos)
154 154 elif (c in '"\'' or c == 'r' and
155 155 program[pos:pos + 2] in ("r'", 'r"')): # handle quoted strings
156 156 if c == 'r':
157 157 pos += 1
158 158 c = program[pos]
159 159 decode = lambda x: x
160 160 else:
161 161 decode = lambda x: x.decode('string-escape')
162 162 pos += 1
163 163 s = pos
164 164 while pos < l: # find closing quote
165 165 d = program[pos]
166 166 if d == '\\': # skip over escaped characters
167 167 pos += 2
168 168 continue
169 169 if d == c:
170 170 yield ('string', decode(program[s:pos]), s)
171 171 break
172 172 pos += 1
173 173 else:
174 174 raise error.ParseError(_("unterminated string"), s)
175 175 # gather up a symbol/keyword
176 176 elif c.isalnum() or c in '._@' or ord(c) > 127:
177 177 s = pos
178 178 pos += 1
179 179 while pos < l: # find end of symbol
180 180 d = program[pos]
181 181 if not (d.isalnum() or d in "-._/@" or ord(d) > 127):
182 182 break
183 183 if d == '.' and program[pos - 1] == '.': # special case for ..
184 184 pos -= 1
185 185 break
186 186 pos += 1
187 187 sym = program[s:pos]
188 188 if sym in keywords: # operator keywords
189 189 yield (sym, None, s)
190 190 elif '-' in sym:
191 191 # some jerk gave us foo-bar-baz, try to check if it's a symbol
192 192 if lookup and lookup(sym):
193 193 # looks like a real symbol
194 194 yield ('symbol', sym, s)
195 195 else:
196 196 # looks like an expression
197 197 parts = sym.split('-')
198 198 for p in parts[:-1]:
199 199 if p: # possible consecutive -
200 200 yield ('symbol', p, s)
201 201 s += len(p)
202 202 yield ('-', None, pos)
203 203 s += 1
204 204 if parts[-1]: # possible trailing -
205 205 yield ('symbol', parts[-1], s)
206 206 else:
207 207 yield ('symbol', sym, s)
208 208 pos -= 1
209 209 else:
210 210 raise error.ParseError(_("syntax error"), pos)
211 211 pos += 1
212 212 yield ('end', None, pos)
213 213
214 214 # helpers
215 215
216 216 def getstring(x, err):
217 217 if x and (x[0] == 'string' or x[0] == 'symbol'):
218 218 return x[1]
219 219 raise error.ParseError(err)
220 220
221 221 def getlist(x):
222 222 if not x:
223 223 return []
224 224 if x[0] == 'list':
225 225 return getlist(x[1]) + [x[2]]
226 226 return [x]
227 227
228 228 def getargs(x, min, max, err):
229 229 l = getlist(x)
230 230 if len(l) < min or (max >= 0 and len(l) > max):
231 231 raise error.ParseError(err)
232 232 return l
233 233
234 234 def getset(repo, subset, x):
235 235 if not x:
236 236 raise error.ParseError(_("missing argument"))
237 237 s = methods[x[0]](repo, subset, *x[1:])
238 238 if util.safehasattr(s, 'isascending'):
239 239 return s
240 240 return baseset(s)
241 241
242 242 def _getrevsource(repo, r):
243 243 extra = repo[r].extra()
244 244 for label in ('source', 'transplant_source', 'rebase_source'):
245 245 if label in extra:
246 246 try:
247 247 return repo[extra[label]].rev()
248 248 except error.RepoLookupError:
249 249 pass
250 250 return None
251 251
252 252 # operator methods
253 253
254 254 def stringset(repo, subset, x):
255 255 x = repo[x].rev()
256 256 if x == -1 and len(subset) == len(repo):
257 257 return baseset([-1])
258 258 if len(subset) == len(repo) or x in subset:
259 259 return baseset([x])
260 260 return baseset()
261 261
262 262 def symbolset(repo, subset, x):
263 263 if x in symbols:
264 264 raise error.ParseError(_("can't use %s here") % x)
265 265 return stringset(repo, subset, x)
266 266
267 267 def rangeset(repo, subset, x, y):
268 268 cl = baseset(repo.changelog)
269 269 m = getset(repo, cl, x)
270 270 n = getset(repo, cl, y)
271 271
272 272 if not m or not n:
273 273 return baseset()
274 274 m, n = m.first(), n.last()
275 275
276 276 if m < n:
277 277 r = spanset(repo, m, n + 1)
278 278 else:
279 279 r = spanset(repo, m, n - 1)
280 280 return r & subset
281 281
282 282 def dagrange(repo, subset, x, y):
283 283 r = spanset(repo)
284 284 xs = _revsbetween(repo, getset(repo, r, x), getset(repo, r, y))
285 285 return xs & subset
286 286
287 287 def andset(repo, subset, x, y):
288 288 return getset(repo, getset(repo, subset, x), y)
289 289
290 290 def orset(repo, subset, x, y):
291 291 xl = getset(repo, subset, x)
292 292 yl = getset(repo, subset - xl, y)
293 293 return xl + yl
294 294
295 295 def notset(repo, subset, x):
296 296 return subset - getset(repo, subset, x)
297 297
298 298 def listset(repo, subset, a, b):
299 299 raise error.ParseError(_("can't use a list in this context"))
300 300
301 301 def func(repo, subset, a, b):
302 302 if a[0] == 'symbol' and a[1] in symbols:
303 303 return symbols[a[1]](repo, subset, b)
304 304 raise error.ParseError(_("not a function: %s") % a[1])
305 305
306 306 # functions
307 307
308 308 def adds(repo, subset, x):
309 309 """``adds(pattern)``
310 310 Changesets that add a file matching pattern.
311 311
312 312 The pattern without explicit kind like ``glob:`` is expected to be
313 313 relative to the current directory and match against a file or a
314 314 directory.
315 315 """
316 316 # i18n: "adds" is a keyword
317 317 pat = getstring(x, _("adds requires a pattern"))
318 318 return checkstatus(repo, subset, pat, 1)
319 319
320 320 def ancestor(repo, subset, x):
321 321 """``ancestor(*changeset)``
322 322 A greatest common ancestor of the changesets.
323 323
324 324 Accepts 0 or more changesets.
325 325 Will return empty list when passed no args.
326 326 Greatest common ancestor of a single changeset is that changeset.
327 327 """
328 328 # i18n: "ancestor" is a keyword
329 329 l = getlist(x)
330 330 rl = spanset(repo)
331 331 anc = None
332 332
333 333 # (getset(repo, rl, i) for i in l) generates a list of lists
334 334 for revs in (getset(repo, rl, i) for i in l):
335 335 for r in revs:
336 336 if anc is None:
337 337 anc = repo[r]
338 338 else:
339 339 anc = anc.ancestor(repo[r])
340 340
341 341 if anc is not None and anc.rev() in subset:
342 342 return baseset([anc.rev()])
343 343 return baseset()
344 344
345 345 def _ancestors(repo, subset, x, followfirst=False):
346 346 heads = getset(repo, spanset(repo), x)
347 347 if not heads:
348 348 return baseset()
349 349 s = _revancestors(repo, heads, followfirst)
350 350 return subset & s
351 351
352 352 def ancestors(repo, subset, x):
353 353 """``ancestors(set)``
354 354 Changesets that are ancestors of a changeset in set.
355 355 """
356 356 return _ancestors(repo, subset, x)
357 357
358 358 def _firstancestors(repo, subset, x):
359 359 # ``_firstancestors(set)``
360 360 # Like ``ancestors(set)`` but follows only the first parents.
361 361 return _ancestors(repo, subset, x, followfirst=True)
362 362
363 363 def ancestorspec(repo, subset, x, n):
364 364 """``set~n``
365 365 Changesets that are the Nth ancestor (first parents only) of a changeset
366 366 in set.
367 367 """
368 368 try:
369 369 n = int(n[1])
370 370 except (TypeError, ValueError):
371 371 raise error.ParseError(_("~ expects a number"))
372 372 ps = set()
373 373 cl = repo.changelog
374 374 for r in getset(repo, baseset(cl), x):
375 375 for i in range(n):
376 376 r = cl.parentrevs(r)[0]
377 377 ps.add(r)
378 378 return subset & ps
379 379
380 380 def author(repo, subset, x):
381 381 """``author(string)``
382 382 Alias for ``user(string)``.
383 383 """
384 384 # i18n: "author" is a keyword
385 385 n = encoding.lower(getstring(x, _("author requires a string")))
386 386 kind, pattern, matcher = _substringmatcher(n)
387 387 return subset.filter(lambda x: matcher(encoding.lower(repo[x].user())))
388 388
389 389 def only(repo, subset, x):
390 390 """``only(set, [set])``
391 391 Changesets that are ancestors of the first set that are not ancestors
392 392 of any other head in the repo. If a second set is specified, the result
393 393 is ancestors of the first set that are not ancestors of the second set
394 394 (i.e. ::<set1> - ::<set2>).
395 395 """
396 396 cl = repo.changelog
397 397 # i18n: "only" is a keyword
398 398 args = getargs(x, 1, 2, _('only takes one or two arguments'))
399 399 include = getset(repo, spanset(repo), args[0])
400 400 if len(args) == 1:
401 401 if not include:
402 402 return baseset()
403 403
404 404 descendants = set(_revdescendants(repo, include, False))
405 405 exclude = [rev for rev in cl.headrevs()
406 406 if not rev in descendants and not rev in include]
407 407 else:
408 408 exclude = getset(repo, spanset(repo), args[1])
409 409
410 410 results = set(ancestormod.missingancestors(include, exclude, cl.parentrevs))
411 411 return subset & results
412 412
413 413 def bisect(repo, subset, x):
414 414 """``bisect(string)``
415 415 Changesets marked in the specified bisect status:
416 416
417 417 - ``good``, ``bad``, ``skip``: csets explicitly marked as good/bad/skip
418 418 - ``goods``, ``bads`` : csets topologically good/bad
419 419 - ``range`` : csets taking part in the bisection
420 420 - ``pruned`` : csets that are goods, bads or skipped
421 421 - ``untested`` : csets whose fate is yet unknown
422 422 - ``ignored`` : csets ignored due to DAG topology
423 423 - ``current`` : the cset currently being bisected
424 424 """
425 425 # i18n: "bisect" is a keyword
426 426 status = getstring(x, _("bisect requires a string")).lower()
427 427 state = set(hbisect.get(repo, status))
428 428 return subset & state
429 429
430 430 # Backward-compatibility
431 431 # - no help entry so that we do not advertise it any more
432 432 def bisected(repo, subset, x):
433 433 return bisect(repo, subset, x)
434 434
435 435 def bookmark(repo, subset, x):
436 436 """``bookmark([name])``
437 437 The named bookmark or all bookmarks.
438 438
439 439 If `name` starts with `re:`, the remainder of the name is treated as
440 440 a regular expression. To match a bookmark that actually starts with `re:`,
441 441 use the prefix `literal:`.
442 442 """
443 443 # i18n: "bookmark" is a keyword
444 444 args = getargs(x, 0, 1, _('bookmark takes one or no arguments'))
445 445 if args:
446 446 bm = getstring(args[0],
447 447 # i18n: "bookmark" is a keyword
448 448 _('the argument to bookmark must be a string'))
449 449 kind, pattern, matcher = _stringmatcher(bm)
450 450 bms = set()
451 451 if kind == 'literal':
452 452 bmrev = repo._bookmarks.get(pattern, None)
453 453 if not bmrev:
454 454 raise util.Abort(_("bookmark '%s' does not exist") % bm)
455 455 bms.add(repo[bmrev].rev())
456 456 else:
457 457 matchrevs = set()
458 458 for name, bmrev in repo._bookmarks.iteritems():
459 459 if matcher(name):
460 460 matchrevs.add(bmrev)
461 461 if not matchrevs:
462 462 raise util.Abort(_("no bookmarks exist that match '%s'")
463 463 % pattern)
464 464 for bmrev in matchrevs:
465 465 bms.add(repo[bmrev].rev())
466 466 else:
467 467 bms = set([repo[r].rev()
468 468 for r in repo._bookmarks.values()])
469 469 bms -= set([node.nullrev])
470 470 return subset & bms
471 471
472 472 def branch(repo, subset, x):
473 473 """``branch(string or set)``
474 474 All changesets belonging to the given branch or the branches of the given
475 475 changesets.
476 476
477 477 If `string` starts with `re:`, the remainder of the name is treated as
478 478 a regular expression. To match a branch that actually starts with `re:`,
479 479 use the prefix `literal:`.
480 480 """
481 481 try:
482 482 b = getstring(x, '')
483 483 except error.ParseError:
484 484 # not a string, but another revspec, e.g. tip()
485 485 pass
486 486 else:
487 487 kind, pattern, matcher = _stringmatcher(b)
488 488 if kind == 'literal':
489 489 # note: falls through to the revspec case if no branch with
490 490 # this name exists
491 491 if pattern in repo.branchmap():
492 492 return subset.filter(lambda r: matcher(repo[r].branch()))
493 493 else:
494 494 return subset.filter(lambda r: matcher(repo[r].branch()))
495 495
496 496 s = getset(repo, spanset(repo), x)
497 497 b = set()
498 498 for r in s:
499 499 b.add(repo[r].branch())
500 500 c = s.__contains__
501 501 return subset.filter(lambda r: c(r) or repo[r].branch() in b)
502 502
503 503 def bumped(repo, subset, x):
504 504 """``bumped()``
505 505 Mutable changesets marked as successors of public changesets.
506 506
507 507 Only non-public and non-obsolete changesets can be `bumped`.
508 508 """
509 509 # i18n: "bumped" is a keyword
510 510 getargs(x, 0, 0, _("bumped takes no arguments"))
511 511 bumped = obsmod.getrevs(repo, 'bumped')
512 512 return subset & bumped
513 513
514 514 def bundle(repo, subset, x):
515 515 """``bundle()``
516 516 Changesets in the bundle.
517 517
518 518 Bundle must be specified by the -R option."""
519 519
520 520 try:
521 521 bundlerevs = repo.changelog.bundlerevs
522 522 except AttributeError:
523 523 raise util.Abort(_("no bundle provided - specify with -R"))
524 524 return subset & bundlerevs
525 525
526 526 def checkstatus(repo, subset, pat, field):
527 527 hasset = matchmod.patkind(pat) == 'set'
528 528
529 529 def matches(x):
530 530 m = None
531 531 fname = None
532 532 c = repo[x]
533 533 if not m or hasset:
534 534 m = matchmod.match(repo.root, repo.getcwd(), [pat], ctx=c)
535 535 if not m.anypats() and len(m.files()) == 1:
536 536 fname = m.files()[0]
537 537 if fname is not None:
538 538 if fname not in c.files():
539 539 return False
540 540 else:
541 541 for f in c.files():
542 542 if m(f):
543 543 break
544 544 else:
545 545 return False
546 546 files = repo.status(c.p1().node(), c.node())[field]
547 547 if fname is not None:
548 548 if fname in files:
549 549 return True
550 550 else:
551 551 for f in files:
552 552 if m(f):
553 553 return True
554 554
555 555 return subset.filter(matches)
556 556
557 557 def _children(repo, narrow, parentset):
558 558 cs = set()
559 559 if not parentset:
560 560 return baseset(cs)
561 561 pr = repo.changelog.parentrevs
562 562 minrev = min(parentset)
563 563 for r in narrow:
564 564 if r <= minrev:
565 565 continue
566 566 for p in pr(r):
567 567 if p in parentset:
568 568 cs.add(r)
569 569 return baseset(cs)
570 570
571 571 def children(repo, subset, x):
572 572 """``children(set)``
573 573 Child changesets of changesets in set.
574 574 """
575 575 s = getset(repo, baseset(repo), x)
576 576 cs = _children(repo, subset, s)
577 577 return subset & cs
578 578
579 579 def closed(repo, subset, x):
580 580 """``closed()``
581 581 Changeset is closed.
582 582 """
583 583 # i18n: "closed" is a keyword
584 584 getargs(x, 0, 0, _("closed takes no arguments"))
585 585 return subset.filter(lambda r: repo[r].closesbranch())
586 586
587 587 def contains(repo, subset, x):
588 588 """``contains(pattern)``
589 589 The revision's manifest contains a file matching pattern (but might not
590 590 modify it). See :hg:`help patterns` for information about file patterns.
591 591
592 592 The pattern without explicit kind like ``glob:`` is expected to be
593 593 relative to the current directory and match against a file exactly
594 594 for efficiency.
595 595 """
596 596 # i18n: "contains" is a keyword
597 597 pat = getstring(x, _("contains requires a pattern"))
598 598
599 599 def matches(x):
600 600 if not matchmod.patkind(pat):
601 601 pats = pathutil.canonpath(repo.root, repo.getcwd(), pat)
602 602 if pats in repo[x]:
603 603 return True
604 604 else:
605 605 c = repo[x]
606 606 m = matchmod.match(repo.root, repo.getcwd(), [pat], ctx=c)
607 607 for f in c.manifest():
608 608 if m(f):
609 609 return True
610 610 return False
611 611
612 612 return subset.filter(matches)
613 613
614 614 def converted(repo, subset, x):
615 615 """``converted([id])``
616 616 Changesets converted from the given identifier in the old repository if
617 617 present, or all converted changesets if no identifier is specified.
618 618 """
619 619
620 620 # There is exactly no chance of resolving the revision, so do a simple
621 621 # string compare and hope for the best
622 622
623 623 rev = None
624 624 # i18n: "converted" is a keyword
625 625 l = getargs(x, 0, 1, _('converted takes one or no arguments'))
626 626 if l:
627 627 # i18n: "converted" is a keyword
628 628 rev = getstring(l[0], _('converted requires a revision'))
629 629
630 630 def _matchvalue(r):
631 631 source = repo[r].extra().get('convert_revision', None)
632 632 return source is not None and (rev is None or source.startswith(rev))
633 633
634 634 return subset.filter(lambda r: _matchvalue(r))
635 635
636 636 def date(repo, subset, x):
637 637 """``date(interval)``
638 638 Changesets within the interval, see :hg:`help dates`.
639 639 """
640 640 # i18n: "date" is a keyword
641 641 ds = getstring(x, _("date requires a string"))
642 642 dm = util.matchdate(ds)
643 643 return subset.filter(lambda x: dm(repo[x].date()[0]))
644 644
645 645 def desc(repo, subset, x):
646 646 """``desc(string)``
647 647 Search commit message for string. The match is case-insensitive.
648 648 """
649 649 # i18n: "desc" is a keyword
650 650 ds = encoding.lower(getstring(x, _("desc requires a string")))
651 651
652 652 def matches(x):
653 653 c = repo[x]
654 654 return ds in encoding.lower(c.description())
655 655
656 656 return subset.filter(matches)
657 657
658 658 def _descendants(repo, subset, x, followfirst=False):
659 659 roots = getset(repo, spanset(repo), x)
660 660 if not roots:
661 661 return baseset()
662 662 s = _revdescendants(repo, roots, followfirst)
663 663
664 664 # Both sets need to be ascending in order to lazily return the union
665 665 # in the correct order.
666 666 base = subset & roots
667 667 desc = subset & s
668 668 result = base + desc
669 669 if subset.isascending():
670 670 result.sort()
671 671 elif subset.isdescending():
672 672 result.sort(reverse=True)
673 673 else:
674 674 result = subset & result
675 675 return result
676 676
677 677 def descendants(repo, subset, x):
678 678 """``descendants(set)``
679 679 Changesets which are descendants of changesets in set.
680 680 """
681 681 return _descendants(repo, subset, x)
682 682
683 683 def _firstdescendants(repo, subset, x):
684 684 # ``_firstdescendants(set)``
685 685 # Like ``descendants(set)`` but follows only the first parents.
686 686 return _descendants(repo, subset, x, followfirst=True)
687 687
688 688 def destination(repo, subset, x):
689 689 """``destination([set])``
690 690 Changesets that were created by a graft, transplant or rebase operation,
691 691 with the given revisions specified as the source. Omitting the optional set
692 692 is the same as passing all().
693 693 """
694 694 if x is not None:
695 695 sources = getset(repo, spanset(repo), x)
696 696 else:
697 697 sources = getall(repo, spanset(repo), x)
698 698
699 699 dests = set()
700 700
701 701 # subset contains all of the possible destinations that can be returned, so
702 702 # iterate over them and see if their source(s) were provided in the arg set.
703 703 # Even if the immediate src of r is not in the arg set, src's source (or
704 704 # further back) may be. Scanning back further than the immediate src allows
705 705 # transitive transplants and rebases to yield the same results as transitive
706 706 # grafts.
707 707 for r in subset:
708 708 src = _getrevsource(repo, r)
709 709 lineage = None
710 710
711 711 while src is not None:
712 712 if lineage is None:
713 713 lineage = list()
714 714
715 715 lineage.append(r)
716 716
717 717 # The visited lineage is a match if the current source is in the arg
718 718 # set. Since every candidate dest is visited by way of iterating
719 719 # subset, any dests further back in the lineage will be tested by a
720 720 # different iteration over subset. Likewise, if the src was already
721 721 # selected, the current lineage can be selected without going back
722 722 # further.
723 723 if src in sources or src in dests:
724 724 dests.update(lineage)
725 725 break
726 726
727 727 r = src
728 728 src = _getrevsource(repo, r)
729 729
730 730 return subset.filter(dests.__contains__)
731 731
732 732 def divergent(repo, subset, x):
733 733 """``divergent()``
734 734 Final successors of changesets with an alternative set of final successors.
735 735 """
736 736 # i18n: "divergent" is a keyword
737 737 getargs(x, 0, 0, _("divergent takes no arguments"))
738 738 divergent = obsmod.getrevs(repo, 'divergent')
739 739 return subset & divergent
740 740
741 741 def draft(repo, subset, x):
742 742 """``draft()``
743 743 Changeset in draft phase."""
744 744 # i18n: "draft" is a keyword
745 745 getargs(x, 0, 0, _("draft takes no arguments"))
746 746 pc = repo._phasecache
747 747 return subset.filter(lambda r: pc.phase(repo, r) == phases.draft)
748 748
749 749 def extinct(repo, subset, x):
750 750 """``extinct()``
751 751 Obsolete changesets with obsolete descendants only.
752 752 """
753 753 # i18n: "extinct" is a keyword
754 754 getargs(x, 0, 0, _("extinct takes no arguments"))
755 755 extincts = obsmod.getrevs(repo, 'extinct')
756 756 return subset & extincts
757 757
758 758 def extra(repo, subset, x):
759 759 """``extra(label, [value])``
760 760 Changesets with the given label in the extra metadata, with the given
761 761 optional value.
762 762
763 763 If `value` starts with `re:`, the remainder of the value is treated as
764 764 a regular expression. To match a value that actually starts with `re:`,
765 765 use the prefix `literal:`.
766 766 """
767 767
768 768 # i18n: "extra" is a keyword
769 769 l = getargs(x, 1, 2, _('extra takes at least 1 and at most 2 arguments'))
770 770 # i18n: "extra" is a keyword
771 771 label = getstring(l[0], _('first argument to extra must be a string'))
772 772 value = None
773 773
774 774 if len(l) > 1:
775 775 # i18n: "extra" is a keyword
776 776 value = getstring(l[1], _('second argument to extra must be a string'))
777 777 kind, value, matcher = _stringmatcher(value)
778 778
779 779 def _matchvalue(r):
780 780 extra = repo[r].extra()
781 781 return label in extra and (value is None or matcher(extra[label]))
782 782
783 783 return subset.filter(lambda r: _matchvalue(r))
784 784
785 785 def filelog(repo, subset, x):
786 786 """``filelog(pattern)``
787 787 Changesets connected to the specified filelog.
788 788
789 789 For performance reasons, visits only revisions mentioned in the file-level
790 790 filelog, rather than filtering through all changesets (much faster, but
791 791 doesn't include deletes or duplicate changes). For a slower, more accurate
792 792 result, use ``file()``.
793 793
794 794 The pattern without explicit kind like ``glob:`` is expected to be
795 795 relative to the current directory and match against a file exactly
796 796 for efficiency.
797 797 """
798 798
799 799 # i18n: "filelog" is a keyword
800 800 pat = getstring(x, _("filelog requires a pattern"))
801 801 s = set()
802 802
803 803 if not matchmod.patkind(pat):
804 804 f = pathutil.canonpath(repo.root, repo.getcwd(), pat)
805 805 fl = repo.file(f)
806 806 for fr in fl:
807 807 s.add(fl.linkrev(fr))
808 808 else:
809 809 m = matchmod.match(repo.root, repo.getcwd(), [pat], ctx=repo[None])
810 810 for f in repo[None]:
811 811 if m(f):
812 812 fl = repo.file(f)
813 813 for fr in fl:
814 814 s.add(fl.linkrev(fr))
815 815
816 816 return subset & s
817 817
818 818 def first(repo, subset, x):
819 819 """``first(set, [n])``
820 820 An alias for limit().
821 821 """
822 822 return limit(repo, subset, x)
823 823
824 824 def _follow(repo, subset, x, name, followfirst=False):
825 825 l = getargs(x, 0, 1, _("%s takes no arguments or a filename") % name)
826 826 c = repo['.']
827 827 if l:
828 828 x = getstring(l[0], _("%s expected a filename") % name)
829 829 if x in c:
830 830 cx = c[x]
831 831 s = set(ctx.rev() for ctx in cx.ancestors(followfirst=followfirst))
832 832 # include the revision responsible for the most recent version
833 833 s.add(cx.linkrev())
834 834 else:
835 835 return baseset()
836 836 else:
837 837 s = _revancestors(repo, baseset([c.rev()]), followfirst)
838 838
839 839 return subset & s
840 840
841 841 def follow(repo, subset, x):
842 842 """``follow([file])``
843 843 An alias for ``::.`` (ancestors of the working copy's first parent).
844 844 If a filename is specified, the history of the given file is followed,
845 845 including copies.
846 846 """
847 847 return _follow(repo, subset, x, 'follow')
848 848
849 849 def _followfirst(repo, subset, x):
850 850 # ``followfirst([file])``
851 851 # Like ``follow([file])`` but follows only the first parent of
852 852 # every revision or file revision.
853 853 return _follow(repo, subset, x, '_followfirst', followfirst=True)
854 854
855 855 def getall(repo, subset, x):
856 856 """``all()``
857 857 All changesets, the same as ``0:tip``.
858 858 """
859 859 # i18n: "all" is a keyword
860 860 getargs(x, 0, 0, _("all takes no arguments"))
861 861 return subset
862 862
863 863 def grep(repo, subset, x):
864 864 """``grep(regex)``
865 865 Like ``keyword(string)`` but accepts a regex. Use ``grep(r'...')``
866 866 to ensure special escape characters are handled correctly. Unlike
867 867 ``keyword(string)``, the match is case-sensitive.
868 868 """
869 869 try:
870 870 # i18n: "grep" is a keyword
871 871 gr = re.compile(getstring(x, _("grep requires a string")))
872 872 except re.error, e:
873 873 raise error.ParseError(_('invalid match pattern: %s') % e)
874 874
875 875 def matches(x):
876 876 c = repo[x]
877 877 for e in c.files() + [c.user(), c.description()]:
878 878 if gr.search(e):
879 879 return True
880 880 return False
881 881
882 882 return subset.filter(matches)
883 883
884 884 def _matchfiles(repo, subset, x):
885 885 # _matchfiles takes a revset list of prefixed arguments:
886 886 #
887 887 # [p:foo, i:bar, x:baz]
888 888 #
889 889 # builds a match object from them and filters subset. Allowed
890 890 # prefixes are 'p:' for regular patterns, 'i:' for include
891 891 # patterns and 'x:' for exclude patterns. Use 'r:' prefix to pass
892 892 # a revision identifier, or the empty string to reference the
893 893 # working directory, from which the match object is
894 894 # initialized. Use 'd:' to set the default matching mode, default
895 895 # to 'glob'. At most one 'r:' and 'd:' argument can be passed.
896 896
897 897 # i18n: "_matchfiles" is a keyword
898 898 l = getargs(x, 1, -1, _("_matchfiles requires at least one argument"))
899 899 pats, inc, exc = [], [], []
900 900 hasset = False
901 901 rev, default = None, None
902 902 for arg in l:
903 903 # i18n: "_matchfiles" is a keyword
904 904 s = getstring(arg, _("_matchfiles requires string arguments"))
905 905 prefix, value = s[:2], s[2:]
906 906 if prefix == 'p:':
907 907 pats.append(value)
908 908 elif prefix == 'i:':
909 909 inc.append(value)
910 910 elif prefix == 'x:':
911 911 exc.append(value)
912 912 elif prefix == 'r:':
913 913 if rev is not None:
914 914 # i18n: "_matchfiles" is a keyword
915 915 raise error.ParseError(_('_matchfiles expected at most one '
916 916 'revision'))
917 917 rev = value
918 918 elif prefix == 'd:':
919 919 if default is not None:
920 920 # i18n: "_matchfiles" is a keyword
921 921 raise error.ParseError(_('_matchfiles expected at most one '
922 922 'default mode'))
923 923 default = value
924 924 else:
925 925 # i18n: "_matchfiles" is a keyword
926 926 raise error.ParseError(_('invalid _matchfiles prefix: %s') % prefix)
927 927 if not hasset and matchmod.patkind(value) == 'set':
928 928 hasset = True
929 929 if not default:
930 930 default = 'glob'
931 931
932 932 def matches(x):
933 933 m = None
934 934 c = repo[x]
935 935 if not m or (hasset and rev is None):
936 936 ctx = c
937 937 if rev is not None:
938 938 ctx = repo[rev or None]
939 939 m = matchmod.match(repo.root, repo.getcwd(), pats, include=inc,
940 940 exclude=exc, ctx=ctx, default=default)
941 941 for f in c.files():
942 942 if m(f):
943 943 return True
944 944 return False
945 945
946 946 return subset.filter(matches)
947 947
948 948 def hasfile(repo, subset, x):
949 949 """``file(pattern)``
950 950 Changesets affecting files matched by pattern.
951 951
952 952 For a faster but less accurate result, consider using ``filelog()``
953 953 instead.
954 954
955 955 This predicate uses ``glob:`` as the default kind of pattern.
956 956 """
957 957 # i18n: "file" is a keyword
958 958 pat = getstring(x, _("file requires a pattern"))
959 959 return _matchfiles(repo, subset, ('string', 'p:' + pat))
960 960
961 961 def head(repo, subset, x):
962 962 """``head()``
963 963 Changeset is a named branch head.
964 964 """
965 965 # i18n: "head" is a keyword
966 966 getargs(x, 0, 0, _("head takes no arguments"))
967 967 hs = set()
968 968 for b, ls in repo.branchmap().iteritems():
969 969 hs.update(repo[h].rev() for h in ls)
970 970 return baseset(hs).filter(subset.__contains__)
971 971
972 972 def heads(repo, subset, x):
973 973 """``heads(set)``
974 974 Members of set with no children in set.
975 975 """
976 976 s = getset(repo, subset, x)
977 977 ps = parents(repo, subset, x)
978 978 return s - ps
979 979
980 980 def hidden(repo, subset, x):
981 981 """``hidden()``
982 982 Hidden changesets.
983 983 """
984 984 # i18n: "hidden" is a keyword
985 985 getargs(x, 0, 0, _("hidden takes no arguments"))
986 986 hiddenrevs = repoview.filterrevs(repo, 'visible')
987 987 return subset & hiddenrevs
988 988
989 989 def keyword(repo, subset, x):
990 990 """``keyword(string)``
991 991 Search commit message, user name, and names of changed files for
992 992 string. The match is case-insensitive.
993 993 """
994 994 # i18n: "keyword" is a keyword
995 995 kw = encoding.lower(getstring(x, _("keyword requires a string")))
996 996
997 997 def matches(r):
998 998 c = repo[r]
999 999 return util.any(kw in encoding.lower(t) for t in c.files() + [c.user(),
1000 1000 c.description()])
1001 1001
1002 1002 return subset.filter(matches)
1003 1003
1004 1004 def limit(repo, subset, x):
1005 1005 """``limit(set, [n])``
1006 1006 First n members of set, defaulting to 1.
1007 1007 """
1008 1008 # i18n: "limit" is a keyword
1009 1009 l = getargs(x, 1, 2, _("limit requires one or two arguments"))
1010 1010 try:
1011 1011 lim = 1
1012 1012 if len(l) == 2:
1013 1013 # i18n: "limit" is a keyword
1014 1014 lim = int(getstring(l[1], _("limit requires a number")))
1015 1015 except (TypeError, ValueError):
1016 1016 # i18n: "limit" is a keyword
1017 1017 raise error.ParseError(_("limit expects a number"))
1018 1018 ss = subset
1019 1019 os = getset(repo, spanset(repo), l[0])
1020 1020 result = []
1021 1021 it = iter(os)
1022 1022 for x in xrange(lim):
1023 1023 try:
1024 1024 y = it.next()
1025 1025 if y in ss:
1026 1026 result.append(y)
1027 1027 except (StopIteration):
1028 1028 break
1029 1029 return baseset(result)
1030 1030
1031 1031 def last(repo, subset, x):
1032 1032 """``last(set, [n])``
1033 1033 Last n members of set, defaulting to 1.
1034 1034 """
1035 1035 # i18n: "last" is a keyword
1036 1036 l = getargs(x, 1, 2, _("last requires one or two arguments"))
1037 1037 try:
1038 1038 lim = 1
1039 1039 if len(l) == 2:
1040 1040 # i18n: "last" is a keyword
1041 1041 lim = int(getstring(l[1], _("last requires a number")))
1042 1042 except (TypeError, ValueError):
1043 1043 # i18n: "last" is a keyword
1044 1044 raise error.ParseError(_("last expects a number"))
1045 1045 ss = subset
1046 1046 os = getset(repo, spanset(repo), l[0])
1047 1047 os.reverse()
1048 1048 result = []
1049 1049 it = iter(os)
1050 1050 for x in xrange(lim):
1051 1051 try:
1052 1052 y = it.next()
1053 1053 if y in ss:
1054 1054 result.append(y)
1055 1055 except (StopIteration):
1056 1056 break
1057 1057 return baseset(result)
1058 1058
1059 1059 def maxrev(repo, subset, x):
1060 1060 """``max(set)``
1061 1061 Changeset with highest revision number in set.
1062 1062 """
1063 1063 os = getset(repo, spanset(repo), x)
1064 1064 if os:
1065 1065 m = os.max()
1066 1066 if m in subset:
1067 1067 return baseset([m])
1068 1068 return baseset()
1069 1069
1070 1070 def merge(repo, subset, x):
1071 1071 """``merge()``
1072 1072 Changeset is a merge changeset.
1073 1073 """
1074 1074 # i18n: "merge" is a keyword
1075 1075 getargs(x, 0, 0, _("merge takes no arguments"))
1076 1076 cl = repo.changelog
1077 1077 return subset.filter(lambda r: cl.parentrevs(r)[1] != -1)
1078 1078
1079 1079 def branchpoint(repo, subset, x):
1080 1080 """``branchpoint()``
1081 1081 Changesets with more than one child.
1082 1082 """
1083 1083 # i18n: "branchpoint" is a keyword
1084 1084 getargs(x, 0, 0, _("branchpoint takes no arguments"))
1085 1085 cl = repo.changelog
1086 1086 if not subset:
1087 1087 return baseset()
1088 1088 baserev = min(subset)
1089 1089 parentscount = [0]*(len(repo) - baserev)
1090 1090 for r in cl.revs(start=baserev + 1):
1091 1091 for p in cl.parentrevs(r):
1092 1092 if p >= baserev:
1093 1093 parentscount[p - baserev] += 1
1094 1094 return subset.filter(lambda r: parentscount[r - baserev] > 1)
1095 1095
1096 1096 def minrev(repo, subset, x):
1097 1097 """``min(set)``
1098 1098 Changeset with lowest revision number in set.
1099 1099 """
1100 1100 os = getset(repo, spanset(repo), x)
1101 1101 if os:
1102 1102 m = os.min()
1103 1103 if m in subset:
1104 1104 return baseset([m])
1105 1105 return baseset()
1106 1106
1107 1107 def modifies(repo, subset, x):
1108 1108 """``modifies(pattern)``
1109 1109 Changesets modifying files matched by pattern.
1110 1110
1111 1111 The pattern without explicit kind like ``glob:`` is expected to be
1112 1112 relative to the current directory and match against a file or a
1113 1113 directory.
1114 1114 """
1115 1115 # i18n: "modifies" is a keyword
1116 1116 pat = getstring(x, _("modifies requires a pattern"))
1117 1117 return checkstatus(repo, subset, pat, 0)
1118 1118
1119 1119 def node_(repo, subset, x):
1120 1120 """``id(string)``
1121 1121 Revision non-ambiguously specified by the given hex string prefix.
1122 1122 """
1123 1123 # i18n: "id" is a keyword
1124 1124 l = getargs(x, 1, 1, _("id requires one argument"))
1125 1125 # i18n: "id" is a keyword
1126 1126 n = getstring(l[0], _("id requires a string"))
1127 1127 if len(n) == 40:
1128 1128 rn = repo[n].rev()
1129 1129 else:
1130 1130 rn = None
1131 1131 pm = repo.changelog._partialmatch(n)
1132 1132 if pm is not None:
1133 1133 rn = repo.changelog.rev(pm)
1134 1134
1135 return subset.filter(lambda r: r == rn)
1135 if rn is None:
1136 return baseset()
1137 result = baseset([rn])
1138 return result & subset
1136 1139
1137 1140 def obsolete(repo, subset, x):
1138 1141 """``obsolete()``
1139 1142 Mutable changeset with a newer version."""
1140 1143 # i18n: "obsolete" is a keyword
1141 1144 getargs(x, 0, 0, _("obsolete takes no arguments"))
1142 1145 obsoletes = obsmod.getrevs(repo, 'obsolete')
1143 1146 return subset & obsoletes
1144 1147
1145 1148 def origin(repo, subset, x):
1146 1149 """``origin([set])``
1147 1150 Changesets that were specified as a source for the grafts, transplants or
1148 1151 rebases that created the given revisions. Omitting the optional set is the
1149 1152 same as passing all(). If a changeset created by these operations is itself
1150 1153 specified as a source for one of these operations, only the source changeset
1151 1154 for the first operation is selected.
1152 1155 """
1153 1156 if x is not None:
1154 1157 dests = getset(repo, spanset(repo), x)
1155 1158 else:
1156 1159 dests = getall(repo, spanset(repo), x)
1157 1160
1158 1161 def _firstsrc(rev):
1159 1162 src = _getrevsource(repo, rev)
1160 1163 if src is None:
1161 1164 return None
1162 1165
1163 1166 while True:
1164 1167 prev = _getrevsource(repo, src)
1165 1168
1166 1169 if prev is None:
1167 1170 return src
1168 1171 src = prev
1169 1172
1170 1173 o = set([_firstsrc(r) for r in dests])
1171 1174 o -= set([None])
1172 1175 return subset & o
1173 1176
1174 1177 def outgoing(repo, subset, x):
1175 1178 """``outgoing([path])``
1176 1179 Changesets not found in the specified destination repository, or the
1177 1180 default push location.
1178 1181 """
1179 1182 import hg # avoid start-up nasties
1180 1183 # i18n: "outgoing" is a keyword
1181 1184 l = getargs(x, 0, 1, _("outgoing takes one or no arguments"))
1182 1185 # i18n: "outgoing" is a keyword
1183 1186 dest = l and getstring(l[0], _("outgoing requires a repository path")) or ''
1184 1187 dest = repo.ui.expandpath(dest or 'default-push', dest or 'default')
1185 1188 dest, branches = hg.parseurl(dest)
1186 1189 revs, checkout = hg.addbranchrevs(repo, repo, branches, [])
1187 1190 if revs:
1188 1191 revs = [repo.lookup(rev) for rev in revs]
1189 1192 other = hg.peer(repo, {}, dest)
1190 1193 repo.ui.pushbuffer()
1191 1194 outgoing = discovery.findcommonoutgoing(repo, other, onlyheads=revs)
1192 1195 repo.ui.popbuffer()
1193 1196 cl = repo.changelog
1194 1197 o = set([cl.rev(r) for r in outgoing.missing])
1195 1198 return subset & o
1196 1199
1197 1200 def p1(repo, subset, x):
1198 1201 """``p1([set])``
1199 1202 First parent of changesets in set, or the working directory.
1200 1203 """
1201 1204 if x is None:
1202 1205 p = repo[x].p1().rev()
1203 1206 if p >= 0:
1204 1207 return subset & baseset([p])
1205 1208 return baseset()
1206 1209
1207 1210 ps = set()
1208 1211 cl = repo.changelog
1209 1212 for r in getset(repo, spanset(repo), x):
1210 1213 ps.add(cl.parentrevs(r)[0])
1211 1214 ps -= set([node.nullrev])
1212 1215 return subset & ps
1213 1216
1214 1217 def p2(repo, subset, x):
1215 1218 """``p2([set])``
1216 1219 Second parent of changesets in set, or the working directory.
1217 1220 """
1218 1221 if x is None:
1219 1222 ps = repo[x].parents()
1220 1223 try:
1221 1224 p = ps[1].rev()
1222 1225 if p >= 0:
1223 1226 return subset & baseset([p])
1224 1227 return baseset()
1225 1228 except IndexError:
1226 1229 return baseset()
1227 1230
1228 1231 ps = set()
1229 1232 cl = repo.changelog
1230 1233 for r in getset(repo, spanset(repo), x):
1231 1234 ps.add(cl.parentrevs(r)[1])
1232 1235 ps -= set([node.nullrev])
1233 1236 return subset & ps
1234 1237
1235 1238 def parents(repo, subset, x):
1236 1239 """``parents([set])``
1237 1240 The set of all parents for all changesets in set, or the working directory.
1238 1241 """
1239 1242 if x is None:
1240 1243 ps = set(p.rev() for p in repo[x].parents())
1241 1244 else:
1242 1245 ps = set()
1243 1246 cl = repo.changelog
1244 1247 for r in getset(repo, spanset(repo), x):
1245 1248 ps.update(cl.parentrevs(r))
1246 1249 ps -= set([node.nullrev])
1247 1250 return subset & ps
1248 1251
1249 1252 def parentspec(repo, subset, x, n):
1250 1253 """``set^0``
1251 1254 The set.
1252 1255 ``set^1`` (or ``set^``), ``set^2``
1253 1256 First or second parent, respectively, of all changesets in set.
1254 1257 """
1255 1258 try:
1256 1259 n = int(n[1])
1257 1260 if n not in (0, 1, 2):
1258 1261 raise ValueError
1259 1262 except (TypeError, ValueError):
1260 1263 raise error.ParseError(_("^ expects a number 0, 1, or 2"))
1261 1264 ps = set()
1262 1265 cl = repo.changelog
1263 1266 for r in getset(repo, baseset(cl), x):
1264 1267 if n == 0:
1265 1268 ps.add(r)
1266 1269 elif n == 1:
1267 1270 ps.add(cl.parentrevs(r)[0])
1268 1271 elif n == 2:
1269 1272 parents = cl.parentrevs(r)
1270 1273 if len(parents) > 1:
1271 1274 ps.add(parents[1])
1272 1275 return subset & ps
1273 1276
1274 1277 def present(repo, subset, x):
1275 1278 """``present(set)``
1276 1279 An empty set, if any revision in set isn't found; otherwise,
1277 1280 all revisions in set.
1278 1281
1279 1282 If any of specified revisions is not present in the local repository,
1280 1283 the query is normally aborted. But this predicate allows the query
1281 1284 to continue even in such cases.
1282 1285 """
1283 1286 try:
1284 1287 return getset(repo, subset, x)
1285 1288 except error.RepoLookupError:
1286 1289 return baseset()
1287 1290
1288 1291 def public(repo, subset, x):
1289 1292 """``public()``
1290 1293 Changeset in public phase."""
1291 1294 # i18n: "public" is a keyword
1292 1295 getargs(x, 0, 0, _("public takes no arguments"))
1293 1296 pc = repo._phasecache
1294 1297 return subset.filter(lambda r: pc.phase(repo, r) == phases.public)
1295 1298
1296 1299 def remote(repo, subset, x):
1297 1300 """``remote([id [,path]])``
1298 1301 Local revision that corresponds to the given identifier in a
1299 1302 remote repository, if present. Here, the '.' identifier is a
1300 1303 synonym for the current local branch.
1301 1304 """
1302 1305
1303 1306 import hg # avoid start-up nasties
1304 1307 # i18n: "remote" is a keyword
1305 1308 l = getargs(x, 0, 2, _("remote takes one, two or no arguments"))
1306 1309
1307 1310 q = '.'
1308 1311 if len(l) > 0:
1309 1312 # i18n: "remote" is a keyword
1310 1313 q = getstring(l[0], _("remote requires a string id"))
1311 1314 if q == '.':
1312 1315 q = repo['.'].branch()
1313 1316
1314 1317 dest = ''
1315 1318 if len(l) > 1:
1316 1319 # i18n: "remote" is a keyword
1317 1320 dest = getstring(l[1], _("remote requires a repository path"))
1318 1321 dest = repo.ui.expandpath(dest or 'default')
1319 1322 dest, branches = hg.parseurl(dest)
1320 1323 revs, checkout = hg.addbranchrevs(repo, repo, branches, [])
1321 1324 if revs:
1322 1325 revs = [repo.lookup(rev) for rev in revs]
1323 1326 other = hg.peer(repo, {}, dest)
1324 1327 n = other.lookup(q)
1325 1328 if n in repo:
1326 1329 r = repo[n].rev()
1327 1330 if r in subset:
1328 1331 return baseset([r])
1329 1332 return baseset()
1330 1333
1331 1334 def removes(repo, subset, x):
1332 1335 """``removes(pattern)``
1333 1336 Changesets which remove files matching pattern.
1334 1337
1335 1338 The pattern without explicit kind like ``glob:`` is expected to be
1336 1339 relative to the current directory and match against a file or a
1337 1340 directory.
1338 1341 """
1339 1342 # i18n: "removes" is a keyword
1340 1343 pat = getstring(x, _("removes requires a pattern"))
1341 1344 return checkstatus(repo, subset, pat, 2)
1342 1345
1343 1346 def rev(repo, subset, x):
1344 1347 """``rev(number)``
1345 1348 Revision with the given numeric identifier.
1346 1349 """
1347 1350 # i18n: "rev" is a keyword
1348 1351 l = getargs(x, 1, 1, _("rev requires one argument"))
1349 1352 try:
1350 1353 # i18n: "rev" is a keyword
1351 1354 l = int(getstring(l[0], _("rev requires a number")))
1352 1355 except (TypeError, ValueError):
1353 1356 # i18n: "rev" is a keyword
1354 1357 raise error.ParseError(_("rev expects a number"))
1355 1358 return subset & baseset([l])
1356 1359
1357 1360 def matching(repo, subset, x):
1358 1361 """``matching(revision [, field])``
1359 1362 Changesets in which a given set of fields match the set of fields in the
1360 1363 selected revision or set.
1361 1364
1362 1365 To match more than one field pass the list of fields to match separated
1363 1366 by spaces (e.g. ``author description``).
1364 1367
1365 1368 Valid fields are most regular revision fields and some special fields.
1366 1369
1367 1370 Regular revision fields are ``description``, ``author``, ``branch``,
1368 1371 ``date``, ``files``, ``phase``, ``parents``, ``substate``, ``user``
1369 1372 and ``diff``.
1370 1373 Note that ``author`` and ``user`` are synonyms. ``diff`` refers to the
1371 1374 contents of the revision. Two revisions matching their ``diff`` will
1372 1375 also match their ``files``.
1373 1376
1374 1377 Special fields are ``summary`` and ``metadata``:
1375 1378 ``summary`` matches the first line of the description.
1376 1379 ``metadata`` is equivalent to matching ``description user date``
1377 1380 (i.e. it matches the main metadata fields).
1378 1381
1379 1382 ``metadata`` is the default field which is used when no fields are
1380 1383 specified. You can match more than one field at a time.
1381 1384 """
1382 1385 # i18n: "matching" is a keyword
1383 1386 l = getargs(x, 1, 2, _("matching takes 1 or 2 arguments"))
1384 1387
1385 1388 revs = getset(repo, baseset(repo.changelog), l[0])
1386 1389
1387 1390 fieldlist = ['metadata']
1388 1391 if len(l) > 1:
1389 1392 fieldlist = getstring(l[1],
1390 1393 # i18n: "matching" is a keyword
1391 1394 _("matching requires a string "
1392 1395 "as its second argument")).split()
1393 1396
1394 1397 # Make sure that there are no repeated fields,
1395 1398 # expand the 'special' 'metadata' field type
1396 1399 # and check the 'files' whenever we check the 'diff'
1397 1400 fields = []
1398 1401 for field in fieldlist:
1399 1402 if field == 'metadata':
1400 1403 fields += ['user', 'description', 'date']
1401 1404 elif field == 'diff':
1402 1405 # a revision matching the diff must also match the files
1403 1406 # since matching the diff is very costly, make sure to
1404 1407 # also match the files first
1405 1408 fields += ['files', 'diff']
1406 1409 else:
1407 1410 if field == 'author':
1408 1411 field = 'user'
1409 1412 fields.append(field)
1410 1413 fields = set(fields)
1411 1414 if 'summary' in fields and 'description' in fields:
1412 1415 # If a revision matches its description it also matches its summary
1413 1416 fields.discard('summary')
1414 1417
1415 1418 # We may want to match more than one field
1416 1419 # Not all fields take the same amount of time to be matched
1417 1420 # Sort the selected fields in order of increasing matching cost
1418 1421 fieldorder = ['phase', 'parents', 'user', 'date', 'branch', 'summary',
1419 1422 'files', 'description', 'substate', 'diff']
1420 1423 def fieldkeyfunc(f):
1421 1424 try:
1422 1425 return fieldorder.index(f)
1423 1426 except ValueError:
1424 1427 # assume an unknown field is very costly
1425 1428 return len(fieldorder)
1426 1429 fields = list(fields)
1427 1430 fields.sort(key=fieldkeyfunc)
1428 1431
1429 1432 # Each field will be matched with its own "getfield" function
1430 1433 # which will be added to the getfieldfuncs array of functions
1431 1434 getfieldfuncs = []
1432 1435 _funcs = {
1433 1436 'user': lambda r: repo[r].user(),
1434 1437 'branch': lambda r: repo[r].branch(),
1435 1438 'date': lambda r: repo[r].date(),
1436 1439 'description': lambda r: repo[r].description(),
1437 1440 'files': lambda r: repo[r].files(),
1438 1441 'parents': lambda r: repo[r].parents(),
1439 1442 'phase': lambda r: repo[r].phase(),
1440 1443 'substate': lambda r: repo[r].substate,
1441 1444 'summary': lambda r: repo[r].description().splitlines()[0],
1442 1445 'diff': lambda r: list(repo[r].diff(git=True),)
1443 1446 }
1444 1447 for info in fields:
1445 1448 getfield = _funcs.get(info, None)
1446 1449 if getfield is None:
1447 1450 raise error.ParseError(
1448 1451 # i18n: "matching" is a keyword
1449 1452 _("unexpected field name passed to matching: %s") % info)
1450 1453 getfieldfuncs.append(getfield)
1451 1454 # convert the getfield array of functions into a "getinfo" function
1452 1455 # which returns an array of field values (or a single value if there
1453 1456 # is only one field to match)
1454 1457 getinfo = lambda r: [f(r) for f in getfieldfuncs]
1455 1458
1456 1459 def matches(x):
1457 1460 for rev in revs:
1458 1461 target = getinfo(rev)
1459 1462 match = True
1460 1463 for n, f in enumerate(getfieldfuncs):
1461 1464 if target[n] != f(x):
1462 1465 match = False
1463 1466 if match:
1464 1467 return True
1465 1468 return False
1466 1469
1467 1470 return subset.filter(matches)
1468 1471
1469 1472 def reverse(repo, subset, x):
1470 1473 """``reverse(set)``
1471 1474 Reverse order of set.
1472 1475 """
1473 1476 l = getset(repo, subset, x)
1474 1477 l.reverse()
1475 1478 return l
1476 1479
1477 1480 def roots(repo, subset, x):
1478 1481 """``roots(set)``
1479 1482 Changesets in set with no parent changeset in set.
1480 1483 """
1481 1484 s = getset(repo, spanset(repo), x)
1482 1485 subset = baseset([r for r in s if r in subset])
1483 1486 cs = _children(repo, subset, s)
1484 1487 return subset - cs
1485 1488
1486 1489 def secret(repo, subset, x):
1487 1490 """``secret()``
1488 1491 Changeset in secret phase."""
1489 1492 # i18n: "secret" is a keyword
1490 1493 getargs(x, 0, 0, _("secret takes no arguments"))
1491 1494 pc = repo._phasecache
1492 1495 return subset.filter(lambda x: pc.phase(repo, x) == phases.secret)
1493 1496
1494 1497 def sort(repo, subset, x):
1495 1498 """``sort(set[, [-]key...])``
1496 1499 Sort set by keys. The default sort order is ascending, specify a key
1497 1500 as ``-key`` to sort in descending order.
1498 1501
1499 1502 The keys can be:
1500 1503
1501 1504 - ``rev`` for the revision number,
1502 1505 - ``branch`` for the branch name,
1503 1506 - ``desc`` for the commit message (description),
1504 1507 - ``user`` for user name (``author`` can be used as an alias),
1505 1508 - ``date`` for the commit date
1506 1509 """
1507 1510 # i18n: "sort" is a keyword
1508 1511 l = getargs(x, 1, 2, _("sort requires one or two arguments"))
1509 1512 keys = "rev"
1510 1513 if len(l) == 2:
1511 1514 # i18n: "sort" is a keyword
1512 1515 keys = getstring(l[1], _("sort spec must be a string"))
1513 1516
1514 1517 s = l[0]
1515 1518 keys = keys.split()
1516 1519 l = []
1517 1520 def invert(s):
1518 1521 return "".join(chr(255 - ord(c)) for c in s)
1519 1522 revs = getset(repo, subset, s)
1520 1523 if keys == ["rev"]:
1521 1524 revs.sort()
1522 1525 return revs
1523 1526 elif keys == ["-rev"]:
1524 1527 revs.sort(reverse=True)
1525 1528 return revs
1526 1529 for r in revs:
1527 1530 c = repo[r]
1528 1531 e = []
1529 1532 for k in keys:
1530 1533 if k == 'rev':
1531 1534 e.append(r)
1532 1535 elif k == '-rev':
1533 1536 e.append(-r)
1534 1537 elif k == 'branch':
1535 1538 e.append(c.branch())
1536 1539 elif k == '-branch':
1537 1540 e.append(invert(c.branch()))
1538 1541 elif k == 'desc':
1539 1542 e.append(c.description())
1540 1543 elif k == '-desc':
1541 1544 e.append(invert(c.description()))
1542 1545 elif k in 'user author':
1543 1546 e.append(c.user())
1544 1547 elif k in '-user -author':
1545 1548 e.append(invert(c.user()))
1546 1549 elif k == 'date':
1547 1550 e.append(c.date()[0])
1548 1551 elif k == '-date':
1549 1552 e.append(-c.date()[0])
1550 1553 else:
1551 1554 raise error.ParseError(_("unknown sort key %r") % k)
1552 1555 e.append(r)
1553 1556 l.append(e)
1554 1557 l.sort()
1555 1558 return baseset([e[-1] for e in l])
1556 1559
1557 1560 def _stringmatcher(pattern):
1558 1561 """
1559 1562 accepts a string, possibly starting with 're:' or 'literal:' prefix.
1560 1563 returns the matcher name, pattern, and matcher function.
1561 1564 missing or unknown prefixes are treated as literal matches.
1562 1565
1563 1566 helper for tests:
1564 1567 >>> def test(pattern, *tests):
1565 1568 ... kind, pattern, matcher = _stringmatcher(pattern)
1566 1569 ... return (kind, pattern, [bool(matcher(t)) for t in tests])
1567 1570
1568 1571 exact matching (no prefix):
1569 1572 >>> test('abcdefg', 'abc', 'def', 'abcdefg')
1570 1573 ('literal', 'abcdefg', [False, False, True])
1571 1574
1572 1575 regex matching ('re:' prefix)
1573 1576 >>> test('re:a.+b', 'nomatch', 'fooadef', 'fooadefbar')
1574 1577 ('re', 'a.+b', [False, False, True])
1575 1578
1576 1579 force exact matches ('literal:' prefix)
1577 1580 >>> test('literal:re:foobar', 'foobar', 're:foobar')
1578 1581 ('literal', 're:foobar', [False, True])
1579 1582
1580 1583 unknown prefixes are ignored and treated as literals
1581 1584 >>> test('foo:bar', 'foo', 'bar', 'foo:bar')
1582 1585 ('literal', 'foo:bar', [False, False, True])
1583 1586 """
1584 1587 if pattern.startswith('re:'):
1585 1588 pattern = pattern[3:]
1586 1589 try:
1587 1590 regex = re.compile(pattern)
1588 1591 except re.error, e:
1589 1592 raise error.ParseError(_('invalid regular expression: %s')
1590 1593 % e)
1591 1594 return 're', pattern, regex.search
1592 1595 elif pattern.startswith('literal:'):
1593 1596 pattern = pattern[8:]
1594 1597 return 'literal', pattern, pattern.__eq__
1595 1598
1596 1599 def _substringmatcher(pattern):
1597 1600 kind, pattern, matcher = _stringmatcher(pattern)
1598 1601 if kind == 'literal':
1599 1602 matcher = lambda s: pattern in s
1600 1603 return kind, pattern, matcher
1601 1604
1602 1605 def tag(repo, subset, x):
1603 1606 """``tag([name])``
1604 1607 The specified tag by name, or all tagged revisions if no name is given.
1605 1608
1606 1609 If `name` starts with `re:`, the remainder of the name is treated as
1607 1610 a regular expression. To match a tag that actually starts with `re:`,
1608 1611 use the prefix `literal:`.
1609 1612 """
1610 1613 # i18n: "tag" is a keyword
1611 1614 args = getargs(x, 0, 1, _("tag takes one or no arguments"))
1612 1615 cl = repo.changelog
1613 1616 if args:
1614 1617 pattern = getstring(args[0],
1615 1618 # i18n: "tag" is a keyword
1616 1619 _('the argument to tag must be a string'))
1617 1620 kind, pattern, matcher = _stringmatcher(pattern)
1618 1621 if kind == 'literal':
1619 1622 # avoid resolving all tags
1620 1623 tn = repo._tagscache.tags.get(pattern, None)
1621 1624 if tn is None:
1622 1625 raise util.Abort(_("tag '%s' does not exist") % pattern)
1623 1626 s = set([repo[tn].rev()])
1624 1627 else:
1625 1628 s = set([cl.rev(n) for t, n in repo.tagslist() if matcher(t)])
1626 1629 else:
1627 1630 s = set([cl.rev(n) for t, n in repo.tagslist() if t != 'tip'])
1628 1631 return subset & s
1629 1632
1630 1633 def tagged(repo, subset, x):
1631 1634 return tag(repo, subset, x)
1632 1635
1633 1636 def unstable(repo, subset, x):
1634 1637 """``unstable()``
1635 1638 Non-obsolete changesets with obsolete ancestors.
1636 1639 """
1637 1640 # i18n: "unstable" is a keyword
1638 1641 getargs(x, 0, 0, _("unstable takes no arguments"))
1639 1642 unstables = obsmod.getrevs(repo, 'unstable')
1640 1643 return subset & unstables
1641 1644
1642 1645
1643 1646 def user(repo, subset, x):
1644 1647 """``user(string)``
1645 1648 User name contains string. The match is case-insensitive.
1646 1649
1647 1650 If `string` starts with `re:`, the remainder of the string is treated as
1648 1651 a regular expression. To match a user that actually contains `re:`, use
1649 1652 the prefix `literal:`.
1650 1653 """
1651 1654 return author(repo, subset, x)
1652 1655
1653 1656 # for internal use
1654 1657 def _list(repo, subset, x):
1655 1658 s = getstring(x, "internal error")
1656 1659 if not s:
1657 1660 return baseset()
1658 1661 ls = [repo[r].rev() for r in s.split('\0')]
1659 1662 s = subset
1660 1663 return baseset([r for r in ls if r in s])
1661 1664
1662 1665 # for internal use
1663 1666 def _intlist(repo, subset, x):
1664 1667 s = getstring(x, "internal error")
1665 1668 if not s:
1666 1669 return baseset()
1667 1670 ls = [int(r) for r in s.split('\0')]
1668 1671 s = subset
1669 1672 return baseset([r for r in ls if r in s])
1670 1673
1671 1674 # for internal use
1672 1675 def _hexlist(repo, subset, x):
1673 1676 s = getstring(x, "internal error")
1674 1677 if not s:
1675 1678 return baseset()
1676 1679 cl = repo.changelog
1677 1680 ls = [cl.rev(node.bin(r)) for r in s.split('\0')]
1678 1681 s = subset
1679 1682 return baseset([r for r in ls if r in s])
1680 1683
1681 1684 symbols = {
1682 1685 "adds": adds,
1683 1686 "all": getall,
1684 1687 "ancestor": ancestor,
1685 1688 "ancestors": ancestors,
1686 1689 "_firstancestors": _firstancestors,
1687 1690 "author": author,
1688 1691 "only": only,
1689 1692 "bisect": bisect,
1690 1693 "bisected": bisected,
1691 1694 "bookmark": bookmark,
1692 1695 "branch": branch,
1693 1696 "branchpoint": branchpoint,
1694 1697 "bumped": bumped,
1695 1698 "bundle": bundle,
1696 1699 "children": children,
1697 1700 "closed": closed,
1698 1701 "contains": contains,
1699 1702 "converted": converted,
1700 1703 "date": date,
1701 1704 "desc": desc,
1702 1705 "descendants": descendants,
1703 1706 "_firstdescendants": _firstdescendants,
1704 1707 "destination": destination,
1705 1708 "divergent": divergent,
1706 1709 "draft": draft,
1707 1710 "extinct": extinct,
1708 1711 "extra": extra,
1709 1712 "file": hasfile,
1710 1713 "filelog": filelog,
1711 1714 "first": first,
1712 1715 "follow": follow,
1713 1716 "_followfirst": _followfirst,
1714 1717 "grep": grep,
1715 1718 "head": head,
1716 1719 "heads": heads,
1717 1720 "hidden": hidden,
1718 1721 "id": node_,
1719 1722 "keyword": keyword,
1720 1723 "last": last,
1721 1724 "limit": limit,
1722 1725 "_matchfiles": _matchfiles,
1723 1726 "max": maxrev,
1724 1727 "merge": merge,
1725 1728 "min": minrev,
1726 1729 "modifies": modifies,
1727 1730 "obsolete": obsolete,
1728 1731 "origin": origin,
1729 1732 "outgoing": outgoing,
1730 1733 "p1": p1,
1731 1734 "p2": p2,
1732 1735 "parents": parents,
1733 1736 "present": present,
1734 1737 "public": public,
1735 1738 "remote": remote,
1736 1739 "removes": removes,
1737 1740 "rev": rev,
1738 1741 "reverse": reverse,
1739 1742 "roots": roots,
1740 1743 "sort": sort,
1741 1744 "secret": secret,
1742 1745 "matching": matching,
1743 1746 "tag": tag,
1744 1747 "tagged": tagged,
1745 1748 "user": user,
1746 1749 "unstable": unstable,
1747 1750 "_list": _list,
1748 1751 "_intlist": _intlist,
1749 1752 "_hexlist": _hexlist,
1750 1753 }
1751 1754
1752 1755 # symbols which can't be used for a DoS attack for any given input
1753 1756 # (e.g. those which accept regexes as plain strings shouldn't be included)
1754 1757 # functions that just return a lot of changesets (like all) don't count here
1755 1758 safesymbols = set([
1756 1759 "adds",
1757 1760 "all",
1758 1761 "ancestor",
1759 1762 "ancestors",
1760 1763 "_firstancestors",
1761 1764 "author",
1762 1765 "bisect",
1763 1766 "bisected",
1764 1767 "bookmark",
1765 1768 "branch",
1766 1769 "branchpoint",
1767 1770 "bumped",
1768 1771 "bundle",
1769 1772 "children",
1770 1773 "closed",
1771 1774 "converted",
1772 1775 "date",
1773 1776 "desc",
1774 1777 "descendants",
1775 1778 "_firstdescendants",
1776 1779 "destination",
1777 1780 "divergent",
1778 1781 "draft",
1779 1782 "extinct",
1780 1783 "extra",
1781 1784 "file",
1782 1785 "filelog",
1783 1786 "first",
1784 1787 "follow",
1785 1788 "_followfirst",
1786 1789 "head",
1787 1790 "heads",
1788 1791 "hidden",
1789 1792 "id",
1790 1793 "keyword",
1791 1794 "last",
1792 1795 "limit",
1793 1796 "_matchfiles",
1794 1797 "max",
1795 1798 "merge",
1796 1799 "min",
1797 1800 "modifies",
1798 1801 "obsolete",
1799 1802 "origin",
1800 1803 "outgoing",
1801 1804 "p1",
1802 1805 "p2",
1803 1806 "parents",
1804 1807 "present",
1805 1808 "public",
1806 1809 "remote",
1807 1810 "removes",
1808 1811 "rev",
1809 1812 "reverse",
1810 1813 "roots",
1811 1814 "sort",
1812 1815 "secret",
1813 1816 "matching",
1814 1817 "tag",
1815 1818 "tagged",
1816 1819 "user",
1817 1820 "unstable",
1818 1821 "_list",
1819 1822 "_intlist",
1820 1823 "_hexlist",
1821 1824 ])
1822 1825
1823 1826 methods = {
1824 1827 "range": rangeset,
1825 1828 "dagrange": dagrange,
1826 1829 "string": stringset,
1827 1830 "symbol": symbolset,
1828 1831 "and": andset,
1829 1832 "or": orset,
1830 1833 "not": notset,
1831 1834 "list": listset,
1832 1835 "func": func,
1833 1836 "ancestor": ancestorspec,
1834 1837 "parent": parentspec,
1835 1838 "parentpost": p1,
1836 1839 }
1837 1840
1838 1841 def optimize(x, small):
1839 1842 if x is None:
1840 1843 return 0, x
1841 1844
1842 1845 smallbonus = 1
1843 1846 if small:
1844 1847 smallbonus = .5
1845 1848
1846 1849 op = x[0]
1847 1850 if op == 'minus':
1848 1851 return optimize(('and', x[1], ('not', x[2])), small)
1849 1852 elif op == 'dagrangepre':
1850 1853 return optimize(('func', ('symbol', 'ancestors'), x[1]), small)
1851 1854 elif op == 'dagrangepost':
1852 1855 return optimize(('func', ('symbol', 'descendants'), x[1]), small)
1853 1856 elif op == 'rangepre':
1854 1857 return optimize(('range', ('string', '0'), x[1]), small)
1855 1858 elif op == 'rangepost':
1856 1859 return optimize(('range', x[1], ('string', 'tip')), small)
1857 1860 elif op == 'negate':
1858 1861 return optimize(('string',
1859 1862 '-' + getstring(x[1], _("can't negate that"))), small)
1860 1863 elif op in 'string symbol negate':
1861 1864 return smallbonus, x # single revisions are small
1862 1865 elif op == 'and':
1863 1866 wa, ta = optimize(x[1], True)
1864 1867 wb, tb = optimize(x[2], True)
1865 1868
1866 1869 # (::x and not ::y)/(not ::y and ::x) have a fast path
1867 1870 def isonly(revs, bases):
1868 1871 return (
1869 1872 revs[0] == 'func'
1870 1873 and getstring(revs[1], _('not a symbol')) == 'ancestors'
1871 1874 and bases[0] == 'not'
1872 1875 and bases[1][0] == 'func'
1873 1876 and getstring(bases[1][1], _('not a symbol')) == 'ancestors')
1874 1877
1875 1878 w = min(wa, wb)
1876 1879 if isonly(ta, tb):
1877 1880 return w, ('func', ('symbol', 'only'), ('list', ta[2], tb[1][2]))
1878 1881 if isonly(tb, ta):
1879 1882 return w, ('func', ('symbol', 'only'), ('list', tb[2], ta[1][2]))
1880 1883
1881 1884 if wa > wb:
1882 1885 return w, (op, tb, ta)
1883 1886 return w, (op, ta, tb)
1884 1887 elif op == 'or':
1885 1888 wa, ta = optimize(x[1], False)
1886 1889 wb, tb = optimize(x[2], False)
1887 1890 if wb < wa:
1888 1891 wb, wa = wa, wb
1889 1892 return max(wa, wb), (op, ta, tb)
1890 1893 elif op == 'not':
1891 1894 o = optimize(x[1], not small)
1892 1895 return o[0], (op, o[1])
1893 1896 elif op == 'parentpost':
1894 1897 o = optimize(x[1], small)
1895 1898 return o[0], (op, o[1])
1896 1899 elif op == 'group':
1897 1900 return optimize(x[1], small)
1898 1901 elif op in 'dagrange range list parent ancestorspec':
1899 1902 if op == 'parent':
1900 1903 # x^:y means (x^) : y, not x ^ (:y)
1901 1904 post = ('parentpost', x[1])
1902 1905 if x[2][0] == 'dagrangepre':
1903 1906 return optimize(('dagrange', post, x[2][1]), small)
1904 1907 elif x[2][0] == 'rangepre':
1905 1908 return optimize(('range', post, x[2][1]), small)
1906 1909
1907 1910 wa, ta = optimize(x[1], small)
1908 1911 wb, tb = optimize(x[2], small)
1909 1912 return wa + wb, (op, ta, tb)
1910 1913 elif op == 'func':
1911 1914 f = getstring(x[1], _("not a symbol"))
1912 1915 wa, ta = optimize(x[2], small)
1913 1916 if f in ("author branch closed date desc file grep keyword "
1914 1917 "outgoing user"):
1915 1918 w = 10 # slow
1916 1919 elif f in "modifies adds removes":
1917 1920 w = 30 # slower
1918 1921 elif f == "contains":
1919 1922 w = 100 # very slow
1920 1923 elif f == "ancestor":
1921 1924 w = 1 * smallbonus
1922 1925 elif f in "reverse limit first _intlist":
1923 1926 w = 0
1924 1927 elif f in "sort":
1925 1928 w = 10 # assume most sorts look at changelog
1926 1929 else:
1927 1930 w = 1
1928 1931 return w + wa, (op, x[1], ta)
1929 1932 return 1, x
1930 1933
1931 1934 _aliasarg = ('func', ('symbol', '_aliasarg'))
1932 1935 def _getaliasarg(tree):
1933 1936 """If tree matches ('func', ('symbol', '_aliasarg'), ('string', X))
1934 1937 return X, None otherwise.
1935 1938 """
1936 1939 if (len(tree) == 3 and tree[:2] == _aliasarg
1937 1940 and tree[2][0] == 'string'):
1938 1941 return tree[2][1]
1939 1942 return None
1940 1943
1941 1944 def _checkaliasarg(tree, known=None):
1942 1945 """Check tree contains no _aliasarg construct or only ones which
1943 1946 value is in known. Used to avoid alias placeholders injection.
1944 1947 """
1945 1948 if isinstance(tree, tuple):
1946 1949 arg = _getaliasarg(tree)
1947 1950 if arg is not None and (not known or arg not in known):
1948 1951 raise error.ParseError(_("not a function: %s") % '_aliasarg')
1949 1952 for t in tree:
1950 1953 _checkaliasarg(t, known)
1951 1954
1952 1955 class revsetalias(object):
1953 1956 funcre = re.compile('^([^(]+)\(([^)]+)\)$')
1954 1957 args = None
1955 1958
1956 1959 def __init__(self, name, value):
1957 1960 '''Aliases like:
1958 1961
1959 1962 h = heads(default)
1960 1963 b($1) = ancestors($1) - ancestors(default)
1961 1964 '''
1962 1965 m = self.funcre.search(name)
1963 1966 if m:
1964 1967 self.name = m.group(1)
1965 1968 self.tree = ('func', ('symbol', m.group(1)))
1966 1969 self.args = [x.strip() for x in m.group(2).split(',')]
1967 1970 for arg in self.args:
1968 1971 # _aliasarg() is an unknown symbol only used separate
1969 1972 # alias argument placeholders from regular strings.
1970 1973 value = value.replace(arg, '_aliasarg(%r)' % (arg,))
1971 1974 else:
1972 1975 self.name = name
1973 1976 self.tree = ('symbol', name)
1974 1977
1975 1978 self.replacement, pos = parse(value)
1976 1979 if pos != len(value):
1977 1980 raise error.ParseError(_('invalid token'), pos)
1978 1981 # Check for placeholder injection
1979 1982 _checkaliasarg(self.replacement, self.args)
1980 1983
1981 1984 def _getalias(aliases, tree):
1982 1985 """If tree looks like an unexpanded alias, return it. Return None
1983 1986 otherwise.
1984 1987 """
1985 1988 if isinstance(tree, tuple) and tree:
1986 1989 if tree[0] == 'symbol' and len(tree) == 2:
1987 1990 name = tree[1]
1988 1991 alias = aliases.get(name)
1989 1992 if alias and alias.args is None and alias.tree == tree:
1990 1993 return alias
1991 1994 if tree[0] == 'func' and len(tree) > 1:
1992 1995 if tree[1][0] == 'symbol' and len(tree[1]) == 2:
1993 1996 name = tree[1][1]
1994 1997 alias = aliases.get(name)
1995 1998 if alias and alias.args is not None and alias.tree == tree[:2]:
1996 1999 return alias
1997 2000 return None
1998 2001
1999 2002 def _expandargs(tree, args):
2000 2003 """Replace _aliasarg instances with the substitution value of the
2001 2004 same name in args, recursively.
2002 2005 """
2003 2006 if not tree or not isinstance(tree, tuple):
2004 2007 return tree
2005 2008 arg = _getaliasarg(tree)
2006 2009 if arg is not None:
2007 2010 return args[arg]
2008 2011 return tuple(_expandargs(t, args) for t in tree)
2009 2012
2010 2013 def _expandaliases(aliases, tree, expanding, cache):
2011 2014 """Expand aliases in tree, recursively.
2012 2015
2013 2016 'aliases' is a dictionary mapping user defined aliases to
2014 2017 revsetalias objects.
2015 2018 """
2016 2019 if not isinstance(tree, tuple):
2017 2020 # Do not expand raw strings
2018 2021 return tree
2019 2022 alias = _getalias(aliases, tree)
2020 2023 if alias is not None:
2021 2024 if alias in expanding:
2022 2025 raise error.ParseError(_('infinite expansion of revset alias "%s" '
2023 2026 'detected') % alias.name)
2024 2027 expanding.append(alias)
2025 2028 if alias.name not in cache:
2026 2029 cache[alias.name] = _expandaliases(aliases, alias.replacement,
2027 2030 expanding, cache)
2028 2031 result = cache[alias.name]
2029 2032 expanding.pop()
2030 2033 if alias.args is not None:
2031 2034 l = getlist(tree[2])
2032 2035 if len(l) != len(alias.args):
2033 2036 raise error.ParseError(
2034 2037 _('invalid number of arguments: %s') % len(l))
2035 2038 l = [_expandaliases(aliases, a, [], cache) for a in l]
2036 2039 result = _expandargs(result, dict(zip(alias.args, l)))
2037 2040 else:
2038 2041 result = tuple(_expandaliases(aliases, t, expanding, cache)
2039 2042 for t in tree)
2040 2043 return result
2041 2044
2042 2045 def findaliases(ui, tree):
2043 2046 _checkaliasarg(tree)
2044 2047 aliases = {}
2045 2048 for k, v in ui.configitems('revsetalias'):
2046 2049 alias = revsetalias(k, v)
2047 2050 aliases[alias.name] = alias
2048 2051 return _expandaliases(aliases, tree, [], {})
2049 2052
2050 2053 def parse(spec, lookup=None):
2051 2054 p = parser.parser(tokenize, elements)
2052 2055 return p.parse(spec, lookup=lookup)
2053 2056
2054 2057 def match(ui, spec, repo=None):
2055 2058 if not spec:
2056 2059 raise error.ParseError(_("empty query"))
2057 2060 lookup = None
2058 2061 if repo:
2059 2062 lookup = repo.__contains__
2060 2063 tree, pos = parse(spec, lookup)
2061 2064 if (pos != len(spec)):
2062 2065 raise error.ParseError(_("invalid token"), pos)
2063 2066 if ui:
2064 2067 tree = findaliases(ui, tree)
2065 2068 weight, tree = optimize(tree, True)
2066 2069 def mfunc(repo, subset):
2067 2070 if util.safehasattr(subset, 'isascending'):
2068 2071 result = getset(repo, subset, tree)
2069 2072 else:
2070 2073 result = getset(repo, baseset(subset), tree)
2071 2074 return result
2072 2075 return mfunc
2073 2076
2074 2077 def formatspec(expr, *args):
2075 2078 '''
2076 2079 This is a convenience function for using revsets internally, and
2077 2080 escapes arguments appropriately. Aliases are intentionally ignored
2078 2081 so that intended expression behavior isn't accidentally subverted.
2079 2082
2080 2083 Supported arguments:
2081 2084
2082 2085 %r = revset expression, parenthesized
2083 2086 %d = int(arg), no quoting
2084 2087 %s = string(arg), escaped and single-quoted
2085 2088 %b = arg.branch(), escaped and single-quoted
2086 2089 %n = hex(arg), single-quoted
2087 2090 %% = a literal '%'
2088 2091
2089 2092 Prefixing the type with 'l' specifies a parenthesized list of that type.
2090 2093
2091 2094 >>> formatspec('%r:: and %lr', '10 or 11', ("this()", "that()"))
2092 2095 '(10 or 11):: and ((this()) or (that()))'
2093 2096 >>> formatspec('%d:: and not %d::', 10, 20)
2094 2097 '10:: and not 20::'
2095 2098 >>> formatspec('%ld or %ld', [], [1])
2096 2099 "_list('') or 1"
2097 2100 >>> formatspec('keyword(%s)', 'foo\\xe9')
2098 2101 "keyword('foo\\\\xe9')"
2099 2102 >>> b = lambda: 'default'
2100 2103 >>> b.branch = b
2101 2104 >>> formatspec('branch(%b)', b)
2102 2105 "branch('default')"
2103 2106 >>> formatspec('root(%ls)', ['a', 'b', 'c', 'd'])
2104 2107 "root(_list('a\\x00b\\x00c\\x00d'))"
2105 2108 '''
2106 2109
2107 2110 def quote(s):
2108 2111 return repr(str(s))
2109 2112
2110 2113 def argtype(c, arg):
2111 2114 if c == 'd':
2112 2115 return str(int(arg))
2113 2116 elif c == 's':
2114 2117 return quote(arg)
2115 2118 elif c == 'r':
2116 2119 parse(arg) # make sure syntax errors are confined
2117 2120 return '(%s)' % arg
2118 2121 elif c == 'n':
2119 2122 return quote(node.hex(arg))
2120 2123 elif c == 'b':
2121 2124 return quote(arg.branch())
2122 2125
2123 2126 def listexp(s, t):
2124 2127 l = len(s)
2125 2128 if l == 0:
2126 2129 return "_list('')"
2127 2130 elif l == 1:
2128 2131 return argtype(t, s[0])
2129 2132 elif t == 'd':
2130 2133 return "_intlist('%s')" % "\0".join(str(int(a)) for a in s)
2131 2134 elif t == 's':
2132 2135 return "_list('%s')" % "\0".join(s)
2133 2136 elif t == 'n':
2134 2137 return "_hexlist('%s')" % "\0".join(node.hex(a) for a in s)
2135 2138 elif t == 'b':
2136 2139 return "_list('%s')" % "\0".join(a.branch() for a in s)
2137 2140
2138 2141 m = l // 2
2139 2142 return '(%s or %s)' % (listexp(s[:m], t), listexp(s[m:], t))
2140 2143
2141 2144 ret = ''
2142 2145 pos = 0
2143 2146 arg = 0
2144 2147 while pos < len(expr):
2145 2148 c = expr[pos]
2146 2149 if c == '%':
2147 2150 pos += 1
2148 2151 d = expr[pos]
2149 2152 if d == '%':
2150 2153 ret += d
2151 2154 elif d in 'dsnbr':
2152 2155 ret += argtype(d, args[arg])
2153 2156 arg += 1
2154 2157 elif d == 'l':
2155 2158 # a list of some type
2156 2159 pos += 1
2157 2160 d = expr[pos]
2158 2161 ret += listexp(list(args[arg]), d)
2159 2162 arg += 1
2160 2163 else:
2161 2164 raise util.Abort('unexpected revspec format character %s' % d)
2162 2165 else:
2163 2166 ret += c
2164 2167 pos += 1
2165 2168
2166 2169 return ret
2167 2170
2168 2171 def prettyformat(tree):
2169 2172 def _prettyformat(tree, level, lines):
2170 2173 if not isinstance(tree, tuple) or tree[0] in ('string', 'symbol'):
2171 2174 lines.append((level, str(tree)))
2172 2175 else:
2173 2176 lines.append((level, '(%s' % tree[0]))
2174 2177 for s in tree[1:]:
2175 2178 _prettyformat(s, level + 1, lines)
2176 2179 lines[-1:] = [(lines[-1][0], lines[-1][1] + ')')]
2177 2180
2178 2181 lines = []
2179 2182 _prettyformat(tree, 0, lines)
2180 2183 output = '\n'.join((' '*l + s) for l, s in lines)
2181 2184 return output
2182 2185
2183 2186 def depth(tree):
2184 2187 if isinstance(tree, tuple):
2185 2188 return max(map(depth, tree)) + 1
2186 2189 else:
2187 2190 return 0
2188 2191
2189 2192 def funcsused(tree):
2190 2193 if not isinstance(tree, tuple) or tree[0] in ('string', 'symbol'):
2191 2194 return set()
2192 2195 else:
2193 2196 funcs = set()
2194 2197 for s in tree[1:]:
2195 2198 funcs |= funcsused(s)
2196 2199 if tree[0] == 'func':
2197 2200 funcs.add(tree[1][1])
2198 2201 return funcs
2199 2202
2200 2203 class abstractsmartset(object):
2201 2204
2202 2205 def __nonzero__(self):
2203 2206 """True if the smartset is not empty"""
2204 2207 raise NotImplementedError()
2205 2208
2206 2209 def __contains__(self, rev):
2207 2210 """provide fast membership testing"""
2208 2211 raise NotImplementedError()
2209 2212
2210 2213 def __iter__(self):
2211 2214 """iterate the set in the order it is supposed to be iterated"""
2212 2215 raise NotImplementedError()
2213 2216
2214 2217 # Attributes containing a function to perform a fast iteration in a given
2215 2218 # direction. A smartset can have none, one, or both defined.
2216 2219 #
2217 2220 # Default value is None instead of a function returning None to avoid
2218 2221 # initializing an iterator just for testing if a fast method exists.
2219 2222 fastasc = None
2220 2223 fastdesc = None
2221 2224
2222 2225 def isascending(self):
2223 2226 """True if the set will iterate in ascending order"""
2224 2227 raise NotImplementedError()
2225 2228
2226 2229 def isdescending(self):
2227 2230 """True if the set will iterate in descending order"""
2228 2231 raise NotImplementedError()
2229 2232
2230 2233 def min(self):
2231 2234 """return the minimum element in the set"""
2232 2235 if self.fastasc is not None:
2233 2236 for r in self.fastasc():
2234 2237 return r
2235 2238 raise ValueError('arg is an empty sequence')
2236 2239 return min(self)
2237 2240
2238 2241 def max(self):
2239 2242 """return the maximum element in the set"""
2240 2243 if self.fastdesc is not None:
2241 2244 for r in self.fastdesc():
2242 2245 return r
2243 2246 raise ValueError('arg is an empty sequence')
2244 2247 return max(self)
2245 2248
2246 2249 def first(self):
2247 2250 """return the first element in the set (user iteration perspective)
2248 2251
2249 2252 Return None if the set is empty"""
2250 2253 raise NotImplementedError()
2251 2254
2252 2255 def last(self):
2253 2256 """return the last element in the set (user iteration perspective)
2254 2257
2255 2258 Return None if the set is empty"""
2256 2259 raise NotImplementedError()
2257 2260
2258 2261 def __len__(self):
2259 2262 """return the length of the smartsets
2260 2263
2261 2264 This can be expensive on smartset that could be lazy otherwise."""
2262 2265 raise NotImplementedError()
2263 2266
2264 2267 def reverse(self):
2265 2268 """reverse the expected iteration order"""
2266 2269 raise NotImplementedError()
2267 2270
2268 2271 def sort(self, reverse=True):
2269 2272 """get the set to iterate in an ascending or descending order"""
2270 2273 raise NotImplementedError()
2271 2274
2272 2275 def __and__(self, other):
2273 2276 """Returns a new object with the intersection of the two collections.
2274 2277
2275 2278 This is part of the mandatory API for smartset."""
2276 2279 return self.filter(other.__contains__, cache=False)
2277 2280
2278 2281 def __add__(self, other):
2279 2282 """Returns a new object with the union of the two collections.
2280 2283
2281 2284 This is part of the mandatory API for smartset."""
2282 2285 return addset(self, other)
2283 2286
2284 2287 def __sub__(self, other):
2285 2288 """Returns a new object with the substraction of the two collections.
2286 2289
2287 2290 This is part of the mandatory API for smartset."""
2288 2291 c = other.__contains__
2289 2292 return self.filter(lambda r: not c(r), cache=False)
2290 2293
2291 2294 def filter(self, condition, cache=True):
2292 2295 """Returns this smartset filtered by condition as a new smartset.
2293 2296
2294 2297 `condition` is a callable which takes a revision number and returns a
2295 2298 boolean.
2296 2299
2297 2300 This is part of the mandatory API for smartset."""
2298 2301 # builtin cannot be cached. but do not needs to
2299 2302 if cache and util.safehasattr(condition, 'func_code'):
2300 2303 condition = util.cachefunc(condition)
2301 2304 return filteredset(self, condition)
2302 2305
2303 2306 class baseset(abstractsmartset):
2304 2307 """Basic data structure that represents a revset and contains the basic
2305 2308 operation that it should be able to perform.
2306 2309
2307 2310 Every method in this class should be implemented by any smartset class.
2308 2311 """
2309 2312 def __init__(self, data=()):
2310 2313 if not isinstance(data, list):
2311 2314 data = list(data)
2312 2315 self._list = data
2313 2316 self._ascending = None
2314 2317
2315 2318 @util.propertycache
2316 2319 def _set(self):
2317 2320 return set(self._list)
2318 2321
2319 2322 @util.propertycache
2320 2323 def _asclist(self):
2321 2324 asclist = self._list[:]
2322 2325 asclist.sort()
2323 2326 return asclist
2324 2327
2325 2328 def __iter__(self):
2326 2329 if self._ascending is None:
2327 2330 return iter(self._list)
2328 2331 elif self._ascending:
2329 2332 return iter(self._asclist)
2330 2333 else:
2331 2334 return reversed(self._asclist)
2332 2335
2333 2336 def fastasc(self):
2334 2337 return iter(self._asclist)
2335 2338
2336 2339 def fastdesc(self):
2337 2340 return reversed(self._asclist)
2338 2341
2339 2342 @util.propertycache
2340 2343 def __contains__(self):
2341 2344 return self._set.__contains__
2342 2345
2343 2346 def __nonzero__(self):
2344 2347 return bool(self._list)
2345 2348
2346 2349 def sort(self, reverse=False):
2347 2350 self._ascending = not bool(reverse)
2348 2351
2349 2352 def reverse(self):
2350 2353 if self._ascending is None:
2351 2354 self._list.reverse()
2352 2355 else:
2353 2356 self._ascending = not self._ascending
2354 2357
2355 2358 def __len__(self):
2356 2359 return len(self._list)
2357 2360
2358 2361 def isascending(self):
2359 2362 """Returns True if the collection is ascending order, False if not.
2360 2363
2361 2364 This is part of the mandatory API for smartset."""
2362 2365 if len(self) <= 1:
2363 2366 return True
2364 2367 return self._ascending is not None and self._ascending
2365 2368
2366 2369 def isdescending(self):
2367 2370 """Returns True if the collection is descending order, False if not.
2368 2371
2369 2372 This is part of the mandatory API for smartset."""
2370 2373 if len(self) <= 1:
2371 2374 return True
2372 2375 return self._ascending is not None and not self._ascending
2373 2376
2374 2377 def first(self):
2375 2378 if self:
2376 2379 if self._ascending is None:
2377 2380 return self._list[0]
2378 2381 elif self._ascending:
2379 2382 return self._asclist[0]
2380 2383 else:
2381 2384 return self._asclist[-1]
2382 2385 return None
2383 2386
2384 2387 def last(self):
2385 2388 if self:
2386 2389 if self._ascending is None:
2387 2390 return self._list[-1]
2388 2391 elif self._ascending:
2389 2392 return self._asclist[-1]
2390 2393 else:
2391 2394 return self._asclist[0]
2392 2395 return None
2393 2396
2394 2397 class filteredset(abstractsmartset):
2395 2398 """Duck type for baseset class which iterates lazily over the revisions in
2396 2399 the subset and contains a function which tests for membership in the
2397 2400 revset
2398 2401 """
2399 2402 def __init__(self, subset, condition=lambda x: True):
2400 2403 """
2401 2404 condition: a function that decide whether a revision in the subset
2402 2405 belongs to the revset or not.
2403 2406 """
2404 2407 self._subset = subset
2405 2408 self._condition = condition
2406 2409 self._cache = {}
2407 2410
2408 2411 def __contains__(self, x):
2409 2412 c = self._cache
2410 2413 if x not in c:
2411 2414 v = c[x] = x in self._subset and self._condition(x)
2412 2415 return v
2413 2416 return c[x]
2414 2417
2415 2418 def __iter__(self):
2416 2419 return self._iterfilter(self._subset)
2417 2420
2418 2421 def _iterfilter(self, it):
2419 2422 cond = self._condition
2420 2423 for x in it:
2421 2424 if cond(x):
2422 2425 yield x
2423 2426
2424 2427 @property
2425 2428 def fastasc(self):
2426 2429 it = self._subset.fastasc
2427 2430 if it is None:
2428 2431 return None
2429 2432 return lambda: self._iterfilter(it())
2430 2433
2431 2434 @property
2432 2435 def fastdesc(self):
2433 2436 it = self._subset.fastdesc
2434 2437 if it is None:
2435 2438 return None
2436 2439 return lambda: self._iterfilter(it())
2437 2440
2438 2441 def __nonzero__(self):
2439 2442 for r in self:
2440 2443 return True
2441 2444 return False
2442 2445
2443 2446 def __len__(self):
2444 2447 # Basic implementation to be changed in future patches.
2445 2448 l = baseset([r for r in self])
2446 2449 return len(l)
2447 2450
2448 2451 def sort(self, reverse=False):
2449 2452 self._subset.sort(reverse=reverse)
2450 2453
2451 2454 def reverse(self):
2452 2455 self._subset.reverse()
2453 2456
2454 2457 def isascending(self):
2455 2458 return self._subset.isascending()
2456 2459
2457 2460 def isdescending(self):
2458 2461 return self._subset.isdescending()
2459 2462
2460 2463 def first(self):
2461 2464 for x in self:
2462 2465 return x
2463 2466 return None
2464 2467
2465 2468 def last(self):
2466 2469 it = None
2467 2470 if self._subset.isascending:
2468 2471 it = self.fastdesc
2469 2472 elif self._subset.isdescending:
2470 2473 it = self.fastdesc
2471 2474 if it is None:
2472 2475 # slowly consume everything. This needs improvement
2473 2476 it = lambda: reversed(list(self))
2474 2477 for x in it():
2475 2478 return x
2476 2479 return None
2477 2480
2478 2481 class addset(abstractsmartset):
2479 2482 """Represent the addition of two sets
2480 2483
2481 2484 Wrapper structure for lazily adding two structures without losing much
2482 2485 performance on the __contains__ method
2483 2486
2484 2487 If the ascending attribute is set, that means the two structures are
2485 2488 ordered in either an ascending or descending way. Therefore, we can add
2486 2489 them maintaining the order by iterating over both at the same time
2487 2490 """
2488 2491 def __init__(self, revs1, revs2, ascending=None):
2489 2492 self._r1 = revs1
2490 2493 self._r2 = revs2
2491 2494 self._iter = None
2492 2495 self._ascending = ascending
2493 2496 self._genlist = None
2494 2497 self._asclist = None
2495 2498
2496 2499 def __len__(self):
2497 2500 return len(self._list)
2498 2501
2499 2502 def __nonzero__(self):
2500 2503 return bool(self._r1 or self._r2)
2501 2504
2502 2505 @util.propertycache
2503 2506 def _list(self):
2504 2507 if not self._genlist:
2505 2508 self._genlist = baseset(self._iterator())
2506 2509 return self._genlist
2507 2510
2508 2511 def _iterator(self):
2509 2512 """Iterate over both collections without repeating elements
2510 2513
2511 2514 If the ascending attribute is not set, iterate over the first one and
2512 2515 then over the second one checking for membership on the first one so we
2513 2516 dont yield any duplicates.
2514 2517
2515 2518 If the ascending attribute is set, iterate over both collections at the
2516 2519 same time, yielding only one value at a time in the given order.
2517 2520 """
2518 2521 if self._ascending is None:
2519 2522 def gen():
2520 2523 for r in self._r1:
2521 2524 yield r
2522 2525 inr1 = self._r1.__contains__
2523 2526 for r in self._r2:
2524 2527 if not inr1(r):
2525 2528 yield r
2526 2529 gen = gen()
2527 2530 else:
2528 2531 iter1 = iter(self._r1)
2529 2532 iter2 = iter(self._r2)
2530 2533 gen = self._iterordered(self._ascending, iter1, iter2)
2531 2534 return gen
2532 2535
2533 2536 def __iter__(self):
2534 2537 if self._ascending is None:
2535 2538 if self._genlist:
2536 2539 return iter(self._genlist)
2537 2540 return iter(self._iterator())
2538 2541 self._trysetasclist()
2539 2542 if self._ascending:
2540 2543 it = self.fastasc
2541 2544 else:
2542 2545 it = self.fastdesc
2543 2546 if it is None:
2544 2547 # consume the gen and try again
2545 2548 self._list
2546 2549 return iter(self)
2547 2550 return it()
2548 2551
2549 2552 def _trysetasclist(self):
2550 2553 """populate the _asclist attribut if possible and necessary"""
2551 2554 if self._genlist is not None and self._asclist is None:
2552 2555 self._asclist = sorted(self._genlist)
2553 2556
2554 2557 @property
2555 2558 def fastasc(self):
2556 2559 self._trysetasclist()
2557 2560 if self._asclist is not None:
2558 2561 return self._asclist.__iter__
2559 2562 iter1 = self._r1.fastasc
2560 2563 iter2 = self._r2.fastasc
2561 2564 if None in (iter1, iter2):
2562 2565 return None
2563 2566 return lambda: self._iterordered(True, iter1(), iter2())
2564 2567
2565 2568 @property
2566 2569 def fastdesc(self):
2567 2570 self._trysetasclist()
2568 2571 if self._asclist is not None:
2569 2572 return self._asclist.__reversed__
2570 2573 iter1 = self._r1.fastdesc
2571 2574 iter2 = self._r2.fastdesc
2572 2575 if None in (iter1, iter2):
2573 2576 return None
2574 2577 return lambda: self._iterordered(False, iter1(), iter2())
2575 2578
2576 2579 def _iterordered(self, ascending, iter1, iter2):
2577 2580 """produce an ordered iteration from two iterators with the same order
2578 2581
2579 2582 The ascending is used to indicated the iteration direction.
2580 2583 """
2581 2584 choice = max
2582 2585 if ascending:
2583 2586 choice = min
2584 2587
2585 2588 val1 = None
2586 2589 val2 = None
2587 2590
2588 2591 choice = max
2589 2592 if ascending:
2590 2593 choice = min
2591 2594 try:
2592 2595 # Consume both iterators in an ordered way until one is
2593 2596 # empty
2594 2597 while True:
2595 2598 if val1 is None:
2596 2599 val1 = iter1.next()
2597 2600 if val2 is None:
2598 2601 val2 = iter2.next()
2599 2602 next = choice(val1, val2)
2600 2603 yield next
2601 2604 if val1 == next:
2602 2605 val1 = None
2603 2606 if val2 == next:
2604 2607 val2 = None
2605 2608 except StopIteration:
2606 2609 # Flush any remaining values and consume the other one
2607 2610 it = iter2
2608 2611 if val1 is not None:
2609 2612 yield val1
2610 2613 it = iter1
2611 2614 elif val2 is not None:
2612 2615 # might have been equality and both are empty
2613 2616 yield val2
2614 2617 for val in it:
2615 2618 yield val
2616 2619
2617 2620 def __contains__(self, x):
2618 2621 return x in self._r1 or x in self._r2
2619 2622
2620 2623 def sort(self, reverse=False):
2621 2624 """Sort the added set
2622 2625
2623 2626 For this we use the cached list with all the generated values and if we
2624 2627 know they are ascending or descending we can sort them in a smart way.
2625 2628 """
2626 2629 self._ascending = not reverse
2627 2630
2628 2631 def isascending(self):
2629 2632 return self._ascending is not None and self._ascending
2630 2633
2631 2634 def isdescending(self):
2632 2635 return self._ascending is not None and not self._ascending
2633 2636
2634 2637 def reverse(self):
2635 2638 if self._ascending is None:
2636 2639 self._list.reverse()
2637 2640 else:
2638 2641 self._ascending = not self._ascending
2639 2642
2640 2643 def first(self):
2641 2644 if self:
2642 2645 return self._list.first()
2643 2646 return None
2644 2647
2645 2648 def last(self):
2646 2649 if self:
2647 2650 return self._list.last()
2648 2651 return None
2649 2652
2650 2653 class generatorset(abstractsmartset):
2651 2654 """Wrap a generator for lazy iteration
2652 2655
2653 2656 Wrapper structure for generators that provides lazy membership and can
2654 2657 be iterated more than once.
2655 2658 When asked for membership it generates values until either it finds the
2656 2659 requested one or has gone through all the elements in the generator
2657 2660 """
2658 2661 def __init__(self, gen, iterasc=None):
2659 2662 """
2660 2663 gen: a generator producing the values for the generatorset.
2661 2664 """
2662 2665 self._gen = gen
2663 2666 self._asclist = None
2664 2667 self._cache = {}
2665 2668 self._genlist = []
2666 2669 self._finished = False
2667 2670 self._ascending = True
2668 2671 if iterasc is not None:
2669 2672 if iterasc:
2670 2673 self.fastasc = self._iterator
2671 2674 self.__contains__ = self._asccontains
2672 2675 else:
2673 2676 self.fastdesc = self._iterator
2674 2677 self.__contains__ = self._desccontains
2675 2678
2676 2679 def __nonzero__(self):
2677 2680 for r in self:
2678 2681 return True
2679 2682 return False
2680 2683
2681 2684 def __contains__(self, x):
2682 2685 if x in self._cache:
2683 2686 return self._cache[x]
2684 2687
2685 2688 # Use new values only, as existing values would be cached.
2686 2689 for l in self._consumegen():
2687 2690 if l == x:
2688 2691 return True
2689 2692
2690 2693 self._cache[x] = False
2691 2694 return False
2692 2695
2693 2696 def _asccontains(self, x):
2694 2697 """version of contains optimised for ascending generator"""
2695 2698 if x in self._cache:
2696 2699 return self._cache[x]
2697 2700
2698 2701 # Use new values only, as existing values would be cached.
2699 2702 for l in self._consumegen():
2700 2703 if l == x:
2701 2704 return True
2702 2705 if l > x:
2703 2706 break
2704 2707
2705 2708 self._cache[x] = False
2706 2709 return False
2707 2710
2708 2711 def _desccontains(self, x):
2709 2712 """version of contains optimised for descending generator"""
2710 2713 if x in self._cache:
2711 2714 return self._cache[x]
2712 2715
2713 2716 # Use new values only, as existing values would be cached.
2714 2717 for l in self._consumegen():
2715 2718 if l == x:
2716 2719 return True
2717 2720 if l < x:
2718 2721 break
2719 2722
2720 2723 self._cache[x] = False
2721 2724 return False
2722 2725
2723 2726 def __iter__(self):
2724 2727 if self._ascending:
2725 2728 it = self.fastasc
2726 2729 else:
2727 2730 it = self.fastdesc
2728 2731 if it is not None:
2729 2732 return it()
2730 2733 # we need to consume the iterator
2731 2734 for x in self._consumegen():
2732 2735 pass
2733 2736 # recall the same code
2734 2737 return iter(self)
2735 2738
2736 2739 def _iterator(self):
2737 2740 if self._finished:
2738 2741 return iter(self._genlist)
2739 2742
2740 2743 # We have to use this complex iteration strategy to allow multiple
2741 2744 # iterations at the same time. We need to be able to catch revision
2742 2745 # removed from `consumegen` and added to genlist in another instance.
2743 2746 #
2744 2747 # Getting rid of it would provide an about 15% speed up on this
2745 2748 # iteration.
2746 2749 genlist = self._genlist
2747 2750 nextrev = self._consumegen().next
2748 2751 _len = len # cache global lookup
2749 2752 def gen():
2750 2753 i = 0
2751 2754 while True:
2752 2755 if i < _len(genlist):
2753 2756 yield genlist[i]
2754 2757 else:
2755 2758 yield nextrev()
2756 2759 i += 1
2757 2760 return gen()
2758 2761
2759 2762 def _consumegen(self):
2760 2763 cache = self._cache
2761 2764 genlist = self._genlist.append
2762 2765 for item in self._gen:
2763 2766 cache[item] = True
2764 2767 genlist(item)
2765 2768 yield item
2766 2769 if not self._finished:
2767 2770 self._finished = True
2768 2771 asc = self._genlist[:]
2769 2772 asc.sort()
2770 2773 self._asclist = asc
2771 2774 self.fastasc = asc.__iter__
2772 2775 self.fastdesc = asc.__reversed__
2773 2776
2774 2777 def __len__(self):
2775 2778 for x in self._consumegen():
2776 2779 pass
2777 2780 return len(self._genlist)
2778 2781
2779 2782 def sort(self, reverse=False):
2780 2783 self._ascending = not reverse
2781 2784
2782 2785 def reverse(self):
2783 2786 self._ascending = not self._ascending
2784 2787
2785 2788 def isascending(self):
2786 2789 return self._ascending
2787 2790
2788 2791 def isdescending(self):
2789 2792 return not self._ascending
2790 2793
2791 2794 def first(self):
2792 2795 if self._ascending:
2793 2796 it = self.fastasc
2794 2797 else:
2795 2798 it = self.fastdesc
2796 2799 if it is None:
2797 2800 # we need to consume all and try again
2798 2801 for x in self._consumegen():
2799 2802 pass
2800 2803 return self.first()
2801 2804 if self:
2802 2805 return it.next()
2803 2806 return None
2804 2807
2805 2808 def last(self):
2806 2809 if self._ascending:
2807 2810 it = self.fastdesc
2808 2811 else:
2809 2812 it = self.fastasc
2810 2813 if it is None:
2811 2814 # we need to consume all and try again
2812 2815 for x in self._consumegen():
2813 2816 pass
2814 2817 return self.first()
2815 2818 if self:
2816 2819 return it.next()
2817 2820 return None
2818 2821
2819 2822 def spanset(repo, start=None, end=None):
2820 2823 """factory function to dispatch between fullreposet and actual spanset
2821 2824
2822 2825 Feel free to update all spanset call sites and kill this function at some
2823 2826 point.
2824 2827 """
2825 2828 if start is None and end is None:
2826 2829 return fullreposet(repo)
2827 2830 return _spanset(repo, start, end)
2828 2831
2829 2832
2830 2833 class _spanset(abstractsmartset):
2831 2834 """Duck type for baseset class which represents a range of revisions and
2832 2835 can work lazily and without having all the range in memory
2833 2836
2834 2837 Note that spanset(x, y) behave almost like xrange(x, y) except for two
2835 2838 notable points:
2836 2839 - when x < y it will be automatically descending,
2837 2840 - revision filtered with this repoview will be skipped.
2838 2841
2839 2842 """
2840 2843 def __init__(self, repo, start=0, end=None):
2841 2844 """
2842 2845 start: first revision included the set
2843 2846 (default to 0)
2844 2847 end: first revision excluded (last+1)
2845 2848 (default to len(repo)
2846 2849
2847 2850 Spanset will be descending if `end` < `start`.
2848 2851 """
2849 2852 if end is None:
2850 2853 end = len(repo)
2851 2854 self._ascending = start <= end
2852 2855 if not self._ascending:
2853 2856 start, end = end + 1, start +1
2854 2857 self._start = start
2855 2858 self._end = end
2856 2859 self._hiddenrevs = repo.changelog.filteredrevs
2857 2860
2858 2861 def sort(self, reverse=False):
2859 2862 self._ascending = not reverse
2860 2863
2861 2864 def reverse(self):
2862 2865 self._ascending = not self._ascending
2863 2866
2864 2867 def _iterfilter(self, iterrange):
2865 2868 s = self._hiddenrevs
2866 2869 for r in iterrange:
2867 2870 if r not in s:
2868 2871 yield r
2869 2872
2870 2873 def __iter__(self):
2871 2874 if self._ascending:
2872 2875 return self.fastasc()
2873 2876 else:
2874 2877 return self.fastdesc()
2875 2878
2876 2879 def fastasc(self):
2877 2880 iterrange = xrange(self._start, self._end)
2878 2881 if self._hiddenrevs:
2879 2882 return self._iterfilter(iterrange)
2880 2883 return iter(iterrange)
2881 2884
2882 2885 def fastdesc(self):
2883 2886 iterrange = xrange(self._end - 1, self._start - 1, -1)
2884 2887 if self._hiddenrevs:
2885 2888 return self._iterfilter(iterrange)
2886 2889 return iter(iterrange)
2887 2890
2888 2891 def __contains__(self, rev):
2889 2892 hidden = self._hiddenrevs
2890 2893 return ((self._start <= rev < self._end)
2891 2894 and not (hidden and rev in hidden))
2892 2895
2893 2896 def __nonzero__(self):
2894 2897 for r in self:
2895 2898 return True
2896 2899 return False
2897 2900
2898 2901 def __len__(self):
2899 2902 if not self._hiddenrevs:
2900 2903 return abs(self._end - self._start)
2901 2904 else:
2902 2905 count = 0
2903 2906 start = self._start
2904 2907 end = self._end
2905 2908 for rev in self._hiddenrevs:
2906 2909 if (end < rev <= start) or (start <= rev < end):
2907 2910 count += 1
2908 2911 return abs(self._end - self._start) - count
2909 2912
2910 2913 def isascending(self):
2911 2914 return self._start <= self._end
2912 2915
2913 2916 def isdescending(self):
2914 2917 return self._start >= self._end
2915 2918
2916 2919 def first(self):
2917 2920 if self._ascending:
2918 2921 it = self.fastasc
2919 2922 else:
2920 2923 it = self.fastdesc
2921 2924 for x in it():
2922 2925 return x
2923 2926 return None
2924 2927
2925 2928 def last(self):
2926 2929 if self._ascending:
2927 2930 it = self.fastdesc
2928 2931 else:
2929 2932 it = self.fastasc
2930 2933 for x in it():
2931 2934 return x
2932 2935 return None
2933 2936
2934 2937 class fullreposet(_spanset):
2935 2938 """a set containing all revisions in the repo
2936 2939
2937 2940 This class exists to host special optimisation.
2938 2941 """
2939 2942
2940 2943 def __init__(self, repo):
2941 2944 super(fullreposet, self).__init__(repo)
2942 2945
2943 2946 def __and__(self, other):
2944 2947 """fullrepo & other -> other
2945 2948
2946 2949 As self contains the whole repo, all of the other set should also be in
2947 2950 self. Therefor `self & other = other`.
2948 2951
2949 2952 This boldly assumes the other contains valid revs only.
2950 2953 """
2951 2954 # other not a smartset, make is so
2952 2955 if not util.safehasattr(other, 'isascending'):
2953 2956 # filter out hidden revision
2954 2957 # (this boldly assumes all smartset are pure)
2955 2958 #
2956 2959 # `other` was used with "&", let's assume this is a set like
2957 2960 # object.
2958 2961 other = baseset(other - self._hiddenrevs)
2959 2962
2960 2963 if self.isascending():
2961 2964 other.sort()
2962 2965 else:
2963 2966 other.sort(reverse)
2964 2967 return other
2965 2968
2966 2969 # tell hggettext to extract docstrings from these functions:
2967 2970 i18nfunctions = symbols.values()
General Comments 0
You need to be logged in to leave comments. Login now