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