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