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