##// END OF EJS Templates
fileset, revset: do not use global parser object for thread safety...
Yuya Nishihara -
r20208:61a47fd6 stable
parent child Browse files
Show More
@@ -1,504 +1,506 b''
1 1 # fileset.py - file 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 parser, error, util, merge, re
9 9 from i18n import _
10 10
11 11 elements = {
12 12 "(": (20, ("group", 1, ")"), ("func", 1, ")")),
13 13 "-": (5, ("negate", 19), ("minus", 5)),
14 14 "not": (10, ("not", 10)),
15 15 "!": (10, ("not", 10)),
16 16 "and": (5, None, ("and", 5)),
17 17 "&": (5, None, ("and", 5)),
18 18 "or": (4, None, ("or", 4)),
19 19 "|": (4, None, ("or", 4)),
20 20 "+": (4, None, ("or", 4)),
21 21 ",": (2, None, ("list", 2)),
22 22 ")": (0, None, None),
23 23 "symbol": (0, ("symbol",), None),
24 24 "string": (0, ("string",), None),
25 25 "end": (0, None, None),
26 26 }
27 27
28 28 keywords = set(['and', 'or', 'not'])
29 29
30 30 globchars = ".*{}[]?/\\_"
31 31
32 32 def tokenize(program):
33 33 pos, l = 0, len(program)
34 34 while pos < l:
35 35 c = program[pos]
36 36 if c.isspace(): # skip inter-token whitespace
37 37 pass
38 38 elif c in "(),-|&+!": # handle simple operators
39 39 yield (c, None, pos)
40 40 elif (c in '"\'' or c == 'r' and
41 41 program[pos:pos + 2] in ("r'", 'r"')): # handle quoted strings
42 42 if c == 'r':
43 43 pos += 1
44 44 c = program[pos]
45 45 decode = lambda x: x
46 46 else:
47 47 decode = lambda x: x.decode('string-escape')
48 48 pos += 1
49 49 s = pos
50 50 while pos < l: # find closing quote
51 51 d = program[pos]
52 52 if d == '\\': # skip over escaped characters
53 53 pos += 2
54 54 continue
55 55 if d == c:
56 56 yield ('string', decode(program[s:pos]), s)
57 57 break
58 58 pos += 1
59 59 else:
60 60 raise error.ParseError(_("unterminated string"), s)
61 61 elif c.isalnum() or c in globchars or ord(c) > 127:
62 62 # gather up a symbol/keyword
63 63 s = pos
64 64 pos += 1
65 65 while pos < l: # find end of symbol
66 66 d = program[pos]
67 67 if not (d.isalnum() or d in globchars or ord(d) > 127):
68 68 break
69 69 pos += 1
70 70 sym = program[s:pos]
71 71 if sym in keywords: # operator keywords
72 72 yield (sym, None, s)
73 73 else:
74 74 yield ('symbol', sym, s)
75 75 pos -= 1
76 76 else:
77 77 raise error.ParseError(_("syntax error"), pos)
78 78 pos += 1
79 79 yield ('end', None, pos)
80 80
81 parse = parser.parser(tokenize, elements).parse
81 def parse(expr):
82 p = parser.parser(tokenize, elements)
83 return p.parse(expr)
82 84
83 85 def getstring(x, err):
84 86 if x and (x[0] == 'string' or x[0] == 'symbol'):
85 87 return x[1]
86 88 raise error.ParseError(err)
87 89
88 90 def getset(mctx, x):
89 91 if not x:
90 92 raise error.ParseError(_("missing argument"))
91 93 return methods[x[0]](mctx, *x[1:])
92 94
93 95 def stringset(mctx, x):
94 96 m = mctx.matcher([x])
95 97 return [f for f in mctx.subset if m(f)]
96 98
97 99 def andset(mctx, x, y):
98 100 return getset(mctx.narrow(getset(mctx, x)), y)
99 101
100 102 def orset(mctx, x, y):
101 103 # needs optimizing
102 104 xl = getset(mctx, x)
103 105 yl = getset(mctx, y)
104 106 return xl + [f for f in yl if f not in xl]
105 107
106 108 def notset(mctx, x):
107 109 s = set(getset(mctx, x))
108 110 return [r for r in mctx.subset if r not in s]
109 111
110 112 def minusset(mctx, x, y):
111 113 xl = getset(mctx, x)
112 114 yl = set(getset(mctx, y))
113 115 return [f for f in xl if f not in yl]
114 116
115 117 def listset(mctx, a, b):
116 118 raise error.ParseError(_("can't use a list in this context"))
117 119
118 120 def modified(mctx, x):
119 121 """``modified()``
120 122 File that is modified according to status.
121 123 """
122 124 # i18n: "modified" is a keyword
123 125 getargs(x, 0, 0, _("modified takes no arguments"))
124 126 s = mctx.status()[0]
125 127 return [f for f in mctx.subset if f in s]
126 128
127 129 def added(mctx, x):
128 130 """``added()``
129 131 File that is added according to status.
130 132 """
131 133 # i18n: "added" is a keyword
132 134 getargs(x, 0, 0, _("added takes no arguments"))
133 135 s = mctx.status()[1]
134 136 return [f for f in mctx.subset if f in s]
135 137
136 138 def removed(mctx, x):
137 139 """``removed()``
138 140 File that is removed according to status.
139 141 """
140 142 # i18n: "removed" is a keyword
141 143 getargs(x, 0, 0, _("removed takes no arguments"))
142 144 s = mctx.status()[2]
143 145 return [f for f in mctx.subset if f in s]
144 146
145 147 def deleted(mctx, x):
146 148 """``deleted()``
147 149 File that is deleted according to status.
148 150 """
149 151 # i18n: "deleted" is a keyword
150 152 getargs(x, 0, 0, _("deleted takes no arguments"))
151 153 s = mctx.status()[3]
152 154 return [f for f in mctx.subset if f in s]
153 155
154 156 def unknown(mctx, x):
155 157 """``unknown()``
156 158 File that is unknown according to status. These files will only be
157 159 considered if this predicate is used.
158 160 """
159 161 # i18n: "unknown" is a keyword
160 162 getargs(x, 0, 0, _("unknown takes no arguments"))
161 163 s = mctx.status()[4]
162 164 return [f for f in mctx.subset if f in s]
163 165
164 166 def ignored(mctx, x):
165 167 """``ignored()``
166 168 File that is ignored according to status. These files will only be
167 169 considered if this predicate is used.
168 170 """
169 171 # i18n: "ignored" is a keyword
170 172 getargs(x, 0, 0, _("ignored takes no arguments"))
171 173 s = mctx.status()[5]
172 174 return [f for f in mctx.subset if f in s]
173 175
174 176 def clean(mctx, x):
175 177 """``clean()``
176 178 File that is clean according to status.
177 179 """
178 180 # i18n: "clean" is a keyword
179 181 getargs(x, 0, 0, _("clean takes no arguments"))
180 182 s = mctx.status()[6]
181 183 return [f for f in mctx.subset if f in s]
182 184
183 185 def func(mctx, a, b):
184 186 if a[0] == 'symbol' and a[1] in symbols:
185 187 return symbols[a[1]](mctx, b)
186 188 raise error.ParseError(_("not a function: %s") % a[1])
187 189
188 190 def getlist(x):
189 191 if not x:
190 192 return []
191 193 if x[0] == 'list':
192 194 return getlist(x[1]) + [x[2]]
193 195 return [x]
194 196
195 197 def getargs(x, min, max, err):
196 198 l = getlist(x)
197 199 if len(l) < min or len(l) > max:
198 200 raise error.ParseError(err)
199 201 return l
200 202
201 203 def binary(mctx, x):
202 204 """``binary()``
203 205 File that appears to be binary (contains NUL bytes).
204 206 """
205 207 # i18n: "binary" is a keyword
206 208 getargs(x, 0, 0, _("binary takes no arguments"))
207 209 return [f for f in mctx.existing() if util.binary(mctx.ctx[f].data())]
208 210
209 211 def exec_(mctx, x):
210 212 """``exec()``
211 213 File that is marked as executable.
212 214 """
213 215 # i18n: "exec" is a keyword
214 216 getargs(x, 0, 0, _("exec takes no arguments"))
215 217 return [f for f in mctx.existing() if mctx.ctx.flags(f) == 'x']
216 218
217 219 def symlink(mctx, x):
218 220 """``symlink()``
219 221 File that is marked as a symlink.
220 222 """
221 223 # i18n: "symlink" is a keyword
222 224 getargs(x, 0, 0, _("symlink takes no arguments"))
223 225 return [f for f in mctx.existing() if mctx.ctx.flags(f) == 'l']
224 226
225 227 def resolved(mctx, x):
226 228 """``resolved()``
227 229 File that is marked resolved according to the resolve state.
228 230 """
229 231 # i18n: "resolved" is a keyword
230 232 getargs(x, 0, 0, _("resolved takes no arguments"))
231 233 if mctx.ctx.rev() is not None:
232 234 return []
233 235 ms = merge.mergestate(mctx.ctx._repo)
234 236 return [f for f in mctx.subset if f in ms and ms[f] == 'r']
235 237
236 238 def unresolved(mctx, x):
237 239 """``unresolved()``
238 240 File that is marked unresolved according to the resolve state.
239 241 """
240 242 # i18n: "unresolved" is a keyword
241 243 getargs(x, 0, 0, _("unresolved takes no arguments"))
242 244 if mctx.ctx.rev() is not None:
243 245 return []
244 246 ms = merge.mergestate(mctx.ctx._repo)
245 247 return [f for f in mctx.subset if f in ms and ms[f] == 'u']
246 248
247 249 def hgignore(mctx, x):
248 250 """``hgignore()``
249 251 File that matches the active .hgignore pattern.
250 252 """
251 253 getargs(x, 0, 0, _("hgignore takes no arguments"))
252 254 ignore = mctx.ctx._repo.dirstate._ignore
253 255 return [f for f in mctx.subset if ignore(f)]
254 256
255 257 def grep(mctx, x):
256 258 """``grep(regex)``
257 259 File contains the given regular expression.
258 260 """
259 261 try:
260 262 # i18n: "grep" is a keyword
261 263 r = re.compile(getstring(x, _("grep requires a pattern")))
262 264 except re.error, e:
263 265 raise error.ParseError(_('invalid match pattern: %s') % e)
264 266 return [f for f in mctx.existing() if r.search(mctx.ctx[f].data())]
265 267
266 268 def _sizetomax(s):
267 269 try:
268 270 s = s.strip()
269 271 for k, v in util._sizeunits:
270 272 if s.endswith(k):
271 273 # max(4k) = 5k - 1, max(4.5k) = 4.6k - 1
272 274 n = s[:-len(k)]
273 275 inc = 1.0
274 276 if "." in n:
275 277 inc /= 10 ** len(n.split(".")[1])
276 278 return int((float(n) + inc) * v) - 1
277 279 # no extension, this is a precise value
278 280 return int(s)
279 281 except ValueError:
280 282 raise error.ParseError(_("couldn't parse size: %s") % s)
281 283
282 284 def size(mctx, x):
283 285 """``size(expression)``
284 286 File size matches the given expression. Examples:
285 287
286 288 - 1k (files from 1024 to 2047 bytes)
287 289 - < 20k (files less than 20480 bytes)
288 290 - >= .5MB (files at least 524288 bytes)
289 291 - 4k - 1MB (files from 4096 bytes to 1048576 bytes)
290 292 """
291 293
292 294 # i18n: "size" is a keyword
293 295 expr = getstring(x, _("size requires an expression")).strip()
294 296 if '-' in expr: # do we have a range?
295 297 a, b = expr.split('-', 1)
296 298 a = util.sizetoint(a)
297 299 b = util.sizetoint(b)
298 300 m = lambda x: x >= a and x <= b
299 301 elif expr.startswith("<="):
300 302 a = util.sizetoint(expr[2:])
301 303 m = lambda x: x <= a
302 304 elif expr.startswith("<"):
303 305 a = util.sizetoint(expr[1:])
304 306 m = lambda x: x < a
305 307 elif expr.startswith(">="):
306 308 a = util.sizetoint(expr[2:])
307 309 m = lambda x: x >= a
308 310 elif expr.startswith(">"):
309 311 a = util.sizetoint(expr[1:])
310 312 m = lambda x: x > a
311 313 elif expr[0].isdigit or expr[0] == '.':
312 314 a = util.sizetoint(expr)
313 315 b = _sizetomax(expr)
314 316 m = lambda x: x >= a and x <= b
315 317 else:
316 318 raise error.ParseError(_("couldn't parse size: %s") % expr)
317 319
318 320 return [f for f in mctx.existing() if m(mctx.ctx[f].size())]
319 321
320 322 def encoding(mctx, x):
321 323 """``encoding(name)``
322 324 File can be successfully decoded with the given character
323 325 encoding. May not be useful for encodings other than ASCII and
324 326 UTF-8.
325 327 """
326 328
327 329 # i18n: "encoding" is a keyword
328 330 enc = getstring(x, _("encoding requires an encoding name"))
329 331
330 332 s = []
331 333 for f in mctx.existing():
332 334 d = mctx.ctx[f].data()
333 335 try:
334 336 d.decode(enc)
335 337 except LookupError:
336 338 raise util.Abort(_("unknown encoding '%s'") % enc)
337 339 except UnicodeDecodeError:
338 340 continue
339 341 s.append(f)
340 342
341 343 return s
342 344
343 345 def eol(mctx, x):
344 346 """``eol(style)``
345 347 File contains newlines of the given style (dos, unix, mac). Binary
346 348 files are excluded, files with mixed line endings match multiple
347 349 styles.
348 350 """
349 351
350 352 # i18n: "encoding" is a keyword
351 353 enc = getstring(x, _("encoding requires an encoding name"))
352 354
353 355 s = []
354 356 for f in mctx.existing():
355 357 d = mctx.ctx[f].data()
356 358 if util.binary(d):
357 359 continue
358 360 if (enc == 'dos' or enc == 'win') and '\r\n' in d:
359 361 s.append(f)
360 362 elif enc == 'unix' and re.search('(?<!\r)\n', d):
361 363 s.append(f)
362 364 elif enc == 'mac' and re.search('\r(?!\n)', d):
363 365 s.append(f)
364 366 return s
365 367
366 368 def copied(mctx, x):
367 369 """``copied()``
368 370 File that is recorded as being copied.
369 371 """
370 372 # i18n: "copied" is a keyword
371 373 getargs(x, 0, 0, _("copied takes no arguments"))
372 374 s = []
373 375 for f in mctx.subset:
374 376 p = mctx.ctx[f].parents()
375 377 if p and p[0].path() != f:
376 378 s.append(f)
377 379 return s
378 380
379 381 def subrepo(mctx, x):
380 382 """``subrepo([pattern])``
381 383 Subrepositories whose paths match the given pattern.
382 384 """
383 385 # i18n: "subrepo" is a keyword
384 386 getargs(x, 0, 1, _("subrepo takes at most one argument"))
385 387 ctx = mctx.ctx
386 388 sstate = sorted(ctx.substate)
387 389 if x:
388 390 pat = getstring(x, _("subrepo requires a pattern or no arguments"))
389 391
390 392 import match as matchmod # avoid circular import issues
391 393 fast = not matchmod.patkind(pat)
392 394 if fast:
393 395 def m(s):
394 396 return (s == pat)
395 397 else:
396 398 m = matchmod.match(ctx._repo.root, '', [pat], ctx=ctx)
397 399 return [sub for sub in sstate if m(sub)]
398 400 else:
399 401 return [sub for sub in sstate]
400 402
401 403 symbols = {
402 404 'added': added,
403 405 'binary': binary,
404 406 'clean': clean,
405 407 'copied': copied,
406 408 'deleted': deleted,
407 409 'encoding': encoding,
408 410 'eol': eol,
409 411 'exec': exec_,
410 412 'grep': grep,
411 413 'ignored': ignored,
412 414 'hgignore': hgignore,
413 415 'modified': modified,
414 416 'removed': removed,
415 417 'resolved': resolved,
416 418 'size': size,
417 419 'symlink': symlink,
418 420 'unknown': unknown,
419 421 'unresolved': unresolved,
420 422 'subrepo': subrepo,
421 423 }
422 424
423 425 methods = {
424 426 'string': stringset,
425 427 'symbol': stringset,
426 428 'and': andset,
427 429 'or': orset,
428 430 'minus': minusset,
429 431 'list': listset,
430 432 'group': getset,
431 433 'not': notset,
432 434 'func': func,
433 435 }
434 436
435 437 class matchctx(object):
436 438 def __init__(self, ctx, subset=None, status=None):
437 439 self.ctx = ctx
438 440 self.subset = subset
439 441 self._status = status
440 442 def status(self):
441 443 return self._status
442 444 def matcher(self, patterns):
443 445 return self.ctx.match(patterns)
444 446 def filter(self, files):
445 447 return [f for f in files if f in self.subset]
446 448 def existing(self):
447 449 if self._status is not None:
448 450 removed = set(self._status[3])
449 451 unknown = set(self._status[4] + self._status[5])
450 452 else:
451 453 removed = set()
452 454 unknown = set()
453 455 return (f for f in self.subset
454 456 if (f in self.ctx and f not in removed) or f in unknown)
455 457 def narrow(self, files):
456 458 return matchctx(self.ctx, self.filter(files), self._status)
457 459
458 460 def _intree(funcs, tree):
459 461 if isinstance(tree, tuple):
460 462 if tree[0] == 'func' and tree[1][0] == 'symbol':
461 463 if tree[1][1] in funcs:
462 464 return True
463 465 for s in tree[1:]:
464 466 if _intree(funcs, s):
465 467 return True
466 468 return False
467 469
468 470 # filesets using matchctx.existing()
469 471 _existingcallers = [
470 472 'binary',
471 473 'exec',
472 474 'grep',
473 475 'size',
474 476 'symlink',
475 477 ]
476 478
477 479 def getfileset(ctx, expr):
478 480 tree, pos = parse(expr)
479 481 if (pos != len(expr)):
480 482 raise error.ParseError(_("invalid token"), pos)
481 483
482 484 # do we need status info?
483 485 if (_intree(['modified', 'added', 'removed', 'deleted',
484 486 'unknown', 'ignored', 'clean'], tree) or
485 487 # Using matchctx.existing() on a workingctx requires us to check
486 488 # for deleted files.
487 489 (ctx.rev() is None and _intree(_existingcallers, tree))):
488 490 unknown = _intree(['unknown'], tree)
489 491 ignored = _intree(['ignored'], tree)
490 492
491 493 r = ctx._repo
492 494 status = r.status(ctx.p1(), ctx,
493 495 unknown=unknown, ignored=ignored, clean=True)
494 496 subset = []
495 497 for c in status:
496 498 subset.extend(c)
497 499 else:
498 500 status = None
499 501 subset = list(ctx.walk(ctx.match([])))
500 502
501 503 return getset(matchctx(ctx, subset, status), tree)
502 504
503 505 # tell hggettext to extract docstrings from these functions:
504 506 i18nfunctions = symbols.values()
@@ -1,2025 +1,2027 b''
1 1 # revset.py - revision set queries for mercurial
2 2 #
3 3 # Copyright 2010 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 import re
9 9 import parser, util, error, discovery, hbisect, phases
10 10 import node
11 11 import match as matchmod
12 12 from i18n import _
13 13 import encoding
14 14 import obsolete as obsmod
15 15 import repoview
16 16
17 17 def _revancestors(repo, revs, followfirst):
18 18 """Like revlog.ancestors(), but supports followfirst."""
19 19 cut = followfirst and 1 or None
20 20 cl = repo.changelog
21 21 visit = util.deque(revs)
22 22 seen = set([node.nullrev])
23 23 while visit:
24 24 for parent in cl.parentrevs(visit.popleft())[:cut]:
25 25 if parent not in seen:
26 26 visit.append(parent)
27 27 seen.add(parent)
28 28 yield parent
29 29
30 30 def _revdescendants(repo, revs, followfirst):
31 31 """Like revlog.descendants() but supports followfirst."""
32 32 cut = followfirst and 1 or None
33 33 cl = repo.changelog
34 34 first = min(revs)
35 35 nullrev = node.nullrev
36 36 if first == nullrev:
37 37 # Are there nodes with a null first parent and a non-null
38 38 # second one? Maybe. Do we care? Probably not.
39 39 for i in cl:
40 40 yield i
41 41 return
42 42
43 43 seen = set(revs)
44 44 for i in cl.revs(first + 1):
45 45 for x in cl.parentrevs(i)[:cut]:
46 46 if x != nullrev and x in seen:
47 47 seen.add(i)
48 48 yield i
49 49 break
50 50
51 51 def _revsbetween(repo, roots, heads):
52 52 """Return all paths between roots and heads, inclusive of both endpoint
53 53 sets."""
54 54 if not roots:
55 55 return []
56 56 parentrevs = repo.changelog.parentrevs
57 57 visit = heads[:]
58 58 reachable = set()
59 59 seen = {}
60 60 minroot = min(roots)
61 61 roots = set(roots)
62 62 # open-code the post-order traversal due to the tiny size of
63 63 # sys.getrecursionlimit()
64 64 while visit:
65 65 rev = visit.pop()
66 66 if rev in roots:
67 67 reachable.add(rev)
68 68 parents = parentrevs(rev)
69 69 seen[rev] = parents
70 70 for parent in parents:
71 71 if parent >= minroot and parent not in seen:
72 72 visit.append(parent)
73 73 if not reachable:
74 74 return []
75 75 for rev in sorted(seen):
76 76 for parent in seen[rev]:
77 77 if parent in reachable:
78 78 reachable.add(rev)
79 79 return sorted(reachable)
80 80
81 81 elements = {
82 82 "(": (20, ("group", 1, ")"), ("func", 1, ")")),
83 83 "~": (18, None, ("ancestor", 18)),
84 84 "^": (18, None, ("parent", 18), ("parentpost", 18)),
85 85 "-": (5, ("negate", 19), ("minus", 5)),
86 86 "::": (17, ("dagrangepre", 17), ("dagrange", 17),
87 87 ("dagrangepost", 17)),
88 88 "..": (17, ("dagrangepre", 17), ("dagrange", 17),
89 89 ("dagrangepost", 17)),
90 90 ":": (15, ("rangepre", 15), ("range", 15), ("rangepost", 15)),
91 91 "not": (10, ("not", 10)),
92 92 "!": (10, ("not", 10)),
93 93 "and": (5, None, ("and", 5)),
94 94 "&": (5, None, ("and", 5)),
95 95 "or": (4, None, ("or", 4)),
96 96 "|": (4, None, ("or", 4)),
97 97 "+": (4, None, ("or", 4)),
98 98 ",": (2, None, ("list", 2)),
99 99 ")": (0, None, None),
100 100 "symbol": (0, ("symbol",), None),
101 101 "string": (0, ("string",), None),
102 102 "end": (0, None, None),
103 103 }
104 104
105 105 keywords = set(['and', 'or', 'not'])
106 106
107 107 def tokenize(program):
108 108 '''
109 109 Parse a revset statement into a stream of tokens
110 110
111 111 Check that @ is a valid unquoted token character (issue3686):
112 112 >>> list(tokenize("@::"))
113 113 [('symbol', '@', 0), ('::', None, 1), ('end', None, 3)]
114 114
115 115 '''
116 116
117 117 pos, l = 0, len(program)
118 118 while pos < l:
119 119 c = program[pos]
120 120 if c.isspace(): # skip inter-token whitespace
121 121 pass
122 122 elif c == ':' and program[pos:pos + 2] == '::': # look ahead carefully
123 123 yield ('::', None, pos)
124 124 pos += 1 # skip ahead
125 125 elif c == '.' and program[pos:pos + 2] == '..': # look ahead carefully
126 126 yield ('..', None, pos)
127 127 pos += 1 # skip ahead
128 128 elif c in "():,-|&+!~^": # handle simple operators
129 129 yield (c, None, pos)
130 130 elif (c in '"\'' or c == 'r' and
131 131 program[pos:pos + 2] in ("r'", 'r"')): # handle quoted strings
132 132 if c == 'r':
133 133 pos += 1
134 134 c = program[pos]
135 135 decode = lambda x: x
136 136 else:
137 137 decode = lambda x: x.decode('string-escape')
138 138 pos += 1
139 139 s = pos
140 140 while pos < l: # find closing quote
141 141 d = program[pos]
142 142 if d == '\\': # skip over escaped characters
143 143 pos += 2
144 144 continue
145 145 if d == c:
146 146 yield ('string', decode(program[s:pos]), s)
147 147 break
148 148 pos += 1
149 149 else:
150 150 raise error.ParseError(_("unterminated string"), s)
151 151 # gather up a symbol/keyword
152 152 elif c.isalnum() or c in '._@' or ord(c) > 127:
153 153 s = pos
154 154 pos += 1
155 155 while pos < l: # find end of symbol
156 156 d = program[pos]
157 157 if not (d.isalnum() or d in "._/@" or ord(d) > 127):
158 158 break
159 159 if d == '.' and program[pos - 1] == '.': # special case for ..
160 160 pos -= 1
161 161 break
162 162 pos += 1
163 163 sym = program[s:pos]
164 164 if sym in keywords: # operator keywords
165 165 yield (sym, None, s)
166 166 else:
167 167 yield ('symbol', sym, s)
168 168 pos -= 1
169 169 else:
170 170 raise error.ParseError(_("syntax error"), pos)
171 171 pos += 1
172 172 yield ('end', None, pos)
173 173
174 174 # helpers
175 175
176 176 def getstring(x, err):
177 177 if x and (x[0] == 'string' or x[0] == 'symbol'):
178 178 return x[1]
179 179 raise error.ParseError(err)
180 180
181 181 def getlist(x):
182 182 if not x:
183 183 return []
184 184 if x[0] == 'list':
185 185 return getlist(x[1]) + [x[2]]
186 186 return [x]
187 187
188 188 def getargs(x, min, max, err):
189 189 l = getlist(x)
190 190 if len(l) < min or (max >= 0 and len(l) > max):
191 191 raise error.ParseError(err)
192 192 return l
193 193
194 194 def getset(repo, subset, x):
195 195 if not x:
196 196 raise error.ParseError(_("missing argument"))
197 197 return methods[x[0]](repo, subset, *x[1:])
198 198
199 199 def _getrevsource(repo, r):
200 200 extra = repo[r].extra()
201 201 for label in ('source', 'transplant_source', 'rebase_source'):
202 202 if label in extra:
203 203 try:
204 204 return repo[extra[label]].rev()
205 205 except error.RepoLookupError:
206 206 pass
207 207 return None
208 208
209 209 # operator methods
210 210
211 211 def stringset(repo, subset, x):
212 212 x = repo[x].rev()
213 213 if x == -1 and len(subset) == len(repo):
214 214 return [-1]
215 215 if len(subset) == len(repo) or x in subset:
216 216 return [x]
217 217 return []
218 218
219 219 def symbolset(repo, subset, x):
220 220 if x in symbols:
221 221 raise error.ParseError(_("can't use %s here") % x)
222 222 return stringset(repo, subset, x)
223 223
224 224 def rangeset(repo, subset, x, y):
225 225 cl = repo.changelog
226 226 m = getset(repo, cl, x)
227 227 n = getset(repo, cl, y)
228 228
229 229 if not m or not n:
230 230 return []
231 231 m, n = m[0], n[-1]
232 232
233 233 if m < n:
234 234 r = range(m, n + 1)
235 235 else:
236 236 r = range(m, n - 1, -1)
237 237 s = set(subset)
238 238 return [x for x in r if x in s]
239 239
240 240 def dagrange(repo, subset, x, y):
241 241 r = list(repo)
242 242 xs = _revsbetween(repo, getset(repo, r, x), getset(repo, r, y))
243 243 s = set(subset)
244 244 return [r for r in xs if r in s]
245 245
246 246 def andset(repo, subset, x, y):
247 247 return getset(repo, getset(repo, subset, x), y)
248 248
249 249 def orset(repo, subset, x, y):
250 250 xl = getset(repo, subset, x)
251 251 s = set(xl)
252 252 yl = getset(repo, [r for r in subset if r not in s], y)
253 253 return xl + yl
254 254
255 255 def notset(repo, subset, x):
256 256 s = set(getset(repo, subset, x))
257 257 return [r for r in subset if r not in s]
258 258
259 259 def listset(repo, subset, a, b):
260 260 raise error.ParseError(_("can't use a list in this context"))
261 261
262 262 def func(repo, subset, a, b):
263 263 if a[0] == 'symbol' and a[1] in symbols:
264 264 return symbols[a[1]](repo, subset, b)
265 265 raise error.ParseError(_("not a function: %s") % a[1])
266 266
267 267 # functions
268 268
269 269 def adds(repo, subset, x):
270 270 """``adds(pattern)``
271 271 Changesets that add a file matching pattern.
272 272 """
273 273 # i18n: "adds" is a keyword
274 274 pat = getstring(x, _("adds requires a pattern"))
275 275 return checkstatus(repo, subset, pat, 1)
276 276
277 277 def ancestor(repo, subset, x):
278 278 """``ancestor(*changeset)``
279 279 Greatest common ancestor of the changesets.
280 280
281 281 Accepts 0 or more changesets.
282 282 Will return empty list when passed no args.
283 283 Greatest common ancestor of a single changeset is that changeset.
284 284 """
285 285 # i18n: "ancestor" is a keyword
286 286 l = getlist(x)
287 287 rl = list(repo)
288 288 anc = None
289 289
290 290 # (getset(repo, rl, i) for i in l) generates a list of lists
291 291 rev = repo.changelog.rev
292 292 ancestor = repo.changelog.ancestor
293 293 node = repo.changelog.node
294 294 for revs in (getset(repo, rl, i) for i in l):
295 295 for r in revs:
296 296 if anc is None:
297 297 anc = r
298 298 else:
299 299 anc = rev(ancestor(node(anc), node(r)))
300 300
301 301 if anc is not None and anc in subset:
302 302 return [anc]
303 303 return []
304 304
305 305 def _ancestors(repo, subset, x, followfirst=False):
306 306 args = getset(repo, list(repo), x)
307 307 if not args:
308 308 return []
309 309 s = set(_revancestors(repo, args, followfirst)) | set(args)
310 310 return [r for r in subset if r in s]
311 311
312 312 def ancestors(repo, subset, x):
313 313 """``ancestors(set)``
314 314 Changesets that are ancestors of a changeset in set.
315 315 """
316 316 return _ancestors(repo, subset, x)
317 317
318 318 def _firstancestors(repo, subset, x):
319 319 # ``_firstancestors(set)``
320 320 # Like ``ancestors(set)`` but follows only the first parents.
321 321 return _ancestors(repo, subset, x, followfirst=True)
322 322
323 323 def ancestorspec(repo, subset, x, n):
324 324 """``set~n``
325 325 Changesets that are the Nth ancestor (first parents only) of a changeset
326 326 in set.
327 327 """
328 328 try:
329 329 n = int(n[1])
330 330 except (TypeError, ValueError):
331 331 raise error.ParseError(_("~ expects a number"))
332 332 ps = set()
333 333 cl = repo.changelog
334 334 for r in getset(repo, cl, x):
335 335 for i in range(n):
336 336 r = cl.parentrevs(r)[0]
337 337 ps.add(r)
338 338 return [r for r in subset if r in ps]
339 339
340 340 def author(repo, subset, x):
341 341 """``author(string)``
342 342 Alias for ``user(string)``.
343 343 """
344 344 # i18n: "author" is a keyword
345 345 n = encoding.lower(getstring(x, _("author requires a string")))
346 346 kind, pattern, matcher = _substringmatcher(n)
347 347 return [r for r in subset if matcher(encoding.lower(repo[r].user()))]
348 348
349 349 def bisect(repo, subset, x):
350 350 """``bisect(string)``
351 351 Changesets marked in the specified bisect status:
352 352
353 353 - ``good``, ``bad``, ``skip``: csets explicitly marked as good/bad/skip
354 354 - ``goods``, ``bads`` : csets topologically good/bad
355 355 - ``range`` : csets taking part in the bisection
356 356 - ``pruned`` : csets that are goods, bads or skipped
357 357 - ``untested`` : csets whose fate is yet unknown
358 358 - ``ignored`` : csets ignored due to DAG topology
359 359 - ``current`` : the cset currently being bisected
360 360 """
361 361 # i18n: "bisect" is a keyword
362 362 status = getstring(x, _("bisect requires a string")).lower()
363 363 state = set(hbisect.get(repo, status))
364 364 return [r for r in subset if r in state]
365 365
366 366 # Backward-compatibility
367 367 # - no help entry so that we do not advertise it any more
368 368 def bisected(repo, subset, x):
369 369 return bisect(repo, subset, x)
370 370
371 371 def bookmark(repo, subset, x):
372 372 """``bookmark([name])``
373 373 The named bookmark or all bookmarks.
374 374
375 375 If `name` starts with `re:`, the remainder of the name is treated as
376 376 a regular expression. To match a bookmark that actually starts with `re:`,
377 377 use the prefix `literal:`.
378 378 """
379 379 # i18n: "bookmark" is a keyword
380 380 args = getargs(x, 0, 1, _('bookmark takes one or no arguments'))
381 381 if args:
382 382 bm = getstring(args[0],
383 383 # i18n: "bookmark" is a keyword
384 384 _('the argument to bookmark must be a string'))
385 385 kind, pattern, matcher = _stringmatcher(bm)
386 386 if kind == 'literal':
387 387 bmrev = repo._bookmarks.get(bm, None)
388 388 if not bmrev:
389 389 raise util.Abort(_("bookmark '%s' does not exist") % bm)
390 390 bmrev = repo[bmrev].rev()
391 391 return [r for r in subset if r == bmrev]
392 392 else:
393 393 matchrevs = set()
394 394 for name, bmrev in repo._bookmarks.iteritems():
395 395 if matcher(name):
396 396 matchrevs.add(bmrev)
397 397 if not matchrevs:
398 398 raise util.Abort(_("no bookmarks exist that match '%s'")
399 399 % pattern)
400 400 bmrevs = set()
401 401 for bmrev in matchrevs:
402 402 bmrevs.add(repo[bmrev].rev())
403 403 return [r for r in subset if r in bmrevs]
404 404
405 405 bms = set([repo[r].rev()
406 406 for r in repo._bookmarks.values()])
407 407 return [r for r in subset if r in bms]
408 408
409 409 def branch(repo, subset, x):
410 410 """``branch(string or set)``
411 411 All changesets belonging to the given branch or the branches of the given
412 412 changesets.
413 413
414 414 If `string` starts with `re:`, the remainder of the name is treated as
415 415 a regular expression. To match a branch that actually starts with `re:`,
416 416 use the prefix `literal:`.
417 417 """
418 418 try:
419 419 b = getstring(x, '')
420 420 except error.ParseError:
421 421 # not a string, but another revspec, e.g. tip()
422 422 pass
423 423 else:
424 424 kind, pattern, matcher = _stringmatcher(b)
425 425 if kind == 'literal':
426 426 # note: falls through to the revspec case if no branch with
427 427 # this name exists
428 428 if pattern in repo.branchmap():
429 429 return [r for r in subset if matcher(repo[r].branch())]
430 430 else:
431 431 return [r for r in subset if matcher(repo[r].branch())]
432 432
433 433 s = getset(repo, list(repo), x)
434 434 b = set()
435 435 for r in s:
436 436 b.add(repo[r].branch())
437 437 s = set(s)
438 438 return [r for r in subset if r in s or repo[r].branch() in b]
439 439
440 440 def bumped(repo, subset, x):
441 441 """``bumped()``
442 442 Mutable changesets marked as successors of public changesets.
443 443
444 444 Only non-public and non-obsolete changesets can be `bumped`.
445 445 """
446 446 # i18n: "bumped" is a keyword
447 447 getargs(x, 0, 0, _("bumped takes no arguments"))
448 448 bumped = obsmod.getrevs(repo, 'bumped')
449 449 return [r for r in subset if r in bumped]
450 450
451 451 def bundle(repo, subset, x):
452 452 """``bundle()``
453 453 Changesets in the bundle.
454 454
455 455 Bundle must be specified by the -R option."""
456 456
457 457 try:
458 458 bundlerevs = repo.changelog.bundlerevs
459 459 except AttributeError:
460 460 raise util.Abort(_("no bundle provided - specify with -R"))
461 461 return [r for r in subset if r in bundlerevs]
462 462
463 463 def checkstatus(repo, subset, pat, field):
464 464 m = None
465 465 s = []
466 466 hasset = matchmod.patkind(pat) == 'set'
467 467 fname = None
468 468 for r in subset:
469 469 c = repo[r]
470 470 if not m or hasset:
471 471 m = matchmod.match(repo.root, repo.getcwd(), [pat], ctx=c)
472 472 if not m.anypats() and len(m.files()) == 1:
473 473 fname = m.files()[0]
474 474 if fname is not None:
475 475 if fname not in c.files():
476 476 continue
477 477 else:
478 478 for f in c.files():
479 479 if m(f):
480 480 break
481 481 else:
482 482 continue
483 483 files = repo.status(c.p1().node(), c.node())[field]
484 484 if fname is not None:
485 485 if fname in files:
486 486 s.append(r)
487 487 else:
488 488 for f in files:
489 489 if m(f):
490 490 s.append(r)
491 491 break
492 492 return s
493 493
494 494 def _children(repo, narrow, parentset):
495 495 cs = set()
496 496 if not parentset:
497 497 return cs
498 498 pr = repo.changelog.parentrevs
499 499 minrev = min(parentset)
500 500 for r in narrow:
501 501 if r <= minrev:
502 502 continue
503 503 for p in pr(r):
504 504 if p in parentset:
505 505 cs.add(r)
506 506 return cs
507 507
508 508 def children(repo, subset, x):
509 509 """``children(set)``
510 510 Child changesets of changesets in set.
511 511 """
512 512 s = set(getset(repo, list(repo), x))
513 513 cs = _children(repo, subset, s)
514 514 return [r for r in subset if r in cs]
515 515
516 516 def closed(repo, subset, x):
517 517 """``closed()``
518 518 Changeset is closed.
519 519 """
520 520 # i18n: "closed" is a keyword
521 521 getargs(x, 0, 0, _("closed takes no arguments"))
522 522 return [r for r in subset if repo[r].closesbranch()]
523 523
524 524 def contains(repo, subset, x):
525 525 """``contains(pattern)``
526 526 Revision contains a file matching pattern. See :hg:`help patterns`
527 527 for information about file patterns.
528 528 """
529 529 # i18n: "contains" is a keyword
530 530 pat = getstring(x, _("contains requires a pattern"))
531 531 m = None
532 532 s = []
533 533 if not matchmod.patkind(pat):
534 534 for r in subset:
535 535 if pat in repo[r]:
536 536 s.append(r)
537 537 else:
538 538 for r in subset:
539 539 c = repo[r]
540 540 if not m or matchmod.patkind(pat) == 'set':
541 541 m = matchmod.match(repo.root, repo.getcwd(), [pat], ctx=c)
542 542 for f in c.manifest():
543 543 if m(f):
544 544 s.append(r)
545 545 break
546 546 return s
547 547
548 548 def converted(repo, subset, x):
549 549 """``converted([id])``
550 550 Changesets converted from the given identifier in the old repository if
551 551 present, or all converted changesets if no identifier is specified.
552 552 """
553 553
554 554 # There is exactly no chance of resolving the revision, so do a simple
555 555 # string compare and hope for the best
556 556
557 557 rev = None
558 558 # i18n: "converted" is a keyword
559 559 l = getargs(x, 0, 1, _('converted takes one or no arguments'))
560 560 if l:
561 561 # i18n: "converted" is a keyword
562 562 rev = getstring(l[0], _('converted requires a revision'))
563 563
564 564 def _matchvalue(r):
565 565 source = repo[r].extra().get('convert_revision', None)
566 566 return source is not None and (rev is None or source.startswith(rev))
567 567
568 568 return [r for r in subset if _matchvalue(r)]
569 569
570 570 def date(repo, subset, x):
571 571 """``date(interval)``
572 572 Changesets within the interval, see :hg:`help dates`.
573 573 """
574 574 # i18n: "date" is a keyword
575 575 ds = getstring(x, _("date requires a string"))
576 576 dm = util.matchdate(ds)
577 577 return [r for r in subset if dm(repo[r].date()[0])]
578 578
579 579 def desc(repo, subset, x):
580 580 """``desc(string)``
581 581 Search commit message for string. The match is case-insensitive.
582 582 """
583 583 # i18n: "desc" is a keyword
584 584 ds = encoding.lower(getstring(x, _("desc requires a string")))
585 585 l = []
586 586 for r in subset:
587 587 c = repo[r]
588 588 if ds in encoding.lower(c.description()):
589 589 l.append(r)
590 590 return l
591 591
592 592 def _descendants(repo, subset, x, followfirst=False):
593 593 args = getset(repo, list(repo), x)
594 594 if not args:
595 595 return []
596 596 s = set(_revdescendants(repo, args, followfirst)) | set(args)
597 597 return [r for r in subset if r in s]
598 598
599 599 def descendants(repo, subset, x):
600 600 """``descendants(set)``
601 601 Changesets which are descendants of changesets in set.
602 602 """
603 603 return _descendants(repo, subset, x)
604 604
605 605 def _firstdescendants(repo, subset, x):
606 606 # ``_firstdescendants(set)``
607 607 # Like ``descendants(set)`` but follows only the first parents.
608 608 return _descendants(repo, subset, x, followfirst=True)
609 609
610 610 def destination(repo, subset, x):
611 611 """``destination([set])``
612 612 Changesets that were created by a graft, transplant or rebase operation,
613 613 with the given revisions specified as the source. Omitting the optional set
614 614 is the same as passing all().
615 615 """
616 616 if x is not None:
617 617 args = set(getset(repo, list(repo), x))
618 618 else:
619 619 args = set(getall(repo, list(repo), x))
620 620
621 621 dests = set()
622 622
623 623 # subset contains all of the possible destinations that can be returned, so
624 624 # iterate over them and see if their source(s) were provided in the args.
625 625 # Even if the immediate src of r is not in the args, src's source (or
626 626 # further back) may be. Scanning back further than the immediate src allows
627 627 # transitive transplants and rebases to yield the same results as transitive
628 628 # grafts.
629 629 for r in subset:
630 630 src = _getrevsource(repo, r)
631 631 lineage = None
632 632
633 633 while src is not None:
634 634 if lineage is None:
635 635 lineage = list()
636 636
637 637 lineage.append(r)
638 638
639 639 # The visited lineage is a match if the current source is in the arg
640 640 # set. Since every candidate dest is visited by way of iterating
641 641 # subset, any dests further back in the lineage will be tested by a
642 642 # different iteration over subset. Likewise, if the src was already
643 643 # selected, the current lineage can be selected without going back
644 644 # further.
645 645 if src in args or src in dests:
646 646 dests.update(lineage)
647 647 break
648 648
649 649 r = src
650 650 src = _getrevsource(repo, r)
651 651
652 652 return [r for r in subset if r in dests]
653 653
654 654 def divergent(repo, subset, x):
655 655 """``divergent()``
656 656 Final successors of changesets with an alternative set of final successors.
657 657 """
658 658 # i18n: "divergent" is a keyword
659 659 getargs(x, 0, 0, _("divergent takes no arguments"))
660 660 divergent = obsmod.getrevs(repo, 'divergent')
661 661 return [r for r in subset if r in divergent]
662 662
663 663 def draft(repo, subset, x):
664 664 """``draft()``
665 665 Changeset in draft phase."""
666 666 # i18n: "draft" is a keyword
667 667 getargs(x, 0, 0, _("draft takes no arguments"))
668 668 pc = repo._phasecache
669 669 return [r for r in subset if pc.phase(repo, r) == phases.draft]
670 670
671 671 def extinct(repo, subset, x):
672 672 """``extinct()``
673 673 Obsolete changesets with obsolete descendants only.
674 674 """
675 675 # i18n: "extinct" is a keyword
676 676 getargs(x, 0, 0, _("extinct takes no arguments"))
677 677 extincts = obsmod.getrevs(repo, 'extinct')
678 678 return [r for r in subset if r in extincts]
679 679
680 680 def extra(repo, subset, x):
681 681 """``extra(label, [value])``
682 682 Changesets with the given label in the extra metadata, with the given
683 683 optional value.
684 684
685 685 If `value` starts with `re:`, the remainder of the value is treated as
686 686 a regular expression. To match a value that actually starts with `re:`,
687 687 use the prefix `literal:`.
688 688 """
689 689
690 690 # i18n: "extra" is a keyword
691 691 l = getargs(x, 1, 2, _('extra takes at least 1 and at most 2 arguments'))
692 692 # i18n: "extra" is a keyword
693 693 label = getstring(l[0], _('first argument to extra must be a string'))
694 694 value = None
695 695
696 696 if len(l) > 1:
697 697 # i18n: "extra" is a keyword
698 698 value = getstring(l[1], _('second argument to extra must be a string'))
699 699 kind, value, matcher = _stringmatcher(value)
700 700
701 701 def _matchvalue(r):
702 702 extra = repo[r].extra()
703 703 return label in extra and (value is None or matcher(extra[label]))
704 704
705 705 return [r for r in subset if _matchvalue(r)]
706 706
707 707 def filelog(repo, subset, x):
708 708 """``filelog(pattern)``
709 709 Changesets connected to the specified filelog.
710 710
711 711 For performance reasons, ``filelog()`` does not show every changeset
712 712 that affects the requested file(s). See :hg:`help log` for details. For
713 713 a slower, more accurate result, use ``file()``.
714 714 """
715 715
716 716 # i18n: "filelog" is a keyword
717 717 pat = getstring(x, _("filelog requires a pattern"))
718 718 m = matchmod.match(repo.root, repo.getcwd(), [pat], default='relpath',
719 719 ctx=repo[None])
720 720 s = set()
721 721
722 722 if not matchmod.patkind(pat):
723 723 for f in m.files():
724 724 fl = repo.file(f)
725 725 for fr in fl:
726 726 s.add(fl.linkrev(fr))
727 727 else:
728 728 for f in repo[None]:
729 729 if m(f):
730 730 fl = repo.file(f)
731 731 for fr in fl:
732 732 s.add(fl.linkrev(fr))
733 733
734 734 return [r for r in subset if r in s]
735 735
736 736 def first(repo, subset, x):
737 737 """``first(set, [n])``
738 738 An alias for limit().
739 739 """
740 740 return limit(repo, subset, x)
741 741
742 742 def _follow(repo, subset, x, name, followfirst=False):
743 743 l = getargs(x, 0, 1, _("%s takes no arguments or a filename") % name)
744 744 c = repo['.']
745 745 if l:
746 746 x = getstring(l[0], _("%s expected a filename") % name)
747 747 if x in c:
748 748 cx = c[x]
749 749 s = set(ctx.rev() for ctx in cx.ancestors(followfirst=followfirst))
750 750 # include the revision responsible for the most recent version
751 751 s.add(cx.linkrev())
752 752 else:
753 753 return []
754 754 else:
755 755 s = set(_revancestors(repo, [c.rev()], followfirst)) | set([c.rev()])
756 756
757 757 return [r for r in subset if r in s]
758 758
759 759 def follow(repo, subset, x):
760 760 """``follow([file])``
761 761 An alias for ``::.`` (ancestors of the working copy's first parent).
762 762 If a filename is specified, the history of the given file is followed,
763 763 including copies.
764 764 """
765 765 return _follow(repo, subset, x, 'follow')
766 766
767 767 def _followfirst(repo, subset, x):
768 768 # ``followfirst([file])``
769 769 # Like ``follow([file])`` but follows only the first parent of
770 770 # every revision or file revision.
771 771 return _follow(repo, subset, x, '_followfirst', followfirst=True)
772 772
773 773 def getall(repo, subset, x):
774 774 """``all()``
775 775 All changesets, the same as ``0:tip``.
776 776 """
777 777 # i18n: "all" is a keyword
778 778 getargs(x, 0, 0, _("all takes no arguments"))
779 779 return subset
780 780
781 781 def grep(repo, subset, x):
782 782 """``grep(regex)``
783 783 Like ``keyword(string)`` but accepts a regex. Use ``grep(r'...')``
784 784 to ensure special escape characters are handled correctly. Unlike
785 785 ``keyword(string)``, the match is case-sensitive.
786 786 """
787 787 try:
788 788 # i18n: "grep" is a keyword
789 789 gr = re.compile(getstring(x, _("grep requires a string")))
790 790 except re.error, e:
791 791 raise error.ParseError(_('invalid match pattern: %s') % e)
792 792 l = []
793 793 for r in subset:
794 794 c = repo[r]
795 795 for e in c.files() + [c.user(), c.description()]:
796 796 if gr.search(e):
797 797 l.append(r)
798 798 break
799 799 return l
800 800
801 801 def _matchfiles(repo, subset, x):
802 802 # _matchfiles takes a revset list of prefixed arguments:
803 803 #
804 804 # [p:foo, i:bar, x:baz]
805 805 #
806 806 # builds a match object from them and filters subset. Allowed
807 807 # prefixes are 'p:' for regular patterns, 'i:' for include
808 808 # patterns and 'x:' for exclude patterns. Use 'r:' prefix to pass
809 809 # a revision identifier, or the empty string to reference the
810 810 # working directory, from which the match object is
811 811 # initialized. Use 'd:' to set the default matching mode, default
812 812 # to 'glob'. At most one 'r:' and 'd:' argument can be passed.
813 813
814 814 # i18n: "_matchfiles" is a keyword
815 815 l = getargs(x, 1, -1, _("_matchfiles requires at least one argument"))
816 816 pats, inc, exc = [], [], []
817 817 hasset = False
818 818 rev, default = None, None
819 819 for arg in l:
820 820 # i18n: "_matchfiles" is a keyword
821 821 s = getstring(arg, _("_matchfiles requires string arguments"))
822 822 prefix, value = s[:2], s[2:]
823 823 if prefix == 'p:':
824 824 pats.append(value)
825 825 elif prefix == 'i:':
826 826 inc.append(value)
827 827 elif prefix == 'x:':
828 828 exc.append(value)
829 829 elif prefix == 'r:':
830 830 if rev is not None:
831 831 # i18n: "_matchfiles" is a keyword
832 832 raise error.ParseError(_('_matchfiles expected at most one '
833 833 'revision'))
834 834 rev = value
835 835 elif prefix == 'd:':
836 836 if default is not None:
837 837 # i18n: "_matchfiles" is a keyword
838 838 raise error.ParseError(_('_matchfiles expected at most one '
839 839 'default mode'))
840 840 default = value
841 841 else:
842 842 # i18n: "_matchfiles" is a keyword
843 843 raise error.ParseError(_('invalid _matchfiles prefix: %s') % prefix)
844 844 if not hasset and matchmod.patkind(value) == 'set':
845 845 hasset = True
846 846 if not default:
847 847 default = 'glob'
848 848 m = None
849 849 s = []
850 850 for r in subset:
851 851 c = repo[r]
852 852 if not m or (hasset and rev is None):
853 853 ctx = c
854 854 if rev is not None:
855 855 ctx = repo[rev or None]
856 856 m = matchmod.match(repo.root, repo.getcwd(), pats, include=inc,
857 857 exclude=exc, ctx=ctx, default=default)
858 858 for f in c.files():
859 859 if m(f):
860 860 s.append(r)
861 861 break
862 862 return s
863 863
864 864 def hasfile(repo, subset, x):
865 865 """``file(pattern)``
866 866 Changesets affecting files matched by pattern.
867 867
868 868 For a faster but less accurate result, consider using ``filelog()``
869 869 instead.
870 870 """
871 871 # i18n: "file" is a keyword
872 872 pat = getstring(x, _("file requires a pattern"))
873 873 return _matchfiles(repo, subset, ('string', 'p:' + pat))
874 874
875 875 def head(repo, subset, x):
876 876 """``head()``
877 877 Changeset is a named branch head.
878 878 """
879 879 # i18n: "head" is a keyword
880 880 getargs(x, 0, 0, _("head takes no arguments"))
881 881 hs = set()
882 882 for b, ls in repo.branchmap().iteritems():
883 883 hs.update(repo[h].rev() for h in ls)
884 884 return [r for r in subset if r in hs]
885 885
886 886 def heads(repo, subset, x):
887 887 """``heads(set)``
888 888 Members of set with no children in set.
889 889 """
890 890 s = getset(repo, subset, x)
891 891 ps = set(parents(repo, subset, x))
892 892 return [r for r in s if r not in ps]
893 893
894 894 def hidden(repo, subset, x):
895 895 """``hidden()``
896 896 Hidden changesets.
897 897 """
898 898 # i18n: "hidden" is a keyword
899 899 getargs(x, 0, 0, _("hidden takes no arguments"))
900 900 hiddenrevs = repoview.filterrevs(repo, 'visible')
901 901 return [r for r in subset if r in hiddenrevs]
902 902
903 903 def keyword(repo, subset, x):
904 904 """``keyword(string)``
905 905 Search commit message, user name, and names of changed files for
906 906 string. The match is case-insensitive.
907 907 """
908 908 # i18n: "keyword" is a keyword
909 909 kw = encoding.lower(getstring(x, _("keyword requires a string")))
910 910 l = []
911 911 for r in subset:
912 912 c = repo[r]
913 913 if util.any(kw in encoding.lower(t)
914 914 for t in c.files() + [c.user(), c.description()]):
915 915 l.append(r)
916 916 return l
917 917
918 918 def limit(repo, subset, x):
919 919 """``limit(set, [n])``
920 920 First n members of set, defaulting to 1.
921 921 """
922 922 # i18n: "limit" is a keyword
923 923 l = getargs(x, 1, 2, _("limit requires one or two arguments"))
924 924 try:
925 925 lim = 1
926 926 if len(l) == 2:
927 927 # i18n: "limit" is a keyword
928 928 lim = int(getstring(l[1], _("limit requires a number")))
929 929 except (TypeError, ValueError):
930 930 # i18n: "limit" is a keyword
931 931 raise error.ParseError(_("limit expects a number"))
932 932 ss = set(subset)
933 933 os = getset(repo, list(repo), l[0])[:lim]
934 934 return [r for r in os if r in ss]
935 935
936 936 def last(repo, subset, x):
937 937 """``last(set, [n])``
938 938 Last n members of set, defaulting to 1.
939 939 """
940 940 # i18n: "last" is a keyword
941 941 l = getargs(x, 1, 2, _("last requires one or two arguments"))
942 942 try:
943 943 lim = 1
944 944 if len(l) == 2:
945 945 # i18n: "last" is a keyword
946 946 lim = int(getstring(l[1], _("last requires a number")))
947 947 except (TypeError, ValueError):
948 948 # i18n: "last" is a keyword
949 949 raise error.ParseError(_("last expects a number"))
950 950 ss = set(subset)
951 951 os = getset(repo, list(repo), l[0])[-lim:]
952 952 return [r for r in os if r in ss]
953 953
954 954 def maxrev(repo, subset, x):
955 955 """``max(set)``
956 956 Changeset with highest revision number in set.
957 957 """
958 958 os = getset(repo, list(repo), x)
959 959 if os:
960 960 m = max(os)
961 961 if m in subset:
962 962 return [m]
963 963 return []
964 964
965 965 def merge(repo, subset, x):
966 966 """``merge()``
967 967 Changeset is a merge changeset.
968 968 """
969 969 # i18n: "merge" is a keyword
970 970 getargs(x, 0, 0, _("merge takes no arguments"))
971 971 cl = repo.changelog
972 972 return [r for r in subset if cl.parentrevs(r)[1] != -1]
973 973
974 974 def branchpoint(repo, subset, x):
975 975 """``branchpoint()``
976 976 Changesets with more than one child.
977 977 """
978 978 # i18n: "branchpoint" is a keyword
979 979 getargs(x, 0, 0, _("branchpoint takes no arguments"))
980 980 cl = repo.changelog
981 981 if not subset:
982 982 return []
983 983 baserev = min(subset)
984 984 parentscount = [0]*(len(repo) - baserev)
985 985 for r in cl.revs(start=baserev + 1):
986 986 for p in cl.parentrevs(r):
987 987 if p >= baserev:
988 988 parentscount[p - baserev] += 1
989 989 return [r for r in subset if (parentscount[r - baserev] > 1)]
990 990
991 991 def minrev(repo, subset, x):
992 992 """``min(set)``
993 993 Changeset with lowest revision number in set.
994 994 """
995 995 os = getset(repo, list(repo), x)
996 996 if os:
997 997 m = min(os)
998 998 if m in subset:
999 999 return [m]
1000 1000 return []
1001 1001
1002 1002 def modifies(repo, subset, x):
1003 1003 """``modifies(pattern)``
1004 1004 Changesets modifying files matched by pattern.
1005 1005 """
1006 1006 # i18n: "modifies" is a keyword
1007 1007 pat = getstring(x, _("modifies requires a pattern"))
1008 1008 return checkstatus(repo, subset, pat, 0)
1009 1009
1010 1010 def node_(repo, subset, x):
1011 1011 """``id(string)``
1012 1012 Revision non-ambiguously specified by the given hex string prefix.
1013 1013 """
1014 1014 # i18n: "id" is a keyword
1015 1015 l = getargs(x, 1, 1, _("id requires one argument"))
1016 1016 # i18n: "id" is a keyword
1017 1017 n = getstring(l[0], _("id requires a string"))
1018 1018 if len(n) == 40:
1019 1019 rn = repo[n].rev()
1020 1020 else:
1021 1021 rn = None
1022 1022 pm = repo.changelog._partialmatch(n)
1023 1023 if pm is not None:
1024 1024 rn = repo.changelog.rev(pm)
1025 1025
1026 1026 return [r for r in subset if r == rn]
1027 1027
1028 1028 def obsolete(repo, subset, x):
1029 1029 """``obsolete()``
1030 1030 Mutable changeset with a newer version."""
1031 1031 # i18n: "obsolete" is a keyword
1032 1032 getargs(x, 0, 0, _("obsolete takes no arguments"))
1033 1033 obsoletes = obsmod.getrevs(repo, 'obsolete')
1034 1034 return [r for r in subset if r in obsoletes]
1035 1035
1036 1036 def origin(repo, subset, x):
1037 1037 """``origin([set])``
1038 1038 Changesets that were specified as a source for the grafts, transplants or
1039 1039 rebases that created the given revisions. Omitting the optional set is the
1040 1040 same as passing all(). If a changeset created by these operations is itself
1041 1041 specified as a source for one of these operations, only the source changeset
1042 1042 for the first operation is selected.
1043 1043 """
1044 1044 if x is not None:
1045 1045 args = set(getset(repo, list(repo), x))
1046 1046 else:
1047 1047 args = set(getall(repo, list(repo), x))
1048 1048
1049 1049 def _firstsrc(rev):
1050 1050 src = _getrevsource(repo, rev)
1051 1051 if src is None:
1052 1052 return None
1053 1053
1054 1054 while True:
1055 1055 prev = _getrevsource(repo, src)
1056 1056
1057 1057 if prev is None:
1058 1058 return src
1059 1059 src = prev
1060 1060
1061 1061 o = set([_firstsrc(r) for r in args])
1062 1062 return [r for r in subset if r in o]
1063 1063
1064 1064 def outgoing(repo, subset, x):
1065 1065 """``outgoing([path])``
1066 1066 Changesets not found in the specified destination repository, or the
1067 1067 default push location.
1068 1068 """
1069 1069 import hg # avoid start-up nasties
1070 1070 # i18n: "outgoing" is a keyword
1071 1071 l = getargs(x, 0, 1, _("outgoing takes one or no arguments"))
1072 1072 # i18n: "outgoing" is a keyword
1073 1073 dest = l and getstring(l[0], _("outgoing requires a repository path")) or ''
1074 1074 dest = repo.ui.expandpath(dest or 'default-push', dest or 'default')
1075 1075 dest, branches = hg.parseurl(dest)
1076 1076 revs, checkout = hg.addbranchrevs(repo, repo, branches, [])
1077 1077 if revs:
1078 1078 revs = [repo.lookup(rev) for rev in revs]
1079 1079 other = hg.peer(repo, {}, dest)
1080 1080 repo.ui.pushbuffer()
1081 1081 outgoing = discovery.findcommonoutgoing(repo, other, onlyheads=revs)
1082 1082 repo.ui.popbuffer()
1083 1083 cl = repo.changelog
1084 1084 o = set([cl.rev(r) for r in outgoing.missing])
1085 1085 return [r for r in subset if r in o]
1086 1086
1087 1087 def p1(repo, subset, x):
1088 1088 """``p1([set])``
1089 1089 First parent of changesets in set, or the working directory.
1090 1090 """
1091 1091 if x is None:
1092 1092 p = repo[x].p1().rev()
1093 1093 return [r for r in subset if r == p]
1094 1094
1095 1095 ps = set()
1096 1096 cl = repo.changelog
1097 1097 for r in getset(repo, list(repo), x):
1098 1098 ps.add(cl.parentrevs(r)[0])
1099 1099 return [r for r in subset if r in ps]
1100 1100
1101 1101 def p2(repo, subset, x):
1102 1102 """``p2([set])``
1103 1103 Second parent of changesets in set, or the working directory.
1104 1104 """
1105 1105 if x is None:
1106 1106 ps = repo[x].parents()
1107 1107 try:
1108 1108 p = ps[1].rev()
1109 1109 return [r for r in subset if r == p]
1110 1110 except IndexError:
1111 1111 return []
1112 1112
1113 1113 ps = set()
1114 1114 cl = repo.changelog
1115 1115 for r in getset(repo, list(repo), x):
1116 1116 ps.add(cl.parentrevs(r)[1])
1117 1117 return [r for r in subset if r in ps]
1118 1118
1119 1119 def parents(repo, subset, x):
1120 1120 """``parents([set])``
1121 1121 The set of all parents for all changesets in set, or the working directory.
1122 1122 """
1123 1123 if x is None:
1124 1124 ps = tuple(p.rev() for p in repo[x].parents())
1125 1125 return [r for r in subset if r in ps]
1126 1126
1127 1127 ps = set()
1128 1128 cl = repo.changelog
1129 1129 for r in getset(repo, list(repo), x):
1130 1130 ps.update(cl.parentrevs(r))
1131 1131 return [r for r in subset if r in ps]
1132 1132
1133 1133 def parentspec(repo, subset, x, n):
1134 1134 """``set^0``
1135 1135 The set.
1136 1136 ``set^1`` (or ``set^``), ``set^2``
1137 1137 First or second parent, respectively, of all changesets in set.
1138 1138 """
1139 1139 try:
1140 1140 n = int(n[1])
1141 1141 if n not in (0, 1, 2):
1142 1142 raise ValueError
1143 1143 except (TypeError, ValueError):
1144 1144 raise error.ParseError(_("^ expects a number 0, 1, or 2"))
1145 1145 ps = set()
1146 1146 cl = repo.changelog
1147 1147 for r in getset(repo, cl, x):
1148 1148 if n == 0:
1149 1149 ps.add(r)
1150 1150 elif n == 1:
1151 1151 ps.add(cl.parentrevs(r)[0])
1152 1152 elif n == 2:
1153 1153 parents = cl.parentrevs(r)
1154 1154 if len(parents) > 1:
1155 1155 ps.add(parents[1])
1156 1156 return [r for r in subset if r in ps]
1157 1157
1158 1158 def present(repo, subset, x):
1159 1159 """``present(set)``
1160 1160 An empty set, if any revision in set isn't found; otherwise,
1161 1161 all revisions in set.
1162 1162
1163 1163 If any of specified revisions is not present in the local repository,
1164 1164 the query is normally aborted. But this predicate allows the query
1165 1165 to continue even in such cases.
1166 1166 """
1167 1167 try:
1168 1168 return getset(repo, subset, x)
1169 1169 except error.RepoLookupError:
1170 1170 return []
1171 1171
1172 1172 def public(repo, subset, x):
1173 1173 """``public()``
1174 1174 Changeset in public phase."""
1175 1175 # i18n: "public" is a keyword
1176 1176 getargs(x, 0, 0, _("public takes no arguments"))
1177 1177 pc = repo._phasecache
1178 1178 return [r for r in subset if pc.phase(repo, r) == phases.public]
1179 1179
1180 1180 def remote(repo, subset, x):
1181 1181 """``remote([id [,path]])``
1182 1182 Local revision that corresponds to the given identifier in a
1183 1183 remote repository, if present. Here, the '.' identifier is a
1184 1184 synonym for the current local branch.
1185 1185 """
1186 1186
1187 1187 import hg # avoid start-up nasties
1188 1188 # i18n: "remote" is a keyword
1189 1189 l = getargs(x, 0, 2, _("remote takes one, two or no arguments"))
1190 1190
1191 1191 q = '.'
1192 1192 if len(l) > 0:
1193 1193 # i18n: "remote" is a keyword
1194 1194 q = getstring(l[0], _("remote requires a string id"))
1195 1195 if q == '.':
1196 1196 q = repo['.'].branch()
1197 1197
1198 1198 dest = ''
1199 1199 if len(l) > 1:
1200 1200 # i18n: "remote" is a keyword
1201 1201 dest = getstring(l[1], _("remote requires a repository path"))
1202 1202 dest = repo.ui.expandpath(dest or 'default')
1203 1203 dest, branches = hg.parseurl(dest)
1204 1204 revs, checkout = hg.addbranchrevs(repo, repo, branches, [])
1205 1205 if revs:
1206 1206 revs = [repo.lookup(rev) for rev in revs]
1207 1207 other = hg.peer(repo, {}, dest)
1208 1208 n = other.lookup(q)
1209 1209 if n in repo:
1210 1210 r = repo[n].rev()
1211 1211 if r in subset:
1212 1212 return [r]
1213 1213 return []
1214 1214
1215 1215 def removes(repo, subset, x):
1216 1216 """``removes(pattern)``
1217 1217 Changesets which remove files matching pattern.
1218 1218 """
1219 1219 # i18n: "removes" is a keyword
1220 1220 pat = getstring(x, _("removes requires a pattern"))
1221 1221 return checkstatus(repo, subset, pat, 2)
1222 1222
1223 1223 def rev(repo, subset, x):
1224 1224 """``rev(number)``
1225 1225 Revision with the given numeric identifier.
1226 1226 """
1227 1227 # i18n: "rev" is a keyword
1228 1228 l = getargs(x, 1, 1, _("rev requires one argument"))
1229 1229 try:
1230 1230 # i18n: "rev" is a keyword
1231 1231 l = int(getstring(l[0], _("rev requires a number")))
1232 1232 except (TypeError, ValueError):
1233 1233 # i18n: "rev" is a keyword
1234 1234 raise error.ParseError(_("rev expects a number"))
1235 1235 return [r for r in subset if r == l]
1236 1236
1237 1237 def matching(repo, subset, x):
1238 1238 """``matching(revision [, field])``
1239 1239 Changesets in which a given set of fields match the set of fields in the
1240 1240 selected revision or set.
1241 1241
1242 1242 To match more than one field pass the list of fields to match separated
1243 1243 by spaces (e.g. ``author description``).
1244 1244
1245 1245 Valid fields are most regular revision fields and some special fields.
1246 1246
1247 1247 Regular revision fields are ``description``, ``author``, ``branch``,
1248 1248 ``date``, ``files``, ``phase``, ``parents``, ``substate``, ``user``
1249 1249 and ``diff``.
1250 1250 Note that ``author`` and ``user`` are synonyms. ``diff`` refers to the
1251 1251 contents of the revision. Two revisions matching their ``diff`` will
1252 1252 also match their ``files``.
1253 1253
1254 1254 Special fields are ``summary`` and ``metadata``:
1255 1255 ``summary`` matches the first line of the description.
1256 1256 ``metadata`` is equivalent to matching ``description user date``
1257 1257 (i.e. it matches the main metadata fields).
1258 1258
1259 1259 ``metadata`` is the default field which is used when no fields are
1260 1260 specified. You can match more than one field at a time.
1261 1261 """
1262 1262 # i18n: "matching" is a keyword
1263 1263 l = getargs(x, 1, 2, _("matching takes 1 or 2 arguments"))
1264 1264
1265 1265 revs = getset(repo, repo.changelog, l[0])
1266 1266
1267 1267 fieldlist = ['metadata']
1268 1268 if len(l) > 1:
1269 1269 fieldlist = getstring(l[1],
1270 1270 # i18n: "matching" is a keyword
1271 1271 _("matching requires a string "
1272 1272 "as its second argument")).split()
1273 1273
1274 1274 # Make sure that there are no repeated fields,
1275 1275 # expand the 'special' 'metadata' field type
1276 1276 # and check the 'files' whenever we check the 'diff'
1277 1277 fields = []
1278 1278 for field in fieldlist:
1279 1279 if field == 'metadata':
1280 1280 fields += ['user', 'description', 'date']
1281 1281 elif field == 'diff':
1282 1282 # a revision matching the diff must also match the files
1283 1283 # since matching the diff is very costly, make sure to
1284 1284 # also match the files first
1285 1285 fields += ['files', 'diff']
1286 1286 else:
1287 1287 if field == 'author':
1288 1288 field = 'user'
1289 1289 fields.append(field)
1290 1290 fields = set(fields)
1291 1291 if 'summary' in fields and 'description' in fields:
1292 1292 # If a revision matches its description it also matches its summary
1293 1293 fields.discard('summary')
1294 1294
1295 1295 # We may want to match more than one field
1296 1296 # Not all fields take the same amount of time to be matched
1297 1297 # Sort the selected fields in order of increasing matching cost
1298 1298 fieldorder = ['phase', 'parents', 'user', 'date', 'branch', 'summary',
1299 1299 'files', 'description', 'substate', 'diff']
1300 1300 def fieldkeyfunc(f):
1301 1301 try:
1302 1302 return fieldorder.index(f)
1303 1303 except ValueError:
1304 1304 # assume an unknown field is very costly
1305 1305 return len(fieldorder)
1306 1306 fields = list(fields)
1307 1307 fields.sort(key=fieldkeyfunc)
1308 1308
1309 1309 # Each field will be matched with its own "getfield" function
1310 1310 # which will be added to the getfieldfuncs array of functions
1311 1311 getfieldfuncs = []
1312 1312 _funcs = {
1313 1313 'user': lambda r: repo[r].user(),
1314 1314 'branch': lambda r: repo[r].branch(),
1315 1315 'date': lambda r: repo[r].date(),
1316 1316 'description': lambda r: repo[r].description(),
1317 1317 'files': lambda r: repo[r].files(),
1318 1318 'parents': lambda r: repo[r].parents(),
1319 1319 'phase': lambda r: repo[r].phase(),
1320 1320 'substate': lambda r: repo[r].substate,
1321 1321 'summary': lambda r: repo[r].description().splitlines()[0],
1322 1322 'diff': lambda r: list(repo[r].diff(git=True),)
1323 1323 }
1324 1324 for info in fields:
1325 1325 getfield = _funcs.get(info, None)
1326 1326 if getfield is None:
1327 1327 raise error.ParseError(
1328 1328 # i18n: "matching" is a keyword
1329 1329 _("unexpected field name passed to matching: %s") % info)
1330 1330 getfieldfuncs.append(getfield)
1331 1331 # convert the getfield array of functions into a "getinfo" function
1332 1332 # which returns an array of field values (or a single value if there
1333 1333 # is only one field to match)
1334 1334 getinfo = lambda r: [f(r) for f in getfieldfuncs]
1335 1335
1336 1336 matches = set()
1337 1337 for rev in revs:
1338 1338 target = getinfo(rev)
1339 1339 for r in subset:
1340 1340 match = True
1341 1341 for n, f in enumerate(getfieldfuncs):
1342 1342 if target[n] != f(r):
1343 1343 match = False
1344 1344 break
1345 1345 if match:
1346 1346 matches.add(r)
1347 1347 return [r for r in subset if r in matches]
1348 1348
1349 1349 def reverse(repo, subset, x):
1350 1350 """``reverse(set)``
1351 1351 Reverse order of set.
1352 1352 """
1353 1353 l = getset(repo, subset, x)
1354 1354 if not isinstance(l, list):
1355 1355 l = list(l)
1356 1356 l.reverse()
1357 1357 return l
1358 1358
1359 1359 def roots(repo, subset, x):
1360 1360 """``roots(set)``
1361 1361 Changesets in set with no parent changeset in set.
1362 1362 """
1363 1363 s = set(getset(repo, repo.changelog, x))
1364 1364 subset = [r for r in subset if r in s]
1365 1365 cs = _children(repo, subset, s)
1366 1366 return [r for r in subset if r not in cs]
1367 1367
1368 1368 def secret(repo, subset, x):
1369 1369 """``secret()``
1370 1370 Changeset in secret phase."""
1371 1371 # i18n: "secret" is a keyword
1372 1372 getargs(x, 0, 0, _("secret takes no arguments"))
1373 1373 pc = repo._phasecache
1374 1374 return [r for r in subset if pc.phase(repo, r) == phases.secret]
1375 1375
1376 1376 def sort(repo, subset, x):
1377 1377 """``sort(set[, [-]key...])``
1378 1378 Sort set by keys. The default sort order is ascending, specify a key
1379 1379 as ``-key`` to sort in descending order.
1380 1380
1381 1381 The keys can be:
1382 1382
1383 1383 - ``rev`` for the revision number,
1384 1384 - ``branch`` for the branch name,
1385 1385 - ``desc`` for the commit message (description),
1386 1386 - ``user`` for user name (``author`` can be used as an alias),
1387 1387 - ``date`` for the commit date
1388 1388 """
1389 1389 # i18n: "sort" is a keyword
1390 1390 l = getargs(x, 1, 2, _("sort requires one or two arguments"))
1391 1391 keys = "rev"
1392 1392 if len(l) == 2:
1393 1393 # i18n: "sort" is a keyword
1394 1394 keys = getstring(l[1], _("sort spec must be a string"))
1395 1395
1396 1396 s = l[0]
1397 1397 keys = keys.split()
1398 1398 l = []
1399 1399 def invert(s):
1400 1400 return "".join(chr(255 - ord(c)) for c in s)
1401 1401 for r in getset(repo, subset, s):
1402 1402 c = repo[r]
1403 1403 e = []
1404 1404 for k in keys:
1405 1405 if k == 'rev':
1406 1406 e.append(r)
1407 1407 elif k == '-rev':
1408 1408 e.append(-r)
1409 1409 elif k == 'branch':
1410 1410 e.append(c.branch())
1411 1411 elif k == '-branch':
1412 1412 e.append(invert(c.branch()))
1413 1413 elif k == 'desc':
1414 1414 e.append(c.description())
1415 1415 elif k == '-desc':
1416 1416 e.append(invert(c.description()))
1417 1417 elif k in 'user author':
1418 1418 e.append(c.user())
1419 1419 elif k in '-user -author':
1420 1420 e.append(invert(c.user()))
1421 1421 elif k == 'date':
1422 1422 e.append(c.date()[0])
1423 1423 elif k == '-date':
1424 1424 e.append(-c.date()[0])
1425 1425 else:
1426 1426 raise error.ParseError(_("unknown sort key %r") % k)
1427 1427 e.append(r)
1428 1428 l.append(e)
1429 1429 l.sort()
1430 1430 return [e[-1] for e in l]
1431 1431
1432 1432 def _stringmatcher(pattern):
1433 1433 """
1434 1434 accepts a string, possibly starting with 're:' or 'literal:' prefix.
1435 1435 returns the matcher name, pattern, and matcher function.
1436 1436 missing or unknown prefixes are treated as literal matches.
1437 1437
1438 1438 helper for tests:
1439 1439 >>> def test(pattern, *tests):
1440 1440 ... kind, pattern, matcher = _stringmatcher(pattern)
1441 1441 ... return (kind, pattern, [bool(matcher(t)) for t in tests])
1442 1442
1443 1443 exact matching (no prefix):
1444 1444 >>> test('abcdefg', 'abc', 'def', 'abcdefg')
1445 1445 ('literal', 'abcdefg', [False, False, True])
1446 1446
1447 1447 regex matching ('re:' prefix)
1448 1448 >>> test('re:a.+b', 'nomatch', 'fooadef', 'fooadefbar')
1449 1449 ('re', 'a.+b', [False, False, True])
1450 1450
1451 1451 force exact matches ('literal:' prefix)
1452 1452 >>> test('literal:re:foobar', 'foobar', 're:foobar')
1453 1453 ('literal', 're:foobar', [False, True])
1454 1454
1455 1455 unknown prefixes are ignored and treated as literals
1456 1456 >>> test('foo:bar', 'foo', 'bar', 'foo:bar')
1457 1457 ('literal', 'foo:bar', [False, False, True])
1458 1458 """
1459 1459 if pattern.startswith('re:'):
1460 1460 pattern = pattern[3:]
1461 1461 try:
1462 1462 regex = re.compile(pattern)
1463 1463 except re.error, e:
1464 1464 raise error.ParseError(_('invalid regular expression: %s')
1465 1465 % e)
1466 1466 return 're', pattern, regex.search
1467 1467 elif pattern.startswith('literal:'):
1468 1468 pattern = pattern[8:]
1469 1469 return 'literal', pattern, pattern.__eq__
1470 1470
1471 1471 def _substringmatcher(pattern):
1472 1472 kind, pattern, matcher = _stringmatcher(pattern)
1473 1473 if kind == 'literal':
1474 1474 matcher = lambda s: pattern in s
1475 1475 return kind, pattern, matcher
1476 1476
1477 1477 def tag(repo, subset, x):
1478 1478 """``tag([name])``
1479 1479 The specified tag by name, or all tagged revisions if no name is given.
1480 1480 """
1481 1481 # i18n: "tag" is a keyword
1482 1482 args = getargs(x, 0, 1, _("tag takes one or no arguments"))
1483 1483 cl = repo.changelog
1484 1484 if args:
1485 1485 pattern = getstring(args[0],
1486 1486 # i18n: "tag" is a keyword
1487 1487 _('the argument to tag must be a string'))
1488 1488 kind, pattern, matcher = _stringmatcher(pattern)
1489 1489 if kind == 'literal':
1490 1490 # avoid resolving all tags
1491 1491 tn = repo._tagscache.tags.get(pattern, None)
1492 1492 if tn is None:
1493 1493 raise util.Abort(_("tag '%s' does not exist") % pattern)
1494 1494 s = set([repo[tn].rev()])
1495 1495 else:
1496 1496 s = set([cl.rev(n) for t, n in repo.tagslist() if matcher(t)])
1497 1497 else:
1498 1498 s = set([cl.rev(n) for t, n in repo.tagslist() if t != 'tip'])
1499 1499 return [r for r in subset if r in s]
1500 1500
1501 1501 def tagged(repo, subset, x):
1502 1502 return tag(repo, subset, x)
1503 1503
1504 1504 def unstable(repo, subset, x):
1505 1505 """``unstable()``
1506 1506 Non-obsolete changesets with obsolete ancestors.
1507 1507 """
1508 1508 # i18n: "unstable" is a keyword
1509 1509 getargs(x, 0, 0, _("unstable takes no arguments"))
1510 1510 unstables = obsmod.getrevs(repo, 'unstable')
1511 1511 return [r for r in subset if r in unstables]
1512 1512
1513 1513
1514 1514 def user(repo, subset, x):
1515 1515 """``user(string)``
1516 1516 User name contains string. The match is case-insensitive.
1517 1517
1518 1518 If `string` starts with `re:`, the remainder of the string is treated as
1519 1519 a regular expression. To match a user that actually contains `re:`, use
1520 1520 the prefix `literal:`.
1521 1521 """
1522 1522 return author(repo, subset, x)
1523 1523
1524 1524 # for internal use
1525 1525 def _list(repo, subset, x):
1526 1526 s = getstring(x, "internal error")
1527 1527 if not s:
1528 1528 return []
1529 1529 if not isinstance(subset, set):
1530 1530 subset = set(subset)
1531 1531 ls = [repo[r].rev() for r in s.split('\0')]
1532 1532 return [r for r in ls if r in subset]
1533 1533
1534 1534 symbols = {
1535 1535 "adds": adds,
1536 1536 "all": getall,
1537 1537 "ancestor": ancestor,
1538 1538 "ancestors": ancestors,
1539 1539 "_firstancestors": _firstancestors,
1540 1540 "author": author,
1541 1541 "bisect": bisect,
1542 1542 "bisected": bisected,
1543 1543 "bookmark": bookmark,
1544 1544 "branch": branch,
1545 1545 "branchpoint": branchpoint,
1546 1546 "bumped": bumped,
1547 1547 "bundle": bundle,
1548 1548 "children": children,
1549 1549 "closed": closed,
1550 1550 "contains": contains,
1551 1551 "converted": converted,
1552 1552 "date": date,
1553 1553 "desc": desc,
1554 1554 "descendants": descendants,
1555 1555 "_firstdescendants": _firstdescendants,
1556 1556 "destination": destination,
1557 1557 "divergent": divergent,
1558 1558 "draft": draft,
1559 1559 "extinct": extinct,
1560 1560 "extra": extra,
1561 1561 "file": hasfile,
1562 1562 "filelog": filelog,
1563 1563 "first": first,
1564 1564 "follow": follow,
1565 1565 "_followfirst": _followfirst,
1566 1566 "grep": grep,
1567 1567 "head": head,
1568 1568 "heads": heads,
1569 1569 "hidden": hidden,
1570 1570 "id": node_,
1571 1571 "keyword": keyword,
1572 1572 "last": last,
1573 1573 "limit": limit,
1574 1574 "_matchfiles": _matchfiles,
1575 1575 "max": maxrev,
1576 1576 "merge": merge,
1577 1577 "min": minrev,
1578 1578 "modifies": modifies,
1579 1579 "obsolete": obsolete,
1580 1580 "origin": origin,
1581 1581 "outgoing": outgoing,
1582 1582 "p1": p1,
1583 1583 "p2": p2,
1584 1584 "parents": parents,
1585 1585 "present": present,
1586 1586 "public": public,
1587 1587 "remote": remote,
1588 1588 "removes": removes,
1589 1589 "rev": rev,
1590 1590 "reverse": reverse,
1591 1591 "roots": roots,
1592 1592 "sort": sort,
1593 1593 "secret": secret,
1594 1594 "matching": matching,
1595 1595 "tag": tag,
1596 1596 "tagged": tagged,
1597 1597 "user": user,
1598 1598 "unstable": unstable,
1599 1599 "_list": _list,
1600 1600 }
1601 1601
1602 1602 # symbols which can't be used for a DoS attack for any given input
1603 1603 # (e.g. those which accept regexes as plain strings shouldn't be included)
1604 1604 # functions that just return a lot of changesets (like all) don't count here
1605 1605 safesymbols = set([
1606 1606 "adds",
1607 1607 "all",
1608 1608 "ancestor",
1609 1609 "ancestors",
1610 1610 "_firstancestors",
1611 1611 "author",
1612 1612 "bisect",
1613 1613 "bisected",
1614 1614 "bookmark",
1615 1615 "branch",
1616 1616 "branchpoint",
1617 1617 "bumped",
1618 1618 "bundle",
1619 1619 "children",
1620 1620 "closed",
1621 1621 "converted",
1622 1622 "date",
1623 1623 "desc",
1624 1624 "descendants",
1625 1625 "_firstdescendants",
1626 1626 "destination",
1627 1627 "divergent",
1628 1628 "draft",
1629 1629 "extinct",
1630 1630 "extra",
1631 1631 "file",
1632 1632 "filelog",
1633 1633 "first",
1634 1634 "follow",
1635 1635 "_followfirst",
1636 1636 "head",
1637 1637 "heads",
1638 1638 "hidden",
1639 1639 "id",
1640 1640 "keyword",
1641 1641 "last",
1642 1642 "limit",
1643 1643 "_matchfiles",
1644 1644 "max",
1645 1645 "merge",
1646 1646 "min",
1647 1647 "modifies",
1648 1648 "obsolete",
1649 1649 "origin",
1650 1650 "outgoing",
1651 1651 "p1",
1652 1652 "p2",
1653 1653 "parents",
1654 1654 "present",
1655 1655 "public",
1656 1656 "remote",
1657 1657 "removes",
1658 1658 "rev",
1659 1659 "reverse",
1660 1660 "roots",
1661 1661 "sort",
1662 1662 "secret",
1663 1663 "matching",
1664 1664 "tag",
1665 1665 "tagged",
1666 1666 "user",
1667 1667 "unstable",
1668 1668 "_list",
1669 1669 ])
1670 1670
1671 1671 methods = {
1672 1672 "range": rangeset,
1673 1673 "dagrange": dagrange,
1674 1674 "string": stringset,
1675 1675 "symbol": symbolset,
1676 1676 "and": andset,
1677 1677 "or": orset,
1678 1678 "not": notset,
1679 1679 "list": listset,
1680 1680 "func": func,
1681 1681 "ancestor": ancestorspec,
1682 1682 "parent": parentspec,
1683 1683 "parentpost": p1,
1684 1684 }
1685 1685
1686 1686 def optimize(x, small):
1687 1687 if x is None:
1688 1688 return 0, x
1689 1689
1690 1690 smallbonus = 1
1691 1691 if small:
1692 1692 smallbonus = .5
1693 1693
1694 1694 op = x[0]
1695 1695 if op == 'minus':
1696 1696 return optimize(('and', x[1], ('not', x[2])), small)
1697 1697 elif op == 'dagrangepre':
1698 1698 return optimize(('func', ('symbol', 'ancestors'), x[1]), small)
1699 1699 elif op == 'dagrangepost':
1700 1700 return optimize(('func', ('symbol', 'descendants'), x[1]), small)
1701 1701 elif op == 'rangepre':
1702 1702 return optimize(('range', ('string', '0'), x[1]), small)
1703 1703 elif op == 'rangepost':
1704 1704 return optimize(('range', x[1], ('string', 'tip')), small)
1705 1705 elif op == 'negate':
1706 1706 return optimize(('string',
1707 1707 '-' + getstring(x[1], _("can't negate that"))), small)
1708 1708 elif op in 'string symbol negate':
1709 1709 return smallbonus, x # single revisions are small
1710 1710 elif op == 'and':
1711 1711 wa, ta = optimize(x[1], True)
1712 1712 wb, tb = optimize(x[2], True)
1713 1713 w = min(wa, wb)
1714 1714 if wa > wb:
1715 1715 return w, (op, tb, ta)
1716 1716 return w, (op, ta, tb)
1717 1717 elif op == 'or':
1718 1718 wa, ta = optimize(x[1], False)
1719 1719 wb, tb = optimize(x[2], False)
1720 1720 if wb < wa:
1721 1721 wb, wa = wa, wb
1722 1722 return max(wa, wb), (op, ta, tb)
1723 1723 elif op == 'not':
1724 1724 o = optimize(x[1], not small)
1725 1725 return o[0], (op, o[1])
1726 1726 elif op == 'parentpost':
1727 1727 o = optimize(x[1], small)
1728 1728 return o[0], (op, o[1])
1729 1729 elif op == 'group':
1730 1730 return optimize(x[1], small)
1731 1731 elif op in 'dagrange range list parent ancestorspec':
1732 1732 if op == 'parent':
1733 1733 # x^:y means (x^) : y, not x ^ (:y)
1734 1734 post = ('parentpost', x[1])
1735 1735 if x[2][0] == 'dagrangepre':
1736 1736 return optimize(('dagrange', post, x[2][1]), small)
1737 1737 elif x[2][0] == 'rangepre':
1738 1738 return optimize(('range', post, x[2][1]), small)
1739 1739
1740 1740 wa, ta = optimize(x[1], small)
1741 1741 wb, tb = optimize(x[2], small)
1742 1742 return wa + wb, (op, ta, tb)
1743 1743 elif op == 'func':
1744 1744 f = getstring(x[1], _("not a symbol"))
1745 1745 wa, ta = optimize(x[2], small)
1746 1746 if f in ("author branch closed date desc file grep keyword "
1747 1747 "outgoing user"):
1748 1748 w = 10 # slow
1749 1749 elif f in "modifies adds removes":
1750 1750 w = 30 # slower
1751 1751 elif f == "contains":
1752 1752 w = 100 # very slow
1753 1753 elif f == "ancestor":
1754 1754 w = 1 * smallbonus
1755 1755 elif f in "reverse limit first":
1756 1756 w = 0
1757 1757 elif f in "sort":
1758 1758 w = 10 # assume most sorts look at changelog
1759 1759 else:
1760 1760 w = 1
1761 1761 return w + wa, (op, x[1], ta)
1762 1762 return 1, x
1763 1763
1764 1764 _aliasarg = ('func', ('symbol', '_aliasarg'))
1765 1765 def _getaliasarg(tree):
1766 1766 """If tree matches ('func', ('symbol', '_aliasarg'), ('string', X))
1767 1767 return X, None otherwise.
1768 1768 """
1769 1769 if (len(tree) == 3 and tree[:2] == _aliasarg
1770 1770 and tree[2][0] == 'string'):
1771 1771 return tree[2][1]
1772 1772 return None
1773 1773
1774 1774 def _checkaliasarg(tree, known=None):
1775 1775 """Check tree contains no _aliasarg construct or only ones which
1776 1776 value is in known. Used to avoid alias placeholders injection.
1777 1777 """
1778 1778 if isinstance(tree, tuple):
1779 1779 arg = _getaliasarg(tree)
1780 1780 if arg is not None and (not known or arg not in known):
1781 1781 raise error.ParseError(_("not a function: %s") % '_aliasarg')
1782 1782 for t in tree:
1783 1783 _checkaliasarg(t, known)
1784 1784
1785 1785 class revsetalias(object):
1786 1786 funcre = re.compile('^([^(]+)\(([^)]+)\)$')
1787 1787 args = None
1788 1788
1789 1789 def __init__(self, name, value):
1790 1790 '''Aliases like:
1791 1791
1792 1792 h = heads(default)
1793 1793 b($1) = ancestors($1) - ancestors(default)
1794 1794 '''
1795 1795 m = self.funcre.search(name)
1796 1796 if m:
1797 1797 self.name = m.group(1)
1798 1798 self.tree = ('func', ('symbol', m.group(1)))
1799 1799 self.args = [x.strip() for x in m.group(2).split(',')]
1800 1800 for arg in self.args:
1801 1801 # _aliasarg() is an unknown symbol only used separate
1802 1802 # alias argument placeholders from regular strings.
1803 1803 value = value.replace(arg, '_aliasarg(%r)' % (arg,))
1804 1804 else:
1805 1805 self.name = name
1806 1806 self.tree = ('symbol', name)
1807 1807
1808 1808 self.replacement, pos = parse(value)
1809 1809 if pos != len(value):
1810 1810 raise error.ParseError(_('invalid token'), pos)
1811 1811 # Check for placeholder injection
1812 1812 _checkaliasarg(self.replacement, self.args)
1813 1813
1814 1814 def _getalias(aliases, tree):
1815 1815 """If tree looks like an unexpanded alias, return it. Return None
1816 1816 otherwise.
1817 1817 """
1818 1818 if isinstance(tree, tuple) and tree:
1819 1819 if tree[0] == 'symbol' and len(tree) == 2:
1820 1820 name = tree[1]
1821 1821 alias = aliases.get(name)
1822 1822 if alias and alias.args is None and alias.tree == tree:
1823 1823 return alias
1824 1824 if tree[0] == 'func' and len(tree) > 1:
1825 1825 if tree[1][0] == 'symbol' and len(tree[1]) == 2:
1826 1826 name = tree[1][1]
1827 1827 alias = aliases.get(name)
1828 1828 if alias and alias.args is not None and alias.tree == tree[:2]:
1829 1829 return alias
1830 1830 return None
1831 1831
1832 1832 def _expandargs(tree, args):
1833 1833 """Replace _aliasarg instances with the substitution value of the
1834 1834 same name in args, recursively.
1835 1835 """
1836 1836 if not tree or not isinstance(tree, tuple):
1837 1837 return tree
1838 1838 arg = _getaliasarg(tree)
1839 1839 if arg is not None:
1840 1840 return args[arg]
1841 1841 return tuple(_expandargs(t, args) for t in tree)
1842 1842
1843 1843 def _expandaliases(aliases, tree, expanding, cache):
1844 1844 """Expand aliases in tree, recursively.
1845 1845
1846 1846 'aliases' is a dictionary mapping user defined aliases to
1847 1847 revsetalias objects.
1848 1848 """
1849 1849 if not isinstance(tree, tuple):
1850 1850 # Do not expand raw strings
1851 1851 return tree
1852 1852 alias = _getalias(aliases, tree)
1853 1853 if alias is not None:
1854 1854 if alias in expanding:
1855 1855 raise error.ParseError(_('infinite expansion of revset alias "%s" '
1856 1856 'detected') % alias.name)
1857 1857 expanding.append(alias)
1858 1858 if alias.name not in cache:
1859 1859 cache[alias.name] = _expandaliases(aliases, alias.replacement,
1860 1860 expanding, cache)
1861 1861 result = cache[alias.name]
1862 1862 expanding.pop()
1863 1863 if alias.args is not None:
1864 1864 l = getlist(tree[2])
1865 1865 if len(l) != len(alias.args):
1866 1866 raise error.ParseError(
1867 1867 _('invalid number of arguments: %s') % len(l))
1868 1868 l = [_expandaliases(aliases, a, [], cache) for a in l]
1869 1869 result = _expandargs(result, dict(zip(alias.args, l)))
1870 1870 else:
1871 1871 result = tuple(_expandaliases(aliases, t, expanding, cache)
1872 1872 for t in tree)
1873 1873 return result
1874 1874
1875 1875 def findaliases(ui, tree):
1876 1876 _checkaliasarg(tree)
1877 1877 aliases = {}
1878 1878 for k, v in ui.configitems('revsetalias'):
1879 1879 alias = revsetalias(k, v)
1880 1880 aliases[alias.name] = alias
1881 1881 return _expandaliases(aliases, tree, [], {})
1882 1882
1883 parse = parser.parser(tokenize, elements).parse
1883 def parse(spec):
1884 p = parser.parser(tokenize, elements)
1885 return p.parse(spec)
1884 1886
1885 1887 def match(ui, spec):
1886 1888 if not spec:
1887 1889 raise error.ParseError(_("empty query"))
1888 1890 tree, pos = parse(spec)
1889 1891 if (pos != len(spec)):
1890 1892 raise error.ParseError(_("invalid token"), pos)
1891 1893 if ui:
1892 1894 tree = findaliases(ui, tree)
1893 1895 weight, tree = optimize(tree, True)
1894 1896 def mfunc(repo, subset):
1895 1897 return getset(repo, subset, tree)
1896 1898 return mfunc
1897 1899
1898 1900 def formatspec(expr, *args):
1899 1901 '''
1900 1902 This is a convenience function for using revsets internally, and
1901 1903 escapes arguments appropriately. Aliases are intentionally ignored
1902 1904 so that intended expression behavior isn't accidentally subverted.
1903 1905
1904 1906 Supported arguments:
1905 1907
1906 1908 %r = revset expression, parenthesized
1907 1909 %d = int(arg), no quoting
1908 1910 %s = string(arg), escaped and single-quoted
1909 1911 %b = arg.branch(), escaped and single-quoted
1910 1912 %n = hex(arg), single-quoted
1911 1913 %% = a literal '%'
1912 1914
1913 1915 Prefixing the type with 'l' specifies a parenthesized list of that type.
1914 1916
1915 1917 >>> formatspec('%r:: and %lr', '10 or 11', ("this()", "that()"))
1916 1918 '(10 or 11):: and ((this()) or (that()))'
1917 1919 >>> formatspec('%d:: and not %d::', 10, 20)
1918 1920 '10:: and not 20::'
1919 1921 >>> formatspec('%ld or %ld', [], [1])
1920 1922 "_list('') or 1"
1921 1923 >>> formatspec('keyword(%s)', 'foo\\xe9')
1922 1924 "keyword('foo\\\\xe9')"
1923 1925 >>> b = lambda: 'default'
1924 1926 >>> b.branch = b
1925 1927 >>> formatspec('branch(%b)', b)
1926 1928 "branch('default')"
1927 1929 >>> formatspec('root(%ls)', ['a', 'b', 'c', 'd'])
1928 1930 "root(_list('a\\x00b\\x00c\\x00d'))"
1929 1931 '''
1930 1932
1931 1933 def quote(s):
1932 1934 return repr(str(s))
1933 1935
1934 1936 def argtype(c, arg):
1935 1937 if c == 'd':
1936 1938 return str(int(arg))
1937 1939 elif c == 's':
1938 1940 return quote(arg)
1939 1941 elif c == 'r':
1940 1942 parse(arg) # make sure syntax errors are confined
1941 1943 return '(%s)' % arg
1942 1944 elif c == 'n':
1943 1945 return quote(node.hex(arg))
1944 1946 elif c == 'b':
1945 1947 return quote(arg.branch())
1946 1948
1947 1949 def listexp(s, t):
1948 1950 l = len(s)
1949 1951 if l == 0:
1950 1952 return "_list('')"
1951 1953 elif l == 1:
1952 1954 return argtype(t, s[0])
1953 1955 elif t == 'd':
1954 1956 return "_list('%s')" % "\0".join(str(int(a)) for a in s)
1955 1957 elif t == 's':
1956 1958 return "_list('%s')" % "\0".join(s)
1957 1959 elif t == 'n':
1958 1960 return "_list('%s')" % "\0".join(node.hex(a) for a in s)
1959 1961 elif t == 'b':
1960 1962 return "_list('%s')" % "\0".join(a.branch() for a in s)
1961 1963
1962 1964 m = l // 2
1963 1965 return '(%s or %s)' % (listexp(s[:m], t), listexp(s[m:], t))
1964 1966
1965 1967 ret = ''
1966 1968 pos = 0
1967 1969 arg = 0
1968 1970 while pos < len(expr):
1969 1971 c = expr[pos]
1970 1972 if c == '%':
1971 1973 pos += 1
1972 1974 d = expr[pos]
1973 1975 if d == '%':
1974 1976 ret += d
1975 1977 elif d in 'dsnbr':
1976 1978 ret += argtype(d, args[arg])
1977 1979 arg += 1
1978 1980 elif d == 'l':
1979 1981 # a list of some type
1980 1982 pos += 1
1981 1983 d = expr[pos]
1982 1984 ret += listexp(list(args[arg]), d)
1983 1985 arg += 1
1984 1986 else:
1985 1987 raise util.Abort('unexpected revspec format character %s' % d)
1986 1988 else:
1987 1989 ret += c
1988 1990 pos += 1
1989 1991
1990 1992 return ret
1991 1993
1992 1994 def prettyformat(tree):
1993 1995 def _prettyformat(tree, level, lines):
1994 1996 if not isinstance(tree, tuple) or tree[0] in ('string', 'symbol'):
1995 1997 lines.append((level, str(tree)))
1996 1998 else:
1997 1999 lines.append((level, '(%s' % tree[0]))
1998 2000 for s in tree[1:]:
1999 2001 _prettyformat(s, level + 1, lines)
2000 2002 lines[-1:] = [(lines[-1][0], lines[-1][1] + ')')]
2001 2003
2002 2004 lines = []
2003 2005 _prettyformat(tree, 0, lines)
2004 2006 output = '\n'.join((' '*l + s) for l, s in lines)
2005 2007 return output
2006 2008
2007 2009 def depth(tree):
2008 2010 if isinstance(tree, tuple):
2009 2011 return max(map(depth, tree)) + 1
2010 2012 else:
2011 2013 return 0
2012 2014
2013 2015 def funcsused(tree):
2014 2016 if not isinstance(tree, tuple) or tree[0] in ('string', 'symbol'):
2015 2017 return set()
2016 2018 else:
2017 2019 funcs = set()
2018 2020 for s in tree[1:]:
2019 2021 funcs |= funcsused(s)
2020 2022 if tree[0] == 'func':
2021 2023 funcs.add(tree[1][1])
2022 2024 return funcs
2023 2025
2024 2026 # tell hggettext to extract docstrings from these functions:
2025 2027 i18nfunctions = symbols.values()
General Comments 0
You need to be logged in to leave comments. Login now