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