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