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