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