##// END OF EJS Templates
revset: infer ordering flag to teach if operation should define/follow order...
Yuya Nishihara -
r29930:90455e7b default
parent child Browse files
Show More
@@ -1,3738 +1,3785
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 from __future__ import absolute_import
9 9
10 10 import heapq
11 11 import re
12 12
13 13 from .i18n import _
14 14 from . import (
15 15 destutil,
16 16 encoding,
17 17 error,
18 18 hbisect,
19 19 match as matchmod,
20 20 node,
21 21 obsolete as obsmod,
22 22 parser,
23 23 pathutil,
24 24 phases,
25 25 registrar,
26 26 repoview,
27 27 util,
28 28 )
29 29
30 30 def _revancestors(repo, revs, followfirst):
31 31 """Like revlog.ancestors(), but supports followfirst."""
32 32 if followfirst:
33 33 cut = 1
34 34 else:
35 35 cut = None
36 36 cl = repo.changelog
37 37
38 38 def iterate():
39 39 revs.sort(reverse=True)
40 40 irevs = iter(revs)
41 41 h = []
42 42
43 43 inputrev = next(irevs, None)
44 44 if inputrev is not None:
45 45 heapq.heappush(h, -inputrev)
46 46
47 47 seen = set()
48 48 while h:
49 49 current = -heapq.heappop(h)
50 50 if current == inputrev:
51 51 inputrev = next(irevs, None)
52 52 if inputrev is not None:
53 53 heapq.heappush(h, -inputrev)
54 54 if current not in seen:
55 55 seen.add(current)
56 56 yield current
57 57 for parent in cl.parentrevs(current)[:cut]:
58 58 if parent != node.nullrev:
59 59 heapq.heappush(h, -parent)
60 60
61 61 return generatorset(iterate(), iterasc=False)
62 62
63 63 def _revdescendants(repo, revs, followfirst):
64 64 """Like revlog.descendants() but supports followfirst."""
65 65 if followfirst:
66 66 cut = 1
67 67 else:
68 68 cut = None
69 69
70 70 def iterate():
71 71 cl = repo.changelog
72 72 # XXX this should be 'parentset.min()' assuming 'parentset' is a
73 73 # smartset (and if it is not, it should.)
74 74 first = min(revs)
75 75 nullrev = node.nullrev
76 76 if first == nullrev:
77 77 # Are there nodes with a null first parent and a non-null
78 78 # second one? Maybe. Do we care? Probably not.
79 79 for i in cl:
80 80 yield i
81 81 else:
82 82 seen = set(revs)
83 83 for i in cl.revs(first + 1):
84 84 for x in cl.parentrevs(i)[:cut]:
85 85 if x != nullrev and x in seen:
86 86 seen.add(i)
87 87 yield i
88 88 break
89 89
90 90 return generatorset(iterate(), iterasc=True)
91 91
92 92 def _reachablerootspure(repo, minroot, roots, heads, includepath):
93 93 """return (heads(::<roots> and ::<heads>))
94 94
95 95 If includepath is True, return (<roots>::<heads>)."""
96 96 if not roots:
97 97 return []
98 98 parentrevs = repo.changelog.parentrevs
99 99 roots = set(roots)
100 100 visit = list(heads)
101 101 reachable = set()
102 102 seen = {}
103 103 # prefetch all the things! (because python is slow)
104 104 reached = reachable.add
105 105 dovisit = visit.append
106 106 nextvisit = visit.pop
107 107 # open-code the post-order traversal due to the tiny size of
108 108 # sys.getrecursionlimit()
109 109 while visit:
110 110 rev = nextvisit()
111 111 if rev in roots:
112 112 reached(rev)
113 113 if not includepath:
114 114 continue
115 115 parents = parentrevs(rev)
116 116 seen[rev] = parents
117 117 for parent in parents:
118 118 if parent >= minroot and parent not in seen:
119 119 dovisit(parent)
120 120 if not reachable:
121 121 return baseset()
122 122 if not includepath:
123 123 return reachable
124 124 for rev in sorted(seen):
125 125 for parent in seen[rev]:
126 126 if parent in reachable:
127 127 reached(rev)
128 128 return reachable
129 129
130 130 def reachableroots(repo, roots, heads, includepath=False):
131 131 """return (heads(::<roots> and ::<heads>))
132 132
133 133 If includepath is True, return (<roots>::<heads>)."""
134 134 if not roots:
135 135 return baseset()
136 136 minroot = roots.min()
137 137 roots = list(roots)
138 138 heads = list(heads)
139 139 try:
140 140 revs = repo.changelog.reachableroots(minroot, heads, roots, includepath)
141 141 except AttributeError:
142 142 revs = _reachablerootspure(repo, minroot, roots, heads, includepath)
143 143 revs = baseset(revs)
144 144 revs.sort()
145 145 return revs
146 146
147 147 elements = {
148 148 # token-type: binding-strength, primary, prefix, infix, suffix
149 149 "(": (21, None, ("group", 1, ")"), ("func", 1, ")"), None),
150 150 "##": (20, None, None, ("_concat", 20), None),
151 151 "~": (18, None, None, ("ancestor", 18), None),
152 152 "^": (18, None, None, ("parent", 18), "parentpost"),
153 153 "-": (5, None, ("negate", 19), ("minus", 5), None),
154 154 "::": (17, None, ("dagrangepre", 17), ("dagrange", 17), "dagrangepost"),
155 155 "..": (17, None, ("dagrangepre", 17), ("dagrange", 17), "dagrangepost"),
156 156 ":": (15, "rangeall", ("rangepre", 15), ("range", 15), "rangepost"),
157 157 "not": (10, None, ("not", 10), None, None),
158 158 "!": (10, None, ("not", 10), None, None),
159 159 "and": (5, None, None, ("and", 5), None),
160 160 "&": (5, None, None, ("and", 5), None),
161 161 "%": (5, None, None, ("only", 5), "onlypost"),
162 162 "or": (4, None, None, ("or", 4), None),
163 163 "|": (4, None, None, ("or", 4), None),
164 164 "+": (4, None, None, ("or", 4), None),
165 165 "=": (3, None, None, ("keyvalue", 3), None),
166 166 ",": (2, None, None, ("list", 2), None),
167 167 ")": (0, None, None, None, None),
168 168 "symbol": (0, "symbol", None, None, None),
169 169 "string": (0, "string", None, None, None),
170 170 "end": (0, None, None, None, None),
171 171 }
172 172
173 173 keywords = set(['and', 'or', 'not'])
174 174
175 175 # default set of valid characters for the initial letter of symbols
176 176 _syminitletters = set(c for c in [chr(i) for i in xrange(256)]
177 177 if c.isalnum() or c in '._@' or ord(c) > 127)
178 178
179 179 # default set of valid characters for non-initial letters of symbols
180 180 _symletters = set(c for c in [chr(i) for i in xrange(256)]
181 181 if c.isalnum() or c in '-._/@' or ord(c) > 127)
182 182
183 183 def tokenize(program, lookup=None, syminitletters=None, symletters=None):
184 184 '''
185 185 Parse a revset statement into a stream of tokens
186 186
187 187 ``syminitletters`` is the set of valid characters for the initial
188 188 letter of symbols.
189 189
190 190 By default, character ``c`` is recognized as valid for initial
191 191 letter of symbols, if ``c.isalnum() or c in '._@' or ord(c) > 127``.
192 192
193 193 ``symletters`` is the set of valid characters for non-initial
194 194 letters of symbols.
195 195
196 196 By default, character ``c`` is recognized as valid for non-initial
197 197 letters of symbols, if ``c.isalnum() or c in '-._/@' or ord(c) > 127``.
198 198
199 199 Check that @ is a valid unquoted token character (issue3686):
200 200 >>> list(tokenize("@::"))
201 201 [('symbol', '@', 0), ('::', None, 1), ('end', None, 3)]
202 202
203 203 '''
204 204 if syminitletters is None:
205 205 syminitletters = _syminitletters
206 206 if symletters is None:
207 207 symletters = _symletters
208 208
209 209 if program and lookup:
210 210 # attempt to parse old-style ranges first to deal with
211 211 # things like old-tag which contain query metacharacters
212 212 parts = program.split(':', 1)
213 213 if all(lookup(sym) for sym in parts if sym):
214 214 if parts[0]:
215 215 yield ('symbol', parts[0], 0)
216 216 if len(parts) > 1:
217 217 s = len(parts[0])
218 218 yield (':', None, s)
219 219 if parts[1]:
220 220 yield ('symbol', parts[1], s + 1)
221 221 yield ('end', None, len(program))
222 222 return
223 223
224 224 pos, l = 0, len(program)
225 225 while pos < l:
226 226 c = program[pos]
227 227 if c.isspace(): # skip inter-token whitespace
228 228 pass
229 229 elif c == ':' and program[pos:pos + 2] == '::': # look ahead carefully
230 230 yield ('::', None, pos)
231 231 pos += 1 # skip ahead
232 232 elif c == '.' and program[pos:pos + 2] == '..': # look ahead carefully
233 233 yield ('..', None, pos)
234 234 pos += 1 # skip ahead
235 235 elif c == '#' and program[pos:pos + 2] == '##': # look ahead carefully
236 236 yield ('##', None, pos)
237 237 pos += 1 # skip ahead
238 238 elif c in "():=,-|&+!~^%": # handle simple operators
239 239 yield (c, None, pos)
240 240 elif (c in '"\'' or c == 'r' and
241 241 program[pos:pos + 2] in ("r'", 'r"')): # handle quoted strings
242 242 if c == 'r':
243 243 pos += 1
244 244 c = program[pos]
245 245 decode = lambda x: x
246 246 else:
247 247 decode = parser.unescapestr
248 248 pos += 1
249 249 s = pos
250 250 while pos < l: # find closing quote
251 251 d = program[pos]
252 252 if d == '\\': # skip over escaped characters
253 253 pos += 2
254 254 continue
255 255 if d == c:
256 256 yield ('string', decode(program[s:pos]), s)
257 257 break
258 258 pos += 1
259 259 else:
260 260 raise error.ParseError(_("unterminated string"), s)
261 261 # gather up a symbol/keyword
262 262 elif c in syminitletters:
263 263 s = pos
264 264 pos += 1
265 265 while pos < l: # find end of symbol
266 266 d = program[pos]
267 267 if d not in symletters:
268 268 break
269 269 if d == '.' and program[pos - 1] == '.': # special case for ..
270 270 pos -= 1
271 271 break
272 272 pos += 1
273 273 sym = program[s:pos]
274 274 if sym in keywords: # operator keywords
275 275 yield (sym, None, s)
276 276 elif '-' in sym:
277 277 # some jerk gave us foo-bar-baz, try to check if it's a symbol
278 278 if lookup and lookup(sym):
279 279 # looks like a real symbol
280 280 yield ('symbol', sym, s)
281 281 else:
282 282 # looks like an expression
283 283 parts = sym.split('-')
284 284 for p in parts[:-1]:
285 285 if p: # possible consecutive -
286 286 yield ('symbol', p, s)
287 287 s += len(p)
288 288 yield ('-', None, pos)
289 289 s += 1
290 290 if parts[-1]: # possible trailing -
291 291 yield ('symbol', parts[-1], s)
292 292 else:
293 293 yield ('symbol', sym, s)
294 294 pos -= 1
295 295 else:
296 296 raise error.ParseError(_("syntax error in revset '%s'") %
297 297 program, pos)
298 298 pos += 1
299 299 yield ('end', None, pos)
300 300
301 301 # helpers
302 302
303 303 def getsymbol(x):
304 304 if x and x[0] == 'symbol':
305 305 return x[1]
306 306 raise error.ParseError(_('not a symbol'))
307 307
308 308 def getstring(x, err):
309 309 if x and (x[0] == 'string' or x[0] == 'symbol'):
310 310 return x[1]
311 311 raise error.ParseError(err)
312 312
313 313 def getlist(x):
314 314 if not x:
315 315 return []
316 316 if x[0] == 'list':
317 317 return list(x[1:])
318 318 return [x]
319 319
320 320 def getargs(x, min, max, err):
321 321 l = getlist(x)
322 322 if len(l) < min or (max >= 0 and len(l) > max):
323 323 raise error.ParseError(err)
324 324 return l
325 325
326 326 def getargsdict(x, funcname, keys):
327 327 return parser.buildargsdict(getlist(x), funcname, keys.split(),
328 328 keyvaluenode='keyvalue', keynode='symbol')
329 329
330 330 def getset(repo, subset, x):
331 331 if not x:
332 332 raise error.ParseError(_("missing argument"))
333 333 s = methods[x[0]](repo, subset, *x[1:])
334 334 if util.safehasattr(s, 'isascending'):
335 335 return s
336 336 # else case should not happen, because all non-func are internal,
337 337 # ignoring for now.
338 338 if x[0] == 'func' and x[1][0] == 'symbol' and x[1][1] in symbols:
339 339 repo.ui.deprecwarn('revset "%s" uses list instead of smartset'
340 340 % x[1][1],
341 341 '3.9')
342 342 return baseset(s)
343 343
344 344 def _getrevsource(repo, r):
345 345 extra = repo[r].extra()
346 346 for label in ('source', 'transplant_source', 'rebase_source'):
347 347 if label in extra:
348 348 try:
349 349 return repo[extra[label]].rev()
350 350 except error.RepoLookupError:
351 351 pass
352 352 return None
353 353
354 354 # operator methods
355 355
356 356 def stringset(repo, subset, x):
357 357 x = repo[x].rev()
358 358 if (x in subset
359 359 or x == node.nullrev and isinstance(subset, fullreposet)):
360 360 return baseset([x])
361 361 return baseset()
362 362
363 363 def rangeset(repo, subset, x, y):
364 364 m = getset(repo, fullreposet(repo), x)
365 365 n = getset(repo, fullreposet(repo), y)
366 366
367 367 if not m or not n:
368 368 return baseset()
369 369 m, n = m.first(), n.last()
370 370
371 371 if m == n:
372 372 r = baseset([m])
373 373 elif n == node.wdirrev:
374 374 r = spanset(repo, m, len(repo)) + baseset([n])
375 375 elif m == node.wdirrev:
376 376 r = baseset([m]) + spanset(repo, len(repo) - 1, n - 1)
377 377 elif m < n:
378 378 r = spanset(repo, m, n + 1)
379 379 else:
380 380 r = spanset(repo, m, n - 1)
381 381 # XXX We should combine with subset first: 'subset & baseset(...)'. This is
382 382 # necessary to ensure we preserve the order in subset.
383 383 #
384 384 # This has performance implication, carrying the sorting over when possible
385 385 # would be more efficient.
386 386 return r & subset
387 387
388 388 def dagrange(repo, subset, x, y):
389 389 r = fullreposet(repo)
390 390 xs = reachableroots(repo, getset(repo, r, x), getset(repo, r, y),
391 391 includepath=True)
392 392 return subset & xs
393 393
394 394 def andset(repo, subset, x, y):
395 395 return getset(repo, getset(repo, subset, x), y)
396 396
397 397 def differenceset(repo, subset, x, y):
398 398 return getset(repo, subset, x) - getset(repo, subset, y)
399 399
400 400 def _orsetlist(repo, subset, xs):
401 401 assert xs
402 402 if len(xs) == 1:
403 403 return getset(repo, subset, xs[0])
404 404 p = len(xs) // 2
405 405 a = _orsetlist(repo, subset, xs[:p])
406 406 b = _orsetlist(repo, subset, xs[p:])
407 407 return a + b
408 408
409 409 def orset(repo, subset, x):
410 410 return _orsetlist(repo, subset, getlist(x))
411 411
412 412 def notset(repo, subset, x):
413 413 return subset - getset(repo, subset, x)
414 414
415 415 def listset(repo, subset, *xs):
416 416 raise error.ParseError(_("can't use a list in this context"),
417 417 hint=_('see hg help "revsets.x or y"'))
418 418
419 419 def keyvaluepair(repo, subset, k, v):
420 420 raise error.ParseError(_("can't use a key-value pair in this context"))
421 421
422 422 def func(repo, subset, a, b):
423 423 f = getsymbol(a)
424 424 if f in symbols:
425 425 return symbols[f](repo, subset, b)
426 426
427 427 keep = lambda fn: getattr(fn, '__doc__', None) is not None
428 428
429 429 syms = [s for (s, fn) in symbols.items() if keep(fn)]
430 430 raise error.UnknownIdentifier(f, syms)
431 431
432 432 # functions
433 433
434 434 # symbols are callables like:
435 435 # fn(repo, subset, x)
436 436 # with:
437 437 # repo - current repository instance
438 438 # subset - of revisions to be examined
439 439 # x - argument in tree form
440 440 symbols = {}
441 441
442 442 # symbols which can't be used for a DoS attack for any given input
443 443 # (e.g. those which accept regexes as plain strings shouldn't be included)
444 444 # functions that just return a lot of changesets (like all) don't count here
445 445 safesymbols = set()
446 446
447 447 predicate = registrar.revsetpredicate()
448 448
449 449 @predicate('_destupdate')
450 450 def _destupdate(repo, subset, x):
451 451 # experimental revset for update destination
452 452 args = getargsdict(x, 'limit', 'clean check')
453 453 return subset & baseset([destutil.destupdate(repo, **args)[0]])
454 454
455 455 @predicate('_destmerge')
456 456 def _destmerge(repo, subset, x):
457 457 # experimental revset for merge destination
458 458 sourceset = None
459 459 if x is not None:
460 460 sourceset = getset(repo, fullreposet(repo), x)
461 461 return subset & baseset([destutil.destmerge(repo, sourceset=sourceset)])
462 462
463 463 @predicate('adds(pattern)', safe=True)
464 464 def adds(repo, subset, x):
465 465 """Changesets that add a file matching pattern.
466 466
467 467 The pattern without explicit kind like ``glob:`` is expected to be
468 468 relative to the current directory and match against a file or a
469 469 directory.
470 470 """
471 471 # i18n: "adds" is a keyword
472 472 pat = getstring(x, _("adds requires a pattern"))
473 473 return checkstatus(repo, subset, pat, 1)
474 474
475 475 @predicate('ancestor(*changeset)', safe=True)
476 476 def ancestor(repo, subset, x):
477 477 """A greatest common ancestor of the changesets.
478 478
479 479 Accepts 0 or more changesets.
480 480 Will return empty list when passed no args.
481 481 Greatest common ancestor of a single changeset is that changeset.
482 482 """
483 483 # i18n: "ancestor" is a keyword
484 484 l = getlist(x)
485 485 rl = fullreposet(repo)
486 486 anc = None
487 487
488 488 # (getset(repo, rl, i) for i in l) generates a list of lists
489 489 for revs in (getset(repo, rl, i) for i in l):
490 490 for r in revs:
491 491 if anc is None:
492 492 anc = repo[r]
493 493 else:
494 494 anc = anc.ancestor(repo[r])
495 495
496 496 if anc is not None and anc.rev() in subset:
497 497 return baseset([anc.rev()])
498 498 return baseset()
499 499
500 500 def _ancestors(repo, subset, x, followfirst=False):
501 501 heads = getset(repo, fullreposet(repo), x)
502 502 if not heads:
503 503 return baseset()
504 504 s = _revancestors(repo, heads, followfirst)
505 505 return subset & s
506 506
507 507 @predicate('ancestors(set)', safe=True)
508 508 def ancestors(repo, subset, x):
509 509 """Changesets that are ancestors of a changeset in set.
510 510 """
511 511 return _ancestors(repo, subset, x)
512 512
513 513 @predicate('_firstancestors', safe=True)
514 514 def _firstancestors(repo, subset, x):
515 515 # ``_firstancestors(set)``
516 516 # Like ``ancestors(set)`` but follows only the first parents.
517 517 return _ancestors(repo, subset, x, followfirst=True)
518 518
519 519 def ancestorspec(repo, subset, x, n):
520 520 """``set~n``
521 521 Changesets that are the Nth ancestor (first parents only) of a changeset
522 522 in set.
523 523 """
524 524 try:
525 525 n = int(n[1])
526 526 except (TypeError, ValueError):
527 527 raise error.ParseError(_("~ expects a number"))
528 528 ps = set()
529 529 cl = repo.changelog
530 530 for r in getset(repo, fullreposet(repo), x):
531 531 for i in range(n):
532 532 r = cl.parentrevs(r)[0]
533 533 ps.add(r)
534 534 return subset & ps
535 535
536 536 @predicate('author(string)', safe=True)
537 537 def author(repo, subset, x):
538 538 """Alias for ``user(string)``.
539 539 """
540 540 # i18n: "author" is a keyword
541 541 n = encoding.lower(getstring(x, _("author requires a string")))
542 542 kind, pattern, matcher = _substringmatcher(n)
543 543 return subset.filter(lambda x: matcher(encoding.lower(repo[x].user())),
544 544 condrepr=('<user %r>', n))
545 545
546 546 @predicate('bisect(string)', safe=True)
547 547 def bisect(repo, subset, x):
548 548 """Changesets marked in the specified bisect status:
549 549
550 550 - ``good``, ``bad``, ``skip``: csets explicitly marked as good/bad/skip
551 551 - ``goods``, ``bads`` : csets topologically good/bad
552 552 - ``range`` : csets taking part in the bisection
553 553 - ``pruned`` : csets that are goods, bads or skipped
554 554 - ``untested`` : csets whose fate is yet unknown
555 555 - ``ignored`` : csets ignored due to DAG topology
556 556 - ``current`` : the cset currently being bisected
557 557 """
558 558 # i18n: "bisect" is a keyword
559 559 status = getstring(x, _("bisect requires a string")).lower()
560 560 state = set(hbisect.get(repo, status))
561 561 return subset & state
562 562
563 563 # Backward-compatibility
564 564 # - no help entry so that we do not advertise it any more
565 565 @predicate('bisected', safe=True)
566 566 def bisected(repo, subset, x):
567 567 return bisect(repo, subset, x)
568 568
569 569 @predicate('bookmark([name])', safe=True)
570 570 def bookmark(repo, subset, x):
571 571 """The named bookmark or all bookmarks.
572 572
573 573 If `name` starts with `re:`, the remainder of the name is treated as
574 574 a regular expression. To match a bookmark that actually starts with `re:`,
575 575 use the prefix `literal:`.
576 576 """
577 577 # i18n: "bookmark" is a keyword
578 578 args = getargs(x, 0, 1, _('bookmark takes one or no arguments'))
579 579 if args:
580 580 bm = getstring(args[0],
581 581 # i18n: "bookmark" is a keyword
582 582 _('the argument to bookmark must be a string'))
583 583 kind, pattern, matcher = util.stringmatcher(bm)
584 584 bms = set()
585 585 if kind == 'literal':
586 586 bmrev = repo._bookmarks.get(pattern, None)
587 587 if not bmrev:
588 588 raise error.RepoLookupError(_("bookmark '%s' does not exist")
589 589 % pattern)
590 590 bms.add(repo[bmrev].rev())
591 591 else:
592 592 matchrevs = set()
593 593 for name, bmrev in repo._bookmarks.iteritems():
594 594 if matcher(name):
595 595 matchrevs.add(bmrev)
596 596 if not matchrevs:
597 597 raise error.RepoLookupError(_("no bookmarks exist"
598 598 " that match '%s'") % pattern)
599 599 for bmrev in matchrevs:
600 600 bms.add(repo[bmrev].rev())
601 601 else:
602 602 bms = set([repo[r].rev()
603 603 for r in repo._bookmarks.values()])
604 604 bms -= set([node.nullrev])
605 605 return subset & bms
606 606
607 607 @predicate('branch(string or set)', safe=True)
608 608 def branch(repo, subset, x):
609 609 """
610 610 All changesets belonging to the given branch or the branches of the given
611 611 changesets.
612 612
613 613 If `string` starts with `re:`, the remainder of the name is treated as
614 614 a regular expression. To match a branch that actually starts with `re:`,
615 615 use the prefix `literal:`.
616 616 """
617 617 getbi = repo.revbranchcache().branchinfo
618 618
619 619 try:
620 620 b = getstring(x, '')
621 621 except error.ParseError:
622 622 # not a string, but another revspec, e.g. tip()
623 623 pass
624 624 else:
625 625 kind, pattern, matcher = util.stringmatcher(b)
626 626 if kind == 'literal':
627 627 # note: falls through to the revspec case if no branch with
628 628 # this name exists and pattern kind is not specified explicitly
629 629 if pattern in repo.branchmap():
630 630 return subset.filter(lambda r: matcher(getbi(r)[0]),
631 631 condrepr=('<branch %r>', b))
632 632 if b.startswith('literal:'):
633 633 raise error.RepoLookupError(_("branch '%s' does not exist")
634 634 % pattern)
635 635 else:
636 636 return subset.filter(lambda r: matcher(getbi(r)[0]),
637 637 condrepr=('<branch %r>', b))
638 638
639 639 s = getset(repo, fullreposet(repo), x)
640 640 b = set()
641 641 for r in s:
642 642 b.add(getbi(r)[0])
643 643 c = s.__contains__
644 644 return subset.filter(lambda r: c(r) or getbi(r)[0] in b,
645 645 condrepr=lambda: '<branch %r>' % sorted(b))
646 646
647 647 @predicate('bumped()', safe=True)
648 648 def bumped(repo, subset, x):
649 649 """Mutable changesets marked as successors of public changesets.
650 650
651 651 Only non-public and non-obsolete changesets can be `bumped`.
652 652 """
653 653 # i18n: "bumped" is a keyword
654 654 getargs(x, 0, 0, _("bumped takes no arguments"))
655 655 bumped = obsmod.getrevs(repo, 'bumped')
656 656 return subset & bumped
657 657
658 658 @predicate('bundle()', safe=True)
659 659 def bundle(repo, subset, x):
660 660 """Changesets in the bundle.
661 661
662 662 Bundle must be specified by the -R option."""
663 663
664 664 try:
665 665 bundlerevs = repo.changelog.bundlerevs
666 666 except AttributeError:
667 667 raise error.Abort(_("no bundle provided - specify with -R"))
668 668 return subset & bundlerevs
669 669
670 670 def checkstatus(repo, subset, pat, field):
671 671 hasset = matchmod.patkind(pat) == 'set'
672 672
673 673 mcache = [None]
674 674 def matches(x):
675 675 c = repo[x]
676 676 if not mcache[0] or hasset:
677 677 mcache[0] = matchmod.match(repo.root, repo.getcwd(), [pat], ctx=c)
678 678 m = mcache[0]
679 679 fname = None
680 680 if not m.anypats() and len(m.files()) == 1:
681 681 fname = m.files()[0]
682 682 if fname is not None:
683 683 if fname not in c.files():
684 684 return False
685 685 else:
686 686 for f in c.files():
687 687 if m(f):
688 688 break
689 689 else:
690 690 return False
691 691 files = repo.status(c.p1().node(), c.node())[field]
692 692 if fname is not None:
693 693 if fname in files:
694 694 return True
695 695 else:
696 696 for f in files:
697 697 if m(f):
698 698 return True
699 699
700 700 return subset.filter(matches, condrepr=('<status[%r] %r>', field, pat))
701 701
702 702 def _children(repo, subset, parentset):
703 703 if not parentset:
704 704 return baseset()
705 705 cs = set()
706 706 pr = repo.changelog.parentrevs
707 707 minrev = parentset.min()
708 708 for r in subset:
709 709 if r <= minrev:
710 710 continue
711 711 for p in pr(r):
712 712 if p in parentset:
713 713 cs.add(r)
714 714 return baseset(cs)
715 715
716 716 @predicate('children(set)', safe=True)
717 717 def children(repo, subset, x):
718 718 """Child changesets of changesets in set.
719 719 """
720 720 s = getset(repo, fullreposet(repo), x)
721 721 cs = _children(repo, subset, s)
722 722 return subset & cs
723 723
724 724 @predicate('closed()', safe=True)
725 725 def closed(repo, subset, x):
726 726 """Changeset is closed.
727 727 """
728 728 # i18n: "closed" is a keyword
729 729 getargs(x, 0, 0, _("closed takes no arguments"))
730 730 return subset.filter(lambda r: repo[r].closesbranch(),
731 731 condrepr='<branch closed>')
732 732
733 733 @predicate('contains(pattern)')
734 734 def contains(repo, subset, x):
735 735 """The revision's manifest contains a file matching pattern (but might not
736 736 modify it). See :hg:`help patterns` for information about file patterns.
737 737
738 738 The pattern without explicit kind like ``glob:`` is expected to be
739 739 relative to the current directory and match against a file exactly
740 740 for efficiency.
741 741 """
742 742 # i18n: "contains" is a keyword
743 743 pat = getstring(x, _("contains requires a pattern"))
744 744
745 745 def matches(x):
746 746 if not matchmod.patkind(pat):
747 747 pats = pathutil.canonpath(repo.root, repo.getcwd(), pat)
748 748 if pats in repo[x]:
749 749 return True
750 750 else:
751 751 c = repo[x]
752 752 m = matchmod.match(repo.root, repo.getcwd(), [pat], ctx=c)
753 753 for f in c.manifest():
754 754 if m(f):
755 755 return True
756 756 return False
757 757
758 758 return subset.filter(matches, condrepr=('<contains %r>', pat))
759 759
760 760 @predicate('converted([id])', safe=True)
761 761 def converted(repo, subset, x):
762 762 """Changesets converted from the given identifier in the old repository if
763 763 present, or all converted changesets if no identifier is specified.
764 764 """
765 765
766 766 # There is exactly no chance of resolving the revision, so do a simple
767 767 # string compare and hope for the best
768 768
769 769 rev = None
770 770 # i18n: "converted" is a keyword
771 771 l = getargs(x, 0, 1, _('converted takes one or no arguments'))
772 772 if l:
773 773 # i18n: "converted" is a keyword
774 774 rev = getstring(l[0], _('converted requires a revision'))
775 775
776 776 def _matchvalue(r):
777 777 source = repo[r].extra().get('convert_revision', None)
778 778 return source is not None and (rev is None or source.startswith(rev))
779 779
780 780 return subset.filter(lambda r: _matchvalue(r),
781 781 condrepr=('<converted %r>', rev))
782 782
783 783 @predicate('date(interval)', safe=True)
784 784 def date(repo, subset, x):
785 785 """Changesets within the interval, see :hg:`help dates`.
786 786 """
787 787 # i18n: "date" is a keyword
788 788 ds = getstring(x, _("date requires a string"))
789 789 dm = util.matchdate(ds)
790 790 return subset.filter(lambda x: dm(repo[x].date()[0]),
791 791 condrepr=('<date %r>', ds))
792 792
793 793 @predicate('desc(string)', safe=True)
794 794 def desc(repo, subset, x):
795 795 """Search commit message for string. The match is case-insensitive.
796 796 """
797 797 # i18n: "desc" is a keyword
798 798 ds = encoding.lower(getstring(x, _("desc requires a string")))
799 799
800 800 def matches(x):
801 801 c = repo[x]
802 802 return ds in encoding.lower(c.description())
803 803
804 804 return subset.filter(matches, condrepr=('<desc %r>', ds))
805 805
806 806 def _descendants(repo, subset, x, followfirst=False):
807 807 roots = getset(repo, fullreposet(repo), x)
808 808 if not roots:
809 809 return baseset()
810 810 s = _revdescendants(repo, roots, followfirst)
811 811
812 812 # Both sets need to be ascending in order to lazily return the union
813 813 # in the correct order.
814 814 base = subset & roots
815 815 desc = subset & s
816 816 result = base + desc
817 817 if subset.isascending():
818 818 result.sort()
819 819 elif subset.isdescending():
820 820 result.sort(reverse=True)
821 821 else:
822 822 result = subset & result
823 823 return result
824 824
825 825 @predicate('descendants(set)', safe=True)
826 826 def descendants(repo, subset, x):
827 827 """Changesets which are descendants of changesets in set.
828 828 """
829 829 return _descendants(repo, subset, x)
830 830
831 831 @predicate('_firstdescendants', safe=True)
832 832 def _firstdescendants(repo, subset, x):
833 833 # ``_firstdescendants(set)``
834 834 # Like ``descendants(set)`` but follows only the first parents.
835 835 return _descendants(repo, subset, x, followfirst=True)
836 836
837 837 @predicate('destination([set])', safe=True)
838 838 def destination(repo, subset, x):
839 839 """Changesets that were created by a graft, transplant or rebase operation,
840 840 with the given revisions specified as the source. Omitting the optional set
841 841 is the same as passing all().
842 842 """
843 843 if x is not None:
844 844 sources = getset(repo, fullreposet(repo), x)
845 845 else:
846 846 sources = fullreposet(repo)
847 847
848 848 dests = set()
849 849
850 850 # subset contains all of the possible destinations that can be returned, so
851 851 # iterate over them and see if their source(s) were provided in the arg set.
852 852 # Even if the immediate src of r is not in the arg set, src's source (or
853 853 # further back) may be. Scanning back further than the immediate src allows
854 854 # transitive transplants and rebases to yield the same results as transitive
855 855 # grafts.
856 856 for r in subset:
857 857 src = _getrevsource(repo, r)
858 858 lineage = None
859 859
860 860 while src is not None:
861 861 if lineage is None:
862 862 lineage = list()
863 863
864 864 lineage.append(r)
865 865
866 866 # The visited lineage is a match if the current source is in the arg
867 867 # set. Since every candidate dest is visited by way of iterating
868 868 # subset, any dests further back in the lineage will be tested by a
869 869 # different iteration over subset. Likewise, if the src was already
870 870 # selected, the current lineage can be selected without going back
871 871 # further.
872 872 if src in sources or src in dests:
873 873 dests.update(lineage)
874 874 break
875 875
876 876 r = src
877 877 src = _getrevsource(repo, r)
878 878
879 879 return subset.filter(dests.__contains__,
880 880 condrepr=lambda: '<destination %r>' % sorted(dests))
881 881
882 882 @predicate('divergent()', safe=True)
883 883 def divergent(repo, subset, x):
884 884 """
885 885 Final successors of changesets with an alternative set of final successors.
886 886 """
887 887 # i18n: "divergent" is a keyword
888 888 getargs(x, 0, 0, _("divergent takes no arguments"))
889 889 divergent = obsmod.getrevs(repo, 'divergent')
890 890 return subset & divergent
891 891
892 892 @predicate('extinct()', safe=True)
893 893 def extinct(repo, subset, x):
894 894 """Obsolete changesets with obsolete descendants only.
895 895 """
896 896 # i18n: "extinct" is a keyword
897 897 getargs(x, 0, 0, _("extinct takes no arguments"))
898 898 extincts = obsmod.getrevs(repo, 'extinct')
899 899 return subset & extincts
900 900
901 901 @predicate('extra(label, [value])', safe=True)
902 902 def extra(repo, subset, x):
903 903 """Changesets with the given label in the extra metadata, with the given
904 904 optional value.
905 905
906 906 If `value` starts with `re:`, the remainder of the value is treated as
907 907 a regular expression. To match a value that actually starts with `re:`,
908 908 use the prefix `literal:`.
909 909 """
910 910 args = getargsdict(x, 'extra', 'label value')
911 911 if 'label' not in args:
912 912 # i18n: "extra" is a keyword
913 913 raise error.ParseError(_('extra takes at least 1 argument'))
914 914 # i18n: "extra" is a keyword
915 915 label = getstring(args['label'], _('first argument to extra must be '
916 916 'a string'))
917 917 value = None
918 918
919 919 if 'value' in args:
920 920 # i18n: "extra" is a keyword
921 921 value = getstring(args['value'], _('second argument to extra must be '
922 922 'a string'))
923 923 kind, value, matcher = util.stringmatcher(value)
924 924
925 925 def _matchvalue(r):
926 926 extra = repo[r].extra()
927 927 return label in extra and (value is None or matcher(extra[label]))
928 928
929 929 return subset.filter(lambda r: _matchvalue(r),
930 930 condrepr=('<extra[%r] %r>', label, value))
931 931
932 932 @predicate('filelog(pattern)', safe=True)
933 933 def filelog(repo, subset, x):
934 934 """Changesets connected to the specified filelog.
935 935
936 936 For performance reasons, visits only revisions mentioned in the file-level
937 937 filelog, rather than filtering through all changesets (much faster, but
938 938 doesn't include deletes or duplicate changes). For a slower, more accurate
939 939 result, use ``file()``.
940 940
941 941 The pattern without explicit kind like ``glob:`` is expected to be
942 942 relative to the current directory and match against a file exactly
943 943 for efficiency.
944 944
945 945 If some linkrev points to revisions filtered by the current repoview, we'll
946 946 work around it to return a non-filtered value.
947 947 """
948 948
949 949 # i18n: "filelog" is a keyword
950 950 pat = getstring(x, _("filelog requires a pattern"))
951 951 s = set()
952 952 cl = repo.changelog
953 953
954 954 if not matchmod.patkind(pat):
955 955 f = pathutil.canonpath(repo.root, repo.getcwd(), pat)
956 956 files = [f]
957 957 else:
958 958 m = matchmod.match(repo.root, repo.getcwd(), [pat], ctx=repo[None])
959 959 files = (f for f in repo[None] if m(f))
960 960
961 961 for f in files:
962 962 fl = repo.file(f)
963 963 known = {}
964 964 scanpos = 0
965 965 for fr in list(fl):
966 966 fn = fl.node(fr)
967 967 if fn in known:
968 968 s.add(known[fn])
969 969 continue
970 970
971 971 lr = fl.linkrev(fr)
972 972 if lr in cl:
973 973 s.add(lr)
974 974 elif scanpos is not None:
975 975 # lowest matching changeset is filtered, scan further
976 976 # ahead in changelog
977 977 start = max(lr, scanpos) + 1
978 978 scanpos = None
979 979 for r in cl.revs(start):
980 980 # minimize parsing of non-matching entries
981 981 if f in cl.revision(r) and f in cl.readfiles(r):
982 982 try:
983 983 # try to use manifest delta fastpath
984 984 n = repo[r].filenode(f)
985 985 if n not in known:
986 986 if n == fn:
987 987 s.add(r)
988 988 scanpos = r
989 989 break
990 990 else:
991 991 known[n] = r
992 992 except error.ManifestLookupError:
993 993 # deletion in changelog
994 994 continue
995 995
996 996 return subset & s
997 997
998 998 @predicate('first(set, [n])', safe=True)
999 999 def first(repo, subset, x):
1000 1000 """An alias for limit().
1001 1001 """
1002 1002 return limit(repo, subset, x)
1003 1003
1004 1004 def _follow(repo, subset, x, name, followfirst=False):
1005 1005 l = getargs(x, 0, 2, _("%s takes no arguments or a pattern "
1006 1006 "and an optional revset") % name)
1007 1007 c = repo['.']
1008 1008 if l:
1009 1009 x = getstring(l[0], _("%s expected a pattern") % name)
1010 1010 rev = None
1011 1011 if len(l) >= 2:
1012 1012 rev = getset(repo, fullreposet(repo), l[1]).last()
1013 1013 if rev is None:
1014 1014 raise error.RepoLookupError(
1015 1015 _("%s: starting revision set cannot be empty") % name)
1016 1016 c = repo[rev]
1017 1017 matcher = matchmod.match(repo.root, repo.getcwd(), [x],
1018 1018 ctx=repo[rev], default='path')
1019 1019
1020 1020 files = c.manifest().walk(matcher)
1021 1021
1022 1022 s = set()
1023 1023 for fname in files:
1024 1024 fctx = c[fname]
1025 1025 s = s.union(set(c.rev() for c in fctx.ancestors(followfirst)))
1026 1026 # include the revision responsible for the most recent version
1027 1027 s.add(fctx.introrev())
1028 1028 else:
1029 1029 s = _revancestors(repo, baseset([c.rev()]), followfirst)
1030 1030
1031 1031 return subset & s
1032 1032
1033 1033 @predicate('follow([pattern[, startrev]])', safe=True)
1034 1034 def follow(repo, subset, x):
1035 1035 """
1036 1036 An alias for ``::.`` (ancestors of the working directory's first parent).
1037 1037 If pattern is specified, the histories of files matching given
1038 1038 pattern in the revision given by startrev are followed, including copies.
1039 1039 """
1040 1040 return _follow(repo, subset, x, 'follow')
1041 1041
1042 1042 @predicate('_followfirst', safe=True)
1043 1043 def _followfirst(repo, subset, x):
1044 1044 # ``followfirst([pattern[, startrev]])``
1045 1045 # Like ``follow([pattern[, startrev]])`` but follows only the first parent
1046 1046 # of every revisions or files revisions.
1047 1047 return _follow(repo, subset, x, '_followfirst', followfirst=True)
1048 1048
1049 1049 @predicate('all()', safe=True)
1050 1050 def getall(repo, subset, x):
1051 1051 """All changesets, the same as ``0:tip``.
1052 1052 """
1053 1053 # i18n: "all" is a keyword
1054 1054 getargs(x, 0, 0, _("all takes no arguments"))
1055 1055 return subset & spanset(repo) # drop "null" if any
1056 1056
1057 1057 @predicate('grep(regex)')
1058 1058 def grep(repo, subset, x):
1059 1059 """Like ``keyword(string)`` but accepts a regex. Use ``grep(r'...')``
1060 1060 to ensure special escape characters are handled correctly. Unlike
1061 1061 ``keyword(string)``, the match is case-sensitive.
1062 1062 """
1063 1063 try:
1064 1064 # i18n: "grep" is a keyword
1065 1065 gr = re.compile(getstring(x, _("grep requires a string")))
1066 1066 except re.error as e:
1067 1067 raise error.ParseError(_('invalid match pattern: %s') % e)
1068 1068
1069 1069 def matches(x):
1070 1070 c = repo[x]
1071 1071 for e in c.files() + [c.user(), c.description()]:
1072 1072 if gr.search(e):
1073 1073 return True
1074 1074 return False
1075 1075
1076 1076 return subset.filter(matches, condrepr=('<grep %r>', gr.pattern))
1077 1077
1078 1078 @predicate('_matchfiles', safe=True)
1079 1079 def _matchfiles(repo, subset, x):
1080 1080 # _matchfiles takes a revset list of prefixed arguments:
1081 1081 #
1082 1082 # [p:foo, i:bar, x:baz]
1083 1083 #
1084 1084 # builds a match object from them and filters subset. Allowed
1085 1085 # prefixes are 'p:' for regular patterns, 'i:' for include
1086 1086 # patterns and 'x:' for exclude patterns. Use 'r:' prefix to pass
1087 1087 # a revision identifier, or the empty string to reference the
1088 1088 # working directory, from which the match object is
1089 1089 # initialized. Use 'd:' to set the default matching mode, default
1090 1090 # to 'glob'. At most one 'r:' and 'd:' argument can be passed.
1091 1091
1092 1092 l = getargs(x, 1, -1, "_matchfiles requires at least one argument")
1093 1093 pats, inc, exc = [], [], []
1094 1094 rev, default = None, None
1095 1095 for arg in l:
1096 1096 s = getstring(arg, "_matchfiles requires string arguments")
1097 1097 prefix, value = s[:2], s[2:]
1098 1098 if prefix == 'p:':
1099 1099 pats.append(value)
1100 1100 elif prefix == 'i:':
1101 1101 inc.append(value)
1102 1102 elif prefix == 'x:':
1103 1103 exc.append(value)
1104 1104 elif prefix == 'r:':
1105 1105 if rev is not None:
1106 1106 raise error.ParseError('_matchfiles expected at most one '
1107 1107 'revision')
1108 1108 if value != '': # empty means working directory; leave rev as None
1109 1109 rev = value
1110 1110 elif prefix == 'd:':
1111 1111 if default is not None:
1112 1112 raise error.ParseError('_matchfiles expected at most one '
1113 1113 'default mode')
1114 1114 default = value
1115 1115 else:
1116 1116 raise error.ParseError('invalid _matchfiles prefix: %s' % prefix)
1117 1117 if not default:
1118 1118 default = 'glob'
1119 1119
1120 1120 m = matchmod.match(repo.root, repo.getcwd(), pats, include=inc,
1121 1121 exclude=exc, ctx=repo[rev], default=default)
1122 1122
1123 1123 # This directly read the changelog data as creating changectx for all
1124 1124 # revisions is quite expensive.
1125 1125 getfiles = repo.changelog.readfiles
1126 1126 wdirrev = node.wdirrev
1127 1127 def matches(x):
1128 1128 if x == wdirrev:
1129 1129 files = repo[x].files()
1130 1130 else:
1131 1131 files = getfiles(x)
1132 1132 for f in files:
1133 1133 if m(f):
1134 1134 return True
1135 1135 return False
1136 1136
1137 1137 return subset.filter(matches,
1138 1138 condrepr=('<matchfiles patterns=%r, include=%r '
1139 1139 'exclude=%r, default=%r, rev=%r>',
1140 1140 pats, inc, exc, default, rev))
1141 1141
1142 1142 @predicate('file(pattern)', safe=True)
1143 1143 def hasfile(repo, subset, x):
1144 1144 """Changesets affecting files matched by pattern.
1145 1145
1146 1146 For a faster but less accurate result, consider using ``filelog()``
1147 1147 instead.
1148 1148
1149 1149 This predicate uses ``glob:`` as the default kind of pattern.
1150 1150 """
1151 1151 # i18n: "file" is a keyword
1152 1152 pat = getstring(x, _("file requires a pattern"))
1153 1153 return _matchfiles(repo, subset, ('string', 'p:' + pat))
1154 1154
1155 1155 @predicate('head()', safe=True)
1156 1156 def head(repo, subset, x):
1157 1157 """Changeset is a named branch head.
1158 1158 """
1159 1159 # i18n: "head" is a keyword
1160 1160 getargs(x, 0, 0, _("head takes no arguments"))
1161 1161 hs = set()
1162 1162 cl = repo.changelog
1163 1163 for ls in repo.branchmap().itervalues():
1164 1164 hs.update(cl.rev(h) for h in ls)
1165 1165 return subset & baseset(hs)
1166 1166
1167 1167 @predicate('heads(set)', safe=True)
1168 1168 def heads(repo, subset, x):
1169 1169 """Members of set with no children in set.
1170 1170 """
1171 1171 s = getset(repo, subset, x)
1172 1172 ps = parents(repo, subset, x)
1173 1173 return s - ps
1174 1174
1175 1175 @predicate('hidden()', safe=True)
1176 1176 def hidden(repo, subset, x):
1177 1177 """Hidden changesets.
1178 1178 """
1179 1179 # i18n: "hidden" is a keyword
1180 1180 getargs(x, 0, 0, _("hidden takes no arguments"))
1181 1181 hiddenrevs = repoview.filterrevs(repo, 'visible')
1182 1182 return subset & hiddenrevs
1183 1183
1184 1184 @predicate('keyword(string)', safe=True)
1185 1185 def keyword(repo, subset, x):
1186 1186 """Search commit message, user name, and names of changed files for
1187 1187 string. The match is case-insensitive.
1188 1188 """
1189 1189 # i18n: "keyword" is a keyword
1190 1190 kw = encoding.lower(getstring(x, _("keyword requires a string")))
1191 1191
1192 1192 def matches(r):
1193 1193 c = repo[r]
1194 1194 return any(kw in encoding.lower(t)
1195 1195 for t in c.files() + [c.user(), c.description()])
1196 1196
1197 1197 return subset.filter(matches, condrepr=('<keyword %r>', kw))
1198 1198
1199 1199 @predicate('limit(set[, n[, offset]])', safe=True)
1200 1200 def limit(repo, subset, x):
1201 1201 """First n members of set, defaulting to 1, starting from offset.
1202 1202 """
1203 1203 args = getargsdict(x, 'limit', 'set n offset')
1204 1204 if 'set' not in args:
1205 1205 # i18n: "limit" is a keyword
1206 1206 raise error.ParseError(_("limit requires one to three arguments"))
1207 1207 try:
1208 1208 lim, ofs = 1, 0
1209 1209 if 'n' in args:
1210 1210 # i18n: "limit" is a keyword
1211 1211 lim = int(getstring(args['n'], _("limit requires a number")))
1212 1212 if 'offset' in args:
1213 1213 # i18n: "limit" is a keyword
1214 1214 ofs = int(getstring(args['offset'], _("limit requires a number")))
1215 1215 if ofs < 0:
1216 1216 raise error.ParseError(_("negative offset"))
1217 1217 except (TypeError, ValueError):
1218 1218 # i18n: "limit" is a keyword
1219 1219 raise error.ParseError(_("limit expects a number"))
1220 1220 os = getset(repo, fullreposet(repo), args['set'])
1221 1221 result = []
1222 1222 it = iter(os)
1223 1223 for x in xrange(ofs):
1224 1224 y = next(it, None)
1225 1225 if y is None:
1226 1226 break
1227 1227 for x in xrange(lim):
1228 1228 y = next(it, None)
1229 1229 if y is None:
1230 1230 break
1231 1231 elif y in subset:
1232 1232 result.append(y)
1233 1233 return baseset(result, datarepr=('<limit n=%d, offset=%d, %r, %r>',
1234 1234 lim, ofs, subset, os))
1235 1235
1236 1236 @predicate('last(set, [n])', safe=True)
1237 1237 def last(repo, subset, x):
1238 1238 """Last n members of set, defaulting to 1.
1239 1239 """
1240 1240 # i18n: "last" is a keyword
1241 1241 l = getargs(x, 1, 2, _("last requires one or two arguments"))
1242 1242 try:
1243 1243 lim = 1
1244 1244 if len(l) == 2:
1245 1245 # i18n: "last" is a keyword
1246 1246 lim = int(getstring(l[1], _("last requires a number")))
1247 1247 except (TypeError, ValueError):
1248 1248 # i18n: "last" is a keyword
1249 1249 raise error.ParseError(_("last expects a number"))
1250 1250 os = getset(repo, fullreposet(repo), l[0])
1251 1251 os.reverse()
1252 1252 result = []
1253 1253 it = iter(os)
1254 1254 for x in xrange(lim):
1255 1255 y = next(it, None)
1256 1256 if y is None:
1257 1257 break
1258 1258 elif y in subset:
1259 1259 result.append(y)
1260 1260 return baseset(result, datarepr=('<last n=%d, %r, %r>', lim, subset, os))
1261 1261
1262 1262 @predicate('max(set)', safe=True)
1263 1263 def maxrev(repo, subset, x):
1264 1264 """Changeset with highest revision number in set.
1265 1265 """
1266 1266 os = getset(repo, fullreposet(repo), x)
1267 1267 try:
1268 1268 m = os.max()
1269 1269 if m in subset:
1270 1270 return baseset([m], datarepr=('<max %r, %r>', subset, os))
1271 1271 except ValueError:
1272 1272 # os.max() throws a ValueError when the collection is empty.
1273 1273 # Same as python's max().
1274 1274 pass
1275 1275 return baseset(datarepr=('<max %r, %r>', subset, os))
1276 1276
1277 1277 @predicate('merge()', safe=True)
1278 1278 def merge(repo, subset, x):
1279 1279 """Changeset is a merge changeset.
1280 1280 """
1281 1281 # i18n: "merge" is a keyword
1282 1282 getargs(x, 0, 0, _("merge takes no arguments"))
1283 1283 cl = repo.changelog
1284 1284 return subset.filter(lambda r: cl.parentrevs(r)[1] != -1,
1285 1285 condrepr='<merge>')
1286 1286
1287 1287 @predicate('branchpoint()', safe=True)
1288 1288 def branchpoint(repo, subset, x):
1289 1289 """Changesets with more than one child.
1290 1290 """
1291 1291 # i18n: "branchpoint" is a keyword
1292 1292 getargs(x, 0, 0, _("branchpoint takes no arguments"))
1293 1293 cl = repo.changelog
1294 1294 if not subset:
1295 1295 return baseset()
1296 1296 # XXX this should be 'parentset.min()' assuming 'parentset' is a smartset
1297 1297 # (and if it is not, it should.)
1298 1298 baserev = min(subset)
1299 1299 parentscount = [0]*(len(repo) - baserev)
1300 1300 for r in cl.revs(start=baserev + 1):
1301 1301 for p in cl.parentrevs(r):
1302 1302 if p >= baserev:
1303 1303 parentscount[p - baserev] += 1
1304 1304 return subset.filter(lambda r: parentscount[r - baserev] > 1,
1305 1305 condrepr='<branchpoint>')
1306 1306
1307 1307 @predicate('min(set)', safe=True)
1308 1308 def minrev(repo, subset, x):
1309 1309 """Changeset with lowest revision number in set.
1310 1310 """
1311 1311 os = getset(repo, fullreposet(repo), x)
1312 1312 try:
1313 1313 m = os.min()
1314 1314 if m in subset:
1315 1315 return baseset([m], datarepr=('<min %r, %r>', subset, os))
1316 1316 except ValueError:
1317 1317 # os.min() throws a ValueError when the collection is empty.
1318 1318 # Same as python's min().
1319 1319 pass
1320 1320 return baseset(datarepr=('<min %r, %r>', subset, os))
1321 1321
1322 1322 @predicate('modifies(pattern)', safe=True)
1323 1323 def modifies(repo, subset, x):
1324 1324 """Changesets modifying files matched by pattern.
1325 1325
1326 1326 The pattern without explicit kind like ``glob:`` is expected to be
1327 1327 relative to the current directory and match against a file or a
1328 1328 directory.
1329 1329 """
1330 1330 # i18n: "modifies" is a keyword
1331 1331 pat = getstring(x, _("modifies requires a pattern"))
1332 1332 return checkstatus(repo, subset, pat, 0)
1333 1333
1334 1334 @predicate('named(namespace)')
1335 1335 def named(repo, subset, x):
1336 1336 """The changesets in a given namespace.
1337 1337
1338 1338 If `namespace` starts with `re:`, the remainder of the string is treated as
1339 1339 a regular expression. To match a namespace that actually starts with `re:`,
1340 1340 use the prefix `literal:`.
1341 1341 """
1342 1342 # i18n: "named" is a keyword
1343 1343 args = getargs(x, 1, 1, _('named requires a namespace argument'))
1344 1344
1345 1345 ns = getstring(args[0],
1346 1346 # i18n: "named" is a keyword
1347 1347 _('the argument to named must be a string'))
1348 1348 kind, pattern, matcher = util.stringmatcher(ns)
1349 1349 namespaces = set()
1350 1350 if kind == 'literal':
1351 1351 if pattern not in repo.names:
1352 1352 raise error.RepoLookupError(_("namespace '%s' does not exist")
1353 1353 % ns)
1354 1354 namespaces.add(repo.names[pattern])
1355 1355 else:
1356 1356 for name, ns in repo.names.iteritems():
1357 1357 if matcher(name):
1358 1358 namespaces.add(ns)
1359 1359 if not namespaces:
1360 1360 raise error.RepoLookupError(_("no namespace exists"
1361 1361 " that match '%s'") % pattern)
1362 1362
1363 1363 names = set()
1364 1364 for ns in namespaces:
1365 1365 for name in ns.listnames(repo):
1366 1366 if name not in ns.deprecated:
1367 1367 names.update(repo[n].rev() for n in ns.nodes(repo, name))
1368 1368
1369 1369 names -= set([node.nullrev])
1370 1370 return subset & names
1371 1371
1372 1372 @predicate('id(string)', safe=True)
1373 1373 def node_(repo, subset, x):
1374 1374 """Revision non-ambiguously specified by the given hex string prefix.
1375 1375 """
1376 1376 # i18n: "id" is a keyword
1377 1377 l = getargs(x, 1, 1, _("id requires one argument"))
1378 1378 # i18n: "id" is a keyword
1379 1379 n = getstring(l[0], _("id requires a string"))
1380 1380 if len(n) == 40:
1381 1381 try:
1382 1382 rn = repo.changelog.rev(node.bin(n))
1383 1383 except (LookupError, TypeError):
1384 1384 rn = None
1385 1385 else:
1386 1386 rn = None
1387 1387 pm = repo.changelog._partialmatch(n)
1388 1388 if pm is not None:
1389 1389 rn = repo.changelog.rev(pm)
1390 1390
1391 1391 if rn is None:
1392 1392 return baseset()
1393 1393 result = baseset([rn])
1394 1394 return result & subset
1395 1395
1396 1396 @predicate('obsolete()', safe=True)
1397 1397 def obsolete(repo, subset, x):
1398 1398 """Mutable changeset with a newer version."""
1399 1399 # i18n: "obsolete" is a keyword
1400 1400 getargs(x, 0, 0, _("obsolete takes no arguments"))
1401 1401 obsoletes = obsmod.getrevs(repo, 'obsolete')
1402 1402 return subset & obsoletes
1403 1403
1404 1404 @predicate('only(set, [set])', safe=True)
1405 1405 def only(repo, subset, x):
1406 1406 """Changesets that are ancestors of the first set that are not ancestors
1407 1407 of any other head in the repo. If a second set is specified, the result
1408 1408 is ancestors of the first set that are not ancestors of the second set
1409 1409 (i.e. ::<set1> - ::<set2>).
1410 1410 """
1411 1411 cl = repo.changelog
1412 1412 # i18n: "only" is a keyword
1413 1413 args = getargs(x, 1, 2, _('only takes one or two arguments'))
1414 1414 include = getset(repo, fullreposet(repo), args[0])
1415 1415 if len(args) == 1:
1416 1416 if not include:
1417 1417 return baseset()
1418 1418
1419 1419 descendants = set(_revdescendants(repo, include, False))
1420 1420 exclude = [rev for rev in cl.headrevs()
1421 1421 if not rev in descendants and not rev in include]
1422 1422 else:
1423 1423 exclude = getset(repo, fullreposet(repo), args[1])
1424 1424
1425 1425 results = set(cl.findmissingrevs(common=exclude, heads=include))
1426 1426 # XXX we should turn this into a baseset instead of a set, smartset may do
1427 1427 # some optimisations from the fact this is a baseset.
1428 1428 return subset & results
1429 1429
1430 1430 @predicate('origin([set])', safe=True)
1431 1431 def origin(repo, subset, x):
1432 1432 """
1433 1433 Changesets that were specified as a source for the grafts, transplants or
1434 1434 rebases that created the given revisions. Omitting the optional set is the
1435 1435 same as passing all(). If a changeset created by these operations is itself
1436 1436 specified as a source for one of these operations, only the source changeset
1437 1437 for the first operation is selected.
1438 1438 """
1439 1439 if x is not None:
1440 1440 dests = getset(repo, fullreposet(repo), x)
1441 1441 else:
1442 1442 dests = fullreposet(repo)
1443 1443
1444 1444 def _firstsrc(rev):
1445 1445 src = _getrevsource(repo, rev)
1446 1446 if src is None:
1447 1447 return None
1448 1448
1449 1449 while True:
1450 1450 prev = _getrevsource(repo, src)
1451 1451
1452 1452 if prev is None:
1453 1453 return src
1454 1454 src = prev
1455 1455
1456 1456 o = set([_firstsrc(r) for r in dests])
1457 1457 o -= set([None])
1458 1458 # XXX we should turn this into a baseset instead of a set, smartset may do
1459 1459 # some optimisations from the fact this is a baseset.
1460 1460 return subset & o
1461 1461
1462 1462 @predicate('outgoing([path])', safe=True)
1463 1463 def outgoing(repo, subset, x):
1464 1464 """Changesets not found in the specified destination repository, or the
1465 1465 default push location.
1466 1466 """
1467 1467 # Avoid cycles.
1468 1468 from . import (
1469 1469 discovery,
1470 1470 hg,
1471 1471 )
1472 1472 # i18n: "outgoing" is a keyword
1473 1473 l = getargs(x, 0, 1, _("outgoing takes one or no arguments"))
1474 1474 # i18n: "outgoing" is a keyword
1475 1475 dest = l and getstring(l[0], _("outgoing requires a repository path")) or ''
1476 1476 dest = repo.ui.expandpath(dest or 'default-push', dest or 'default')
1477 1477 dest, branches = hg.parseurl(dest)
1478 1478 revs, checkout = hg.addbranchrevs(repo, repo, branches, [])
1479 1479 if revs:
1480 1480 revs = [repo.lookup(rev) for rev in revs]
1481 1481 other = hg.peer(repo, {}, dest)
1482 1482 repo.ui.pushbuffer()
1483 1483 outgoing = discovery.findcommonoutgoing(repo, other, onlyheads=revs)
1484 1484 repo.ui.popbuffer()
1485 1485 cl = repo.changelog
1486 1486 o = set([cl.rev(r) for r in outgoing.missing])
1487 1487 return subset & o
1488 1488
1489 1489 @predicate('p1([set])', safe=True)
1490 1490 def p1(repo, subset, x):
1491 1491 """First parent of changesets in set, or the working directory.
1492 1492 """
1493 1493 if x is None:
1494 1494 p = repo[x].p1().rev()
1495 1495 if p >= 0:
1496 1496 return subset & baseset([p])
1497 1497 return baseset()
1498 1498
1499 1499 ps = set()
1500 1500 cl = repo.changelog
1501 1501 for r in getset(repo, fullreposet(repo), x):
1502 1502 ps.add(cl.parentrevs(r)[0])
1503 1503 ps -= set([node.nullrev])
1504 1504 # XXX we should turn this into a baseset instead of a set, smartset may do
1505 1505 # some optimisations from the fact this is a baseset.
1506 1506 return subset & ps
1507 1507
1508 1508 @predicate('p2([set])', safe=True)
1509 1509 def p2(repo, subset, x):
1510 1510 """Second parent of changesets in set, or the working directory.
1511 1511 """
1512 1512 if x is None:
1513 1513 ps = repo[x].parents()
1514 1514 try:
1515 1515 p = ps[1].rev()
1516 1516 if p >= 0:
1517 1517 return subset & baseset([p])
1518 1518 return baseset()
1519 1519 except IndexError:
1520 1520 return baseset()
1521 1521
1522 1522 ps = set()
1523 1523 cl = repo.changelog
1524 1524 for r in getset(repo, fullreposet(repo), x):
1525 1525 ps.add(cl.parentrevs(r)[1])
1526 1526 ps -= set([node.nullrev])
1527 1527 # XXX we should turn this into a baseset instead of a set, smartset may do
1528 1528 # some optimisations from the fact this is a baseset.
1529 1529 return subset & ps
1530 1530
1531 1531 @predicate('parents([set])', safe=True)
1532 1532 def parents(repo, subset, x):
1533 1533 """
1534 1534 The set of all parents for all changesets in set, or the working directory.
1535 1535 """
1536 1536 if x is None:
1537 1537 ps = set(p.rev() for p in repo[x].parents())
1538 1538 else:
1539 1539 ps = set()
1540 1540 cl = repo.changelog
1541 1541 up = ps.update
1542 1542 parentrevs = cl.parentrevs
1543 1543 for r in getset(repo, fullreposet(repo), x):
1544 1544 if r == node.wdirrev:
1545 1545 up(p.rev() for p in repo[r].parents())
1546 1546 else:
1547 1547 up(parentrevs(r))
1548 1548 ps -= set([node.nullrev])
1549 1549 return subset & ps
1550 1550
1551 1551 def _phase(repo, subset, target):
1552 1552 """helper to select all rev in phase <target>"""
1553 1553 repo._phasecache.loadphaserevs(repo) # ensure phase's sets are loaded
1554 1554 if repo._phasecache._phasesets:
1555 1555 s = repo._phasecache._phasesets[target] - repo.changelog.filteredrevs
1556 1556 s = baseset(s)
1557 1557 s.sort() # set are non ordered, so we enforce ascending
1558 1558 return subset & s
1559 1559 else:
1560 1560 phase = repo._phasecache.phase
1561 1561 condition = lambda r: phase(repo, r) == target
1562 1562 return subset.filter(condition, condrepr=('<phase %r>', target),
1563 1563 cache=False)
1564 1564
1565 1565 @predicate('draft()', safe=True)
1566 1566 def draft(repo, subset, x):
1567 1567 """Changeset in draft phase."""
1568 1568 # i18n: "draft" is a keyword
1569 1569 getargs(x, 0, 0, _("draft takes no arguments"))
1570 1570 target = phases.draft
1571 1571 return _phase(repo, subset, target)
1572 1572
1573 1573 @predicate('secret()', safe=True)
1574 1574 def secret(repo, subset, x):
1575 1575 """Changeset in secret phase."""
1576 1576 # i18n: "secret" is a keyword
1577 1577 getargs(x, 0, 0, _("secret takes no arguments"))
1578 1578 target = phases.secret
1579 1579 return _phase(repo, subset, target)
1580 1580
1581 1581 def parentspec(repo, subset, x, n):
1582 1582 """``set^0``
1583 1583 The set.
1584 1584 ``set^1`` (or ``set^``), ``set^2``
1585 1585 First or second parent, respectively, of all changesets in set.
1586 1586 """
1587 1587 try:
1588 1588 n = int(n[1])
1589 1589 if n not in (0, 1, 2):
1590 1590 raise ValueError
1591 1591 except (TypeError, ValueError):
1592 1592 raise error.ParseError(_("^ expects a number 0, 1, or 2"))
1593 1593 ps = set()
1594 1594 cl = repo.changelog
1595 1595 for r in getset(repo, fullreposet(repo), x):
1596 1596 if n == 0:
1597 1597 ps.add(r)
1598 1598 elif n == 1:
1599 1599 ps.add(cl.parentrevs(r)[0])
1600 1600 elif n == 2:
1601 1601 parents = cl.parentrevs(r)
1602 1602 if len(parents) > 1:
1603 1603 ps.add(parents[1])
1604 1604 return subset & ps
1605 1605
1606 1606 @predicate('present(set)', safe=True)
1607 1607 def present(repo, subset, x):
1608 1608 """An empty set, if any revision in set isn't found; otherwise,
1609 1609 all revisions in set.
1610 1610
1611 1611 If any of specified revisions is not present in the local repository,
1612 1612 the query is normally aborted. But this predicate allows the query
1613 1613 to continue even in such cases.
1614 1614 """
1615 1615 try:
1616 1616 return getset(repo, subset, x)
1617 1617 except error.RepoLookupError:
1618 1618 return baseset()
1619 1619
1620 1620 # for internal use
1621 1621 @predicate('_notpublic', safe=True)
1622 1622 def _notpublic(repo, subset, x):
1623 1623 getargs(x, 0, 0, "_notpublic takes no arguments")
1624 1624 repo._phasecache.loadphaserevs(repo) # ensure phase's sets are loaded
1625 1625 if repo._phasecache._phasesets:
1626 1626 s = set()
1627 1627 for u in repo._phasecache._phasesets[1:]:
1628 1628 s.update(u)
1629 1629 s = baseset(s - repo.changelog.filteredrevs)
1630 1630 s.sort()
1631 1631 return subset & s
1632 1632 else:
1633 1633 phase = repo._phasecache.phase
1634 1634 target = phases.public
1635 1635 condition = lambda r: phase(repo, r) != target
1636 1636 return subset.filter(condition, condrepr=('<phase %r>', target),
1637 1637 cache=False)
1638 1638
1639 1639 @predicate('public()', safe=True)
1640 1640 def public(repo, subset, x):
1641 1641 """Changeset in public phase."""
1642 1642 # i18n: "public" is a keyword
1643 1643 getargs(x, 0, 0, _("public takes no arguments"))
1644 1644 phase = repo._phasecache.phase
1645 1645 target = phases.public
1646 1646 condition = lambda r: phase(repo, r) == target
1647 1647 return subset.filter(condition, condrepr=('<phase %r>', target),
1648 1648 cache=False)
1649 1649
1650 1650 @predicate('remote([id [,path]])', safe=True)
1651 1651 def remote(repo, subset, x):
1652 1652 """Local revision that corresponds to the given identifier in a
1653 1653 remote repository, if present. Here, the '.' identifier is a
1654 1654 synonym for the current local branch.
1655 1655 """
1656 1656
1657 1657 from . import hg # avoid start-up nasties
1658 1658 # i18n: "remote" is a keyword
1659 1659 l = getargs(x, 0, 2, _("remote takes zero, one, or two arguments"))
1660 1660
1661 1661 q = '.'
1662 1662 if len(l) > 0:
1663 1663 # i18n: "remote" is a keyword
1664 1664 q = getstring(l[0], _("remote requires a string id"))
1665 1665 if q == '.':
1666 1666 q = repo['.'].branch()
1667 1667
1668 1668 dest = ''
1669 1669 if len(l) > 1:
1670 1670 # i18n: "remote" is a keyword
1671 1671 dest = getstring(l[1], _("remote requires a repository path"))
1672 1672 dest = repo.ui.expandpath(dest or 'default')
1673 1673 dest, branches = hg.parseurl(dest)
1674 1674 revs, checkout = hg.addbranchrevs(repo, repo, branches, [])
1675 1675 if revs:
1676 1676 revs = [repo.lookup(rev) for rev in revs]
1677 1677 other = hg.peer(repo, {}, dest)
1678 1678 n = other.lookup(q)
1679 1679 if n in repo:
1680 1680 r = repo[n].rev()
1681 1681 if r in subset:
1682 1682 return baseset([r])
1683 1683 return baseset()
1684 1684
1685 1685 @predicate('removes(pattern)', safe=True)
1686 1686 def removes(repo, subset, x):
1687 1687 """Changesets which remove files matching pattern.
1688 1688
1689 1689 The pattern without explicit kind like ``glob:`` is expected to be
1690 1690 relative to the current directory and match against a file or a
1691 1691 directory.
1692 1692 """
1693 1693 # i18n: "removes" is a keyword
1694 1694 pat = getstring(x, _("removes requires a pattern"))
1695 1695 return checkstatus(repo, subset, pat, 2)
1696 1696
1697 1697 @predicate('rev(number)', safe=True)
1698 1698 def rev(repo, subset, x):
1699 1699 """Revision with the given numeric identifier.
1700 1700 """
1701 1701 # i18n: "rev" is a keyword
1702 1702 l = getargs(x, 1, 1, _("rev requires one argument"))
1703 1703 try:
1704 1704 # i18n: "rev" is a keyword
1705 1705 l = int(getstring(l[0], _("rev requires a number")))
1706 1706 except (TypeError, ValueError):
1707 1707 # i18n: "rev" is a keyword
1708 1708 raise error.ParseError(_("rev expects a number"))
1709 1709 if l not in repo.changelog and l != node.nullrev:
1710 1710 return baseset()
1711 1711 return subset & baseset([l])
1712 1712
1713 1713 @predicate('matching(revision [, field])', safe=True)
1714 1714 def matching(repo, subset, x):
1715 1715 """Changesets in which a given set of fields match the set of fields in the
1716 1716 selected revision or set.
1717 1717
1718 1718 To match more than one field pass the list of fields to match separated
1719 1719 by spaces (e.g. ``author description``).
1720 1720
1721 1721 Valid fields are most regular revision fields and some special fields.
1722 1722
1723 1723 Regular revision fields are ``description``, ``author``, ``branch``,
1724 1724 ``date``, ``files``, ``phase``, ``parents``, ``substate``, ``user``
1725 1725 and ``diff``.
1726 1726 Note that ``author`` and ``user`` are synonyms. ``diff`` refers to the
1727 1727 contents of the revision. Two revisions matching their ``diff`` will
1728 1728 also match their ``files``.
1729 1729
1730 1730 Special fields are ``summary`` and ``metadata``:
1731 1731 ``summary`` matches the first line of the description.
1732 1732 ``metadata`` is equivalent to matching ``description user date``
1733 1733 (i.e. it matches the main metadata fields).
1734 1734
1735 1735 ``metadata`` is the default field which is used when no fields are
1736 1736 specified. You can match more than one field at a time.
1737 1737 """
1738 1738 # i18n: "matching" is a keyword
1739 1739 l = getargs(x, 1, 2, _("matching takes 1 or 2 arguments"))
1740 1740
1741 1741 revs = getset(repo, fullreposet(repo), l[0])
1742 1742
1743 1743 fieldlist = ['metadata']
1744 1744 if len(l) > 1:
1745 1745 fieldlist = getstring(l[1],
1746 1746 # i18n: "matching" is a keyword
1747 1747 _("matching requires a string "
1748 1748 "as its second argument")).split()
1749 1749
1750 1750 # Make sure that there are no repeated fields,
1751 1751 # expand the 'special' 'metadata' field type
1752 1752 # and check the 'files' whenever we check the 'diff'
1753 1753 fields = []
1754 1754 for field in fieldlist:
1755 1755 if field == 'metadata':
1756 1756 fields += ['user', 'description', 'date']
1757 1757 elif field == 'diff':
1758 1758 # a revision matching the diff must also match the files
1759 1759 # since matching the diff is very costly, make sure to
1760 1760 # also match the files first
1761 1761 fields += ['files', 'diff']
1762 1762 else:
1763 1763 if field == 'author':
1764 1764 field = 'user'
1765 1765 fields.append(field)
1766 1766 fields = set(fields)
1767 1767 if 'summary' in fields and 'description' in fields:
1768 1768 # If a revision matches its description it also matches its summary
1769 1769 fields.discard('summary')
1770 1770
1771 1771 # We may want to match more than one field
1772 1772 # Not all fields take the same amount of time to be matched
1773 1773 # Sort the selected fields in order of increasing matching cost
1774 1774 fieldorder = ['phase', 'parents', 'user', 'date', 'branch', 'summary',
1775 1775 'files', 'description', 'substate', 'diff']
1776 1776 def fieldkeyfunc(f):
1777 1777 try:
1778 1778 return fieldorder.index(f)
1779 1779 except ValueError:
1780 1780 # assume an unknown field is very costly
1781 1781 return len(fieldorder)
1782 1782 fields = list(fields)
1783 1783 fields.sort(key=fieldkeyfunc)
1784 1784
1785 1785 # Each field will be matched with its own "getfield" function
1786 1786 # which will be added to the getfieldfuncs array of functions
1787 1787 getfieldfuncs = []
1788 1788 _funcs = {
1789 1789 'user': lambda r: repo[r].user(),
1790 1790 'branch': lambda r: repo[r].branch(),
1791 1791 'date': lambda r: repo[r].date(),
1792 1792 'description': lambda r: repo[r].description(),
1793 1793 'files': lambda r: repo[r].files(),
1794 1794 'parents': lambda r: repo[r].parents(),
1795 1795 'phase': lambda r: repo[r].phase(),
1796 1796 'substate': lambda r: repo[r].substate,
1797 1797 'summary': lambda r: repo[r].description().splitlines()[0],
1798 1798 'diff': lambda r: list(repo[r].diff(git=True),)
1799 1799 }
1800 1800 for info in fields:
1801 1801 getfield = _funcs.get(info, None)
1802 1802 if getfield is None:
1803 1803 raise error.ParseError(
1804 1804 # i18n: "matching" is a keyword
1805 1805 _("unexpected field name passed to matching: %s") % info)
1806 1806 getfieldfuncs.append(getfield)
1807 1807 # convert the getfield array of functions into a "getinfo" function
1808 1808 # which returns an array of field values (or a single value if there
1809 1809 # is only one field to match)
1810 1810 getinfo = lambda r: [f(r) for f in getfieldfuncs]
1811 1811
1812 1812 def matches(x):
1813 1813 for rev in revs:
1814 1814 target = getinfo(rev)
1815 1815 match = True
1816 1816 for n, f in enumerate(getfieldfuncs):
1817 1817 if target[n] != f(x):
1818 1818 match = False
1819 1819 if match:
1820 1820 return True
1821 1821 return False
1822 1822
1823 1823 return subset.filter(matches, condrepr=('<matching%r %r>', fields, revs))
1824 1824
1825 1825 @predicate('reverse(set)', safe=True)
1826 1826 def reverse(repo, subset, x):
1827 1827 """Reverse order of set.
1828 1828 """
1829 1829 l = getset(repo, subset, x)
1830 1830 l.reverse()
1831 1831 return l
1832 1832
1833 1833 @predicate('roots(set)', safe=True)
1834 1834 def roots(repo, subset, x):
1835 1835 """Changesets in set with no parent changeset in set.
1836 1836 """
1837 1837 s = getset(repo, fullreposet(repo), x)
1838 1838 parents = repo.changelog.parentrevs
1839 1839 def filter(r):
1840 1840 for p in parents(r):
1841 1841 if 0 <= p and p in s:
1842 1842 return False
1843 1843 return True
1844 1844 return subset & s.filter(filter, condrepr='<roots>')
1845 1845
1846 1846 _sortkeyfuncs = {
1847 1847 'rev': lambda c: c.rev(),
1848 1848 'branch': lambda c: c.branch(),
1849 1849 'desc': lambda c: c.description(),
1850 1850 'user': lambda c: c.user(),
1851 1851 'author': lambda c: c.user(),
1852 1852 'date': lambda c: c.date()[0],
1853 1853 }
1854 1854
1855 1855 def _getsortargs(x):
1856 1856 """Parse sort options into (set, [(key, reverse)], opts)"""
1857 1857 args = getargsdict(x, 'sort', 'set keys topo.firstbranch')
1858 1858 if 'set' not in args:
1859 1859 # i18n: "sort" is a keyword
1860 1860 raise error.ParseError(_('sort requires one or two arguments'))
1861 1861 keys = "rev"
1862 1862 if 'keys' in args:
1863 1863 # i18n: "sort" is a keyword
1864 1864 keys = getstring(args['keys'], _("sort spec must be a string"))
1865 1865
1866 1866 keyflags = []
1867 1867 for k in keys.split():
1868 1868 fk = k
1869 1869 reverse = (k[0] == '-')
1870 1870 if reverse:
1871 1871 k = k[1:]
1872 1872 if k not in _sortkeyfuncs and k != 'topo':
1873 1873 raise error.ParseError(_("unknown sort key %r") % fk)
1874 1874 keyflags.append((k, reverse))
1875 1875
1876 1876 if len(keyflags) > 1 and any(k == 'topo' for k, reverse in keyflags):
1877 1877 # i18n: "topo" is a keyword
1878 1878 raise error.ParseError(_('topo sort order cannot be combined '
1879 1879 'with other sort keys'))
1880 1880
1881 1881 opts = {}
1882 1882 if 'topo.firstbranch' in args:
1883 1883 if any(k == 'topo' for k, reverse in keyflags):
1884 1884 opts['topo.firstbranch'] = args['topo.firstbranch']
1885 1885 else:
1886 1886 # i18n: "topo" and "topo.firstbranch" are keywords
1887 1887 raise error.ParseError(_('topo.firstbranch can only be used '
1888 1888 'when using the topo sort key'))
1889 1889
1890 1890 return args['set'], keyflags, opts
1891 1891
1892 1892 @predicate('sort(set[, [-]key... [, ...]])', safe=True)
1893 1893 def sort(repo, subset, x):
1894 1894 """Sort set by keys. The default sort order is ascending, specify a key
1895 1895 as ``-key`` to sort in descending order.
1896 1896
1897 1897 The keys can be:
1898 1898
1899 1899 - ``rev`` for the revision number,
1900 1900 - ``branch`` for the branch name,
1901 1901 - ``desc`` for the commit message (description),
1902 1902 - ``user`` for user name (``author`` can be used as an alias),
1903 1903 - ``date`` for the commit date
1904 1904 - ``topo`` for a reverse topographical sort
1905 1905
1906 1906 The ``topo`` sort order cannot be combined with other sort keys. This sort
1907 1907 takes one optional argument, ``topo.firstbranch``, which takes a revset that
1908 1908 specifies what topographical branches to prioritize in the sort.
1909 1909
1910 1910 """
1911 1911 s, keyflags, opts = _getsortargs(x)
1912 1912 revs = getset(repo, subset, s)
1913 1913
1914 1914 if not keyflags:
1915 1915 return revs
1916 1916 if len(keyflags) == 1 and keyflags[0][0] == "rev":
1917 1917 revs.sort(reverse=keyflags[0][1])
1918 1918 return revs
1919 1919 elif keyflags[0][0] == "topo":
1920 1920 firstbranch = ()
1921 1921 if 'topo.firstbranch' in opts:
1922 1922 firstbranch = getset(repo, subset, opts['topo.firstbranch'])
1923 1923 revs = baseset(_toposort(revs, repo.changelog.parentrevs, firstbranch),
1924 1924 istopo=True)
1925 1925 if keyflags[0][1]:
1926 1926 revs.reverse()
1927 1927 return revs
1928 1928
1929 1929 # sort() is guaranteed to be stable
1930 1930 ctxs = [repo[r] for r in revs]
1931 1931 for k, reverse in reversed(keyflags):
1932 1932 ctxs.sort(key=_sortkeyfuncs[k], reverse=reverse)
1933 1933 return baseset([c.rev() for c in ctxs])
1934 1934
1935 1935 def _toposort(revs, parentsfunc, firstbranch=()):
1936 1936 """Yield revisions from heads to roots one (topo) branch at a time.
1937 1937
1938 1938 This function aims to be used by a graph generator that wishes to minimize
1939 1939 the number of parallel branches and their interleaving.
1940 1940
1941 1941 Example iteration order (numbers show the "true" order in a changelog):
1942 1942
1943 1943 o 4
1944 1944 |
1945 1945 o 1
1946 1946 |
1947 1947 | o 3
1948 1948 | |
1949 1949 | o 2
1950 1950 |/
1951 1951 o 0
1952 1952
1953 1953 Note that the ancestors of merges are understood by the current
1954 1954 algorithm to be on the same branch. This means no reordering will
1955 1955 occur behind a merge.
1956 1956 """
1957 1957
1958 1958 ### Quick summary of the algorithm
1959 1959 #
1960 1960 # This function is based around a "retention" principle. We keep revisions
1961 1961 # in memory until we are ready to emit a whole branch that immediately
1962 1962 # "merges" into an existing one. This reduces the number of parallel
1963 1963 # branches with interleaved revisions.
1964 1964 #
1965 1965 # During iteration revs are split into two groups:
1966 1966 # A) revision already emitted
1967 1967 # B) revision in "retention". They are stored as different subgroups.
1968 1968 #
1969 1969 # for each REV, we do the following logic:
1970 1970 #
1971 1971 # 1) if REV is a parent of (A), we will emit it. If there is a
1972 1972 # retention group ((B) above) that is blocked on REV being
1973 1973 # available, we emit all the revisions out of that retention
1974 1974 # group first.
1975 1975 #
1976 1976 # 2) else, we'll search for a subgroup in (B) awaiting for REV to be
1977 1977 # available, if such subgroup exist, we add REV to it and the subgroup is
1978 1978 # now awaiting for REV.parents() to be available.
1979 1979 #
1980 1980 # 3) finally if no such group existed in (B), we create a new subgroup.
1981 1981 #
1982 1982 #
1983 1983 # To bootstrap the algorithm, we emit the tipmost revision (which
1984 1984 # puts it in group (A) from above).
1985 1985
1986 1986 revs.sort(reverse=True)
1987 1987
1988 1988 # Set of parents of revision that have been emitted. They can be considered
1989 1989 # unblocked as the graph generator is already aware of them so there is no
1990 1990 # need to delay the revisions that reference them.
1991 1991 #
1992 1992 # If someone wants to prioritize a branch over the others, pre-filling this
1993 1993 # set will force all other branches to wait until this branch is ready to be
1994 1994 # emitted.
1995 1995 unblocked = set(firstbranch)
1996 1996
1997 1997 # list of groups waiting to be displayed, each group is defined by:
1998 1998 #
1999 1999 # (revs: lists of revs waiting to be displayed,
2000 2000 # blocked: set of that cannot be displayed before those in 'revs')
2001 2001 #
2002 2002 # The second value ('blocked') correspond to parents of any revision in the
2003 2003 # group ('revs') that is not itself contained in the group. The main idea
2004 2004 # of this algorithm is to delay as much as possible the emission of any
2005 2005 # revision. This means waiting for the moment we are about to display
2006 2006 # these parents to display the revs in a group.
2007 2007 #
2008 2008 # This first implementation is smart until it encounters a merge: it will
2009 2009 # emit revs as soon as any parent is about to be emitted and can grow an
2010 2010 # arbitrary number of revs in 'blocked'. In practice this mean we properly
2011 2011 # retains new branches but gives up on any special ordering for ancestors
2012 2012 # of merges. The implementation can be improved to handle this better.
2013 2013 #
2014 2014 # The first subgroup is special. It corresponds to all the revision that
2015 2015 # were already emitted. The 'revs' lists is expected to be empty and the
2016 2016 # 'blocked' set contains the parents revisions of already emitted revision.
2017 2017 #
2018 2018 # You could pre-seed the <parents> set of groups[0] to a specific
2019 2019 # changesets to select what the first emitted branch should be.
2020 2020 groups = [([], unblocked)]
2021 2021 pendingheap = []
2022 2022 pendingset = set()
2023 2023
2024 2024 heapq.heapify(pendingheap)
2025 2025 heappop = heapq.heappop
2026 2026 heappush = heapq.heappush
2027 2027 for currentrev in revs:
2028 2028 # Heap works with smallest element, we want highest so we invert
2029 2029 if currentrev not in pendingset:
2030 2030 heappush(pendingheap, -currentrev)
2031 2031 pendingset.add(currentrev)
2032 2032 # iterates on pending rev until after the current rev have been
2033 2033 # processed.
2034 2034 rev = None
2035 2035 while rev != currentrev:
2036 2036 rev = -heappop(pendingheap)
2037 2037 pendingset.remove(rev)
2038 2038
2039 2039 # Seek for a subgroup blocked, waiting for the current revision.
2040 2040 matching = [i for i, g in enumerate(groups) if rev in g[1]]
2041 2041
2042 2042 if matching:
2043 2043 # The main idea is to gather together all sets that are blocked
2044 2044 # on the same revision.
2045 2045 #
2046 2046 # Groups are merged when a common blocking ancestor is
2047 2047 # observed. For example, given two groups:
2048 2048 #
2049 2049 # revs [5, 4] waiting for 1
2050 2050 # revs [3, 2] waiting for 1
2051 2051 #
2052 2052 # These two groups will be merged when we process
2053 2053 # 1. In theory, we could have merged the groups when
2054 2054 # we added 2 to the group it is now in (we could have
2055 2055 # noticed the groups were both blocked on 1 then), but
2056 2056 # the way it works now makes the algorithm simpler.
2057 2057 #
2058 2058 # We also always keep the oldest subgroup first. We can
2059 2059 # probably improve the behavior by having the longest set
2060 2060 # first. That way, graph algorithms could minimise the length
2061 2061 # of parallel lines their drawing. This is currently not done.
2062 2062 targetidx = matching.pop(0)
2063 2063 trevs, tparents = groups[targetidx]
2064 2064 for i in matching:
2065 2065 gr = groups[i]
2066 2066 trevs.extend(gr[0])
2067 2067 tparents |= gr[1]
2068 2068 # delete all merged subgroups (except the one we kept)
2069 2069 # (starting from the last subgroup for performance and
2070 2070 # sanity reasons)
2071 2071 for i in reversed(matching):
2072 2072 del groups[i]
2073 2073 else:
2074 2074 # This is a new head. We create a new subgroup for it.
2075 2075 targetidx = len(groups)
2076 2076 groups.append(([], set([rev])))
2077 2077
2078 2078 gr = groups[targetidx]
2079 2079
2080 2080 # We now add the current nodes to this subgroups. This is done
2081 2081 # after the subgroup merging because all elements from a subgroup
2082 2082 # that relied on this rev must precede it.
2083 2083 #
2084 2084 # we also update the <parents> set to include the parents of the
2085 2085 # new nodes.
2086 2086 if rev == currentrev: # only display stuff in rev
2087 2087 gr[0].append(rev)
2088 2088 gr[1].remove(rev)
2089 2089 parents = [p for p in parentsfunc(rev) if p > node.nullrev]
2090 2090 gr[1].update(parents)
2091 2091 for p in parents:
2092 2092 if p not in pendingset:
2093 2093 pendingset.add(p)
2094 2094 heappush(pendingheap, -p)
2095 2095
2096 2096 # Look for a subgroup to display
2097 2097 #
2098 2098 # When unblocked is empty (if clause), we were not waiting for any
2099 2099 # revisions during the first iteration (if no priority was given) or
2100 2100 # if we emitted a whole disconnected set of the graph (reached a
2101 2101 # root). In that case we arbitrarily take the oldest known
2102 2102 # subgroup. The heuristic could probably be better.
2103 2103 #
2104 2104 # Otherwise (elif clause) if the subgroup is blocked on
2105 2105 # a revision we just emitted, we can safely emit it as
2106 2106 # well.
2107 2107 if not unblocked:
2108 2108 if len(groups) > 1: # display other subset
2109 2109 targetidx = 1
2110 2110 gr = groups[1]
2111 2111 elif not gr[1] & unblocked:
2112 2112 gr = None
2113 2113
2114 2114 if gr is not None:
2115 2115 # update the set of awaited revisions with the one from the
2116 2116 # subgroup
2117 2117 unblocked |= gr[1]
2118 2118 # output all revisions in the subgroup
2119 2119 for r in gr[0]:
2120 2120 yield r
2121 2121 # delete the subgroup that you just output
2122 2122 # unless it is groups[0] in which case you just empty it.
2123 2123 if targetidx:
2124 2124 del groups[targetidx]
2125 2125 else:
2126 2126 gr[0][:] = []
2127 2127 # Check if we have some subgroup waiting for revisions we are not going to
2128 2128 # iterate over
2129 2129 for g in groups:
2130 2130 for r in g[0]:
2131 2131 yield r
2132 2132
2133 2133 @predicate('subrepo([pattern])')
2134 2134 def subrepo(repo, subset, x):
2135 2135 """Changesets that add, modify or remove the given subrepo. If no subrepo
2136 2136 pattern is named, any subrepo changes are returned.
2137 2137 """
2138 2138 # i18n: "subrepo" is a keyword
2139 2139 args = getargs(x, 0, 1, _('subrepo takes at most one argument'))
2140 2140 pat = None
2141 2141 if len(args) != 0:
2142 2142 pat = getstring(args[0], _("subrepo requires a pattern"))
2143 2143
2144 2144 m = matchmod.exact(repo.root, repo.root, ['.hgsubstate'])
2145 2145
2146 2146 def submatches(names):
2147 2147 k, p, m = util.stringmatcher(pat)
2148 2148 for name in names:
2149 2149 if m(name):
2150 2150 yield name
2151 2151
2152 2152 def matches(x):
2153 2153 c = repo[x]
2154 2154 s = repo.status(c.p1().node(), c.node(), match=m)
2155 2155
2156 2156 if pat is None:
2157 2157 return s.added or s.modified or s.removed
2158 2158
2159 2159 if s.added:
2160 2160 return any(submatches(c.substate.keys()))
2161 2161
2162 2162 if s.modified:
2163 2163 subs = set(c.p1().substate.keys())
2164 2164 subs.update(c.substate.keys())
2165 2165
2166 2166 for path in submatches(subs):
2167 2167 if c.p1().substate.get(path) != c.substate.get(path):
2168 2168 return True
2169 2169
2170 2170 if s.removed:
2171 2171 return any(submatches(c.p1().substate.keys()))
2172 2172
2173 2173 return False
2174 2174
2175 2175 return subset.filter(matches, condrepr=('<subrepo %r>', pat))
2176 2176
2177 2177 def _substringmatcher(pattern):
2178 2178 kind, pattern, matcher = util.stringmatcher(pattern)
2179 2179 if kind == 'literal':
2180 2180 matcher = lambda s: pattern in s
2181 2181 return kind, pattern, matcher
2182 2182
2183 2183 @predicate('tag([name])', safe=True)
2184 2184 def tag(repo, subset, x):
2185 2185 """The specified tag by name, or all tagged revisions if no name is given.
2186 2186
2187 2187 If `name` starts with `re:`, the remainder of the name is treated as
2188 2188 a regular expression. To match a tag that actually starts with `re:`,
2189 2189 use the prefix `literal:`.
2190 2190 """
2191 2191 # i18n: "tag" is a keyword
2192 2192 args = getargs(x, 0, 1, _("tag takes one or no arguments"))
2193 2193 cl = repo.changelog
2194 2194 if args:
2195 2195 pattern = getstring(args[0],
2196 2196 # i18n: "tag" is a keyword
2197 2197 _('the argument to tag must be a string'))
2198 2198 kind, pattern, matcher = util.stringmatcher(pattern)
2199 2199 if kind == 'literal':
2200 2200 # avoid resolving all tags
2201 2201 tn = repo._tagscache.tags.get(pattern, None)
2202 2202 if tn is None:
2203 2203 raise error.RepoLookupError(_("tag '%s' does not exist")
2204 2204 % pattern)
2205 2205 s = set([repo[tn].rev()])
2206 2206 else:
2207 2207 s = set([cl.rev(n) for t, n in repo.tagslist() if matcher(t)])
2208 2208 else:
2209 2209 s = set([cl.rev(n) for t, n in repo.tagslist() if t != 'tip'])
2210 2210 return subset & s
2211 2211
2212 2212 @predicate('tagged', safe=True)
2213 2213 def tagged(repo, subset, x):
2214 2214 return tag(repo, subset, x)
2215 2215
2216 2216 @predicate('unstable()', safe=True)
2217 2217 def unstable(repo, subset, x):
2218 2218 """Non-obsolete changesets with obsolete ancestors.
2219 2219 """
2220 2220 # i18n: "unstable" is a keyword
2221 2221 getargs(x, 0, 0, _("unstable takes no arguments"))
2222 2222 unstables = obsmod.getrevs(repo, 'unstable')
2223 2223 return subset & unstables
2224 2224
2225 2225
2226 2226 @predicate('user(string)', safe=True)
2227 2227 def user(repo, subset, x):
2228 2228 """User name contains string. The match is case-insensitive.
2229 2229
2230 2230 If `string` starts with `re:`, the remainder of the string is treated as
2231 2231 a regular expression. To match a user that actually contains `re:`, use
2232 2232 the prefix `literal:`.
2233 2233 """
2234 2234 return author(repo, subset, x)
2235 2235
2236 2236 # experimental
2237 2237 @predicate('wdir', safe=True)
2238 2238 def wdir(repo, subset, x):
2239 2239 # i18n: "wdir" is a keyword
2240 2240 getargs(x, 0, 0, _("wdir takes no arguments"))
2241 2241 if node.wdirrev in subset or isinstance(subset, fullreposet):
2242 2242 return baseset([node.wdirrev])
2243 2243 return baseset()
2244 2244
2245 2245 # for internal use
2246 2246 @predicate('_list', safe=True)
2247 2247 def _list(repo, subset, x):
2248 2248 s = getstring(x, "internal error")
2249 2249 if not s:
2250 2250 return baseset()
2251 2251 # remove duplicates here. it's difficult for caller to deduplicate sets
2252 2252 # because different symbols can point to the same rev.
2253 2253 cl = repo.changelog
2254 2254 ls = []
2255 2255 seen = set()
2256 2256 for t in s.split('\0'):
2257 2257 try:
2258 2258 # fast path for integer revision
2259 2259 r = int(t)
2260 2260 if str(r) != t or r not in cl:
2261 2261 raise ValueError
2262 2262 revs = [r]
2263 2263 except ValueError:
2264 2264 revs = stringset(repo, subset, t)
2265 2265
2266 2266 for r in revs:
2267 2267 if r in seen:
2268 2268 continue
2269 2269 if (r in subset
2270 2270 or r == node.nullrev and isinstance(subset, fullreposet)):
2271 2271 ls.append(r)
2272 2272 seen.add(r)
2273 2273 return baseset(ls)
2274 2274
2275 2275 # for internal use
2276 2276 @predicate('_intlist', safe=True)
2277 2277 def _intlist(repo, subset, x):
2278 2278 s = getstring(x, "internal error")
2279 2279 if not s:
2280 2280 return baseset()
2281 2281 ls = [int(r) for r in s.split('\0')]
2282 2282 s = subset
2283 2283 return baseset([r for r in ls if r in s])
2284 2284
2285 2285 # for internal use
2286 2286 @predicate('_hexlist', safe=True)
2287 2287 def _hexlist(repo, subset, x):
2288 2288 s = getstring(x, "internal error")
2289 2289 if not s:
2290 2290 return baseset()
2291 2291 cl = repo.changelog
2292 2292 ls = [cl.rev(node.bin(r)) for r in s.split('\0')]
2293 2293 s = subset
2294 2294 return baseset([r for r in ls if r in s])
2295 2295
2296 2296 methods = {
2297 2297 "range": rangeset,
2298 2298 "dagrange": dagrange,
2299 2299 "string": stringset,
2300 2300 "symbol": stringset,
2301 2301 "and": andset,
2302 2302 "or": orset,
2303 2303 "not": notset,
2304 2304 "difference": differenceset,
2305 2305 "list": listset,
2306 2306 "keyvalue": keyvaluepair,
2307 2307 "func": func,
2308 2308 "ancestor": ancestorspec,
2309 2309 "parent": parentspec,
2310 2310 "parentpost": p1,
2311 2311 }
2312 2312
2313 # Constants for ordering requirement, used in _analyze():
2314 #
2315 # If 'define', any nested functions and operations can change the ordering of
2316 # the entries in the set. If 'follow', any nested functions and operations
2317 # should take the ordering specified by the first operand to the '&' operator.
2318 #
2319 # For instance,
2320 #
2321 # X & (Y | Z)
2322 # ^ ^^^^^^^
2323 # | follow
2324 # define
2325 #
2326 # will be evaluated as 'or(y(x()), z(x()))', where 'x()' can change the order
2327 # of the entries in the set, but 'y()', 'z()' and 'or()' shouldn't.
2328 #
2329 # 'any' means the order doesn't matter. For instance,
2330 #
2331 # X & !Y
2332 # ^
2333 # any
2334 #
2335 # 'y()' can either enforce its ordering requirement or take the ordering
2336 # specified by 'x()' because 'not()' doesn't care the order.
2337 #
2338 # Transition of ordering requirement:
2339 #
2340 # 1. starts with 'define'
2341 # 2. shifts to 'follow' by 'x & y'
2342 # 3. changes back to 'define' on function call 'f(x)' or function-like
2343 # operation 'x (f) y' because 'f' may have its own ordering requirement
2344 # for 'x' and 'y' (e.g. 'first(x)')
2345 #
2346 anyorder = 'any' # don't care the order
2347 defineorder = 'define' # should define the order
2348 followorder = 'follow' # must follow the current order
2349
2350 # transition table for 'x & y', from the current expression 'x' to 'y'
2351 _tofolloworder = {
2352 anyorder: anyorder,
2353 defineorder: followorder,
2354 followorder: followorder,
2355 }
2356
2313 2357 def _matchonly(revs, bases):
2314 2358 """
2315 2359 >>> f = lambda *args: _matchonly(*map(parse, args))
2316 2360 >>> f('ancestors(A)', 'not ancestors(B)')
2317 2361 ('list', ('symbol', 'A'), ('symbol', 'B'))
2318 2362 """
2319 2363 if (revs is not None
2320 2364 and revs[0] == 'func'
2321 2365 and getsymbol(revs[1]) == 'ancestors'
2322 2366 and bases is not None
2323 2367 and bases[0] == 'not'
2324 2368 and bases[1][0] == 'func'
2325 2369 and getsymbol(bases[1][1]) == 'ancestors'):
2326 2370 return ('list', revs[2], bases[1][2])
2327 2371
2328 2372 def _fixops(x):
2329 2373 """Rewrite raw parsed tree to resolve ambiguous syntax which cannot be
2330 2374 handled well by our simple top-down parser"""
2331 2375 if not isinstance(x, tuple):
2332 2376 return x
2333 2377
2334 2378 op = x[0]
2335 2379 if op == 'parent':
2336 2380 # x^:y means (x^) : y, not x ^ (:y)
2337 2381 # x^: means (x^) :, not x ^ (:)
2338 2382 post = ('parentpost', x[1])
2339 2383 if x[2][0] == 'dagrangepre':
2340 2384 return _fixops(('dagrange', post, x[2][1]))
2341 2385 elif x[2][0] == 'rangepre':
2342 2386 return _fixops(('range', post, x[2][1]))
2343 2387 elif x[2][0] == 'rangeall':
2344 2388 return _fixops(('rangepost', post))
2345 2389 elif op == 'or':
2346 2390 # make number of arguments deterministic:
2347 2391 # x + y + z -> (or x y z) -> (or (list x y z))
2348 2392 return (op, _fixops(('list',) + x[1:]))
2349 2393
2350 2394 return (op,) + tuple(_fixops(y) for y in x[1:])
2351 2395
2352 def _analyze(x):
2396 def _analyze(x, order):
2353 2397 if x is None:
2354 2398 return x
2355 2399
2356 2400 op = x[0]
2357 2401 if op == 'minus':
2358 return _analyze(('and', x[1], ('not', x[2])))
2402 return _analyze(('and', x[1], ('not', x[2])), order)
2359 2403 elif op == 'only':
2360 2404 t = ('func', ('symbol', 'only'), ('list', x[1], x[2]))
2361 return _analyze(t)
2405 return _analyze(t, order)
2362 2406 elif op == 'onlypost':
2363 return _analyze(('func', ('symbol', 'only'), x[1]))
2407 return _analyze(('func', ('symbol', 'only'), x[1]), order)
2364 2408 elif op == 'dagrangepre':
2365 return _analyze(('func', ('symbol', 'ancestors'), x[1]))
2409 return _analyze(('func', ('symbol', 'ancestors'), x[1]), order)
2366 2410 elif op == 'dagrangepost':
2367 return _analyze(('func', ('symbol', 'descendants'), x[1]))
2411 return _analyze(('func', ('symbol', 'descendants'), x[1]), order)
2368 2412 elif op == 'rangeall':
2369 return _analyze(('range', ('string', '0'), ('string', 'tip')))
2413 return _analyze(('range', ('string', '0'), ('string', 'tip')), order)
2370 2414 elif op == 'rangepre':
2371 return _analyze(('range', ('string', '0'), x[1]))
2415 return _analyze(('range', ('string', '0'), x[1]), order)
2372 2416 elif op == 'rangepost':
2373 return _analyze(('range', x[1], ('string', 'tip')))
2417 return _analyze(('range', x[1], ('string', 'tip')), order)
2374 2418 elif op == 'negate':
2375 2419 s = getstring(x[1], _("can't negate that"))
2376 return _analyze(('string', '-' + s))
2420 return _analyze(('string', '-' + s), order)
2377 2421 elif op in ('string', 'symbol'):
2378 2422 return x
2379 2423 elif op == 'and':
2380 ta = _analyze(x[1])
2381 tb = _analyze(x[2])
2424 ta = _analyze(x[1], order)
2425 tb = _analyze(x[2], _tofolloworder[order])
2382 2426 return (op, ta, tb)
2383 2427 elif op == 'or':
2384 return (op, _analyze(x[1]))
2428 return (op, _analyze(x[1], order))
2385 2429 elif op == 'not':
2386 return (op, _analyze(x[1]))
2430 return (op, _analyze(x[1], anyorder))
2387 2431 elif op == 'parentpost':
2388 return (op, _analyze(x[1]))
2432 return (op, _analyze(x[1], defineorder))
2389 2433 elif op == 'group':
2390 return _analyze(x[1])
2434 return _analyze(x[1], order)
2391 2435 elif op in ('dagrange', 'range', 'parent', 'ancestor'):
2392 ta = _analyze(x[1])
2393 tb = _analyze(x[2])
2436 ta = _analyze(x[1], defineorder)
2437 tb = _analyze(x[2], defineorder)
2394 2438 return (op, ta, tb)
2395 2439 elif op == 'list':
2396 return (op,) + tuple(_analyze(y) for y in x[1:])
2440 return (op,) + tuple(_analyze(y, order) for y in x[1:])
2397 2441 elif op == 'keyvalue':
2398 return (op, x[1], _analyze(x[2]))
2442 return (op, x[1], _analyze(x[2], order))
2399 2443 elif op == 'func':
2400 return (op, x[1], _analyze(x[2]))
2444 return (op, x[1], _analyze(x[2], defineorder))
2401 2445 raise ValueError('invalid operator %r' % op)
2402 2446
2403 def analyze(x):
2447 def analyze(x, order=defineorder):
2404 2448 """Transform raw parsed tree to evaluatable tree which can be fed to
2405 2449 optimize() or getset()
2406 2450
2407 2451 All pseudo operations should be mapped to real operations or functions
2408 2452 defined in methods or symbols table respectively.
2453
2454 'order' specifies how the current expression 'x' is ordered (see the
2455 constants defined above.)
2409 2456 """
2410 return _analyze(x)
2457 return _analyze(x, order)
2411 2458
2412 2459 def _optimize(x, small):
2413 2460 if x is None:
2414 2461 return 0, x
2415 2462
2416 2463 smallbonus = 1
2417 2464 if small:
2418 2465 smallbonus = .5
2419 2466
2420 2467 op = x[0]
2421 2468 if op in ('string', 'symbol'):
2422 2469 return smallbonus, x # single revisions are small
2423 2470 elif op == 'and':
2424 2471 wa, ta = _optimize(x[1], True)
2425 2472 wb, tb = _optimize(x[2], True)
2426 2473 w = min(wa, wb)
2427 2474
2428 2475 # (::x and not ::y)/(not ::y and ::x) have a fast path
2429 2476 tm = _matchonly(ta, tb) or _matchonly(tb, ta)
2430 2477 if tm:
2431 2478 return w, ('func', ('symbol', 'only'), tm)
2432 2479
2433 2480 if tb is not None and tb[0] == 'not':
2434 2481 return wa, ('difference', ta, tb[1])
2435 2482
2436 2483 if wa > wb:
2437 2484 return w, (op, tb, ta)
2438 2485 return w, (op, ta, tb)
2439 2486 elif op == 'or':
2440 2487 # fast path for machine-generated expression, that is likely to have
2441 2488 # lots of trivial revisions: 'a + b + c()' to '_list(a b) + c()'
2442 2489 ws, ts, ss = [], [], []
2443 2490 def flushss():
2444 2491 if not ss:
2445 2492 return
2446 2493 if len(ss) == 1:
2447 2494 w, t = ss[0]
2448 2495 else:
2449 2496 s = '\0'.join(t[1] for w, t in ss)
2450 2497 y = ('func', ('symbol', '_list'), ('string', s))
2451 2498 w, t = _optimize(y, False)
2452 2499 ws.append(w)
2453 2500 ts.append(t)
2454 2501 del ss[:]
2455 2502 for y in getlist(x[1]):
2456 2503 w, t = _optimize(y, False)
2457 2504 if t is not None and (t[0] == 'string' or t[0] == 'symbol'):
2458 2505 ss.append((w, t))
2459 2506 continue
2460 2507 flushss()
2461 2508 ws.append(w)
2462 2509 ts.append(t)
2463 2510 flushss()
2464 2511 if len(ts) == 1:
2465 2512 return ws[0], ts[0] # 'or' operation is fully optimized out
2466 2513 # we can't reorder trees by weight because it would change the order.
2467 2514 # ("sort(a + b)" == "sort(b + a)", but "a + b" != "b + a")
2468 2515 # ts = tuple(t for w, t in sorted(zip(ws, ts), key=lambda wt: wt[0]))
2469 2516 return max(ws), (op, ('list',) + tuple(ts))
2470 2517 elif op == 'not':
2471 2518 # Optimize not public() to _notpublic() because we have a fast version
2472 2519 if x[1] == ('func', ('symbol', 'public'), None):
2473 2520 newsym = ('func', ('symbol', '_notpublic'), None)
2474 2521 o = _optimize(newsym, not small)
2475 2522 return o[0], o[1]
2476 2523 else:
2477 2524 o = _optimize(x[1], not small)
2478 2525 return o[0], (op, o[1])
2479 2526 elif op == 'parentpost':
2480 2527 o = _optimize(x[1], small)
2481 2528 return o[0], (op, o[1])
2482 2529 elif op in ('dagrange', 'range', 'parent', 'ancestor'):
2483 2530 wa, ta = _optimize(x[1], small)
2484 2531 wb, tb = _optimize(x[2], small)
2485 2532 return wa + wb, (op, ta, tb)
2486 2533 elif op == 'list':
2487 2534 ws, ts = zip(*(_optimize(y, small) for y in x[1:]))
2488 2535 return sum(ws), (op,) + ts
2489 2536 elif op == 'keyvalue':
2490 2537 w, t = _optimize(x[2], small)
2491 2538 return w, (op, x[1], t)
2492 2539 elif op == 'func':
2493 2540 f = getsymbol(x[1])
2494 2541 wa, ta = _optimize(x[2], small)
2495 2542 if f in ('author', 'branch', 'closed', 'date', 'desc', 'file', 'grep',
2496 2543 'keyword', 'outgoing', 'user'):
2497 2544 w = 10 # slow
2498 2545 elif f in ('modifies', 'adds', 'removes'):
2499 2546 w = 30 # slower
2500 2547 elif f == "contains":
2501 2548 w = 100 # very slow
2502 2549 elif f == "ancestor":
2503 2550 w = 1 * smallbonus
2504 2551 elif f in ('reverse', 'limit', 'first', '_intlist'):
2505 2552 w = 0
2506 2553 elif f == "sort":
2507 2554 w = 10 # assume most sorts look at changelog
2508 2555 else:
2509 2556 w = 1
2510 2557 return w + wa, (op, x[1], ta)
2511 2558 raise ValueError('invalid operator %r' % op)
2512 2559
2513 2560 def optimize(tree):
2514 2561 """Optimize evaluatable tree
2515 2562
2516 2563 All pseudo operations should be transformed beforehand.
2517 2564 """
2518 2565 _weight, newtree = _optimize(tree, small=True)
2519 2566 return newtree
2520 2567
2521 2568 # the set of valid characters for the initial letter of symbols in
2522 2569 # alias declarations and definitions
2523 2570 _aliassyminitletters = set(c for c in [chr(i) for i in xrange(256)]
2524 2571 if c.isalnum() or c in '._@$' or ord(c) > 127)
2525 2572
2526 2573 def _parsewith(spec, lookup=None, syminitletters=None):
2527 2574 """Generate a parse tree of given spec with given tokenizing options
2528 2575
2529 2576 >>> _parsewith('foo($1)', syminitletters=_aliassyminitletters)
2530 2577 ('func', ('symbol', 'foo'), ('symbol', '$1'))
2531 2578 >>> _parsewith('$1')
2532 2579 Traceback (most recent call last):
2533 2580 ...
2534 2581 ParseError: ("syntax error in revset '$1'", 0)
2535 2582 >>> _parsewith('foo bar')
2536 2583 Traceback (most recent call last):
2537 2584 ...
2538 2585 ParseError: ('invalid token', 4)
2539 2586 """
2540 2587 p = parser.parser(elements)
2541 2588 tree, pos = p.parse(tokenize(spec, lookup=lookup,
2542 2589 syminitletters=syminitletters))
2543 2590 if pos != len(spec):
2544 2591 raise error.ParseError(_('invalid token'), pos)
2545 2592 return _fixops(parser.simplifyinfixops(tree, ('list', 'or')))
2546 2593
2547 2594 class _aliasrules(parser.basealiasrules):
2548 2595 """Parsing and expansion rule set of revset aliases"""
2549 2596 _section = _('revset alias')
2550 2597
2551 2598 @staticmethod
2552 2599 def _parse(spec):
2553 2600 """Parse alias declaration/definition ``spec``
2554 2601
2555 2602 This allows symbol names to use also ``$`` as an initial letter
2556 2603 (for backward compatibility), and callers of this function should
2557 2604 examine whether ``$`` is used also for unexpected symbols or not.
2558 2605 """
2559 2606 return _parsewith(spec, syminitletters=_aliassyminitletters)
2560 2607
2561 2608 @staticmethod
2562 2609 def _trygetfunc(tree):
2563 2610 if tree[0] == 'func' and tree[1][0] == 'symbol':
2564 2611 return tree[1][1], getlist(tree[2])
2565 2612
2566 2613 def expandaliases(ui, tree):
2567 2614 aliases = _aliasrules.buildmap(ui.configitems('revsetalias'))
2568 2615 tree = _aliasrules.expand(aliases, tree)
2569 2616 # warn about problematic (but not referred) aliases
2570 2617 for name, alias in sorted(aliases.iteritems()):
2571 2618 if alias.error and not alias.warned:
2572 2619 ui.warn(_('warning: %s\n') % (alias.error))
2573 2620 alias.warned = True
2574 2621 return tree
2575 2622
2576 2623 def foldconcat(tree):
2577 2624 """Fold elements to be concatenated by `##`
2578 2625 """
2579 2626 if not isinstance(tree, tuple) or tree[0] in ('string', 'symbol'):
2580 2627 return tree
2581 2628 if tree[0] == '_concat':
2582 2629 pending = [tree]
2583 2630 l = []
2584 2631 while pending:
2585 2632 e = pending.pop()
2586 2633 if e[0] == '_concat':
2587 2634 pending.extend(reversed(e[1:]))
2588 2635 elif e[0] in ('string', 'symbol'):
2589 2636 l.append(e[1])
2590 2637 else:
2591 2638 msg = _("\"##\" can't concatenate \"%s\" element") % (e[0])
2592 2639 raise error.ParseError(msg)
2593 2640 return ('string', ''.join(l))
2594 2641 else:
2595 2642 return tuple(foldconcat(t) for t in tree)
2596 2643
2597 2644 def parse(spec, lookup=None):
2598 2645 return _parsewith(spec, lookup=lookup)
2599 2646
2600 2647 def posttreebuilthook(tree, repo):
2601 2648 # hook for extensions to execute code on the optimized tree
2602 2649 pass
2603 2650
2604 2651 def match(ui, spec, repo=None):
2605 2652 """Create a matcher for a single revision spec."""
2606 2653 return matchany(ui, [spec], repo=repo)
2607 2654
2608 2655 def matchany(ui, specs, repo=None):
2609 2656 """Create a matcher that will include any revisions matching one of the
2610 2657 given specs"""
2611 2658 if not specs:
2612 2659 def mfunc(repo, subset=None):
2613 2660 return baseset()
2614 2661 return mfunc
2615 2662 if not all(specs):
2616 2663 raise error.ParseError(_("empty query"))
2617 2664 lookup = None
2618 2665 if repo:
2619 2666 lookup = repo.__contains__
2620 2667 if len(specs) == 1:
2621 2668 tree = parse(specs[0], lookup)
2622 2669 else:
2623 2670 tree = ('or', ('list',) + tuple(parse(s, lookup) for s in specs))
2624 2671
2625 2672 if ui:
2626 2673 tree = expandaliases(ui, tree)
2627 2674 tree = foldconcat(tree)
2628 2675 tree = analyze(tree)
2629 2676 tree = optimize(tree)
2630 2677 posttreebuilthook(tree, repo)
2631 2678 return makematcher(tree)
2632 2679
2633 2680 def makematcher(tree):
2634 2681 """Create a matcher from an evaluatable tree"""
2635 2682 def mfunc(repo, subset=None):
2636 2683 if subset is None:
2637 2684 subset = fullreposet(repo)
2638 2685 if util.safehasattr(subset, 'isascending'):
2639 2686 result = getset(repo, subset, tree)
2640 2687 else:
2641 2688 result = getset(repo, baseset(subset), tree)
2642 2689 return result
2643 2690 return mfunc
2644 2691
2645 2692 def formatspec(expr, *args):
2646 2693 '''
2647 2694 This is a convenience function for using revsets internally, and
2648 2695 escapes arguments appropriately. Aliases are intentionally ignored
2649 2696 so that intended expression behavior isn't accidentally subverted.
2650 2697
2651 2698 Supported arguments:
2652 2699
2653 2700 %r = revset expression, parenthesized
2654 2701 %d = int(arg), no quoting
2655 2702 %s = string(arg), escaped and single-quoted
2656 2703 %b = arg.branch(), escaped and single-quoted
2657 2704 %n = hex(arg), single-quoted
2658 2705 %% = a literal '%'
2659 2706
2660 2707 Prefixing the type with 'l' specifies a parenthesized list of that type.
2661 2708
2662 2709 >>> formatspec('%r:: and %lr', '10 or 11', ("this()", "that()"))
2663 2710 '(10 or 11):: and ((this()) or (that()))'
2664 2711 >>> formatspec('%d:: and not %d::', 10, 20)
2665 2712 '10:: and not 20::'
2666 2713 >>> formatspec('%ld or %ld', [], [1])
2667 2714 "_list('') or 1"
2668 2715 >>> formatspec('keyword(%s)', 'foo\\xe9')
2669 2716 "keyword('foo\\\\xe9')"
2670 2717 >>> b = lambda: 'default'
2671 2718 >>> b.branch = b
2672 2719 >>> formatspec('branch(%b)', b)
2673 2720 "branch('default')"
2674 2721 >>> formatspec('root(%ls)', ['a', 'b', 'c', 'd'])
2675 2722 "root(_list('a\\x00b\\x00c\\x00d'))"
2676 2723 '''
2677 2724
2678 2725 def quote(s):
2679 2726 return repr(str(s))
2680 2727
2681 2728 def argtype(c, arg):
2682 2729 if c == 'd':
2683 2730 return str(int(arg))
2684 2731 elif c == 's':
2685 2732 return quote(arg)
2686 2733 elif c == 'r':
2687 2734 parse(arg) # make sure syntax errors are confined
2688 2735 return '(%s)' % arg
2689 2736 elif c == 'n':
2690 2737 return quote(node.hex(arg))
2691 2738 elif c == 'b':
2692 2739 return quote(arg.branch())
2693 2740
2694 2741 def listexp(s, t):
2695 2742 l = len(s)
2696 2743 if l == 0:
2697 2744 return "_list('')"
2698 2745 elif l == 1:
2699 2746 return argtype(t, s[0])
2700 2747 elif t == 'd':
2701 2748 return "_intlist('%s')" % "\0".join(str(int(a)) for a in s)
2702 2749 elif t == 's':
2703 2750 return "_list('%s')" % "\0".join(s)
2704 2751 elif t == 'n':
2705 2752 return "_hexlist('%s')" % "\0".join(node.hex(a) for a in s)
2706 2753 elif t == 'b':
2707 2754 return "_list('%s')" % "\0".join(a.branch() for a in s)
2708 2755
2709 2756 m = l // 2
2710 2757 return '(%s or %s)' % (listexp(s[:m], t), listexp(s[m:], t))
2711 2758
2712 2759 ret = ''
2713 2760 pos = 0
2714 2761 arg = 0
2715 2762 while pos < len(expr):
2716 2763 c = expr[pos]
2717 2764 if c == '%':
2718 2765 pos += 1
2719 2766 d = expr[pos]
2720 2767 if d == '%':
2721 2768 ret += d
2722 2769 elif d in 'dsnbr':
2723 2770 ret += argtype(d, args[arg])
2724 2771 arg += 1
2725 2772 elif d == 'l':
2726 2773 # a list of some type
2727 2774 pos += 1
2728 2775 d = expr[pos]
2729 2776 ret += listexp(list(args[arg]), d)
2730 2777 arg += 1
2731 2778 else:
2732 2779 raise error.Abort(_('unexpected revspec format character %s')
2733 2780 % d)
2734 2781 else:
2735 2782 ret += c
2736 2783 pos += 1
2737 2784
2738 2785 return ret
2739 2786
2740 2787 def prettyformat(tree):
2741 2788 return parser.prettyformat(tree, ('string', 'symbol'))
2742 2789
2743 2790 def depth(tree):
2744 2791 if isinstance(tree, tuple):
2745 2792 return max(map(depth, tree)) + 1
2746 2793 else:
2747 2794 return 0
2748 2795
2749 2796 def funcsused(tree):
2750 2797 if not isinstance(tree, tuple) or tree[0] in ('string', 'symbol'):
2751 2798 return set()
2752 2799 else:
2753 2800 funcs = set()
2754 2801 for s in tree[1:]:
2755 2802 funcs |= funcsused(s)
2756 2803 if tree[0] == 'func':
2757 2804 funcs.add(tree[1][1])
2758 2805 return funcs
2759 2806
2760 2807 def _formatsetrepr(r):
2761 2808 """Format an optional printable representation of a set
2762 2809
2763 2810 ======== =================================
2764 2811 type(r) example
2765 2812 ======== =================================
2766 2813 tuple ('<not %r>', other)
2767 2814 str '<branch closed>'
2768 2815 callable lambda: '<branch %r>' % sorted(b)
2769 2816 object other
2770 2817 ======== =================================
2771 2818 """
2772 2819 if r is None:
2773 2820 return ''
2774 2821 elif isinstance(r, tuple):
2775 2822 return r[0] % r[1:]
2776 2823 elif isinstance(r, str):
2777 2824 return r
2778 2825 elif callable(r):
2779 2826 return r()
2780 2827 else:
2781 2828 return repr(r)
2782 2829
2783 2830 class abstractsmartset(object):
2784 2831
2785 2832 def __nonzero__(self):
2786 2833 """True if the smartset is not empty"""
2787 2834 raise NotImplementedError()
2788 2835
2789 2836 def __contains__(self, rev):
2790 2837 """provide fast membership testing"""
2791 2838 raise NotImplementedError()
2792 2839
2793 2840 def __iter__(self):
2794 2841 """iterate the set in the order it is supposed to be iterated"""
2795 2842 raise NotImplementedError()
2796 2843
2797 2844 # Attributes containing a function to perform a fast iteration in a given
2798 2845 # direction. A smartset can have none, one, or both defined.
2799 2846 #
2800 2847 # Default value is None instead of a function returning None to avoid
2801 2848 # initializing an iterator just for testing if a fast method exists.
2802 2849 fastasc = None
2803 2850 fastdesc = None
2804 2851
2805 2852 def isascending(self):
2806 2853 """True if the set will iterate in ascending order"""
2807 2854 raise NotImplementedError()
2808 2855
2809 2856 def isdescending(self):
2810 2857 """True if the set will iterate in descending order"""
2811 2858 raise NotImplementedError()
2812 2859
2813 2860 def istopo(self):
2814 2861 """True if the set will iterate in topographical order"""
2815 2862 raise NotImplementedError()
2816 2863
2817 2864 @util.cachefunc
2818 2865 def min(self):
2819 2866 """return the minimum element in the set"""
2820 2867 if self.fastasc is not None:
2821 2868 for r in self.fastasc():
2822 2869 return r
2823 2870 raise ValueError('arg is an empty sequence')
2824 2871 return min(self)
2825 2872
2826 2873 @util.cachefunc
2827 2874 def max(self):
2828 2875 """return the maximum element in the set"""
2829 2876 if self.fastdesc is not None:
2830 2877 for r in self.fastdesc():
2831 2878 return r
2832 2879 raise ValueError('arg is an empty sequence')
2833 2880 return max(self)
2834 2881
2835 2882 def first(self):
2836 2883 """return the first element in the set (user iteration perspective)
2837 2884
2838 2885 Return None if the set is empty"""
2839 2886 raise NotImplementedError()
2840 2887
2841 2888 def last(self):
2842 2889 """return the last element in the set (user iteration perspective)
2843 2890
2844 2891 Return None if the set is empty"""
2845 2892 raise NotImplementedError()
2846 2893
2847 2894 def __len__(self):
2848 2895 """return the length of the smartsets
2849 2896
2850 2897 This can be expensive on smartset that could be lazy otherwise."""
2851 2898 raise NotImplementedError()
2852 2899
2853 2900 def reverse(self):
2854 2901 """reverse the expected iteration order"""
2855 2902 raise NotImplementedError()
2856 2903
2857 2904 def sort(self, reverse=True):
2858 2905 """get the set to iterate in an ascending or descending order"""
2859 2906 raise NotImplementedError()
2860 2907
2861 2908 def __and__(self, other):
2862 2909 """Returns a new object with the intersection of the two collections.
2863 2910
2864 2911 This is part of the mandatory API for smartset."""
2865 2912 if isinstance(other, fullreposet):
2866 2913 return self
2867 2914 return self.filter(other.__contains__, condrepr=other, cache=False)
2868 2915
2869 2916 def __add__(self, other):
2870 2917 """Returns a new object with the union of the two collections.
2871 2918
2872 2919 This is part of the mandatory API for smartset."""
2873 2920 return addset(self, other)
2874 2921
2875 2922 def __sub__(self, other):
2876 2923 """Returns a new object with the substraction of the two collections.
2877 2924
2878 2925 This is part of the mandatory API for smartset."""
2879 2926 c = other.__contains__
2880 2927 return self.filter(lambda r: not c(r), condrepr=('<not %r>', other),
2881 2928 cache=False)
2882 2929
2883 2930 def filter(self, condition, condrepr=None, cache=True):
2884 2931 """Returns this smartset filtered by condition as a new smartset.
2885 2932
2886 2933 `condition` is a callable which takes a revision number and returns a
2887 2934 boolean. Optional `condrepr` provides a printable representation of
2888 2935 the given `condition`.
2889 2936
2890 2937 This is part of the mandatory API for smartset."""
2891 2938 # builtin cannot be cached. but do not needs to
2892 2939 if cache and util.safehasattr(condition, 'func_code'):
2893 2940 condition = util.cachefunc(condition)
2894 2941 return filteredset(self, condition, condrepr)
2895 2942
2896 2943 class baseset(abstractsmartset):
2897 2944 """Basic data structure that represents a revset and contains the basic
2898 2945 operation that it should be able to perform.
2899 2946
2900 2947 Every method in this class should be implemented by any smartset class.
2901 2948 """
2902 2949 def __init__(self, data=(), datarepr=None, istopo=False):
2903 2950 """
2904 2951 datarepr: a tuple of (format, obj, ...), a function or an object that
2905 2952 provides a printable representation of the given data.
2906 2953 """
2907 2954 self._ascending = None
2908 2955 self._istopo = istopo
2909 2956 if not isinstance(data, list):
2910 2957 if isinstance(data, set):
2911 2958 self._set = data
2912 2959 # set has no order we pick one for stability purpose
2913 2960 self._ascending = True
2914 2961 data = list(data)
2915 2962 self._list = data
2916 2963 self._datarepr = datarepr
2917 2964
2918 2965 @util.propertycache
2919 2966 def _set(self):
2920 2967 return set(self._list)
2921 2968
2922 2969 @util.propertycache
2923 2970 def _asclist(self):
2924 2971 asclist = self._list[:]
2925 2972 asclist.sort()
2926 2973 return asclist
2927 2974
2928 2975 def __iter__(self):
2929 2976 if self._ascending is None:
2930 2977 return iter(self._list)
2931 2978 elif self._ascending:
2932 2979 return iter(self._asclist)
2933 2980 else:
2934 2981 return reversed(self._asclist)
2935 2982
2936 2983 def fastasc(self):
2937 2984 return iter(self._asclist)
2938 2985
2939 2986 def fastdesc(self):
2940 2987 return reversed(self._asclist)
2941 2988
2942 2989 @util.propertycache
2943 2990 def __contains__(self):
2944 2991 return self._set.__contains__
2945 2992
2946 2993 def __nonzero__(self):
2947 2994 return bool(self._list)
2948 2995
2949 2996 def sort(self, reverse=False):
2950 2997 self._ascending = not bool(reverse)
2951 2998 self._istopo = False
2952 2999
2953 3000 def reverse(self):
2954 3001 if self._ascending is None:
2955 3002 self._list.reverse()
2956 3003 else:
2957 3004 self._ascending = not self._ascending
2958 3005 self._istopo = False
2959 3006
2960 3007 def __len__(self):
2961 3008 return len(self._list)
2962 3009
2963 3010 def isascending(self):
2964 3011 """Returns True if the collection is ascending order, False if not.
2965 3012
2966 3013 This is part of the mandatory API for smartset."""
2967 3014 if len(self) <= 1:
2968 3015 return True
2969 3016 return self._ascending is not None and self._ascending
2970 3017
2971 3018 def isdescending(self):
2972 3019 """Returns True if the collection is descending order, False if not.
2973 3020
2974 3021 This is part of the mandatory API for smartset."""
2975 3022 if len(self) <= 1:
2976 3023 return True
2977 3024 return self._ascending is not None and not self._ascending
2978 3025
2979 3026 def istopo(self):
2980 3027 """Is the collection is in topographical order or not.
2981 3028
2982 3029 This is part of the mandatory API for smartset."""
2983 3030 if len(self) <= 1:
2984 3031 return True
2985 3032 return self._istopo
2986 3033
2987 3034 def first(self):
2988 3035 if self:
2989 3036 if self._ascending is None:
2990 3037 return self._list[0]
2991 3038 elif self._ascending:
2992 3039 return self._asclist[0]
2993 3040 else:
2994 3041 return self._asclist[-1]
2995 3042 return None
2996 3043
2997 3044 def last(self):
2998 3045 if self:
2999 3046 if self._ascending is None:
3000 3047 return self._list[-1]
3001 3048 elif self._ascending:
3002 3049 return self._asclist[-1]
3003 3050 else:
3004 3051 return self._asclist[0]
3005 3052 return None
3006 3053
3007 3054 def __repr__(self):
3008 3055 d = {None: '', False: '-', True: '+'}[self._ascending]
3009 3056 s = _formatsetrepr(self._datarepr)
3010 3057 if not s:
3011 3058 l = self._list
3012 3059 # if _list has been built from a set, it might have a different
3013 3060 # order from one python implementation to another.
3014 3061 # We fallback to the sorted version for a stable output.
3015 3062 if self._ascending is not None:
3016 3063 l = self._asclist
3017 3064 s = repr(l)
3018 3065 return '<%s%s %s>' % (type(self).__name__, d, s)
3019 3066
3020 3067 class filteredset(abstractsmartset):
3021 3068 """Duck type for baseset class which iterates lazily over the revisions in
3022 3069 the subset and contains a function which tests for membership in the
3023 3070 revset
3024 3071 """
3025 3072 def __init__(self, subset, condition=lambda x: True, condrepr=None):
3026 3073 """
3027 3074 condition: a function that decide whether a revision in the subset
3028 3075 belongs to the revset or not.
3029 3076 condrepr: a tuple of (format, obj, ...), a function or an object that
3030 3077 provides a printable representation of the given condition.
3031 3078 """
3032 3079 self._subset = subset
3033 3080 self._condition = condition
3034 3081 self._condrepr = condrepr
3035 3082
3036 3083 def __contains__(self, x):
3037 3084 return x in self._subset and self._condition(x)
3038 3085
3039 3086 def __iter__(self):
3040 3087 return self._iterfilter(self._subset)
3041 3088
3042 3089 def _iterfilter(self, it):
3043 3090 cond = self._condition
3044 3091 for x in it:
3045 3092 if cond(x):
3046 3093 yield x
3047 3094
3048 3095 @property
3049 3096 def fastasc(self):
3050 3097 it = self._subset.fastasc
3051 3098 if it is None:
3052 3099 return None
3053 3100 return lambda: self._iterfilter(it())
3054 3101
3055 3102 @property
3056 3103 def fastdesc(self):
3057 3104 it = self._subset.fastdesc
3058 3105 if it is None:
3059 3106 return None
3060 3107 return lambda: self._iterfilter(it())
3061 3108
3062 3109 def __nonzero__(self):
3063 3110 fast = None
3064 3111 candidates = [self.fastasc if self.isascending() else None,
3065 3112 self.fastdesc if self.isdescending() else None,
3066 3113 self.fastasc,
3067 3114 self.fastdesc]
3068 3115 for candidate in candidates:
3069 3116 if candidate is not None:
3070 3117 fast = candidate
3071 3118 break
3072 3119
3073 3120 if fast is not None:
3074 3121 it = fast()
3075 3122 else:
3076 3123 it = self
3077 3124
3078 3125 for r in it:
3079 3126 return True
3080 3127 return False
3081 3128
3082 3129 def __len__(self):
3083 3130 # Basic implementation to be changed in future patches.
3084 3131 # until this gets improved, we use generator expression
3085 3132 # here, since list compr is free to call __len__ again
3086 3133 # causing infinite recursion
3087 3134 l = baseset(r for r in self)
3088 3135 return len(l)
3089 3136
3090 3137 def sort(self, reverse=False):
3091 3138 self._subset.sort(reverse=reverse)
3092 3139
3093 3140 def reverse(self):
3094 3141 self._subset.reverse()
3095 3142
3096 3143 def isascending(self):
3097 3144 return self._subset.isascending()
3098 3145
3099 3146 def isdescending(self):
3100 3147 return self._subset.isdescending()
3101 3148
3102 3149 def istopo(self):
3103 3150 return self._subset.istopo()
3104 3151
3105 3152 def first(self):
3106 3153 for x in self:
3107 3154 return x
3108 3155 return None
3109 3156
3110 3157 def last(self):
3111 3158 it = None
3112 3159 if self.isascending():
3113 3160 it = self.fastdesc
3114 3161 elif self.isdescending():
3115 3162 it = self.fastasc
3116 3163 if it is not None:
3117 3164 for x in it():
3118 3165 return x
3119 3166 return None #empty case
3120 3167 else:
3121 3168 x = None
3122 3169 for x in self:
3123 3170 pass
3124 3171 return x
3125 3172
3126 3173 def __repr__(self):
3127 3174 xs = [repr(self._subset)]
3128 3175 s = _formatsetrepr(self._condrepr)
3129 3176 if s:
3130 3177 xs.append(s)
3131 3178 return '<%s %s>' % (type(self).__name__, ', '.join(xs))
3132 3179
3133 3180 def _iterordered(ascending, iter1, iter2):
3134 3181 """produce an ordered iteration from two iterators with the same order
3135 3182
3136 3183 The ascending is used to indicated the iteration direction.
3137 3184 """
3138 3185 choice = max
3139 3186 if ascending:
3140 3187 choice = min
3141 3188
3142 3189 val1 = None
3143 3190 val2 = None
3144 3191 try:
3145 3192 # Consume both iterators in an ordered way until one is empty
3146 3193 while True:
3147 3194 if val1 is None:
3148 3195 val1 = next(iter1)
3149 3196 if val2 is None:
3150 3197 val2 = next(iter2)
3151 3198 n = choice(val1, val2)
3152 3199 yield n
3153 3200 if val1 == n:
3154 3201 val1 = None
3155 3202 if val2 == n:
3156 3203 val2 = None
3157 3204 except StopIteration:
3158 3205 # Flush any remaining values and consume the other one
3159 3206 it = iter2
3160 3207 if val1 is not None:
3161 3208 yield val1
3162 3209 it = iter1
3163 3210 elif val2 is not None:
3164 3211 # might have been equality and both are empty
3165 3212 yield val2
3166 3213 for val in it:
3167 3214 yield val
3168 3215
3169 3216 class addset(abstractsmartset):
3170 3217 """Represent the addition of two sets
3171 3218
3172 3219 Wrapper structure for lazily adding two structures without losing much
3173 3220 performance on the __contains__ method
3174 3221
3175 3222 If the ascending attribute is set, that means the two structures are
3176 3223 ordered in either an ascending or descending way. Therefore, we can add
3177 3224 them maintaining the order by iterating over both at the same time
3178 3225
3179 3226 >>> xs = baseset([0, 3, 2])
3180 3227 >>> ys = baseset([5, 2, 4])
3181 3228
3182 3229 >>> rs = addset(xs, ys)
3183 3230 >>> bool(rs), 0 in rs, 1 in rs, 5 in rs, rs.first(), rs.last()
3184 3231 (True, True, False, True, 0, 4)
3185 3232 >>> rs = addset(xs, baseset([]))
3186 3233 >>> bool(rs), 0 in rs, 1 in rs, rs.first(), rs.last()
3187 3234 (True, True, False, 0, 2)
3188 3235 >>> rs = addset(baseset([]), baseset([]))
3189 3236 >>> bool(rs), 0 in rs, rs.first(), rs.last()
3190 3237 (False, False, None, None)
3191 3238
3192 3239 iterate unsorted:
3193 3240 >>> rs = addset(xs, ys)
3194 3241 >>> # (use generator because pypy could call len())
3195 3242 >>> list(x for x in rs) # without _genlist
3196 3243 [0, 3, 2, 5, 4]
3197 3244 >>> assert not rs._genlist
3198 3245 >>> len(rs)
3199 3246 5
3200 3247 >>> [x for x in rs] # with _genlist
3201 3248 [0, 3, 2, 5, 4]
3202 3249 >>> assert rs._genlist
3203 3250
3204 3251 iterate ascending:
3205 3252 >>> rs = addset(xs, ys, ascending=True)
3206 3253 >>> # (use generator because pypy could call len())
3207 3254 >>> list(x for x in rs), list(x for x in rs.fastasc()) # without _asclist
3208 3255 ([0, 2, 3, 4, 5], [0, 2, 3, 4, 5])
3209 3256 >>> assert not rs._asclist
3210 3257 >>> len(rs)
3211 3258 5
3212 3259 >>> [x for x in rs], [x for x in rs.fastasc()]
3213 3260 ([0, 2, 3, 4, 5], [0, 2, 3, 4, 5])
3214 3261 >>> assert rs._asclist
3215 3262
3216 3263 iterate descending:
3217 3264 >>> rs = addset(xs, ys, ascending=False)
3218 3265 >>> # (use generator because pypy could call len())
3219 3266 >>> list(x for x in rs), list(x for x in rs.fastdesc()) # without _asclist
3220 3267 ([5, 4, 3, 2, 0], [5, 4, 3, 2, 0])
3221 3268 >>> assert not rs._asclist
3222 3269 >>> len(rs)
3223 3270 5
3224 3271 >>> [x for x in rs], [x for x in rs.fastdesc()]
3225 3272 ([5, 4, 3, 2, 0], [5, 4, 3, 2, 0])
3226 3273 >>> assert rs._asclist
3227 3274
3228 3275 iterate ascending without fastasc:
3229 3276 >>> rs = addset(xs, generatorset(ys), ascending=True)
3230 3277 >>> assert rs.fastasc is None
3231 3278 >>> [x for x in rs]
3232 3279 [0, 2, 3, 4, 5]
3233 3280
3234 3281 iterate descending without fastdesc:
3235 3282 >>> rs = addset(generatorset(xs), ys, ascending=False)
3236 3283 >>> assert rs.fastdesc is None
3237 3284 >>> [x for x in rs]
3238 3285 [5, 4, 3, 2, 0]
3239 3286 """
3240 3287 def __init__(self, revs1, revs2, ascending=None):
3241 3288 self._r1 = revs1
3242 3289 self._r2 = revs2
3243 3290 self._iter = None
3244 3291 self._ascending = ascending
3245 3292 self._genlist = None
3246 3293 self._asclist = None
3247 3294
3248 3295 def __len__(self):
3249 3296 return len(self._list)
3250 3297
3251 3298 def __nonzero__(self):
3252 3299 return bool(self._r1) or bool(self._r2)
3253 3300
3254 3301 @util.propertycache
3255 3302 def _list(self):
3256 3303 if not self._genlist:
3257 3304 self._genlist = baseset(iter(self))
3258 3305 return self._genlist
3259 3306
3260 3307 def __iter__(self):
3261 3308 """Iterate over both collections without repeating elements
3262 3309
3263 3310 If the ascending attribute is not set, iterate over the first one and
3264 3311 then over the second one checking for membership on the first one so we
3265 3312 dont yield any duplicates.
3266 3313
3267 3314 If the ascending attribute is set, iterate over both collections at the
3268 3315 same time, yielding only one value at a time in the given order.
3269 3316 """
3270 3317 if self._ascending is None:
3271 3318 if self._genlist:
3272 3319 return iter(self._genlist)
3273 3320 def arbitraryordergen():
3274 3321 for r in self._r1:
3275 3322 yield r
3276 3323 inr1 = self._r1.__contains__
3277 3324 for r in self._r2:
3278 3325 if not inr1(r):
3279 3326 yield r
3280 3327 return arbitraryordergen()
3281 3328 # try to use our own fast iterator if it exists
3282 3329 self._trysetasclist()
3283 3330 if self._ascending:
3284 3331 attr = 'fastasc'
3285 3332 else:
3286 3333 attr = 'fastdesc'
3287 3334 it = getattr(self, attr)
3288 3335 if it is not None:
3289 3336 return it()
3290 3337 # maybe half of the component supports fast
3291 3338 # get iterator for _r1
3292 3339 iter1 = getattr(self._r1, attr)
3293 3340 if iter1 is None:
3294 3341 # let's avoid side effect (not sure it matters)
3295 3342 iter1 = iter(sorted(self._r1, reverse=not self._ascending))
3296 3343 else:
3297 3344 iter1 = iter1()
3298 3345 # get iterator for _r2
3299 3346 iter2 = getattr(self._r2, attr)
3300 3347 if iter2 is None:
3301 3348 # let's avoid side effect (not sure it matters)
3302 3349 iter2 = iter(sorted(self._r2, reverse=not self._ascending))
3303 3350 else:
3304 3351 iter2 = iter2()
3305 3352 return _iterordered(self._ascending, iter1, iter2)
3306 3353
3307 3354 def _trysetasclist(self):
3308 3355 """populate the _asclist attribute if possible and necessary"""
3309 3356 if self._genlist is not None and self._asclist is None:
3310 3357 self._asclist = sorted(self._genlist)
3311 3358
3312 3359 @property
3313 3360 def fastasc(self):
3314 3361 self._trysetasclist()
3315 3362 if self._asclist is not None:
3316 3363 return self._asclist.__iter__
3317 3364 iter1 = self._r1.fastasc
3318 3365 iter2 = self._r2.fastasc
3319 3366 if None in (iter1, iter2):
3320 3367 return None
3321 3368 return lambda: _iterordered(True, iter1(), iter2())
3322 3369
3323 3370 @property
3324 3371 def fastdesc(self):
3325 3372 self._trysetasclist()
3326 3373 if self._asclist is not None:
3327 3374 return self._asclist.__reversed__
3328 3375 iter1 = self._r1.fastdesc
3329 3376 iter2 = self._r2.fastdesc
3330 3377 if None in (iter1, iter2):
3331 3378 return None
3332 3379 return lambda: _iterordered(False, iter1(), iter2())
3333 3380
3334 3381 def __contains__(self, x):
3335 3382 return x in self._r1 or x in self._r2
3336 3383
3337 3384 def sort(self, reverse=False):
3338 3385 """Sort the added set
3339 3386
3340 3387 For this we use the cached list with all the generated values and if we
3341 3388 know they are ascending or descending we can sort them in a smart way.
3342 3389 """
3343 3390 self._ascending = not reverse
3344 3391
3345 3392 def isascending(self):
3346 3393 return self._ascending is not None and self._ascending
3347 3394
3348 3395 def isdescending(self):
3349 3396 return self._ascending is not None and not self._ascending
3350 3397
3351 3398 def istopo(self):
3352 3399 # not worth the trouble asserting if the two sets combined are still
3353 3400 # in topographical order. Use the sort() predicate to explicitly sort
3354 3401 # again instead.
3355 3402 return False
3356 3403
3357 3404 def reverse(self):
3358 3405 if self._ascending is None:
3359 3406 self._list.reverse()
3360 3407 else:
3361 3408 self._ascending = not self._ascending
3362 3409
3363 3410 def first(self):
3364 3411 for x in self:
3365 3412 return x
3366 3413 return None
3367 3414
3368 3415 def last(self):
3369 3416 self.reverse()
3370 3417 val = self.first()
3371 3418 self.reverse()
3372 3419 return val
3373 3420
3374 3421 def __repr__(self):
3375 3422 d = {None: '', False: '-', True: '+'}[self._ascending]
3376 3423 return '<%s%s %r, %r>' % (type(self).__name__, d, self._r1, self._r2)
3377 3424
3378 3425 class generatorset(abstractsmartset):
3379 3426 """Wrap a generator for lazy iteration
3380 3427
3381 3428 Wrapper structure for generators that provides lazy membership and can
3382 3429 be iterated more than once.
3383 3430 When asked for membership it generates values until either it finds the
3384 3431 requested one or has gone through all the elements in the generator
3385 3432 """
3386 3433 def __init__(self, gen, iterasc=None):
3387 3434 """
3388 3435 gen: a generator producing the values for the generatorset.
3389 3436 """
3390 3437 self._gen = gen
3391 3438 self._asclist = None
3392 3439 self._cache = {}
3393 3440 self._genlist = []
3394 3441 self._finished = False
3395 3442 self._ascending = True
3396 3443 if iterasc is not None:
3397 3444 if iterasc:
3398 3445 self.fastasc = self._iterator
3399 3446 self.__contains__ = self._asccontains
3400 3447 else:
3401 3448 self.fastdesc = self._iterator
3402 3449 self.__contains__ = self._desccontains
3403 3450
3404 3451 def __nonzero__(self):
3405 3452 # Do not use 'for r in self' because it will enforce the iteration
3406 3453 # order (default ascending), possibly unrolling a whole descending
3407 3454 # iterator.
3408 3455 if self._genlist:
3409 3456 return True
3410 3457 for r in self._consumegen():
3411 3458 return True
3412 3459 return False
3413 3460
3414 3461 def __contains__(self, x):
3415 3462 if x in self._cache:
3416 3463 return self._cache[x]
3417 3464
3418 3465 # Use new values only, as existing values would be cached.
3419 3466 for l in self._consumegen():
3420 3467 if l == x:
3421 3468 return True
3422 3469
3423 3470 self._cache[x] = False
3424 3471 return False
3425 3472
3426 3473 def _asccontains(self, x):
3427 3474 """version of contains optimised for ascending generator"""
3428 3475 if x in self._cache:
3429 3476 return self._cache[x]
3430 3477
3431 3478 # Use new values only, as existing values would be cached.
3432 3479 for l in self._consumegen():
3433 3480 if l == x:
3434 3481 return True
3435 3482 if l > x:
3436 3483 break
3437 3484
3438 3485 self._cache[x] = False
3439 3486 return False
3440 3487
3441 3488 def _desccontains(self, x):
3442 3489 """version of contains optimised for descending generator"""
3443 3490 if x in self._cache:
3444 3491 return self._cache[x]
3445 3492
3446 3493 # Use new values only, as existing values would be cached.
3447 3494 for l in self._consumegen():
3448 3495 if l == x:
3449 3496 return True
3450 3497 if l < x:
3451 3498 break
3452 3499
3453 3500 self._cache[x] = False
3454 3501 return False
3455 3502
3456 3503 def __iter__(self):
3457 3504 if self._ascending:
3458 3505 it = self.fastasc
3459 3506 else:
3460 3507 it = self.fastdesc
3461 3508 if it is not None:
3462 3509 return it()
3463 3510 # we need to consume the iterator
3464 3511 for x in self._consumegen():
3465 3512 pass
3466 3513 # recall the same code
3467 3514 return iter(self)
3468 3515
3469 3516 def _iterator(self):
3470 3517 if self._finished:
3471 3518 return iter(self._genlist)
3472 3519
3473 3520 # We have to use this complex iteration strategy to allow multiple
3474 3521 # iterations at the same time. We need to be able to catch revision
3475 3522 # removed from _consumegen and added to genlist in another instance.
3476 3523 #
3477 3524 # Getting rid of it would provide an about 15% speed up on this
3478 3525 # iteration.
3479 3526 genlist = self._genlist
3480 3527 nextrev = self._consumegen().next
3481 3528 _len = len # cache global lookup
3482 3529 def gen():
3483 3530 i = 0
3484 3531 while True:
3485 3532 if i < _len(genlist):
3486 3533 yield genlist[i]
3487 3534 else:
3488 3535 yield nextrev()
3489 3536 i += 1
3490 3537 return gen()
3491 3538
3492 3539 def _consumegen(self):
3493 3540 cache = self._cache
3494 3541 genlist = self._genlist.append
3495 3542 for item in self._gen:
3496 3543 cache[item] = True
3497 3544 genlist(item)
3498 3545 yield item
3499 3546 if not self._finished:
3500 3547 self._finished = True
3501 3548 asc = self._genlist[:]
3502 3549 asc.sort()
3503 3550 self._asclist = asc
3504 3551 self.fastasc = asc.__iter__
3505 3552 self.fastdesc = asc.__reversed__
3506 3553
3507 3554 def __len__(self):
3508 3555 for x in self._consumegen():
3509 3556 pass
3510 3557 return len(self._genlist)
3511 3558
3512 3559 def sort(self, reverse=False):
3513 3560 self._ascending = not reverse
3514 3561
3515 3562 def reverse(self):
3516 3563 self._ascending = not self._ascending
3517 3564
3518 3565 def isascending(self):
3519 3566 return self._ascending
3520 3567
3521 3568 def isdescending(self):
3522 3569 return not self._ascending
3523 3570
3524 3571 def istopo(self):
3525 3572 # not worth the trouble asserting if the two sets combined are still
3526 3573 # in topographical order. Use the sort() predicate to explicitly sort
3527 3574 # again instead.
3528 3575 return False
3529 3576
3530 3577 def first(self):
3531 3578 if self._ascending:
3532 3579 it = self.fastasc
3533 3580 else:
3534 3581 it = self.fastdesc
3535 3582 if it is None:
3536 3583 # we need to consume all and try again
3537 3584 for x in self._consumegen():
3538 3585 pass
3539 3586 return self.first()
3540 3587 return next(it(), None)
3541 3588
3542 3589 def last(self):
3543 3590 if self._ascending:
3544 3591 it = self.fastdesc
3545 3592 else:
3546 3593 it = self.fastasc
3547 3594 if it is None:
3548 3595 # we need to consume all and try again
3549 3596 for x in self._consumegen():
3550 3597 pass
3551 3598 return self.first()
3552 3599 return next(it(), None)
3553 3600
3554 3601 def __repr__(self):
3555 3602 d = {False: '-', True: '+'}[self._ascending]
3556 3603 return '<%s%s>' % (type(self).__name__, d)
3557 3604
3558 3605 class spanset(abstractsmartset):
3559 3606 """Duck type for baseset class which represents a range of revisions and
3560 3607 can work lazily and without having all the range in memory
3561 3608
3562 3609 Note that spanset(x, y) behave almost like xrange(x, y) except for two
3563 3610 notable points:
3564 3611 - when x < y it will be automatically descending,
3565 3612 - revision filtered with this repoview will be skipped.
3566 3613
3567 3614 """
3568 3615 def __init__(self, repo, start=0, end=None):
3569 3616 """
3570 3617 start: first revision included the set
3571 3618 (default to 0)
3572 3619 end: first revision excluded (last+1)
3573 3620 (default to len(repo)
3574 3621
3575 3622 Spanset will be descending if `end` < `start`.
3576 3623 """
3577 3624 if end is None:
3578 3625 end = len(repo)
3579 3626 self._ascending = start <= end
3580 3627 if not self._ascending:
3581 3628 start, end = end + 1, start +1
3582 3629 self._start = start
3583 3630 self._end = end
3584 3631 self._hiddenrevs = repo.changelog.filteredrevs
3585 3632
3586 3633 def sort(self, reverse=False):
3587 3634 self._ascending = not reverse
3588 3635
3589 3636 def reverse(self):
3590 3637 self._ascending = not self._ascending
3591 3638
3592 3639 def istopo(self):
3593 3640 # not worth the trouble asserting if the two sets combined are still
3594 3641 # in topographical order. Use the sort() predicate to explicitly sort
3595 3642 # again instead.
3596 3643 return False
3597 3644
3598 3645 def _iterfilter(self, iterrange):
3599 3646 s = self._hiddenrevs
3600 3647 for r in iterrange:
3601 3648 if r not in s:
3602 3649 yield r
3603 3650
3604 3651 def __iter__(self):
3605 3652 if self._ascending:
3606 3653 return self.fastasc()
3607 3654 else:
3608 3655 return self.fastdesc()
3609 3656
3610 3657 def fastasc(self):
3611 3658 iterrange = xrange(self._start, self._end)
3612 3659 if self._hiddenrevs:
3613 3660 return self._iterfilter(iterrange)
3614 3661 return iter(iterrange)
3615 3662
3616 3663 def fastdesc(self):
3617 3664 iterrange = xrange(self._end - 1, self._start - 1, -1)
3618 3665 if self._hiddenrevs:
3619 3666 return self._iterfilter(iterrange)
3620 3667 return iter(iterrange)
3621 3668
3622 3669 def __contains__(self, rev):
3623 3670 hidden = self._hiddenrevs
3624 3671 return ((self._start <= rev < self._end)
3625 3672 and not (hidden and rev in hidden))
3626 3673
3627 3674 def __nonzero__(self):
3628 3675 for r in self:
3629 3676 return True
3630 3677 return False
3631 3678
3632 3679 def __len__(self):
3633 3680 if not self._hiddenrevs:
3634 3681 return abs(self._end - self._start)
3635 3682 else:
3636 3683 count = 0
3637 3684 start = self._start
3638 3685 end = self._end
3639 3686 for rev in self._hiddenrevs:
3640 3687 if (end < rev <= start) or (start <= rev < end):
3641 3688 count += 1
3642 3689 return abs(self._end - self._start) - count
3643 3690
3644 3691 def isascending(self):
3645 3692 return self._ascending
3646 3693
3647 3694 def isdescending(self):
3648 3695 return not self._ascending
3649 3696
3650 3697 def first(self):
3651 3698 if self._ascending:
3652 3699 it = self.fastasc
3653 3700 else:
3654 3701 it = self.fastdesc
3655 3702 for x in it():
3656 3703 return x
3657 3704 return None
3658 3705
3659 3706 def last(self):
3660 3707 if self._ascending:
3661 3708 it = self.fastdesc
3662 3709 else:
3663 3710 it = self.fastasc
3664 3711 for x in it():
3665 3712 return x
3666 3713 return None
3667 3714
3668 3715 def __repr__(self):
3669 3716 d = {False: '-', True: '+'}[self._ascending]
3670 3717 return '<%s%s %d:%d>' % (type(self).__name__, d,
3671 3718 self._start, self._end - 1)
3672 3719
3673 3720 class fullreposet(spanset):
3674 3721 """a set containing all revisions in the repo
3675 3722
3676 3723 This class exists to host special optimization and magic to handle virtual
3677 3724 revisions such as "null".
3678 3725 """
3679 3726
3680 3727 def __init__(self, repo):
3681 3728 super(fullreposet, self).__init__(repo)
3682 3729
3683 3730 def __and__(self, other):
3684 3731 """As self contains the whole repo, all of the other set should also be
3685 3732 in self. Therefore `self & other = other`.
3686 3733
3687 3734 This boldly assumes the other contains valid revs only.
3688 3735 """
3689 3736 # other not a smartset, make is so
3690 3737 if not util.safehasattr(other, 'isascending'):
3691 3738 # filter out hidden revision
3692 3739 # (this boldly assumes all smartset are pure)
3693 3740 #
3694 3741 # `other` was used with "&", let's assume this is a set like
3695 3742 # object.
3696 3743 other = baseset(other - self._hiddenrevs)
3697 3744
3698 3745 # XXX As fullreposet is also used as bootstrap, this is wrong.
3699 3746 #
3700 3747 # With a giveme312() revset returning [3,1,2], this makes
3701 3748 # 'hg log -r "giveme312()"' -> 1, 2, 3 (wrong)
3702 3749 # We cannot just drop it because other usage still need to sort it:
3703 3750 # 'hg log -r "all() and giveme312()"' -> 1, 2, 3 (right)
3704 3751 #
3705 3752 # There is also some faulty revset implementations that rely on it
3706 3753 # (eg: children as of its state in e8075329c5fb)
3707 3754 #
3708 3755 # When we fix the two points above we can move this into the if clause
3709 3756 other.sort(reverse=self.isdescending())
3710 3757 return other
3711 3758
3712 3759 def prettyformatset(revs):
3713 3760 lines = []
3714 3761 rs = repr(revs)
3715 3762 p = 0
3716 3763 while p < len(rs):
3717 3764 q = rs.find('<', p + 1)
3718 3765 if q < 0:
3719 3766 q = len(rs)
3720 3767 l = rs.count('<', 0, p) - rs.count('>', 0, p)
3721 3768 assert l >= 0
3722 3769 lines.append((l, rs[p:q].rstrip()))
3723 3770 p = q
3724 3771 return '\n'.join(' ' * l + s for l, s in lines)
3725 3772
3726 3773 def loadpredicate(ui, extname, registrarobj):
3727 3774 """Load revset predicates from specified registrarobj
3728 3775 """
3729 3776 for name, func in registrarobj._table.iteritems():
3730 3777 symbols[name] = func
3731 3778 if func._safe:
3732 3779 safesymbols.add(name)
3733 3780
3734 3781 # load built-in predicates explicitly to setup safesymbols
3735 3782 loadpredicate(None, None, predicate)
3736 3783
3737 3784 # tell hggettext to extract docstrings from these functions:
3738 3785 i18nfunctions = symbols.values()
General Comments 0
You need to be logged in to leave comments. Login now