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