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