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