##// END OF EJS Templates
revset: add helper function to get functions used in a revset parse tree...
Alexander Plavin -
r19720:f0b992a9 default
parent child Browse files
Show More
@@ -1,1945 +1,1956
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 methods = {
1603 1603 "range": rangeset,
1604 1604 "dagrange": dagrange,
1605 1605 "string": stringset,
1606 1606 "symbol": symbolset,
1607 1607 "and": andset,
1608 1608 "or": orset,
1609 1609 "not": notset,
1610 1610 "list": listset,
1611 1611 "func": func,
1612 1612 "ancestor": ancestorspec,
1613 1613 "parent": parentspec,
1614 1614 "parentpost": p1,
1615 1615 }
1616 1616
1617 1617 def optimize(x, small):
1618 1618 if x is None:
1619 1619 return 0, x
1620 1620
1621 1621 smallbonus = 1
1622 1622 if small:
1623 1623 smallbonus = .5
1624 1624
1625 1625 op = x[0]
1626 1626 if op == 'minus':
1627 1627 return optimize(('and', x[1], ('not', x[2])), small)
1628 1628 elif op == 'dagrangepre':
1629 1629 return optimize(('func', ('symbol', 'ancestors'), x[1]), small)
1630 1630 elif op == 'dagrangepost':
1631 1631 return optimize(('func', ('symbol', 'descendants'), x[1]), small)
1632 1632 elif op == 'rangepre':
1633 1633 return optimize(('range', ('string', '0'), x[1]), small)
1634 1634 elif op == 'rangepost':
1635 1635 return optimize(('range', x[1], ('string', 'tip')), small)
1636 1636 elif op == 'negate':
1637 1637 return optimize(('string',
1638 1638 '-' + getstring(x[1], _("can't negate that"))), small)
1639 1639 elif op in 'string symbol negate':
1640 1640 return smallbonus, x # single revisions are small
1641 1641 elif op == 'and':
1642 1642 wa, ta = optimize(x[1], True)
1643 1643 wb, tb = optimize(x[2], True)
1644 1644 w = min(wa, wb)
1645 1645 if wa > wb:
1646 1646 return w, (op, tb, ta)
1647 1647 return w, (op, ta, tb)
1648 1648 elif op == 'or':
1649 1649 wa, ta = optimize(x[1], False)
1650 1650 wb, tb = optimize(x[2], False)
1651 1651 if wb < wa:
1652 1652 wb, wa = wa, wb
1653 1653 return max(wa, wb), (op, ta, tb)
1654 1654 elif op == 'not':
1655 1655 o = optimize(x[1], not small)
1656 1656 return o[0], (op, o[1])
1657 1657 elif op == 'parentpost':
1658 1658 o = optimize(x[1], small)
1659 1659 return o[0], (op, o[1])
1660 1660 elif op == 'group':
1661 1661 return optimize(x[1], small)
1662 1662 elif op in 'dagrange range list parent ancestorspec':
1663 1663 if op == 'parent':
1664 1664 # x^:y means (x^) : y, not x ^ (:y)
1665 1665 post = ('parentpost', x[1])
1666 1666 if x[2][0] == 'dagrangepre':
1667 1667 return optimize(('dagrange', post, x[2][1]), small)
1668 1668 elif x[2][0] == 'rangepre':
1669 1669 return optimize(('range', post, x[2][1]), small)
1670 1670
1671 1671 wa, ta = optimize(x[1], small)
1672 1672 wb, tb = optimize(x[2], small)
1673 1673 return wa + wb, (op, ta, tb)
1674 1674 elif op == 'func':
1675 1675 f = getstring(x[1], _("not a symbol"))
1676 1676 wa, ta = optimize(x[2], small)
1677 1677 if f in ("author branch closed date desc file grep keyword "
1678 1678 "outgoing user"):
1679 1679 w = 10 # slow
1680 1680 elif f in "modifies adds removes":
1681 1681 w = 30 # slower
1682 1682 elif f == "contains":
1683 1683 w = 100 # very slow
1684 1684 elif f == "ancestor":
1685 1685 w = 1 * smallbonus
1686 1686 elif f in "reverse limit first":
1687 1687 w = 0
1688 1688 elif f in "sort":
1689 1689 w = 10 # assume most sorts look at changelog
1690 1690 else:
1691 1691 w = 1
1692 1692 return w + wa, (op, x[1], ta)
1693 1693 return 1, x
1694 1694
1695 1695 _aliasarg = ('func', ('symbol', '_aliasarg'))
1696 1696 def _getaliasarg(tree):
1697 1697 """If tree matches ('func', ('symbol', '_aliasarg'), ('string', X))
1698 1698 return X, None otherwise.
1699 1699 """
1700 1700 if (len(tree) == 3 and tree[:2] == _aliasarg
1701 1701 and tree[2][0] == 'string'):
1702 1702 return tree[2][1]
1703 1703 return None
1704 1704
1705 1705 def _checkaliasarg(tree, known=None):
1706 1706 """Check tree contains no _aliasarg construct or only ones which
1707 1707 value is in known. Used to avoid alias placeholders injection.
1708 1708 """
1709 1709 if isinstance(tree, tuple):
1710 1710 arg = _getaliasarg(tree)
1711 1711 if arg is not None and (not known or arg not in known):
1712 1712 raise error.ParseError(_("not a function: %s") % '_aliasarg')
1713 1713 for t in tree:
1714 1714 _checkaliasarg(t, known)
1715 1715
1716 1716 class revsetalias(object):
1717 1717 funcre = re.compile('^([^(]+)\(([^)]+)\)$')
1718 1718 args = None
1719 1719
1720 1720 def __init__(self, name, value):
1721 1721 '''Aliases like:
1722 1722
1723 1723 h = heads(default)
1724 1724 b($1) = ancestors($1) - ancestors(default)
1725 1725 '''
1726 1726 m = self.funcre.search(name)
1727 1727 if m:
1728 1728 self.name = m.group(1)
1729 1729 self.tree = ('func', ('symbol', m.group(1)))
1730 1730 self.args = [x.strip() for x in m.group(2).split(',')]
1731 1731 for arg in self.args:
1732 1732 # _aliasarg() is an unknown symbol only used separate
1733 1733 # alias argument placeholders from regular strings.
1734 1734 value = value.replace(arg, '_aliasarg(%r)' % (arg,))
1735 1735 else:
1736 1736 self.name = name
1737 1737 self.tree = ('symbol', name)
1738 1738
1739 1739 self.replacement, pos = parse(value)
1740 1740 if pos != len(value):
1741 1741 raise error.ParseError(_('invalid token'), pos)
1742 1742 # Check for placeholder injection
1743 1743 _checkaliasarg(self.replacement, self.args)
1744 1744
1745 1745 def _getalias(aliases, tree):
1746 1746 """If tree looks like an unexpanded alias, return it. Return None
1747 1747 otherwise.
1748 1748 """
1749 1749 if isinstance(tree, tuple) and tree:
1750 1750 if tree[0] == 'symbol' and len(tree) == 2:
1751 1751 name = tree[1]
1752 1752 alias = aliases.get(name)
1753 1753 if alias and alias.args is None and alias.tree == tree:
1754 1754 return alias
1755 1755 if tree[0] == 'func' and len(tree) > 1:
1756 1756 if tree[1][0] == 'symbol' and len(tree[1]) == 2:
1757 1757 name = tree[1][1]
1758 1758 alias = aliases.get(name)
1759 1759 if alias and alias.args is not None and alias.tree == tree[:2]:
1760 1760 return alias
1761 1761 return None
1762 1762
1763 1763 def _expandargs(tree, args):
1764 1764 """Replace _aliasarg instances with the substitution value of the
1765 1765 same name in args, recursively.
1766 1766 """
1767 1767 if not tree or not isinstance(tree, tuple):
1768 1768 return tree
1769 1769 arg = _getaliasarg(tree)
1770 1770 if arg is not None:
1771 1771 return args[arg]
1772 1772 return tuple(_expandargs(t, args) for t in tree)
1773 1773
1774 1774 def _expandaliases(aliases, tree, expanding, cache):
1775 1775 """Expand aliases in tree, recursively.
1776 1776
1777 1777 'aliases' is a dictionary mapping user defined aliases to
1778 1778 revsetalias objects.
1779 1779 """
1780 1780 if not isinstance(tree, tuple):
1781 1781 # Do not expand raw strings
1782 1782 return tree
1783 1783 alias = _getalias(aliases, tree)
1784 1784 if alias is not None:
1785 1785 if alias in expanding:
1786 1786 raise error.ParseError(_('infinite expansion of revset alias "%s" '
1787 1787 'detected') % alias.name)
1788 1788 expanding.append(alias)
1789 1789 if alias.name not in cache:
1790 1790 cache[alias.name] = _expandaliases(aliases, alias.replacement,
1791 1791 expanding, cache)
1792 1792 result = cache[alias.name]
1793 1793 expanding.pop()
1794 1794 if alias.args is not None:
1795 1795 l = getlist(tree[2])
1796 1796 if len(l) != len(alias.args):
1797 1797 raise error.ParseError(
1798 1798 _('invalid number of arguments: %s') % len(l))
1799 1799 l = [_expandaliases(aliases, a, [], cache) for a in l]
1800 1800 result = _expandargs(result, dict(zip(alias.args, l)))
1801 1801 else:
1802 1802 result = tuple(_expandaliases(aliases, t, expanding, cache)
1803 1803 for t in tree)
1804 1804 return result
1805 1805
1806 1806 def findaliases(ui, tree):
1807 1807 _checkaliasarg(tree)
1808 1808 aliases = {}
1809 1809 for k, v in ui.configitems('revsetalias'):
1810 1810 alias = revsetalias(k, v)
1811 1811 aliases[alias.name] = alias
1812 1812 return _expandaliases(aliases, tree, [], {})
1813 1813
1814 1814 parse = parser.parser(tokenize, elements).parse
1815 1815
1816 1816 def match(ui, spec):
1817 1817 if not spec:
1818 1818 raise error.ParseError(_("empty query"))
1819 1819 tree, pos = parse(spec)
1820 1820 if (pos != len(spec)):
1821 1821 raise error.ParseError(_("invalid token"), pos)
1822 1822 if ui:
1823 1823 tree = findaliases(ui, tree)
1824 1824 weight, tree = optimize(tree, True)
1825 1825 def mfunc(repo, subset):
1826 1826 return getset(repo, subset, tree)
1827 1827 return mfunc
1828 1828
1829 1829 def formatspec(expr, *args):
1830 1830 '''
1831 1831 This is a convenience function for using revsets internally, and
1832 1832 escapes arguments appropriately. Aliases are intentionally ignored
1833 1833 so that intended expression behavior isn't accidentally subverted.
1834 1834
1835 1835 Supported arguments:
1836 1836
1837 1837 %r = revset expression, parenthesized
1838 1838 %d = int(arg), no quoting
1839 1839 %s = string(arg), escaped and single-quoted
1840 1840 %b = arg.branch(), escaped and single-quoted
1841 1841 %n = hex(arg), single-quoted
1842 1842 %% = a literal '%'
1843 1843
1844 1844 Prefixing the type with 'l' specifies a parenthesized list of that type.
1845 1845
1846 1846 >>> formatspec('%r:: and %lr', '10 or 11', ("this()", "that()"))
1847 1847 '(10 or 11):: and ((this()) or (that()))'
1848 1848 >>> formatspec('%d:: and not %d::', 10, 20)
1849 1849 '10:: and not 20::'
1850 1850 >>> formatspec('%ld or %ld', [], [1])
1851 1851 "_list('') or 1"
1852 1852 >>> formatspec('keyword(%s)', 'foo\\xe9')
1853 1853 "keyword('foo\\\\xe9')"
1854 1854 >>> b = lambda: 'default'
1855 1855 >>> b.branch = b
1856 1856 >>> formatspec('branch(%b)', b)
1857 1857 "branch('default')"
1858 1858 >>> formatspec('root(%ls)', ['a', 'b', 'c', 'd'])
1859 1859 "root(_list('a\\x00b\\x00c\\x00d'))"
1860 1860 '''
1861 1861
1862 1862 def quote(s):
1863 1863 return repr(str(s))
1864 1864
1865 1865 def argtype(c, arg):
1866 1866 if c == 'd':
1867 1867 return str(int(arg))
1868 1868 elif c == 's':
1869 1869 return quote(arg)
1870 1870 elif c == 'r':
1871 1871 parse(arg) # make sure syntax errors are confined
1872 1872 return '(%s)' % arg
1873 1873 elif c == 'n':
1874 1874 return quote(node.hex(arg))
1875 1875 elif c == 'b':
1876 1876 return quote(arg.branch())
1877 1877
1878 1878 def listexp(s, t):
1879 1879 l = len(s)
1880 1880 if l == 0:
1881 1881 return "_list('')"
1882 1882 elif l == 1:
1883 1883 return argtype(t, s[0])
1884 1884 elif t == 'd':
1885 1885 return "_list('%s')" % "\0".join(str(int(a)) for a in s)
1886 1886 elif t == 's':
1887 1887 return "_list('%s')" % "\0".join(s)
1888 1888 elif t == 'n':
1889 1889 return "_list('%s')" % "\0".join(node.hex(a) for a in s)
1890 1890 elif t == 'b':
1891 1891 return "_list('%s')" % "\0".join(a.branch() for a in s)
1892 1892
1893 1893 m = l // 2
1894 1894 return '(%s or %s)' % (listexp(s[:m], t), listexp(s[m:], t))
1895 1895
1896 1896 ret = ''
1897 1897 pos = 0
1898 1898 arg = 0
1899 1899 while pos < len(expr):
1900 1900 c = expr[pos]
1901 1901 if c == '%':
1902 1902 pos += 1
1903 1903 d = expr[pos]
1904 1904 if d == '%':
1905 1905 ret += d
1906 1906 elif d in 'dsnbr':
1907 1907 ret += argtype(d, args[arg])
1908 1908 arg += 1
1909 1909 elif d == 'l':
1910 1910 # a list of some type
1911 1911 pos += 1
1912 1912 d = expr[pos]
1913 1913 ret += listexp(list(args[arg]), d)
1914 1914 arg += 1
1915 1915 else:
1916 1916 raise util.Abort('unexpected revspec format character %s' % d)
1917 1917 else:
1918 1918 ret += c
1919 1919 pos += 1
1920 1920
1921 1921 return ret
1922 1922
1923 1923 def prettyformat(tree):
1924 1924 def _prettyformat(tree, level, lines):
1925 1925 if not isinstance(tree, tuple) or tree[0] in ('string', 'symbol'):
1926 1926 lines.append((level, str(tree)))
1927 1927 else:
1928 1928 lines.append((level, '(%s' % tree[0]))
1929 1929 for s in tree[1:]:
1930 1930 _prettyformat(s, level + 1, lines)
1931 1931 lines[-1:] = [(lines[-1][0], lines[-1][1] + ')')]
1932 1932
1933 1933 lines = []
1934 1934 _prettyformat(tree, 0, lines)
1935 1935 output = '\n'.join((' '*l + s) for l, s in lines)
1936 1936 return output
1937 1937
1938 1938 def depth(tree):
1939 1939 if isinstance(tree, tuple):
1940 1940 return max(map(depth, tree)) + 1
1941 1941 else:
1942 1942 return 0
1943 1943
1944 def funcsused(tree):
1945 if not isinstance(tree, tuple) or tree[0] in ('string', 'symbol'):
1946 return set()
1947 else:
1948 funcs = set()
1949 for s in tree[1:]:
1950 funcs |= funcsused(s)
1951 if tree[0] == 'func':
1952 funcs.add(tree[1][1])
1953 return funcs
1954
1944 1955 # tell hggettext to extract docstrings from these functions:
1945 1956 i18nfunctions = symbols.values()
General Comments 0
You need to be logged in to leave comments. Login now