##// END OF EJS Templates
revset: ensure we have loaded phases data in '_notpublic()'...
Pierre-Yves David -
r25612:97528adb default
parent child Browse files
Show More
@@ -1,3607 +1,3608
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, hbisect, phases
10 10 import node
11 11 import heapq
12 12 import match as matchmod
13 13 from i18n import _
14 14 import encoding
15 15 import obsolete as obsmod
16 16 import pathutil
17 17 import repoview
18 18
19 19 def _revancestors(repo, revs, followfirst):
20 20 """Like revlog.ancestors(), but supports followfirst."""
21 21 if followfirst:
22 22 cut = 1
23 23 else:
24 24 cut = None
25 25 cl = repo.changelog
26 26
27 27 def iterate():
28 28 revs.sort(reverse=True)
29 29 irevs = iter(revs)
30 30 h = []
31 31
32 32 inputrev = next(irevs, None)
33 33 if inputrev is not None:
34 34 heapq.heappush(h, -inputrev)
35 35
36 36 seen = set()
37 37 while h:
38 38 current = -heapq.heappop(h)
39 39 if current == inputrev:
40 40 inputrev = next(irevs, None)
41 41 if inputrev is not None:
42 42 heapq.heappush(h, -inputrev)
43 43 if current not in seen:
44 44 seen.add(current)
45 45 yield current
46 46 for parent in cl.parentrevs(current)[:cut]:
47 47 if parent != node.nullrev:
48 48 heapq.heappush(h, -parent)
49 49
50 50 return generatorset(iterate(), iterasc=False)
51 51
52 52 def _revdescendants(repo, revs, followfirst):
53 53 """Like revlog.descendants() but supports followfirst."""
54 54 if followfirst:
55 55 cut = 1
56 56 else:
57 57 cut = None
58 58
59 59 def iterate():
60 60 cl = repo.changelog
61 61 # XXX this should be 'parentset.min()' assuming 'parentset' is a
62 62 # smartset (and if it is not, it should.)
63 63 first = min(revs)
64 64 nullrev = node.nullrev
65 65 if first == nullrev:
66 66 # Are there nodes with a null first parent and a non-null
67 67 # second one? Maybe. Do we care? Probably not.
68 68 for i in cl:
69 69 yield i
70 70 else:
71 71 seen = set(revs)
72 72 for i in cl.revs(first + 1):
73 73 for x in cl.parentrevs(i)[:cut]:
74 74 if x != nullrev and x in seen:
75 75 seen.add(i)
76 76 yield i
77 77 break
78 78
79 79 return generatorset(iterate(), iterasc=True)
80 80
81 81 def _revsbetween(repo, roots, heads):
82 82 """Return all paths between roots and heads, inclusive of both endpoint
83 83 sets."""
84 84 if not roots:
85 85 return baseset()
86 86 parentrevs = repo.changelog.parentrevs
87 87 visit = list(heads)
88 88 reachable = set()
89 89 seen = {}
90 90 # XXX this should be 'parentset.min()' assuming 'parentset' is a smartset
91 91 # (and if it is not, it should.)
92 92 minroot = min(roots)
93 93 roots = set(roots)
94 94 # prefetch all the things! (because python is slow)
95 95 reached = reachable.add
96 96 dovisit = visit.append
97 97 nextvisit = visit.pop
98 98 # open-code the post-order traversal due to the tiny size of
99 99 # sys.getrecursionlimit()
100 100 while visit:
101 101 rev = nextvisit()
102 102 if rev in roots:
103 103 reached(rev)
104 104 parents = parentrevs(rev)
105 105 seen[rev] = parents
106 106 for parent in parents:
107 107 if parent >= minroot and parent not in seen:
108 108 dovisit(parent)
109 109 if not reachable:
110 110 return baseset()
111 111 for rev in sorted(seen):
112 112 for parent in seen[rev]:
113 113 if parent in reachable:
114 114 reached(rev)
115 115 return baseset(sorted(reachable))
116 116
117 117 elements = {
118 118 "(": (21, ("group", 1, ")"), ("func", 1, ")")),
119 119 "##": (20, None, ("_concat", 20)),
120 120 "~": (18, None, ("ancestor", 18)),
121 121 "^": (18, None, ("parent", 18), ("parentpost", 18)),
122 122 "-": (5, ("negate", 19), ("minus", 5)),
123 123 "::": (17, ("dagrangepre", 17), ("dagrange", 17),
124 124 ("dagrangepost", 17)),
125 125 "..": (17, ("dagrangepre", 17), ("dagrange", 17),
126 126 ("dagrangepost", 17)),
127 127 ":": (15, ("rangepre", 15), ("range", 15), ("rangepost", 15)),
128 128 "not": (10, ("not", 10)),
129 129 "!": (10, ("not", 10)),
130 130 "and": (5, None, ("and", 5)),
131 131 "&": (5, None, ("and", 5)),
132 132 "%": (5, None, ("only", 5), ("onlypost", 5)),
133 133 "or": (4, None, ("or", 4)),
134 134 "|": (4, None, ("or", 4)),
135 135 "+": (4, None, ("or", 4)),
136 136 ",": (2, None, ("list", 2)),
137 137 ")": (0, None, None),
138 138 "symbol": (0, ("symbol",), None),
139 139 "string": (0, ("string",), None),
140 140 "end": (0, None, None),
141 141 }
142 142
143 143 keywords = set(['and', 'or', 'not'])
144 144
145 145 # default set of valid characters for the initial letter of symbols
146 146 _syminitletters = set(c for c in [chr(i) for i in xrange(256)]
147 147 if c.isalnum() or c in '._@' or ord(c) > 127)
148 148
149 149 # default set of valid characters for non-initial letters of symbols
150 150 _symletters = set(c for c in [chr(i) for i in xrange(256)]
151 151 if c.isalnum() or c in '-._/@' or ord(c) > 127)
152 152
153 153 def tokenize(program, lookup=None, syminitletters=None, symletters=None):
154 154 '''
155 155 Parse a revset statement into a stream of tokens
156 156
157 157 ``syminitletters`` is the set of valid characters for the initial
158 158 letter of symbols.
159 159
160 160 By default, character ``c`` is recognized as valid for initial
161 161 letter of symbols, if ``c.isalnum() or c in '._@' or ord(c) > 127``.
162 162
163 163 ``symletters`` is the set of valid characters for non-initial
164 164 letters of symbols.
165 165
166 166 By default, character ``c`` is recognized as valid for non-initial
167 167 letters of symbols, if ``c.isalnum() or c in '-._/@' or ord(c) > 127``.
168 168
169 169 Check that @ is a valid unquoted token character (issue3686):
170 170 >>> list(tokenize("@::"))
171 171 [('symbol', '@', 0), ('::', None, 1), ('end', None, 3)]
172 172
173 173 '''
174 174 if syminitletters is None:
175 175 syminitletters = _syminitletters
176 176 if symletters is None:
177 177 symletters = _symletters
178 178
179 179 pos, l = 0, len(program)
180 180 while pos < l:
181 181 c = program[pos]
182 182 if c.isspace(): # skip inter-token whitespace
183 183 pass
184 184 elif c == ':' and program[pos:pos + 2] == '::': # look ahead carefully
185 185 yield ('::', None, pos)
186 186 pos += 1 # skip ahead
187 187 elif c == '.' and program[pos:pos + 2] == '..': # look ahead carefully
188 188 yield ('..', None, pos)
189 189 pos += 1 # skip ahead
190 190 elif c == '#' and program[pos:pos + 2] == '##': # look ahead carefully
191 191 yield ('##', None, pos)
192 192 pos += 1 # skip ahead
193 193 elif c in "():,-|&+!~^%": # handle simple operators
194 194 yield (c, None, pos)
195 195 elif (c in '"\'' or c == 'r' and
196 196 program[pos:pos + 2] in ("r'", 'r"')): # handle quoted strings
197 197 if c == 'r':
198 198 pos += 1
199 199 c = program[pos]
200 200 decode = lambda x: x
201 201 else:
202 202 decode = lambda x: x.decode('string-escape')
203 203 pos += 1
204 204 s = pos
205 205 while pos < l: # find closing quote
206 206 d = program[pos]
207 207 if d == '\\': # skip over escaped characters
208 208 pos += 2
209 209 continue
210 210 if d == c:
211 211 yield ('string', decode(program[s:pos]), s)
212 212 break
213 213 pos += 1
214 214 else:
215 215 raise error.ParseError(_("unterminated string"), s)
216 216 # gather up a symbol/keyword
217 217 elif c in syminitletters:
218 218 s = pos
219 219 pos += 1
220 220 while pos < l: # find end of symbol
221 221 d = program[pos]
222 222 if d not in symletters:
223 223 break
224 224 if d == '.' and program[pos - 1] == '.': # special case for ..
225 225 pos -= 1
226 226 break
227 227 pos += 1
228 228 sym = program[s:pos]
229 229 if sym in keywords: # operator keywords
230 230 yield (sym, None, s)
231 231 elif '-' in sym:
232 232 # some jerk gave us foo-bar-baz, try to check if it's a symbol
233 233 if lookup and lookup(sym):
234 234 # looks like a real symbol
235 235 yield ('symbol', sym, s)
236 236 else:
237 237 # looks like an expression
238 238 parts = sym.split('-')
239 239 for p in parts[:-1]:
240 240 if p: # possible consecutive -
241 241 yield ('symbol', p, s)
242 242 s += len(p)
243 243 yield ('-', None, pos)
244 244 s += 1
245 245 if parts[-1]: # possible trailing -
246 246 yield ('symbol', parts[-1], s)
247 247 else:
248 248 yield ('symbol', sym, s)
249 249 pos -= 1
250 250 else:
251 251 raise error.ParseError(_("syntax error in revset '%s'") %
252 252 program, pos)
253 253 pos += 1
254 254 yield ('end', None, pos)
255 255
256 256 def parseerrordetail(inst):
257 257 """Compose error message from specified ParseError object
258 258 """
259 259 if len(inst.args) > 1:
260 260 return _('at %s: %s') % (inst.args[1], inst.args[0])
261 261 else:
262 262 return inst.args[0]
263 263
264 264 # helpers
265 265
266 266 def getstring(x, err):
267 267 if x and (x[0] == 'string' or x[0] == 'symbol'):
268 268 return x[1]
269 269 raise error.ParseError(err)
270 270
271 271 def getlist(x):
272 272 if not x:
273 273 return []
274 274 if x[0] == 'list':
275 275 return getlist(x[1]) + [x[2]]
276 276 return [x]
277 277
278 278 def getargs(x, min, max, err):
279 279 l = getlist(x)
280 280 if len(l) < min or (max >= 0 and len(l) > max):
281 281 raise error.ParseError(err)
282 282 return l
283 283
284 284 def isvalidsymbol(tree):
285 285 """Examine whether specified ``tree`` is valid ``symbol`` or not
286 286 """
287 287 return tree[0] == 'symbol' and len(tree) > 1
288 288
289 289 def getsymbol(tree):
290 290 """Get symbol name from valid ``symbol`` in ``tree``
291 291
292 292 This assumes that ``tree`` is already examined by ``isvalidsymbol``.
293 293 """
294 294 return tree[1]
295 295
296 296 def isvalidfunc(tree):
297 297 """Examine whether specified ``tree`` is valid ``func`` or not
298 298 """
299 299 return tree[0] == 'func' and len(tree) > 1 and isvalidsymbol(tree[1])
300 300
301 301 def getfuncname(tree):
302 302 """Get function name from valid ``func`` in ``tree``
303 303
304 304 This assumes that ``tree`` is already examined by ``isvalidfunc``.
305 305 """
306 306 return getsymbol(tree[1])
307 307
308 308 def getfuncargs(tree):
309 309 """Get list of function arguments from valid ``func`` in ``tree``
310 310
311 311 This assumes that ``tree`` is already examined by ``isvalidfunc``.
312 312 """
313 313 if len(tree) > 2:
314 314 return getlist(tree[2])
315 315 else:
316 316 return []
317 317
318 318 def getset(repo, subset, x):
319 319 if not x:
320 320 raise error.ParseError(_("missing argument"))
321 321 s = methods[x[0]](repo, subset, *x[1:])
322 322 if util.safehasattr(s, 'isascending'):
323 323 return s
324 324 return baseset(s)
325 325
326 326 def _getrevsource(repo, r):
327 327 extra = repo[r].extra()
328 328 for label in ('source', 'transplant_source', 'rebase_source'):
329 329 if label in extra:
330 330 try:
331 331 return repo[extra[label]].rev()
332 332 except error.RepoLookupError:
333 333 pass
334 334 return None
335 335
336 336 # operator methods
337 337
338 338 def stringset(repo, subset, x):
339 339 x = repo[x].rev()
340 340 if (x in subset
341 341 or x == node.nullrev and isinstance(subset, fullreposet)):
342 342 return baseset([x])
343 343 return baseset()
344 344
345 345 def rangeset(repo, subset, x, y):
346 346 m = getset(repo, fullreposet(repo), x)
347 347 n = getset(repo, fullreposet(repo), y)
348 348
349 349 if not m or not n:
350 350 return baseset()
351 351 m, n = m.first(), n.last()
352 352
353 353 if m < n:
354 354 r = spanset(repo, m, n + 1)
355 355 else:
356 356 r = spanset(repo, m, n - 1)
357 357 # XXX We should combine with subset first: 'subset & baseset(...)'. This is
358 358 # necessary to ensure we preserve the order in subset.
359 359 #
360 360 # This has performance implication, carrying the sorting over when possible
361 361 # would be more efficient.
362 362 return r & subset
363 363
364 364 def dagrange(repo, subset, x, y):
365 365 r = fullreposet(repo)
366 366 xs = _revsbetween(repo, getset(repo, r, x), getset(repo, r, y))
367 367 # XXX We should combine with subset first: 'subset & baseset(...)'. This is
368 368 # necessary to ensure we preserve the order in subset.
369 369 return xs & subset
370 370
371 371 def andset(repo, subset, x, y):
372 372 return getset(repo, getset(repo, subset, x), y)
373 373
374 374 def orset(repo, subset, *xs):
375 375 rs = [getset(repo, subset, x) for x in xs]
376 376 return _combinesets(rs)
377 377
378 378 def notset(repo, subset, x):
379 379 return subset - getset(repo, subset, x)
380 380
381 381 def listset(repo, subset, a, b):
382 382 raise error.ParseError(_("can't use a list in this context"))
383 383
384 384 def func(repo, subset, a, b):
385 385 if a[0] == 'symbol' and a[1] in symbols:
386 386 return symbols[a[1]](repo, subset, b)
387 387 raise error.UnknownIdentifier(a[1], symbols.keys())
388 388
389 389 # functions
390 390
391 391 def adds(repo, subset, x):
392 392 """``adds(pattern)``
393 393 Changesets that add a file matching pattern.
394 394
395 395 The pattern without explicit kind like ``glob:`` is expected to be
396 396 relative to the current directory and match against a file or a
397 397 directory.
398 398 """
399 399 # i18n: "adds" is a keyword
400 400 pat = getstring(x, _("adds requires a pattern"))
401 401 return checkstatus(repo, subset, pat, 1)
402 402
403 403 def ancestor(repo, subset, x):
404 404 """``ancestor(*changeset)``
405 405 A greatest common ancestor of the changesets.
406 406
407 407 Accepts 0 or more changesets.
408 408 Will return empty list when passed no args.
409 409 Greatest common ancestor of a single changeset is that changeset.
410 410 """
411 411 # i18n: "ancestor" is a keyword
412 412 l = getlist(x)
413 413 rl = fullreposet(repo)
414 414 anc = None
415 415
416 416 # (getset(repo, rl, i) for i in l) generates a list of lists
417 417 for revs in (getset(repo, rl, i) for i in l):
418 418 for r in revs:
419 419 if anc is None:
420 420 anc = repo[r]
421 421 else:
422 422 anc = anc.ancestor(repo[r])
423 423
424 424 if anc is not None and anc.rev() in subset:
425 425 return baseset([anc.rev()])
426 426 return baseset()
427 427
428 428 def _ancestors(repo, subset, x, followfirst=False):
429 429 heads = getset(repo, fullreposet(repo), x)
430 430 if not heads:
431 431 return baseset()
432 432 s = _revancestors(repo, heads, followfirst)
433 433 return subset & s
434 434
435 435 def ancestors(repo, subset, x):
436 436 """``ancestors(set)``
437 437 Changesets that are ancestors of a changeset in set.
438 438 """
439 439 return _ancestors(repo, subset, x)
440 440
441 441 def _firstancestors(repo, subset, x):
442 442 # ``_firstancestors(set)``
443 443 # Like ``ancestors(set)`` but follows only the first parents.
444 444 return _ancestors(repo, subset, x, followfirst=True)
445 445
446 446 def ancestorspec(repo, subset, x, n):
447 447 """``set~n``
448 448 Changesets that are the Nth ancestor (first parents only) of a changeset
449 449 in set.
450 450 """
451 451 try:
452 452 n = int(n[1])
453 453 except (TypeError, ValueError):
454 454 raise error.ParseError(_("~ expects a number"))
455 455 ps = set()
456 456 cl = repo.changelog
457 457 for r in getset(repo, fullreposet(repo), x):
458 458 for i in range(n):
459 459 r = cl.parentrevs(r)[0]
460 460 ps.add(r)
461 461 return subset & ps
462 462
463 463 def author(repo, subset, x):
464 464 """``author(string)``
465 465 Alias for ``user(string)``.
466 466 """
467 467 # i18n: "author" is a keyword
468 468 n = encoding.lower(getstring(x, _("author requires a string")))
469 469 kind, pattern, matcher = _substringmatcher(n)
470 470 return subset.filter(lambda x: matcher(encoding.lower(repo[x].user())))
471 471
472 472 def bisect(repo, subset, x):
473 473 """``bisect(string)``
474 474 Changesets marked in the specified bisect status:
475 475
476 476 - ``good``, ``bad``, ``skip``: csets explicitly marked as good/bad/skip
477 477 - ``goods``, ``bads`` : csets topologically good/bad
478 478 - ``range`` : csets taking part in the bisection
479 479 - ``pruned`` : csets that are goods, bads or skipped
480 480 - ``untested`` : csets whose fate is yet unknown
481 481 - ``ignored`` : csets ignored due to DAG topology
482 482 - ``current`` : the cset currently being bisected
483 483 """
484 484 # i18n: "bisect" is a keyword
485 485 status = getstring(x, _("bisect requires a string")).lower()
486 486 state = set(hbisect.get(repo, status))
487 487 return subset & state
488 488
489 489 # Backward-compatibility
490 490 # - no help entry so that we do not advertise it any more
491 491 def bisected(repo, subset, x):
492 492 return bisect(repo, subset, x)
493 493
494 494 def bookmark(repo, subset, x):
495 495 """``bookmark([name])``
496 496 The named bookmark or all bookmarks.
497 497
498 498 If `name` starts with `re:`, the remainder of the name is treated as
499 499 a regular expression. To match a bookmark that actually starts with `re:`,
500 500 use the prefix `literal:`.
501 501 """
502 502 # i18n: "bookmark" is a keyword
503 503 args = getargs(x, 0, 1, _('bookmark takes one or no arguments'))
504 504 if args:
505 505 bm = getstring(args[0],
506 506 # i18n: "bookmark" is a keyword
507 507 _('the argument to bookmark must be a string'))
508 508 kind, pattern, matcher = _stringmatcher(bm)
509 509 bms = set()
510 510 if kind == 'literal':
511 511 bmrev = repo._bookmarks.get(pattern, None)
512 512 if not bmrev:
513 513 raise error.RepoLookupError(_("bookmark '%s' does not exist")
514 514 % bm)
515 515 bms.add(repo[bmrev].rev())
516 516 else:
517 517 matchrevs = set()
518 518 for name, bmrev in repo._bookmarks.iteritems():
519 519 if matcher(name):
520 520 matchrevs.add(bmrev)
521 521 if not matchrevs:
522 522 raise error.RepoLookupError(_("no bookmarks exist"
523 523 " that match '%s'") % pattern)
524 524 for bmrev in matchrevs:
525 525 bms.add(repo[bmrev].rev())
526 526 else:
527 527 bms = set([repo[r].rev()
528 528 for r in repo._bookmarks.values()])
529 529 bms -= set([node.nullrev])
530 530 return subset & bms
531 531
532 532 def branch(repo, subset, x):
533 533 """``branch(string or set)``
534 534 All changesets belonging to the given branch or the branches of the given
535 535 changesets.
536 536
537 537 If `string` starts with `re:`, the remainder of the name is treated as
538 538 a regular expression. To match a branch that actually starts with `re:`,
539 539 use the prefix `literal:`.
540 540 """
541 541 getbi = repo.revbranchcache().branchinfo
542 542
543 543 try:
544 544 b = getstring(x, '')
545 545 except error.ParseError:
546 546 # not a string, but another revspec, e.g. tip()
547 547 pass
548 548 else:
549 549 kind, pattern, matcher = _stringmatcher(b)
550 550 if kind == 'literal':
551 551 # note: falls through to the revspec case if no branch with
552 552 # this name exists
553 553 if pattern in repo.branchmap():
554 554 return subset.filter(lambda r: matcher(getbi(r)[0]))
555 555 else:
556 556 return subset.filter(lambda r: matcher(getbi(r)[0]))
557 557
558 558 s = getset(repo, fullreposet(repo), x)
559 559 b = set()
560 560 for r in s:
561 561 b.add(getbi(r)[0])
562 562 c = s.__contains__
563 563 return subset.filter(lambda r: c(r) or getbi(r)[0] in b)
564 564
565 565 def bumped(repo, subset, x):
566 566 """``bumped()``
567 567 Mutable changesets marked as successors of public changesets.
568 568
569 569 Only non-public and non-obsolete changesets can be `bumped`.
570 570 """
571 571 # i18n: "bumped" is a keyword
572 572 getargs(x, 0, 0, _("bumped takes no arguments"))
573 573 bumped = obsmod.getrevs(repo, 'bumped')
574 574 return subset & bumped
575 575
576 576 def bundle(repo, subset, x):
577 577 """``bundle()``
578 578 Changesets in the bundle.
579 579
580 580 Bundle must be specified by the -R option."""
581 581
582 582 try:
583 583 bundlerevs = repo.changelog.bundlerevs
584 584 except AttributeError:
585 585 raise util.Abort(_("no bundle provided - specify with -R"))
586 586 return subset & bundlerevs
587 587
588 588 def checkstatus(repo, subset, pat, field):
589 589 hasset = matchmod.patkind(pat) == 'set'
590 590
591 591 mcache = [None]
592 592 def matches(x):
593 593 c = repo[x]
594 594 if not mcache[0] or hasset:
595 595 mcache[0] = matchmod.match(repo.root, repo.getcwd(), [pat], ctx=c)
596 596 m = mcache[0]
597 597 fname = None
598 598 if not m.anypats() and len(m.files()) == 1:
599 599 fname = m.files()[0]
600 600 if fname is not None:
601 601 if fname not in c.files():
602 602 return False
603 603 else:
604 604 for f in c.files():
605 605 if m(f):
606 606 break
607 607 else:
608 608 return False
609 609 files = repo.status(c.p1().node(), c.node())[field]
610 610 if fname is not None:
611 611 if fname in files:
612 612 return True
613 613 else:
614 614 for f in files:
615 615 if m(f):
616 616 return True
617 617
618 618 return subset.filter(matches)
619 619
620 620 def _children(repo, narrow, parentset):
621 621 if not parentset:
622 622 return baseset()
623 623 cs = set()
624 624 pr = repo.changelog.parentrevs
625 625 minrev = parentset.min()
626 626 for r in narrow:
627 627 if r <= minrev:
628 628 continue
629 629 for p in pr(r):
630 630 if p in parentset:
631 631 cs.add(r)
632 632 # XXX using a set to feed the baseset is wrong. Sets are not ordered.
633 633 # This does not break because of other fullreposet misbehavior.
634 634 return baseset(cs)
635 635
636 636 def children(repo, subset, x):
637 637 """``children(set)``
638 638 Child changesets of changesets in set.
639 639 """
640 640 s = getset(repo, fullreposet(repo), x)
641 641 cs = _children(repo, subset, s)
642 642 return subset & cs
643 643
644 644 def closed(repo, subset, x):
645 645 """``closed()``
646 646 Changeset is closed.
647 647 """
648 648 # i18n: "closed" is a keyword
649 649 getargs(x, 0, 0, _("closed takes no arguments"))
650 650 return subset.filter(lambda r: repo[r].closesbranch())
651 651
652 652 def contains(repo, subset, x):
653 653 """``contains(pattern)``
654 654 The revision's manifest contains a file matching pattern (but might not
655 655 modify it). See :hg:`help patterns` for information about file patterns.
656 656
657 657 The pattern without explicit kind like ``glob:`` is expected to be
658 658 relative to the current directory and match against a file exactly
659 659 for efficiency.
660 660 """
661 661 # i18n: "contains" is a keyword
662 662 pat = getstring(x, _("contains requires a pattern"))
663 663
664 664 def matches(x):
665 665 if not matchmod.patkind(pat):
666 666 pats = pathutil.canonpath(repo.root, repo.getcwd(), pat)
667 667 if pats in repo[x]:
668 668 return True
669 669 else:
670 670 c = repo[x]
671 671 m = matchmod.match(repo.root, repo.getcwd(), [pat], ctx=c)
672 672 for f in c.manifest():
673 673 if m(f):
674 674 return True
675 675 return False
676 676
677 677 return subset.filter(matches)
678 678
679 679 def converted(repo, subset, x):
680 680 """``converted([id])``
681 681 Changesets converted from the given identifier in the old repository if
682 682 present, or all converted changesets if no identifier is specified.
683 683 """
684 684
685 685 # There is exactly no chance of resolving the revision, so do a simple
686 686 # string compare and hope for the best
687 687
688 688 rev = None
689 689 # i18n: "converted" is a keyword
690 690 l = getargs(x, 0, 1, _('converted takes one or no arguments'))
691 691 if l:
692 692 # i18n: "converted" is a keyword
693 693 rev = getstring(l[0], _('converted requires a revision'))
694 694
695 695 def _matchvalue(r):
696 696 source = repo[r].extra().get('convert_revision', None)
697 697 return source is not None and (rev is None or source.startswith(rev))
698 698
699 699 return subset.filter(lambda r: _matchvalue(r))
700 700
701 701 def date(repo, subset, x):
702 702 """``date(interval)``
703 703 Changesets within the interval, see :hg:`help dates`.
704 704 """
705 705 # i18n: "date" is a keyword
706 706 ds = getstring(x, _("date requires a string"))
707 707 dm = util.matchdate(ds)
708 708 return subset.filter(lambda x: dm(repo[x].date()[0]))
709 709
710 710 def desc(repo, subset, x):
711 711 """``desc(string)``
712 712 Search commit message for string. The match is case-insensitive.
713 713 """
714 714 # i18n: "desc" is a keyword
715 715 ds = encoding.lower(getstring(x, _("desc requires a string")))
716 716
717 717 def matches(x):
718 718 c = repo[x]
719 719 return ds in encoding.lower(c.description())
720 720
721 721 return subset.filter(matches)
722 722
723 723 def _descendants(repo, subset, x, followfirst=False):
724 724 roots = getset(repo, fullreposet(repo), x)
725 725 if not roots:
726 726 return baseset()
727 727 s = _revdescendants(repo, roots, followfirst)
728 728
729 729 # Both sets need to be ascending in order to lazily return the union
730 730 # in the correct order.
731 731 base = subset & roots
732 732 desc = subset & s
733 733 result = base + desc
734 734 if subset.isascending():
735 735 result.sort()
736 736 elif subset.isdescending():
737 737 result.sort(reverse=True)
738 738 else:
739 739 result = subset & result
740 740 return result
741 741
742 742 def descendants(repo, subset, x):
743 743 """``descendants(set)``
744 744 Changesets which are descendants of changesets in set.
745 745 """
746 746 return _descendants(repo, subset, x)
747 747
748 748 def _firstdescendants(repo, subset, x):
749 749 # ``_firstdescendants(set)``
750 750 # Like ``descendants(set)`` but follows only the first parents.
751 751 return _descendants(repo, subset, x, followfirst=True)
752 752
753 753 def destination(repo, subset, x):
754 754 """``destination([set])``
755 755 Changesets that were created by a graft, transplant or rebase operation,
756 756 with the given revisions specified as the source. Omitting the optional set
757 757 is the same as passing all().
758 758 """
759 759 if x is not None:
760 760 sources = getset(repo, fullreposet(repo), x)
761 761 else:
762 762 sources = fullreposet(repo)
763 763
764 764 dests = set()
765 765
766 766 # subset contains all of the possible destinations that can be returned, so
767 767 # iterate over them and see if their source(s) were provided in the arg set.
768 768 # Even if the immediate src of r is not in the arg set, src's source (or
769 769 # further back) may be. Scanning back further than the immediate src allows
770 770 # transitive transplants and rebases to yield the same results as transitive
771 771 # grafts.
772 772 for r in subset:
773 773 src = _getrevsource(repo, r)
774 774 lineage = None
775 775
776 776 while src is not None:
777 777 if lineage is None:
778 778 lineage = list()
779 779
780 780 lineage.append(r)
781 781
782 782 # The visited lineage is a match if the current source is in the arg
783 783 # set. Since every candidate dest is visited by way of iterating
784 784 # subset, any dests further back in the lineage will be tested by a
785 785 # different iteration over subset. Likewise, if the src was already
786 786 # selected, the current lineage can be selected without going back
787 787 # further.
788 788 if src in sources or src in dests:
789 789 dests.update(lineage)
790 790 break
791 791
792 792 r = src
793 793 src = _getrevsource(repo, r)
794 794
795 795 return subset.filter(dests.__contains__)
796 796
797 797 def divergent(repo, subset, x):
798 798 """``divergent()``
799 799 Final successors of changesets with an alternative set of final successors.
800 800 """
801 801 # i18n: "divergent" is a keyword
802 802 getargs(x, 0, 0, _("divergent takes no arguments"))
803 803 divergent = obsmod.getrevs(repo, 'divergent')
804 804 return subset & divergent
805 805
806 806 def draft(repo, subset, x):
807 807 """``draft()``
808 808 Changeset in draft phase."""
809 809 # i18n: "draft" is a keyword
810 810 getargs(x, 0, 0, _("draft takes no arguments"))
811 811 phase = repo._phasecache.phase
812 812 target = phases.draft
813 813 condition = lambda r: phase(repo, r) == target
814 814 return subset.filter(condition, cache=False)
815 815
816 816 def extinct(repo, subset, x):
817 817 """``extinct()``
818 818 Obsolete changesets with obsolete descendants only.
819 819 """
820 820 # i18n: "extinct" is a keyword
821 821 getargs(x, 0, 0, _("extinct takes no arguments"))
822 822 extincts = obsmod.getrevs(repo, 'extinct')
823 823 return subset & extincts
824 824
825 825 def extra(repo, subset, x):
826 826 """``extra(label, [value])``
827 827 Changesets with the given label in the extra metadata, with the given
828 828 optional value.
829 829
830 830 If `value` starts with `re:`, the remainder of the value is treated as
831 831 a regular expression. To match a value that actually starts with `re:`,
832 832 use the prefix `literal:`.
833 833 """
834 834
835 835 # i18n: "extra" is a keyword
836 836 l = getargs(x, 1, 2, _('extra takes at least 1 and at most 2 arguments'))
837 837 # i18n: "extra" is a keyword
838 838 label = getstring(l[0], _('first argument to extra must be a string'))
839 839 value = None
840 840
841 841 if len(l) > 1:
842 842 # i18n: "extra" is a keyword
843 843 value = getstring(l[1], _('second argument to extra must be a string'))
844 844 kind, value, matcher = _stringmatcher(value)
845 845
846 846 def _matchvalue(r):
847 847 extra = repo[r].extra()
848 848 return label in extra and (value is None or matcher(extra[label]))
849 849
850 850 return subset.filter(lambda r: _matchvalue(r))
851 851
852 852 def filelog(repo, subset, x):
853 853 """``filelog(pattern)``
854 854 Changesets connected to the specified filelog.
855 855
856 856 For performance reasons, visits only revisions mentioned in the file-level
857 857 filelog, rather than filtering through all changesets (much faster, but
858 858 doesn't include deletes or duplicate changes). For a slower, more accurate
859 859 result, use ``file()``.
860 860
861 861 The pattern without explicit kind like ``glob:`` is expected to be
862 862 relative to the current directory and match against a file exactly
863 863 for efficiency.
864 864
865 865 If some linkrev points to revisions filtered by the current repoview, we'll
866 866 work around it to return a non-filtered value.
867 867 """
868 868
869 869 # i18n: "filelog" is a keyword
870 870 pat = getstring(x, _("filelog requires a pattern"))
871 871 s = set()
872 872 cl = repo.changelog
873 873
874 874 if not matchmod.patkind(pat):
875 875 f = pathutil.canonpath(repo.root, repo.getcwd(), pat)
876 876 files = [f]
877 877 else:
878 878 m = matchmod.match(repo.root, repo.getcwd(), [pat], ctx=repo[None])
879 879 files = (f for f in repo[None] if m(f))
880 880
881 881 for f in files:
882 882 backrevref = {} # final value for: filerev -> changerev
883 883 lowestchild = {} # lowest known filerev child of a filerev
884 884 delayed = [] # filerev with filtered linkrev, for post-processing
885 885 lowesthead = None # cache for manifest content of all head revisions
886 886 fl = repo.file(f)
887 887 for fr in list(fl):
888 888 rev = fl.linkrev(fr)
889 889 if rev not in cl:
890 890 # changerev pointed in linkrev is filtered
891 891 # record it for post processing.
892 892 delayed.append((fr, rev))
893 893 continue
894 894 for p in fl.parentrevs(fr):
895 895 if 0 <= p and p not in lowestchild:
896 896 lowestchild[p] = fr
897 897 backrevref[fr] = rev
898 898 s.add(rev)
899 899
900 900 # Post-processing of all filerevs we skipped because they were
901 901 # filtered. If such filerevs have known and unfiltered children, this
902 902 # means they have an unfiltered appearance out there. We'll use linkrev
903 903 # adjustment to find one of these appearances. The lowest known child
904 904 # will be used as a starting point because it is the best upper-bound we
905 905 # have.
906 906 #
907 907 # This approach will fail when an unfiltered but linkrev-shadowed
908 908 # appearance exists in a head changeset without unfiltered filerev
909 909 # children anywhere.
910 910 while delayed:
911 911 # must be a descending iteration. To slowly fill lowest child
912 912 # information that is of potential use by the next item.
913 913 fr, rev = delayed.pop()
914 914 lkr = rev
915 915
916 916 child = lowestchild.get(fr)
917 917
918 918 if child is None:
919 919 # search for existence of this file revision in a head revision.
920 920 # There are three possibilities:
921 921 # - the revision exists in a head and we can find an
922 922 # introduction from there,
923 923 # - the revision does not exist in a head because it has been
924 924 # changed since its introduction: we would have found a child
925 925 # and be in the other 'else' clause,
926 926 # - all versions of the revision are hidden.
927 927 if lowesthead is None:
928 928 lowesthead = {}
929 929 for h in repo.heads():
930 930 fnode = repo[h].manifest().get(f)
931 931 if fnode is not None:
932 932 lowesthead[fl.rev(fnode)] = h
933 933 headrev = lowesthead.get(fr)
934 934 if headrev is None:
935 935 # content is nowhere unfiltered
936 936 continue
937 937 rev = repo[headrev][f].introrev()
938 938 else:
939 939 # the lowest known child is a good upper bound
940 940 childcrev = backrevref[child]
941 941 # XXX this does not guarantee returning the lowest
942 942 # introduction of this revision, but this gives a
943 943 # result which is a good start and will fit in most
944 944 # cases. We probably need to fix the multiple
945 945 # introductions case properly (report each
946 946 # introduction, even for identical file revisions)
947 947 # once and for all at some point anyway.
948 948 for p in repo[childcrev][f].parents():
949 949 if p.filerev() == fr:
950 950 rev = p.rev()
951 951 break
952 952 if rev == lkr: # no shadowed entry found
953 953 # XXX This should never happen unless some manifest points
954 954 # to biggish file revisions (like a revision that uses a
955 955 # parent that never appears in the manifest ancestors)
956 956 continue
957 957
958 958 # Fill the data for the next iteration.
959 959 for p in fl.parentrevs(fr):
960 960 if 0 <= p and p not in lowestchild:
961 961 lowestchild[p] = fr
962 962 backrevref[fr] = rev
963 963 s.add(rev)
964 964
965 965 return subset & s
966 966
967 967 def first(repo, subset, x):
968 968 """``first(set, [n])``
969 969 An alias for limit().
970 970 """
971 971 return limit(repo, subset, x)
972 972
973 973 def _follow(repo, subset, x, name, followfirst=False):
974 974 l = getargs(x, 0, 1, _("%s takes no arguments or a filename") % name)
975 975 c = repo['.']
976 976 if l:
977 977 x = getstring(l[0], _("%s expected a filename") % name)
978 978 if x in c:
979 979 cx = c[x]
980 980 s = set(ctx.rev() for ctx in cx.ancestors(followfirst=followfirst))
981 981 # include the revision responsible for the most recent version
982 982 s.add(cx.introrev())
983 983 else:
984 984 return baseset()
985 985 else:
986 986 s = _revancestors(repo, baseset([c.rev()]), followfirst)
987 987
988 988 return subset & s
989 989
990 990 def follow(repo, subset, x):
991 991 """``follow([file])``
992 992 An alias for ``::.`` (ancestors of the working directory's first parent).
993 993 If a filename is specified, the history of the given file is followed,
994 994 including copies.
995 995 """
996 996 return _follow(repo, subset, x, 'follow')
997 997
998 998 def _followfirst(repo, subset, x):
999 999 # ``followfirst([file])``
1000 1000 # Like ``follow([file])`` but follows only the first parent of
1001 1001 # every revision or file revision.
1002 1002 return _follow(repo, subset, x, '_followfirst', followfirst=True)
1003 1003
1004 1004 def getall(repo, subset, x):
1005 1005 """``all()``
1006 1006 All changesets, the same as ``0:tip``.
1007 1007 """
1008 1008 # i18n: "all" is a keyword
1009 1009 getargs(x, 0, 0, _("all takes no arguments"))
1010 1010 return subset & spanset(repo) # drop "null" if any
1011 1011
1012 1012 def grep(repo, subset, x):
1013 1013 """``grep(regex)``
1014 1014 Like ``keyword(string)`` but accepts a regex. Use ``grep(r'...')``
1015 1015 to ensure special escape characters are handled correctly. Unlike
1016 1016 ``keyword(string)``, the match is case-sensitive.
1017 1017 """
1018 1018 try:
1019 1019 # i18n: "grep" is a keyword
1020 1020 gr = re.compile(getstring(x, _("grep requires a string")))
1021 1021 except re.error, e:
1022 1022 raise error.ParseError(_('invalid match pattern: %s') % e)
1023 1023
1024 1024 def matches(x):
1025 1025 c = repo[x]
1026 1026 for e in c.files() + [c.user(), c.description()]:
1027 1027 if gr.search(e):
1028 1028 return True
1029 1029 return False
1030 1030
1031 1031 return subset.filter(matches)
1032 1032
1033 1033 def _matchfiles(repo, subset, x):
1034 1034 # _matchfiles takes a revset list of prefixed arguments:
1035 1035 #
1036 1036 # [p:foo, i:bar, x:baz]
1037 1037 #
1038 1038 # builds a match object from them and filters subset. Allowed
1039 1039 # prefixes are 'p:' for regular patterns, 'i:' for include
1040 1040 # patterns and 'x:' for exclude patterns. Use 'r:' prefix to pass
1041 1041 # a revision identifier, or the empty string to reference the
1042 1042 # working directory, from which the match object is
1043 1043 # initialized. Use 'd:' to set the default matching mode, default
1044 1044 # to 'glob'. At most one 'r:' and 'd:' argument can be passed.
1045 1045
1046 1046 # i18n: "_matchfiles" is a keyword
1047 1047 l = getargs(x, 1, -1, _("_matchfiles requires at least one argument"))
1048 1048 pats, inc, exc = [], [], []
1049 1049 rev, default = None, None
1050 1050 for arg in l:
1051 1051 # i18n: "_matchfiles" is a keyword
1052 1052 s = getstring(arg, _("_matchfiles requires string arguments"))
1053 1053 prefix, value = s[:2], s[2:]
1054 1054 if prefix == 'p:':
1055 1055 pats.append(value)
1056 1056 elif prefix == 'i:':
1057 1057 inc.append(value)
1058 1058 elif prefix == 'x:':
1059 1059 exc.append(value)
1060 1060 elif prefix == 'r:':
1061 1061 if rev is not None:
1062 1062 # i18n: "_matchfiles" is a keyword
1063 1063 raise error.ParseError(_('_matchfiles expected at most one '
1064 1064 'revision'))
1065 1065 if value != '': # empty means working directory; leave rev as None
1066 1066 rev = value
1067 1067 elif prefix == 'd:':
1068 1068 if default is not None:
1069 1069 # i18n: "_matchfiles" is a keyword
1070 1070 raise error.ParseError(_('_matchfiles expected at most one '
1071 1071 'default mode'))
1072 1072 default = value
1073 1073 else:
1074 1074 # i18n: "_matchfiles" is a keyword
1075 1075 raise error.ParseError(_('invalid _matchfiles prefix: %s') % prefix)
1076 1076 if not default:
1077 1077 default = 'glob'
1078 1078
1079 1079 m = matchmod.match(repo.root, repo.getcwd(), pats, include=inc,
1080 1080 exclude=exc, ctx=repo[rev], default=default)
1081 1081
1082 1082 def matches(x):
1083 1083 for f in repo[x].files():
1084 1084 if m(f):
1085 1085 return True
1086 1086 return False
1087 1087
1088 1088 return subset.filter(matches)
1089 1089
1090 1090 def hasfile(repo, subset, x):
1091 1091 """``file(pattern)``
1092 1092 Changesets affecting files matched by pattern.
1093 1093
1094 1094 For a faster but less accurate result, consider using ``filelog()``
1095 1095 instead.
1096 1096
1097 1097 This predicate uses ``glob:`` as the default kind of pattern.
1098 1098 """
1099 1099 # i18n: "file" is a keyword
1100 1100 pat = getstring(x, _("file requires a pattern"))
1101 1101 return _matchfiles(repo, subset, ('string', 'p:' + pat))
1102 1102
1103 1103 def head(repo, subset, x):
1104 1104 """``head()``
1105 1105 Changeset is a named branch head.
1106 1106 """
1107 1107 # i18n: "head" is a keyword
1108 1108 getargs(x, 0, 0, _("head takes no arguments"))
1109 1109 hs = set()
1110 1110 for b, ls in repo.branchmap().iteritems():
1111 1111 hs.update(repo[h].rev() for h in ls)
1112 1112 # XXX using a set to feed the baseset is wrong. Sets are not ordered.
1113 1113 # This does not break because of other fullreposet misbehavior.
1114 1114 # XXX We should not be using '.filter' here, but combines subset with '&'
1115 1115 # XXX We should combine with subset first: 'subset & baseset(...)'. This is
1116 1116 # necessary to ensure we preserve the order in subset.
1117 1117 return baseset(hs).filter(subset.__contains__)
1118 1118
1119 1119 def heads(repo, subset, x):
1120 1120 """``heads(set)``
1121 1121 Members of set with no children in set.
1122 1122 """
1123 1123 s = getset(repo, subset, x)
1124 1124 ps = parents(repo, subset, x)
1125 1125 return s - ps
1126 1126
1127 1127 def hidden(repo, subset, x):
1128 1128 """``hidden()``
1129 1129 Hidden changesets.
1130 1130 """
1131 1131 # i18n: "hidden" is a keyword
1132 1132 getargs(x, 0, 0, _("hidden takes no arguments"))
1133 1133 hiddenrevs = repoview.filterrevs(repo, 'visible')
1134 1134 return subset & hiddenrevs
1135 1135
1136 1136 def keyword(repo, subset, x):
1137 1137 """``keyword(string)``
1138 1138 Search commit message, user name, and names of changed files for
1139 1139 string. The match is case-insensitive.
1140 1140 """
1141 1141 # i18n: "keyword" is a keyword
1142 1142 kw = encoding.lower(getstring(x, _("keyword requires a string")))
1143 1143
1144 1144 def matches(r):
1145 1145 c = repo[r]
1146 1146 return any(kw in encoding.lower(t)
1147 1147 for t in c.files() + [c.user(), c.description()])
1148 1148
1149 1149 return subset.filter(matches)
1150 1150
1151 1151 def limit(repo, subset, x):
1152 1152 """``limit(set, [n])``
1153 1153 First n members of set, defaulting to 1.
1154 1154 """
1155 1155 # i18n: "limit" is a keyword
1156 1156 l = getargs(x, 1, 2, _("limit requires one or two arguments"))
1157 1157 try:
1158 1158 lim = 1
1159 1159 if len(l) == 2:
1160 1160 # i18n: "limit" is a keyword
1161 1161 lim = int(getstring(l[1], _("limit requires a number")))
1162 1162 except (TypeError, ValueError):
1163 1163 # i18n: "limit" is a keyword
1164 1164 raise error.ParseError(_("limit expects a number"))
1165 1165 ss = subset
1166 1166 os = getset(repo, fullreposet(repo), l[0])
1167 1167 result = []
1168 1168 it = iter(os)
1169 1169 for x in xrange(lim):
1170 1170 y = next(it, None)
1171 1171 if y is None:
1172 1172 break
1173 1173 elif y in ss:
1174 1174 result.append(y)
1175 1175 return baseset(result)
1176 1176
1177 1177 def last(repo, subset, x):
1178 1178 """``last(set, [n])``
1179 1179 Last n members of set, defaulting to 1.
1180 1180 """
1181 1181 # i18n: "last" is a keyword
1182 1182 l = getargs(x, 1, 2, _("last requires one or two arguments"))
1183 1183 try:
1184 1184 lim = 1
1185 1185 if len(l) == 2:
1186 1186 # i18n: "last" is a keyword
1187 1187 lim = int(getstring(l[1], _("last requires a number")))
1188 1188 except (TypeError, ValueError):
1189 1189 # i18n: "last" is a keyword
1190 1190 raise error.ParseError(_("last expects a number"))
1191 1191 ss = subset
1192 1192 os = getset(repo, fullreposet(repo), l[0])
1193 1193 os.reverse()
1194 1194 result = []
1195 1195 it = iter(os)
1196 1196 for x in xrange(lim):
1197 1197 y = next(it, None)
1198 1198 if y is None:
1199 1199 break
1200 1200 elif y in ss:
1201 1201 result.append(y)
1202 1202 return baseset(result)
1203 1203
1204 1204 def maxrev(repo, subset, x):
1205 1205 """``max(set)``
1206 1206 Changeset with highest revision number in set.
1207 1207 """
1208 1208 os = getset(repo, fullreposet(repo), x)
1209 1209 if os:
1210 1210 m = os.max()
1211 1211 if m in subset:
1212 1212 return baseset([m])
1213 1213 return baseset()
1214 1214
1215 1215 def merge(repo, subset, x):
1216 1216 """``merge()``
1217 1217 Changeset is a merge changeset.
1218 1218 """
1219 1219 # i18n: "merge" is a keyword
1220 1220 getargs(x, 0, 0, _("merge takes no arguments"))
1221 1221 cl = repo.changelog
1222 1222 return subset.filter(lambda r: cl.parentrevs(r)[1] != -1)
1223 1223
1224 1224 def branchpoint(repo, subset, x):
1225 1225 """``branchpoint()``
1226 1226 Changesets with more than one child.
1227 1227 """
1228 1228 # i18n: "branchpoint" is a keyword
1229 1229 getargs(x, 0, 0, _("branchpoint takes no arguments"))
1230 1230 cl = repo.changelog
1231 1231 if not subset:
1232 1232 return baseset()
1233 1233 # XXX this should be 'parentset.min()' assuming 'parentset' is a smartset
1234 1234 # (and if it is not, it should.)
1235 1235 baserev = min(subset)
1236 1236 parentscount = [0]*(len(repo) - baserev)
1237 1237 for r in cl.revs(start=baserev + 1):
1238 1238 for p in cl.parentrevs(r):
1239 1239 if p >= baserev:
1240 1240 parentscount[p - baserev] += 1
1241 1241 return subset.filter(lambda r: parentscount[r - baserev] > 1)
1242 1242
1243 1243 def minrev(repo, subset, x):
1244 1244 """``min(set)``
1245 1245 Changeset with lowest revision number in set.
1246 1246 """
1247 1247 os = getset(repo, fullreposet(repo), x)
1248 1248 if os:
1249 1249 m = os.min()
1250 1250 if m in subset:
1251 1251 return baseset([m])
1252 1252 return baseset()
1253 1253
1254 1254 def modifies(repo, subset, x):
1255 1255 """``modifies(pattern)``
1256 1256 Changesets modifying files matched by pattern.
1257 1257
1258 1258 The pattern without explicit kind like ``glob:`` is expected to be
1259 1259 relative to the current directory and match against a file or a
1260 1260 directory.
1261 1261 """
1262 1262 # i18n: "modifies" is a keyword
1263 1263 pat = getstring(x, _("modifies requires a pattern"))
1264 1264 return checkstatus(repo, subset, pat, 0)
1265 1265
1266 1266 def named(repo, subset, x):
1267 1267 """``named(namespace)``
1268 1268 The changesets in a given namespace.
1269 1269
1270 1270 If `namespace` starts with `re:`, the remainder of the string is treated as
1271 1271 a regular expression. To match a namespace that actually starts with `re:`,
1272 1272 use the prefix `literal:`.
1273 1273 """
1274 1274 # i18n: "named" is a keyword
1275 1275 args = getargs(x, 1, 1, _('named requires a namespace argument'))
1276 1276
1277 1277 ns = getstring(args[0],
1278 1278 # i18n: "named" is a keyword
1279 1279 _('the argument to named must be a string'))
1280 1280 kind, pattern, matcher = _stringmatcher(ns)
1281 1281 namespaces = set()
1282 1282 if kind == 'literal':
1283 1283 if pattern not in repo.names:
1284 1284 raise error.RepoLookupError(_("namespace '%s' does not exist")
1285 1285 % ns)
1286 1286 namespaces.add(repo.names[pattern])
1287 1287 else:
1288 1288 for name, ns in repo.names.iteritems():
1289 1289 if matcher(name):
1290 1290 namespaces.add(ns)
1291 1291 if not namespaces:
1292 1292 raise error.RepoLookupError(_("no namespace exists"
1293 1293 " that match '%s'") % pattern)
1294 1294
1295 1295 names = set()
1296 1296 for ns in namespaces:
1297 1297 for name in ns.listnames(repo):
1298 1298 if name not in ns.deprecated:
1299 1299 names.update(repo[n].rev() for n in ns.nodes(repo, name))
1300 1300
1301 1301 names -= set([node.nullrev])
1302 1302 return subset & names
1303 1303
1304 1304 def node_(repo, subset, x):
1305 1305 """``id(string)``
1306 1306 Revision non-ambiguously specified by the given hex string prefix.
1307 1307 """
1308 1308 # i18n: "id" is a keyword
1309 1309 l = getargs(x, 1, 1, _("id requires one argument"))
1310 1310 # i18n: "id" is a keyword
1311 1311 n = getstring(l[0], _("id requires a string"))
1312 1312 if len(n) == 40:
1313 1313 try:
1314 1314 rn = repo.changelog.rev(node.bin(n))
1315 1315 except (LookupError, TypeError):
1316 1316 rn = None
1317 1317 else:
1318 1318 rn = None
1319 1319 pm = repo.changelog._partialmatch(n)
1320 1320 if pm is not None:
1321 1321 rn = repo.changelog.rev(pm)
1322 1322
1323 1323 if rn is None:
1324 1324 return baseset()
1325 1325 result = baseset([rn])
1326 1326 return result & subset
1327 1327
1328 1328 def obsolete(repo, subset, x):
1329 1329 """``obsolete()``
1330 1330 Mutable changeset with a newer version."""
1331 1331 # i18n: "obsolete" is a keyword
1332 1332 getargs(x, 0, 0, _("obsolete takes no arguments"))
1333 1333 obsoletes = obsmod.getrevs(repo, 'obsolete')
1334 1334 return subset & obsoletes
1335 1335
1336 1336 def only(repo, subset, x):
1337 1337 """``only(set, [set])``
1338 1338 Changesets that are ancestors of the first set that are not ancestors
1339 1339 of any other head in the repo. If a second set is specified, the result
1340 1340 is ancestors of the first set that are not ancestors of the second set
1341 1341 (i.e. ::<set1> - ::<set2>).
1342 1342 """
1343 1343 cl = repo.changelog
1344 1344 # i18n: "only" is a keyword
1345 1345 args = getargs(x, 1, 2, _('only takes one or two arguments'))
1346 1346 include = getset(repo, fullreposet(repo), args[0])
1347 1347 if len(args) == 1:
1348 1348 if not include:
1349 1349 return baseset()
1350 1350
1351 1351 descendants = set(_revdescendants(repo, include, False))
1352 1352 exclude = [rev for rev in cl.headrevs()
1353 1353 if not rev in descendants and not rev in include]
1354 1354 else:
1355 1355 exclude = getset(repo, fullreposet(repo), args[1])
1356 1356
1357 1357 results = set(cl.findmissingrevs(common=exclude, heads=include))
1358 1358 # XXX we should turn this into a baseset instead of a set, smartset may do
1359 1359 # some optimisations from the fact this is a baseset.
1360 1360 return subset & results
1361 1361
1362 1362 def origin(repo, subset, x):
1363 1363 """``origin([set])``
1364 1364 Changesets that were specified as a source for the grafts, transplants or
1365 1365 rebases that created the given revisions. Omitting the optional set is the
1366 1366 same as passing all(). If a changeset created by these operations is itself
1367 1367 specified as a source for one of these operations, only the source changeset
1368 1368 for the first operation is selected.
1369 1369 """
1370 1370 if x is not None:
1371 1371 dests = getset(repo, fullreposet(repo), x)
1372 1372 else:
1373 1373 dests = fullreposet(repo)
1374 1374
1375 1375 def _firstsrc(rev):
1376 1376 src = _getrevsource(repo, rev)
1377 1377 if src is None:
1378 1378 return None
1379 1379
1380 1380 while True:
1381 1381 prev = _getrevsource(repo, src)
1382 1382
1383 1383 if prev is None:
1384 1384 return src
1385 1385 src = prev
1386 1386
1387 1387 o = set([_firstsrc(r) for r in dests])
1388 1388 o -= set([None])
1389 1389 # XXX we should turn this into a baseset instead of a set, smartset may do
1390 1390 # some optimisations from the fact this is a baseset.
1391 1391 return subset & o
1392 1392
1393 1393 def outgoing(repo, subset, x):
1394 1394 """``outgoing([path])``
1395 1395 Changesets not found in the specified destination repository, or the
1396 1396 default push location.
1397 1397 """
1398 1398 # Avoid cycles.
1399 1399 import discovery
1400 1400 import hg
1401 1401 # i18n: "outgoing" is a keyword
1402 1402 l = getargs(x, 0, 1, _("outgoing takes one or no arguments"))
1403 1403 # i18n: "outgoing" is a keyword
1404 1404 dest = l and getstring(l[0], _("outgoing requires a repository path")) or ''
1405 1405 dest = repo.ui.expandpath(dest or 'default-push', dest or 'default')
1406 1406 dest, branches = hg.parseurl(dest)
1407 1407 revs, checkout = hg.addbranchrevs(repo, repo, branches, [])
1408 1408 if revs:
1409 1409 revs = [repo.lookup(rev) for rev in revs]
1410 1410 other = hg.peer(repo, {}, dest)
1411 1411 repo.ui.pushbuffer()
1412 1412 outgoing = discovery.findcommonoutgoing(repo, other, onlyheads=revs)
1413 1413 repo.ui.popbuffer()
1414 1414 cl = repo.changelog
1415 1415 o = set([cl.rev(r) for r in outgoing.missing])
1416 1416 return subset & o
1417 1417
1418 1418 def p1(repo, subset, x):
1419 1419 """``p1([set])``
1420 1420 First parent of changesets in set, or the working directory.
1421 1421 """
1422 1422 if x is None:
1423 1423 p = repo[x].p1().rev()
1424 1424 if p >= 0:
1425 1425 return subset & baseset([p])
1426 1426 return baseset()
1427 1427
1428 1428 ps = set()
1429 1429 cl = repo.changelog
1430 1430 for r in getset(repo, fullreposet(repo), x):
1431 1431 ps.add(cl.parentrevs(r)[0])
1432 1432 ps -= set([node.nullrev])
1433 1433 # XXX we should turn this into a baseset instead of a set, smartset may do
1434 1434 # some optimisations from the fact this is a baseset.
1435 1435 return subset & ps
1436 1436
1437 1437 def p2(repo, subset, x):
1438 1438 """``p2([set])``
1439 1439 Second parent of changesets in set, or the working directory.
1440 1440 """
1441 1441 if x is None:
1442 1442 ps = repo[x].parents()
1443 1443 try:
1444 1444 p = ps[1].rev()
1445 1445 if p >= 0:
1446 1446 return subset & baseset([p])
1447 1447 return baseset()
1448 1448 except IndexError:
1449 1449 return baseset()
1450 1450
1451 1451 ps = set()
1452 1452 cl = repo.changelog
1453 1453 for r in getset(repo, fullreposet(repo), x):
1454 1454 ps.add(cl.parentrevs(r)[1])
1455 1455 ps -= set([node.nullrev])
1456 1456 # XXX we should turn this into a baseset instead of a set, smartset may do
1457 1457 # some optimisations from the fact this is a baseset.
1458 1458 return subset & ps
1459 1459
1460 1460 def parents(repo, subset, x):
1461 1461 """``parents([set])``
1462 1462 The set of all parents for all changesets in set, or the working directory.
1463 1463 """
1464 1464 if x is None:
1465 1465 ps = set(p.rev() for p in repo[x].parents())
1466 1466 else:
1467 1467 ps = set()
1468 1468 cl = repo.changelog
1469 1469 for r in getset(repo, fullreposet(repo), x):
1470 1470 ps.update(cl.parentrevs(r))
1471 1471 ps -= set([node.nullrev])
1472 1472 return subset & ps
1473 1473
1474 1474 def parentspec(repo, subset, x, n):
1475 1475 """``set^0``
1476 1476 The set.
1477 1477 ``set^1`` (or ``set^``), ``set^2``
1478 1478 First or second parent, respectively, of all changesets in set.
1479 1479 """
1480 1480 try:
1481 1481 n = int(n[1])
1482 1482 if n not in (0, 1, 2):
1483 1483 raise ValueError
1484 1484 except (TypeError, ValueError):
1485 1485 raise error.ParseError(_("^ expects a number 0, 1, or 2"))
1486 1486 ps = set()
1487 1487 cl = repo.changelog
1488 1488 for r in getset(repo, fullreposet(repo), x):
1489 1489 if n == 0:
1490 1490 ps.add(r)
1491 1491 elif n == 1:
1492 1492 ps.add(cl.parentrevs(r)[0])
1493 1493 elif n == 2:
1494 1494 parents = cl.parentrevs(r)
1495 1495 if len(parents) > 1:
1496 1496 ps.add(parents[1])
1497 1497 return subset & ps
1498 1498
1499 1499 def present(repo, subset, x):
1500 1500 """``present(set)``
1501 1501 An empty set, if any revision in set isn't found; otherwise,
1502 1502 all revisions in set.
1503 1503
1504 1504 If any of specified revisions is not present in the local repository,
1505 1505 the query is normally aborted. But this predicate allows the query
1506 1506 to continue even in such cases.
1507 1507 """
1508 1508 try:
1509 1509 return getset(repo, subset, x)
1510 1510 except error.RepoLookupError:
1511 1511 return baseset()
1512 1512
1513 1513 # for internal use
1514 1514 def _notpublic(repo, subset, x):
1515 1515 getargs(x, 0, 0, "_notpublic takes no arguments")
1516 repo._phasecache.loadphaserevs(repo) # ensure phase's sets are loaded
1516 1517 if repo._phasecache._phasesets:
1517 1518 s = set()
1518 1519 for u in repo._phasecache._phasesets[1:]:
1519 1520 s.update(u)
1520 1521 # XXX we should turn this into a baseset instead of a set, smartset may
1521 1522 # do some optimisations from the fact this is a baseset.
1522 1523 return subset & s
1523 1524 else:
1524 1525 phase = repo._phasecache.phase
1525 1526 target = phases.public
1526 1527 condition = lambda r: phase(repo, r) != target
1527 1528 return subset.filter(condition, cache=False)
1528 1529
1529 1530 def public(repo, subset, x):
1530 1531 """``public()``
1531 1532 Changeset in public phase."""
1532 1533 # i18n: "public" is a keyword
1533 1534 getargs(x, 0, 0, _("public takes no arguments"))
1534 1535 phase = repo._phasecache.phase
1535 1536 target = phases.public
1536 1537 condition = lambda r: phase(repo, r) == target
1537 1538 return subset.filter(condition, cache=False)
1538 1539
1539 1540 def remote(repo, subset, x):
1540 1541 """``remote([id [,path]])``
1541 1542 Local revision that corresponds to the given identifier in a
1542 1543 remote repository, if present. Here, the '.' identifier is a
1543 1544 synonym for the current local branch.
1544 1545 """
1545 1546
1546 1547 import hg # avoid start-up nasties
1547 1548 # i18n: "remote" is a keyword
1548 1549 l = getargs(x, 0, 2, _("remote takes one, two or no arguments"))
1549 1550
1550 1551 q = '.'
1551 1552 if len(l) > 0:
1552 1553 # i18n: "remote" is a keyword
1553 1554 q = getstring(l[0], _("remote requires a string id"))
1554 1555 if q == '.':
1555 1556 q = repo['.'].branch()
1556 1557
1557 1558 dest = ''
1558 1559 if len(l) > 1:
1559 1560 # i18n: "remote" is a keyword
1560 1561 dest = getstring(l[1], _("remote requires a repository path"))
1561 1562 dest = repo.ui.expandpath(dest or 'default')
1562 1563 dest, branches = hg.parseurl(dest)
1563 1564 revs, checkout = hg.addbranchrevs(repo, repo, branches, [])
1564 1565 if revs:
1565 1566 revs = [repo.lookup(rev) for rev in revs]
1566 1567 other = hg.peer(repo, {}, dest)
1567 1568 n = other.lookup(q)
1568 1569 if n in repo:
1569 1570 r = repo[n].rev()
1570 1571 if r in subset:
1571 1572 return baseset([r])
1572 1573 return baseset()
1573 1574
1574 1575 def removes(repo, subset, x):
1575 1576 """``removes(pattern)``
1576 1577 Changesets which remove files matching pattern.
1577 1578
1578 1579 The pattern without explicit kind like ``glob:`` is expected to be
1579 1580 relative to the current directory and match against a file or a
1580 1581 directory.
1581 1582 """
1582 1583 # i18n: "removes" is a keyword
1583 1584 pat = getstring(x, _("removes requires a pattern"))
1584 1585 return checkstatus(repo, subset, pat, 2)
1585 1586
1586 1587 def rev(repo, subset, x):
1587 1588 """``rev(number)``
1588 1589 Revision with the given numeric identifier.
1589 1590 """
1590 1591 # i18n: "rev" is a keyword
1591 1592 l = getargs(x, 1, 1, _("rev requires one argument"))
1592 1593 try:
1593 1594 # i18n: "rev" is a keyword
1594 1595 l = int(getstring(l[0], _("rev requires a number")))
1595 1596 except (TypeError, ValueError):
1596 1597 # i18n: "rev" is a keyword
1597 1598 raise error.ParseError(_("rev expects a number"))
1598 1599 if l not in repo.changelog and l != node.nullrev:
1599 1600 return baseset()
1600 1601 return subset & baseset([l])
1601 1602
1602 1603 def matching(repo, subset, x):
1603 1604 """``matching(revision [, field])``
1604 1605 Changesets in which a given set of fields match the set of fields in the
1605 1606 selected revision or set.
1606 1607
1607 1608 To match more than one field pass the list of fields to match separated
1608 1609 by spaces (e.g. ``author description``).
1609 1610
1610 1611 Valid fields are most regular revision fields and some special fields.
1611 1612
1612 1613 Regular revision fields are ``description``, ``author``, ``branch``,
1613 1614 ``date``, ``files``, ``phase``, ``parents``, ``substate``, ``user``
1614 1615 and ``diff``.
1615 1616 Note that ``author`` and ``user`` are synonyms. ``diff`` refers to the
1616 1617 contents of the revision. Two revisions matching their ``diff`` will
1617 1618 also match their ``files``.
1618 1619
1619 1620 Special fields are ``summary`` and ``metadata``:
1620 1621 ``summary`` matches the first line of the description.
1621 1622 ``metadata`` is equivalent to matching ``description user date``
1622 1623 (i.e. it matches the main metadata fields).
1623 1624
1624 1625 ``metadata`` is the default field which is used when no fields are
1625 1626 specified. You can match more than one field at a time.
1626 1627 """
1627 1628 # i18n: "matching" is a keyword
1628 1629 l = getargs(x, 1, 2, _("matching takes 1 or 2 arguments"))
1629 1630
1630 1631 revs = getset(repo, fullreposet(repo), l[0])
1631 1632
1632 1633 fieldlist = ['metadata']
1633 1634 if len(l) > 1:
1634 1635 fieldlist = getstring(l[1],
1635 1636 # i18n: "matching" is a keyword
1636 1637 _("matching requires a string "
1637 1638 "as its second argument")).split()
1638 1639
1639 1640 # Make sure that there are no repeated fields,
1640 1641 # expand the 'special' 'metadata' field type
1641 1642 # and check the 'files' whenever we check the 'diff'
1642 1643 fields = []
1643 1644 for field in fieldlist:
1644 1645 if field == 'metadata':
1645 1646 fields += ['user', 'description', 'date']
1646 1647 elif field == 'diff':
1647 1648 # a revision matching the diff must also match the files
1648 1649 # since matching the diff is very costly, make sure to
1649 1650 # also match the files first
1650 1651 fields += ['files', 'diff']
1651 1652 else:
1652 1653 if field == 'author':
1653 1654 field = 'user'
1654 1655 fields.append(field)
1655 1656 fields = set(fields)
1656 1657 if 'summary' in fields and 'description' in fields:
1657 1658 # If a revision matches its description it also matches its summary
1658 1659 fields.discard('summary')
1659 1660
1660 1661 # We may want to match more than one field
1661 1662 # Not all fields take the same amount of time to be matched
1662 1663 # Sort the selected fields in order of increasing matching cost
1663 1664 fieldorder = ['phase', 'parents', 'user', 'date', 'branch', 'summary',
1664 1665 'files', 'description', 'substate', 'diff']
1665 1666 def fieldkeyfunc(f):
1666 1667 try:
1667 1668 return fieldorder.index(f)
1668 1669 except ValueError:
1669 1670 # assume an unknown field is very costly
1670 1671 return len(fieldorder)
1671 1672 fields = list(fields)
1672 1673 fields.sort(key=fieldkeyfunc)
1673 1674
1674 1675 # Each field will be matched with its own "getfield" function
1675 1676 # which will be added to the getfieldfuncs array of functions
1676 1677 getfieldfuncs = []
1677 1678 _funcs = {
1678 1679 'user': lambda r: repo[r].user(),
1679 1680 'branch': lambda r: repo[r].branch(),
1680 1681 'date': lambda r: repo[r].date(),
1681 1682 'description': lambda r: repo[r].description(),
1682 1683 'files': lambda r: repo[r].files(),
1683 1684 'parents': lambda r: repo[r].parents(),
1684 1685 'phase': lambda r: repo[r].phase(),
1685 1686 'substate': lambda r: repo[r].substate,
1686 1687 'summary': lambda r: repo[r].description().splitlines()[0],
1687 1688 'diff': lambda r: list(repo[r].diff(git=True),)
1688 1689 }
1689 1690 for info in fields:
1690 1691 getfield = _funcs.get(info, None)
1691 1692 if getfield is None:
1692 1693 raise error.ParseError(
1693 1694 # i18n: "matching" is a keyword
1694 1695 _("unexpected field name passed to matching: %s") % info)
1695 1696 getfieldfuncs.append(getfield)
1696 1697 # convert the getfield array of functions into a "getinfo" function
1697 1698 # which returns an array of field values (or a single value if there
1698 1699 # is only one field to match)
1699 1700 getinfo = lambda r: [f(r) for f in getfieldfuncs]
1700 1701
1701 1702 def matches(x):
1702 1703 for rev in revs:
1703 1704 target = getinfo(rev)
1704 1705 match = True
1705 1706 for n, f in enumerate(getfieldfuncs):
1706 1707 if target[n] != f(x):
1707 1708 match = False
1708 1709 if match:
1709 1710 return True
1710 1711 return False
1711 1712
1712 1713 return subset.filter(matches)
1713 1714
1714 1715 def reverse(repo, subset, x):
1715 1716 """``reverse(set)``
1716 1717 Reverse order of set.
1717 1718 """
1718 1719 l = getset(repo, subset, x)
1719 1720 l.reverse()
1720 1721 return l
1721 1722
1722 1723 def roots(repo, subset, x):
1723 1724 """``roots(set)``
1724 1725 Changesets in set with no parent changeset in set.
1725 1726 """
1726 1727 s = getset(repo, fullreposet(repo), x)
1727 1728 subset = subset & s# baseset([r for r in s if r in subset])
1728 1729 cs = _children(repo, subset, s)
1729 1730 return subset - cs
1730 1731
1731 1732 def secret(repo, subset, x):
1732 1733 """``secret()``
1733 1734 Changeset in secret phase."""
1734 1735 # i18n: "secret" is a keyword
1735 1736 getargs(x, 0, 0, _("secret takes no arguments"))
1736 1737 phase = repo._phasecache.phase
1737 1738 target = phases.secret
1738 1739 condition = lambda r: phase(repo, r) == target
1739 1740 return subset.filter(condition, cache=False)
1740 1741
1741 1742 def sort(repo, subset, x):
1742 1743 """``sort(set[, [-]key...])``
1743 1744 Sort set by keys. The default sort order is ascending, specify a key
1744 1745 as ``-key`` to sort in descending order.
1745 1746
1746 1747 The keys can be:
1747 1748
1748 1749 - ``rev`` for the revision number,
1749 1750 - ``branch`` for the branch name,
1750 1751 - ``desc`` for the commit message (description),
1751 1752 - ``user`` for user name (``author`` can be used as an alias),
1752 1753 - ``date`` for the commit date
1753 1754 """
1754 1755 # i18n: "sort" is a keyword
1755 1756 l = getargs(x, 1, 2, _("sort requires one or two arguments"))
1756 1757 keys = "rev"
1757 1758 if len(l) == 2:
1758 1759 # i18n: "sort" is a keyword
1759 1760 keys = getstring(l[1], _("sort spec must be a string"))
1760 1761
1761 1762 s = l[0]
1762 1763 keys = keys.split()
1763 1764 l = []
1764 1765 def invert(s):
1765 1766 return "".join(chr(255 - ord(c)) for c in s)
1766 1767 revs = getset(repo, subset, s)
1767 1768 if keys == ["rev"]:
1768 1769 revs.sort()
1769 1770 return revs
1770 1771 elif keys == ["-rev"]:
1771 1772 revs.sort(reverse=True)
1772 1773 return revs
1773 1774 for r in revs:
1774 1775 c = repo[r]
1775 1776 e = []
1776 1777 for k in keys:
1777 1778 if k == 'rev':
1778 1779 e.append(r)
1779 1780 elif k == '-rev':
1780 1781 e.append(-r)
1781 1782 elif k == 'branch':
1782 1783 e.append(c.branch())
1783 1784 elif k == '-branch':
1784 1785 e.append(invert(c.branch()))
1785 1786 elif k == 'desc':
1786 1787 e.append(c.description())
1787 1788 elif k == '-desc':
1788 1789 e.append(invert(c.description()))
1789 1790 elif k in 'user author':
1790 1791 e.append(c.user())
1791 1792 elif k in '-user -author':
1792 1793 e.append(invert(c.user()))
1793 1794 elif k == 'date':
1794 1795 e.append(c.date()[0])
1795 1796 elif k == '-date':
1796 1797 e.append(-c.date()[0])
1797 1798 else:
1798 1799 raise error.ParseError(_("unknown sort key %r") % k)
1799 1800 e.append(r)
1800 1801 l.append(e)
1801 1802 l.sort()
1802 1803 return baseset([e[-1] for e in l])
1803 1804
1804 1805 def subrepo(repo, subset, x):
1805 1806 """``subrepo([pattern])``
1806 1807 Changesets that add, modify or remove the given subrepo. If no subrepo
1807 1808 pattern is named, any subrepo changes are returned.
1808 1809 """
1809 1810 # i18n: "subrepo" is a keyword
1810 1811 args = getargs(x, 0, 1, _('subrepo takes at most one argument'))
1811 1812 if len(args) != 0:
1812 1813 pat = getstring(args[0], _("subrepo requires a pattern"))
1813 1814
1814 1815 m = matchmod.exact(repo.root, repo.root, ['.hgsubstate'])
1815 1816
1816 1817 def submatches(names):
1817 1818 k, p, m = _stringmatcher(pat)
1818 1819 for name in names:
1819 1820 if m(name):
1820 1821 yield name
1821 1822
1822 1823 def matches(x):
1823 1824 c = repo[x]
1824 1825 s = repo.status(c.p1().node(), c.node(), match=m)
1825 1826
1826 1827 if len(args) == 0:
1827 1828 return s.added or s.modified or s.removed
1828 1829
1829 1830 if s.added:
1830 1831 return any(submatches(c.substate.keys()))
1831 1832
1832 1833 if s.modified:
1833 1834 subs = set(c.p1().substate.keys())
1834 1835 subs.update(c.substate.keys())
1835 1836
1836 1837 for path in submatches(subs):
1837 1838 if c.p1().substate.get(path) != c.substate.get(path):
1838 1839 return True
1839 1840
1840 1841 if s.removed:
1841 1842 return any(submatches(c.p1().substate.keys()))
1842 1843
1843 1844 return False
1844 1845
1845 1846 return subset.filter(matches)
1846 1847
1847 1848 def _stringmatcher(pattern):
1848 1849 """
1849 1850 accepts a string, possibly starting with 're:' or 'literal:' prefix.
1850 1851 returns the matcher name, pattern, and matcher function.
1851 1852 missing or unknown prefixes are treated as literal matches.
1852 1853
1853 1854 helper for tests:
1854 1855 >>> def test(pattern, *tests):
1855 1856 ... kind, pattern, matcher = _stringmatcher(pattern)
1856 1857 ... return (kind, pattern, [bool(matcher(t)) for t in tests])
1857 1858
1858 1859 exact matching (no prefix):
1859 1860 >>> test('abcdefg', 'abc', 'def', 'abcdefg')
1860 1861 ('literal', 'abcdefg', [False, False, True])
1861 1862
1862 1863 regex matching ('re:' prefix)
1863 1864 >>> test('re:a.+b', 'nomatch', 'fooadef', 'fooadefbar')
1864 1865 ('re', 'a.+b', [False, False, True])
1865 1866
1866 1867 force exact matches ('literal:' prefix)
1867 1868 >>> test('literal:re:foobar', 'foobar', 're:foobar')
1868 1869 ('literal', 're:foobar', [False, True])
1869 1870
1870 1871 unknown prefixes are ignored and treated as literals
1871 1872 >>> test('foo:bar', 'foo', 'bar', 'foo:bar')
1872 1873 ('literal', 'foo:bar', [False, False, True])
1873 1874 """
1874 1875 if pattern.startswith('re:'):
1875 1876 pattern = pattern[3:]
1876 1877 try:
1877 1878 regex = re.compile(pattern)
1878 1879 except re.error, e:
1879 1880 raise error.ParseError(_('invalid regular expression: %s')
1880 1881 % e)
1881 1882 return 're', pattern, regex.search
1882 1883 elif pattern.startswith('literal:'):
1883 1884 pattern = pattern[8:]
1884 1885 return 'literal', pattern, pattern.__eq__
1885 1886
1886 1887 def _substringmatcher(pattern):
1887 1888 kind, pattern, matcher = _stringmatcher(pattern)
1888 1889 if kind == 'literal':
1889 1890 matcher = lambda s: pattern in s
1890 1891 return kind, pattern, matcher
1891 1892
1892 1893 def tag(repo, subset, x):
1893 1894 """``tag([name])``
1894 1895 The specified tag by name, or all tagged revisions if no name is given.
1895 1896
1896 1897 If `name` starts with `re:`, the remainder of the name is treated as
1897 1898 a regular expression. To match a tag that actually starts with `re:`,
1898 1899 use the prefix `literal:`.
1899 1900 """
1900 1901 # i18n: "tag" is a keyword
1901 1902 args = getargs(x, 0, 1, _("tag takes one or no arguments"))
1902 1903 cl = repo.changelog
1903 1904 if args:
1904 1905 pattern = getstring(args[0],
1905 1906 # i18n: "tag" is a keyword
1906 1907 _('the argument to tag must be a string'))
1907 1908 kind, pattern, matcher = _stringmatcher(pattern)
1908 1909 if kind == 'literal':
1909 1910 # avoid resolving all tags
1910 1911 tn = repo._tagscache.tags.get(pattern, None)
1911 1912 if tn is None:
1912 1913 raise error.RepoLookupError(_("tag '%s' does not exist")
1913 1914 % pattern)
1914 1915 s = set([repo[tn].rev()])
1915 1916 else:
1916 1917 s = set([cl.rev(n) for t, n in repo.tagslist() if matcher(t)])
1917 1918 else:
1918 1919 s = set([cl.rev(n) for t, n in repo.tagslist() if t != 'tip'])
1919 1920 return subset & s
1920 1921
1921 1922 def tagged(repo, subset, x):
1922 1923 return tag(repo, subset, x)
1923 1924
1924 1925 def unstable(repo, subset, x):
1925 1926 """``unstable()``
1926 1927 Non-obsolete changesets with obsolete ancestors.
1927 1928 """
1928 1929 # i18n: "unstable" is a keyword
1929 1930 getargs(x, 0, 0, _("unstable takes no arguments"))
1930 1931 unstables = obsmod.getrevs(repo, 'unstable')
1931 1932 return subset & unstables
1932 1933
1933 1934
1934 1935 def user(repo, subset, x):
1935 1936 """``user(string)``
1936 1937 User name contains string. The match is case-insensitive.
1937 1938
1938 1939 If `string` starts with `re:`, the remainder of the string is treated as
1939 1940 a regular expression. To match a user that actually contains `re:`, use
1940 1941 the prefix `literal:`.
1941 1942 """
1942 1943 return author(repo, subset, x)
1943 1944
1944 1945 # experimental
1945 1946 def wdir(repo, subset, x):
1946 1947 # i18n: "wdir" is a keyword
1947 1948 getargs(x, 0, 0, _("wdir takes no arguments"))
1948 1949 if None in subset or isinstance(subset, fullreposet):
1949 1950 return baseset([None])
1950 1951 return baseset()
1951 1952
1952 1953 # for internal use
1953 1954 def _list(repo, subset, x):
1954 1955 s = getstring(x, "internal error")
1955 1956 if not s:
1956 1957 return baseset()
1957 1958 # remove duplicates here. it's difficult for caller to deduplicate sets
1958 1959 # because different symbols can point to the same rev.
1959 1960 cl = repo.changelog
1960 1961 ls = []
1961 1962 seen = set()
1962 1963 for t in s.split('\0'):
1963 1964 try:
1964 1965 # fast path for integer revision
1965 1966 r = int(t)
1966 1967 if str(r) != t or r not in cl:
1967 1968 raise ValueError
1968 1969 except ValueError:
1969 1970 r = repo[t].rev()
1970 1971 if r in seen:
1971 1972 continue
1972 1973 if (r in subset
1973 1974 or r == node.nullrev and isinstance(subset, fullreposet)):
1974 1975 ls.append(r)
1975 1976 seen.add(r)
1976 1977 return baseset(ls)
1977 1978
1978 1979 # for internal use
1979 1980 def _intlist(repo, subset, x):
1980 1981 s = getstring(x, "internal error")
1981 1982 if not s:
1982 1983 return baseset()
1983 1984 ls = [int(r) for r in s.split('\0')]
1984 1985 s = subset
1985 1986 return baseset([r for r in ls if r in s])
1986 1987
1987 1988 # for internal use
1988 1989 def _hexlist(repo, subset, x):
1989 1990 s = getstring(x, "internal error")
1990 1991 if not s:
1991 1992 return baseset()
1992 1993 cl = repo.changelog
1993 1994 ls = [cl.rev(node.bin(r)) for r in s.split('\0')]
1994 1995 s = subset
1995 1996 return baseset([r for r in ls if r in s])
1996 1997
1997 1998 symbols = {
1998 1999 "adds": adds,
1999 2000 "all": getall,
2000 2001 "ancestor": ancestor,
2001 2002 "ancestors": ancestors,
2002 2003 "_firstancestors": _firstancestors,
2003 2004 "author": author,
2004 2005 "bisect": bisect,
2005 2006 "bisected": bisected,
2006 2007 "bookmark": bookmark,
2007 2008 "branch": branch,
2008 2009 "branchpoint": branchpoint,
2009 2010 "bumped": bumped,
2010 2011 "bundle": bundle,
2011 2012 "children": children,
2012 2013 "closed": closed,
2013 2014 "contains": contains,
2014 2015 "converted": converted,
2015 2016 "date": date,
2016 2017 "desc": desc,
2017 2018 "descendants": descendants,
2018 2019 "_firstdescendants": _firstdescendants,
2019 2020 "destination": destination,
2020 2021 "divergent": divergent,
2021 2022 "draft": draft,
2022 2023 "extinct": extinct,
2023 2024 "extra": extra,
2024 2025 "file": hasfile,
2025 2026 "filelog": filelog,
2026 2027 "first": first,
2027 2028 "follow": follow,
2028 2029 "_followfirst": _followfirst,
2029 2030 "grep": grep,
2030 2031 "head": head,
2031 2032 "heads": heads,
2032 2033 "hidden": hidden,
2033 2034 "id": node_,
2034 2035 "keyword": keyword,
2035 2036 "last": last,
2036 2037 "limit": limit,
2037 2038 "_matchfiles": _matchfiles,
2038 2039 "max": maxrev,
2039 2040 "merge": merge,
2040 2041 "min": minrev,
2041 2042 "modifies": modifies,
2042 2043 "named": named,
2043 2044 "obsolete": obsolete,
2044 2045 "only": only,
2045 2046 "origin": origin,
2046 2047 "outgoing": outgoing,
2047 2048 "p1": p1,
2048 2049 "p2": p2,
2049 2050 "parents": parents,
2050 2051 "present": present,
2051 2052 "public": public,
2052 2053 "_notpublic": _notpublic,
2053 2054 "remote": remote,
2054 2055 "removes": removes,
2055 2056 "rev": rev,
2056 2057 "reverse": reverse,
2057 2058 "roots": roots,
2058 2059 "sort": sort,
2059 2060 "secret": secret,
2060 2061 "subrepo": subrepo,
2061 2062 "matching": matching,
2062 2063 "tag": tag,
2063 2064 "tagged": tagged,
2064 2065 "user": user,
2065 2066 "unstable": unstable,
2066 2067 "wdir": wdir,
2067 2068 "_list": _list,
2068 2069 "_intlist": _intlist,
2069 2070 "_hexlist": _hexlist,
2070 2071 }
2071 2072
2072 2073 # symbols which can't be used for a DoS attack for any given input
2073 2074 # (e.g. those which accept regexes as plain strings shouldn't be included)
2074 2075 # functions that just return a lot of changesets (like all) don't count here
2075 2076 safesymbols = set([
2076 2077 "adds",
2077 2078 "all",
2078 2079 "ancestor",
2079 2080 "ancestors",
2080 2081 "_firstancestors",
2081 2082 "author",
2082 2083 "bisect",
2083 2084 "bisected",
2084 2085 "bookmark",
2085 2086 "branch",
2086 2087 "branchpoint",
2087 2088 "bumped",
2088 2089 "bundle",
2089 2090 "children",
2090 2091 "closed",
2091 2092 "converted",
2092 2093 "date",
2093 2094 "desc",
2094 2095 "descendants",
2095 2096 "_firstdescendants",
2096 2097 "destination",
2097 2098 "divergent",
2098 2099 "draft",
2099 2100 "extinct",
2100 2101 "extra",
2101 2102 "file",
2102 2103 "filelog",
2103 2104 "first",
2104 2105 "follow",
2105 2106 "_followfirst",
2106 2107 "head",
2107 2108 "heads",
2108 2109 "hidden",
2109 2110 "id",
2110 2111 "keyword",
2111 2112 "last",
2112 2113 "limit",
2113 2114 "_matchfiles",
2114 2115 "max",
2115 2116 "merge",
2116 2117 "min",
2117 2118 "modifies",
2118 2119 "obsolete",
2119 2120 "only",
2120 2121 "origin",
2121 2122 "outgoing",
2122 2123 "p1",
2123 2124 "p2",
2124 2125 "parents",
2125 2126 "present",
2126 2127 "public",
2127 2128 "_notpublic",
2128 2129 "remote",
2129 2130 "removes",
2130 2131 "rev",
2131 2132 "reverse",
2132 2133 "roots",
2133 2134 "sort",
2134 2135 "secret",
2135 2136 "matching",
2136 2137 "tag",
2137 2138 "tagged",
2138 2139 "user",
2139 2140 "unstable",
2140 2141 "wdir",
2141 2142 "_list",
2142 2143 "_intlist",
2143 2144 "_hexlist",
2144 2145 ])
2145 2146
2146 2147 methods = {
2147 2148 "range": rangeset,
2148 2149 "dagrange": dagrange,
2149 2150 "string": stringset,
2150 2151 "symbol": stringset,
2151 2152 "and": andset,
2152 2153 "or": orset,
2153 2154 "not": notset,
2154 2155 "list": listset,
2155 2156 "func": func,
2156 2157 "ancestor": ancestorspec,
2157 2158 "parent": parentspec,
2158 2159 "parentpost": p1,
2159 2160 }
2160 2161
2161 2162 def optimize(x, small):
2162 2163 if x is None:
2163 2164 return 0, x
2164 2165
2165 2166 smallbonus = 1
2166 2167 if small:
2167 2168 smallbonus = .5
2168 2169
2169 2170 op = x[0]
2170 2171 if op == 'minus':
2171 2172 return optimize(('and', x[1], ('not', x[2])), small)
2172 2173 elif op == 'only':
2173 2174 return optimize(('func', ('symbol', 'only'),
2174 2175 ('list', x[1], x[2])), small)
2175 2176 elif op == 'onlypost':
2176 2177 return optimize(('func', ('symbol', 'only'), x[1]), small)
2177 2178 elif op == 'dagrangepre':
2178 2179 return optimize(('func', ('symbol', 'ancestors'), x[1]), small)
2179 2180 elif op == 'dagrangepost':
2180 2181 return optimize(('func', ('symbol', 'descendants'), x[1]), small)
2181 2182 elif op == 'rangepre':
2182 2183 return optimize(('range', ('string', '0'), x[1]), small)
2183 2184 elif op == 'rangepost':
2184 2185 return optimize(('range', x[1], ('string', 'tip')), small)
2185 2186 elif op == 'negate':
2186 2187 return optimize(('string',
2187 2188 '-' + getstring(x[1], _("can't negate that"))), small)
2188 2189 elif op in 'string symbol negate':
2189 2190 return smallbonus, x # single revisions are small
2190 2191 elif op == 'and':
2191 2192 wa, ta = optimize(x[1], True)
2192 2193 wb, tb = optimize(x[2], True)
2193 2194
2194 2195 # (::x and not ::y)/(not ::y and ::x) have a fast path
2195 2196 def isonly(revs, bases):
2196 2197 return (
2197 2198 revs[0] == 'func'
2198 2199 and getstring(revs[1], _('not a symbol')) == 'ancestors'
2199 2200 and bases[0] == 'not'
2200 2201 and bases[1][0] == 'func'
2201 2202 and getstring(bases[1][1], _('not a symbol')) == 'ancestors')
2202 2203
2203 2204 w = min(wa, wb)
2204 2205 if isonly(ta, tb):
2205 2206 return w, ('func', ('symbol', 'only'), ('list', ta[2], tb[1][2]))
2206 2207 if isonly(tb, ta):
2207 2208 return w, ('func', ('symbol', 'only'), ('list', tb[2], ta[1][2]))
2208 2209
2209 2210 if wa > wb:
2210 2211 return w, (op, tb, ta)
2211 2212 return w, (op, ta, tb)
2212 2213 elif op == 'or':
2213 2214 # fast path for machine-generated expression, that is likely to have
2214 2215 # lots of trivial revisions: 'a + b + c()' to '_list(a b) + c()'
2215 2216 ws, ts, ss = [], [], []
2216 2217 def flushss():
2217 2218 if not ss:
2218 2219 return
2219 2220 if len(ss) == 1:
2220 2221 w, t = ss[0]
2221 2222 else:
2222 2223 s = '\0'.join(t[1] for w, t in ss)
2223 2224 y = ('func', ('symbol', '_list'), ('string', s))
2224 2225 w, t = optimize(y, False)
2225 2226 ws.append(w)
2226 2227 ts.append(t)
2227 2228 del ss[:]
2228 2229 for y in x[1:]:
2229 2230 w, t = optimize(y, False)
2230 2231 if t[0] == 'string' or t[0] == 'symbol':
2231 2232 ss.append((w, t))
2232 2233 continue
2233 2234 flushss()
2234 2235 ws.append(w)
2235 2236 ts.append(t)
2236 2237 flushss()
2237 2238 if len(ts) == 1:
2238 2239 return ws[0], ts[0] # 'or' operation is fully optimized out
2239 2240 # we can't reorder trees by weight because it would change the order.
2240 2241 # ("sort(a + b)" == "sort(b + a)", but "a + b" != "b + a")
2241 2242 # ts = tuple(t for w, t in sorted(zip(ws, ts), key=lambda wt: wt[0]))
2242 2243 return max(ws), (op,) + tuple(ts)
2243 2244 elif op == 'not':
2244 2245 # Optimize not public() to _notpublic() because we have a fast version
2245 2246 if x[1] == ('func', ('symbol', 'public'), None):
2246 2247 newsym = ('func', ('symbol', '_notpublic'), None)
2247 2248 o = optimize(newsym, not small)
2248 2249 return o[0], o[1]
2249 2250 else:
2250 2251 o = optimize(x[1], not small)
2251 2252 return o[0], (op, o[1])
2252 2253 elif op == 'parentpost':
2253 2254 o = optimize(x[1], small)
2254 2255 return o[0], (op, o[1])
2255 2256 elif op == 'group':
2256 2257 return optimize(x[1], small)
2257 2258 elif op in 'dagrange range list parent ancestorspec':
2258 2259 if op == 'parent':
2259 2260 # x^:y means (x^) : y, not x ^ (:y)
2260 2261 post = ('parentpost', x[1])
2261 2262 if x[2][0] == 'dagrangepre':
2262 2263 return optimize(('dagrange', post, x[2][1]), small)
2263 2264 elif x[2][0] == 'rangepre':
2264 2265 return optimize(('range', post, x[2][1]), small)
2265 2266
2266 2267 wa, ta = optimize(x[1], small)
2267 2268 wb, tb = optimize(x[2], small)
2268 2269 return wa + wb, (op, ta, tb)
2269 2270 elif op == 'func':
2270 2271 f = getstring(x[1], _("not a symbol"))
2271 2272 wa, ta = optimize(x[2], small)
2272 2273 if f in ("author branch closed date desc file grep keyword "
2273 2274 "outgoing user"):
2274 2275 w = 10 # slow
2275 2276 elif f in "modifies adds removes":
2276 2277 w = 30 # slower
2277 2278 elif f == "contains":
2278 2279 w = 100 # very slow
2279 2280 elif f == "ancestor":
2280 2281 w = 1 * smallbonus
2281 2282 elif f in "reverse limit first _intlist":
2282 2283 w = 0
2283 2284 elif f in "sort":
2284 2285 w = 10 # assume most sorts look at changelog
2285 2286 else:
2286 2287 w = 1
2287 2288 return w + wa, (op, x[1], ta)
2288 2289 return 1, x
2289 2290
2290 2291 _aliasarg = ('func', ('symbol', '_aliasarg'))
2291 2292 def _getaliasarg(tree):
2292 2293 """If tree matches ('func', ('symbol', '_aliasarg'), ('string', X))
2293 2294 return X, None otherwise.
2294 2295 """
2295 2296 if (len(tree) == 3 and tree[:2] == _aliasarg
2296 2297 and tree[2][0] == 'string'):
2297 2298 return tree[2][1]
2298 2299 return None
2299 2300
2300 2301 def _checkaliasarg(tree, known=None):
2301 2302 """Check tree contains no _aliasarg construct or only ones which
2302 2303 value is in known. Used to avoid alias placeholders injection.
2303 2304 """
2304 2305 if isinstance(tree, tuple):
2305 2306 arg = _getaliasarg(tree)
2306 2307 if arg is not None and (not known or arg not in known):
2307 2308 raise error.UnknownIdentifier('_aliasarg', [])
2308 2309 for t in tree:
2309 2310 _checkaliasarg(t, known)
2310 2311
2311 2312 # the set of valid characters for the initial letter of symbols in
2312 2313 # alias declarations and definitions
2313 2314 _aliassyminitletters = set(c for c in [chr(i) for i in xrange(256)]
2314 2315 if c.isalnum() or c in '._@$' or ord(c) > 127)
2315 2316
2316 2317 def _tokenizealias(program, lookup=None):
2317 2318 """Parse alias declaration/definition into a stream of tokens
2318 2319
2319 2320 This allows symbol names to use also ``$`` as an initial letter
2320 2321 (for backward compatibility), and callers of this function should
2321 2322 examine whether ``$`` is used also for unexpected symbols or not.
2322 2323 """
2323 2324 return tokenize(program, lookup=lookup,
2324 2325 syminitletters=_aliassyminitletters)
2325 2326
2326 2327 def _parsealiasdecl(decl):
2327 2328 """Parse alias declaration ``decl``
2328 2329
2329 2330 This returns ``(name, tree, args, errorstr)`` tuple:
2330 2331
2331 2332 - ``name``: of declared alias (may be ``decl`` itself at error)
2332 2333 - ``tree``: parse result (or ``None`` at error)
2333 2334 - ``args``: list of alias argument names (or None for symbol declaration)
2334 2335 - ``errorstr``: detail about detected error (or None)
2335 2336
2336 2337 >>> _parsealiasdecl('foo')
2337 2338 ('foo', ('symbol', 'foo'), None, None)
2338 2339 >>> _parsealiasdecl('$foo')
2339 2340 ('$foo', None, None, "'$' not for alias arguments")
2340 2341 >>> _parsealiasdecl('foo::bar')
2341 2342 ('foo::bar', None, None, 'invalid format')
2342 2343 >>> _parsealiasdecl('foo bar')
2343 2344 ('foo bar', None, None, 'at 4: invalid token')
2344 2345 >>> _parsealiasdecl('foo()')
2345 2346 ('foo', ('func', ('symbol', 'foo')), [], None)
2346 2347 >>> _parsealiasdecl('$foo()')
2347 2348 ('$foo()', None, None, "'$' not for alias arguments")
2348 2349 >>> _parsealiasdecl('foo($1, $2)')
2349 2350 ('foo', ('func', ('symbol', 'foo')), ['$1', '$2'], None)
2350 2351 >>> _parsealiasdecl('foo(bar_bar, baz.baz)')
2351 2352 ('foo', ('func', ('symbol', 'foo')), ['bar_bar', 'baz.baz'], None)
2352 2353 >>> _parsealiasdecl('foo($1, $2, nested($1, $2))')
2353 2354 ('foo($1, $2, nested($1, $2))', None, None, 'invalid argument list')
2354 2355 >>> _parsealiasdecl('foo(bar($1, $2))')
2355 2356 ('foo(bar($1, $2))', None, None, 'invalid argument list')
2356 2357 >>> _parsealiasdecl('foo("string")')
2357 2358 ('foo("string")', None, None, 'invalid argument list')
2358 2359 >>> _parsealiasdecl('foo($1, $2')
2359 2360 ('foo($1, $2', None, None, 'at 10: unexpected token: end')
2360 2361 >>> _parsealiasdecl('foo("string')
2361 2362 ('foo("string', None, None, 'at 5: unterminated string')
2362 2363 >>> _parsealiasdecl('foo($1, $2, $1)')
2363 2364 ('foo', None, None, 'argument names collide with each other')
2364 2365 """
2365 2366 p = parser.parser(_tokenizealias, elements)
2366 2367 try:
2367 2368 tree, pos = p.parse(decl)
2368 2369 if (pos != len(decl)):
2369 2370 raise error.ParseError(_('invalid token'), pos)
2370 2371
2371 2372 if isvalidsymbol(tree):
2372 2373 # "name = ...." style
2373 2374 name = getsymbol(tree)
2374 2375 if name.startswith('$'):
2375 2376 return (decl, None, None, _("'$' not for alias arguments"))
2376 2377 return (name, ('symbol', name), None, None)
2377 2378
2378 2379 if isvalidfunc(tree):
2379 2380 # "name(arg, ....) = ...." style
2380 2381 name = getfuncname(tree)
2381 2382 if name.startswith('$'):
2382 2383 return (decl, None, None, _("'$' not for alias arguments"))
2383 2384 args = []
2384 2385 for arg in getfuncargs(tree):
2385 2386 if not isvalidsymbol(arg):
2386 2387 return (decl, None, None, _("invalid argument list"))
2387 2388 args.append(getsymbol(arg))
2388 2389 if len(args) != len(set(args)):
2389 2390 return (name, None, None,
2390 2391 _("argument names collide with each other"))
2391 2392 return (name, ('func', ('symbol', name)), args, None)
2392 2393
2393 2394 return (decl, None, None, _("invalid format"))
2394 2395 except error.ParseError, inst:
2395 2396 return (decl, None, None, parseerrordetail(inst))
2396 2397
2397 2398 def _parsealiasdefn(defn, args):
2398 2399 """Parse alias definition ``defn``
2399 2400
2400 2401 This function also replaces alias argument references in the
2401 2402 specified definition by ``_aliasarg(ARGNAME)``.
2402 2403
2403 2404 ``args`` is a list of alias argument names, or None if the alias
2404 2405 is declared as a symbol.
2405 2406
2406 2407 This returns "tree" as parsing result.
2407 2408
2408 2409 >>> args = ['$1', '$2', 'foo']
2409 2410 >>> print prettyformat(_parsealiasdefn('$1 or foo', args))
2410 2411 (or
2411 2412 (func
2412 2413 ('symbol', '_aliasarg')
2413 2414 ('string', '$1'))
2414 2415 (func
2415 2416 ('symbol', '_aliasarg')
2416 2417 ('string', 'foo')))
2417 2418 >>> try:
2418 2419 ... _parsealiasdefn('$1 or $bar', args)
2419 2420 ... except error.ParseError, inst:
2420 2421 ... print parseerrordetail(inst)
2421 2422 at 6: '$' not for alias arguments
2422 2423 >>> args = ['$1', '$10', 'foo']
2423 2424 >>> print prettyformat(_parsealiasdefn('$10 or foobar', args))
2424 2425 (or
2425 2426 (func
2426 2427 ('symbol', '_aliasarg')
2427 2428 ('string', '$10'))
2428 2429 ('symbol', 'foobar'))
2429 2430 >>> print prettyformat(_parsealiasdefn('"$1" or "foo"', args))
2430 2431 (or
2431 2432 ('string', '$1')
2432 2433 ('string', 'foo'))
2433 2434 """
2434 2435 def tokenizedefn(program, lookup=None):
2435 2436 if args:
2436 2437 argset = set(args)
2437 2438 else:
2438 2439 argset = set()
2439 2440
2440 2441 for t, value, pos in _tokenizealias(program, lookup=lookup):
2441 2442 if t == 'symbol':
2442 2443 if value in argset:
2443 2444 # emulate tokenization of "_aliasarg('ARGNAME')":
2444 2445 # "_aliasarg()" is an unknown symbol only used separate
2445 2446 # alias argument placeholders from regular strings.
2446 2447 yield ('symbol', '_aliasarg', pos)
2447 2448 yield ('(', None, pos)
2448 2449 yield ('string', value, pos)
2449 2450 yield (')', None, pos)
2450 2451 continue
2451 2452 elif value.startswith('$'):
2452 2453 raise error.ParseError(_("'$' not for alias arguments"),
2453 2454 pos)
2454 2455 yield (t, value, pos)
2455 2456
2456 2457 p = parser.parser(tokenizedefn, elements)
2457 2458 tree, pos = p.parse(defn)
2458 2459 if pos != len(defn):
2459 2460 raise error.ParseError(_('invalid token'), pos)
2460 2461 return parser.simplifyinfixops(tree, ('or',))
2461 2462
2462 2463 class revsetalias(object):
2463 2464 # whether own `error` information is already shown or not.
2464 2465 # this avoids showing same warning multiple times at each `findaliases`.
2465 2466 warned = False
2466 2467
2467 2468 def __init__(self, name, value):
2468 2469 '''Aliases like:
2469 2470
2470 2471 h = heads(default)
2471 2472 b($1) = ancestors($1) - ancestors(default)
2472 2473 '''
2473 2474 self.name, self.tree, self.args, self.error = _parsealiasdecl(name)
2474 2475 if self.error:
2475 2476 self.error = _('failed to parse the declaration of revset alias'
2476 2477 ' "%s": %s') % (self.name, self.error)
2477 2478 return
2478 2479
2479 2480 try:
2480 2481 self.replacement = _parsealiasdefn(value, self.args)
2481 2482 # Check for placeholder injection
2482 2483 _checkaliasarg(self.replacement, self.args)
2483 2484 except error.ParseError, inst:
2484 2485 self.error = _('failed to parse the definition of revset alias'
2485 2486 ' "%s": %s') % (self.name, parseerrordetail(inst))
2486 2487
2487 2488 def _getalias(aliases, tree):
2488 2489 """If tree looks like an unexpanded alias, return it. Return None
2489 2490 otherwise.
2490 2491 """
2491 2492 if isinstance(tree, tuple) and tree:
2492 2493 if tree[0] == 'symbol' and len(tree) == 2:
2493 2494 name = tree[1]
2494 2495 alias = aliases.get(name)
2495 2496 if alias and alias.args is None and alias.tree == tree:
2496 2497 return alias
2497 2498 if tree[0] == 'func' and len(tree) > 1:
2498 2499 if tree[1][0] == 'symbol' and len(tree[1]) == 2:
2499 2500 name = tree[1][1]
2500 2501 alias = aliases.get(name)
2501 2502 if alias and alias.args is not None and alias.tree == tree[:2]:
2502 2503 return alias
2503 2504 return None
2504 2505
2505 2506 def _expandargs(tree, args):
2506 2507 """Replace _aliasarg instances with the substitution value of the
2507 2508 same name in args, recursively.
2508 2509 """
2509 2510 if not tree or not isinstance(tree, tuple):
2510 2511 return tree
2511 2512 arg = _getaliasarg(tree)
2512 2513 if arg is not None:
2513 2514 return args[arg]
2514 2515 return tuple(_expandargs(t, args) for t in tree)
2515 2516
2516 2517 def _expandaliases(aliases, tree, expanding, cache):
2517 2518 """Expand aliases in tree, recursively.
2518 2519
2519 2520 'aliases' is a dictionary mapping user defined aliases to
2520 2521 revsetalias objects.
2521 2522 """
2522 2523 if not isinstance(tree, tuple):
2523 2524 # Do not expand raw strings
2524 2525 return tree
2525 2526 alias = _getalias(aliases, tree)
2526 2527 if alias is not None:
2527 2528 if alias.error:
2528 2529 raise util.Abort(alias.error)
2529 2530 if alias in expanding:
2530 2531 raise error.ParseError(_('infinite expansion of revset alias "%s" '
2531 2532 'detected') % alias.name)
2532 2533 expanding.append(alias)
2533 2534 if alias.name not in cache:
2534 2535 cache[alias.name] = _expandaliases(aliases, alias.replacement,
2535 2536 expanding, cache)
2536 2537 result = cache[alias.name]
2537 2538 expanding.pop()
2538 2539 if alias.args is not None:
2539 2540 l = getlist(tree[2])
2540 2541 if len(l) != len(alias.args):
2541 2542 raise error.ParseError(
2542 2543 _('invalid number of arguments: %s') % len(l))
2543 2544 l = [_expandaliases(aliases, a, [], cache) for a in l]
2544 2545 result = _expandargs(result, dict(zip(alias.args, l)))
2545 2546 else:
2546 2547 result = tuple(_expandaliases(aliases, t, expanding, cache)
2547 2548 for t in tree)
2548 2549 return result
2549 2550
2550 2551 def findaliases(ui, tree, showwarning=None):
2551 2552 _checkaliasarg(tree)
2552 2553 aliases = {}
2553 2554 for k, v in ui.configitems('revsetalias'):
2554 2555 alias = revsetalias(k, v)
2555 2556 aliases[alias.name] = alias
2556 2557 tree = _expandaliases(aliases, tree, [], {})
2557 2558 if showwarning:
2558 2559 # warn about problematic (but not referred) aliases
2559 2560 for name, alias in sorted(aliases.iteritems()):
2560 2561 if alias.error and not alias.warned:
2561 2562 showwarning(_('warning: %s\n') % (alias.error))
2562 2563 alias.warned = True
2563 2564 return tree
2564 2565
2565 2566 def foldconcat(tree):
2566 2567 """Fold elements to be concatenated by `##`
2567 2568 """
2568 2569 if not isinstance(tree, tuple) or tree[0] in ('string', 'symbol'):
2569 2570 return tree
2570 2571 if tree[0] == '_concat':
2571 2572 pending = [tree]
2572 2573 l = []
2573 2574 while pending:
2574 2575 e = pending.pop()
2575 2576 if e[0] == '_concat':
2576 2577 pending.extend(reversed(e[1:]))
2577 2578 elif e[0] in ('string', 'symbol'):
2578 2579 l.append(e[1])
2579 2580 else:
2580 2581 msg = _("\"##\" can't concatenate \"%s\" element") % (e[0])
2581 2582 raise error.ParseError(msg)
2582 2583 return ('string', ''.join(l))
2583 2584 else:
2584 2585 return tuple(foldconcat(t) for t in tree)
2585 2586
2586 2587 def parse(spec, lookup=None):
2587 2588 p = parser.parser(tokenize, elements)
2588 2589 tree, pos = p.parse(spec, lookup=lookup)
2589 2590 if pos != len(spec):
2590 2591 raise error.ParseError(_("invalid token"), pos)
2591 2592 return parser.simplifyinfixops(tree, ('or',))
2592 2593
2593 2594 def posttreebuilthook(tree, repo):
2594 2595 # hook for extensions to execute code on the optimized tree
2595 2596 pass
2596 2597
2597 2598 def match(ui, spec, repo=None):
2598 2599 if not spec:
2599 2600 raise error.ParseError(_("empty query"))
2600 2601 lookup = None
2601 2602 if repo:
2602 2603 lookup = repo.__contains__
2603 2604 tree = parse(spec, lookup)
2604 2605 if ui:
2605 2606 tree = findaliases(ui, tree, showwarning=ui.warn)
2606 2607 tree = foldconcat(tree)
2607 2608 weight, tree = optimize(tree, True)
2608 2609 posttreebuilthook(tree, repo)
2609 2610 def mfunc(repo, subset=None):
2610 2611 if subset is None:
2611 2612 subset = fullreposet(repo)
2612 2613 if util.safehasattr(subset, 'isascending'):
2613 2614 result = getset(repo, subset, tree)
2614 2615 else:
2615 2616 result = getset(repo, baseset(subset), tree)
2616 2617 return result
2617 2618 return mfunc
2618 2619
2619 2620 def formatspec(expr, *args):
2620 2621 '''
2621 2622 This is a convenience function for using revsets internally, and
2622 2623 escapes arguments appropriately. Aliases are intentionally ignored
2623 2624 so that intended expression behavior isn't accidentally subverted.
2624 2625
2625 2626 Supported arguments:
2626 2627
2627 2628 %r = revset expression, parenthesized
2628 2629 %d = int(arg), no quoting
2629 2630 %s = string(arg), escaped and single-quoted
2630 2631 %b = arg.branch(), escaped and single-quoted
2631 2632 %n = hex(arg), single-quoted
2632 2633 %% = a literal '%'
2633 2634
2634 2635 Prefixing the type with 'l' specifies a parenthesized list of that type.
2635 2636
2636 2637 >>> formatspec('%r:: and %lr', '10 or 11', ("this()", "that()"))
2637 2638 '(10 or 11):: and ((this()) or (that()))'
2638 2639 >>> formatspec('%d:: and not %d::', 10, 20)
2639 2640 '10:: and not 20::'
2640 2641 >>> formatspec('%ld or %ld', [], [1])
2641 2642 "_list('') or 1"
2642 2643 >>> formatspec('keyword(%s)', 'foo\\xe9')
2643 2644 "keyword('foo\\\\xe9')"
2644 2645 >>> b = lambda: 'default'
2645 2646 >>> b.branch = b
2646 2647 >>> formatspec('branch(%b)', b)
2647 2648 "branch('default')"
2648 2649 >>> formatspec('root(%ls)', ['a', 'b', 'c', 'd'])
2649 2650 "root(_list('a\\x00b\\x00c\\x00d'))"
2650 2651 '''
2651 2652
2652 2653 def quote(s):
2653 2654 return repr(str(s))
2654 2655
2655 2656 def argtype(c, arg):
2656 2657 if c == 'd':
2657 2658 return str(int(arg))
2658 2659 elif c == 's':
2659 2660 return quote(arg)
2660 2661 elif c == 'r':
2661 2662 parse(arg) # make sure syntax errors are confined
2662 2663 return '(%s)' % arg
2663 2664 elif c == 'n':
2664 2665 return quote(node.hex(arg))
2665 2666 elif c == 'b':
2666 2667 return quote(arg.branch())
2667 2668
2668 2669 def listexp(s, t):
2669 2670 l = len(s)
2670 2671 if l == 0:
2671 2672 return "_list('')"
2672 2673 elif l == 1:
2673 2674 return argtype(t, s[0])
2674 2675 elif t == 'd':
2675 2676 return "_intlist('%s')" % "\0".join(str(int(a)) for a in s)
2676 2677 elif t == 's':
2677 2678 return "_list('%s')" % "\0".join(s)
2678 2679 elif t == 'n':
2679 2680 return "_hexlist('%s')" % "\0".join(node.hex(a) for a in s)
2680 2681 elif t == 'b':
2681 2682 return "_list('%s')" % "\0".join(a.branch() for a in s)
2682 2683
2683 2684 m = l // 2
2684 2685 return '(%s or %s)' % (listexp(s[:m], t), listexp(s[m:], t))
2685 2686
2686 2687 ret = ''
2687 2688 pos = 0
2688 2689 arg = 0
2689 2690 while pos < len(expr):
2690 2691 c = expr[pos]
2691 2692 if c == '%':
2692 2693 pos += 1
2693 2694 d = expr[pos]
2694 2695 if d == '%':
2695 2696 ret += d
2696 2697 elif d in 'dsnbr':
2697 2698 ret += argtype(d, args[arg])
2698 2699 arg += 1
2699 2700 elif d == 'l':
2700 2701 # a list of some type
2701 2702 pos += 1
2702 2703 d = expr[pos]
2703 2704 ret += listexp(list(args[arg]), d)
2704 2705 arg += 1
2705 2706 else:
2706 2707 raise util.Abort('unexpected revspec format character %s' % d)
2707 2708 else:
2708 2709 ret += c
2709 2710 pos += 1
2710 2711
2711 2712 return ret
2712 2713
2713 2714 def prettyformat(tree):
2714 2715 return parser.prettyformat(tree, ('string', 'symbol'))
2715 2716
2716 2717 def depth(tree):
2717 2718 if isinstance(tree, tuple):
2718 2719 return max(map(depth, tree)) + 1
2719 2720 else:
2720 2721 return 0
2721 2722
2722 2723 def funcsused(tree):
2723 2724 if not isinstance(tree, tuple) or tree[0] in ('string', 'symbol'):
2724 2725 return set()
2725 2726 else:
2726 2727 funcs = set()
2727 2728 for s in tree[1:]:
2728 2729 funcs |= funcsused(s)
2729 2730 if tree[0] == 'func':
2730 2731 funcs.add(tree[1][1])
2731 2732 return funcs
2732 2733
2733 2734 class abstractsmartset(object):
2734 2735
2735 2736 def __nonzero__(self):
2736 2737 """True if the smartset is not empty"""
2737 2738 raise NotImplementedError()
2738 2739
2739 2740 def __contains__(self, rev):
2740 2741 """provide fast membership testing"""
2741 2742 raise NotImplementedError()
2742 2743
2743 2744 def __iter__(self):
2744 2745 """iterate the set in the order it is supposed to be iterated"""
2745 2746 raise NotImplementedError()
2746 2747
2747 2748 # Attributes containing a function to perform a fast iteration in a given
2748 2749 # direction. A smartset can have none, one, or both defined.
2749 2750 #
2750 2751 # Default value is None instead of a function returning None to avoid
2751 2752 # initializing an iterator just for testing if a fast method exists.
2752 2753 fastasc = None
2753 2754 fastdesc = None
2754 2755
2755 2756 def isascending(self):
2756 2757 """True if the set will iterate in ascending order"""
2757 2758 raise NotImplementedError()
2758 2759
2759 2760 def isdescending(self):
2760 2761 """True if the set will iterate in descending order"""
2761 2762 raise NotImplementedError()
2762 2763
2763 2764 def min(self):
2764 2765 """return the minimum element in the set"""
2765 2766 if self.fastasc is not None:
2766 2767 for r in self.fastasc():
2767 2768 return r
2768 2769 raise ValueError('arg is an empty sequence')
2769 2770 return min(self)
2770 2771
2771 2772 def max(self):
2772 2773 """return the maximum element in the set"""
2773 2774 if self.fastdesc is not None:
2774 2775 for r in self.fastdesc():
2775 2776 return r
2776 2777 raise ValueError('arg is an empty sequence')
2777 2778 return max(self)
2778 2779
2779 2780 def first(self):
2780 2781 """return the first element in the set (user iteration perspective)
2781 2782
2782 2783 Return None if the set is empty"""
2783 2784 raise NotImplementedError()
2784 2785
2785 2786 def last(self):
2786 2787 """return the last element in the set (user iteration perspective)
2787 2788
2788 2789 Return None if the set is empty"""
2789 2790 raise NotImplementedError()
2790 2791
2791 2792 def __len__(self):
2792 2793 """return the length of the smartsets
2793 2794
2794 2795 This can be expensive on smartset that could be lazy otherwise."""
2795 2796 raise NotImplementedError()
2796 2797
2797 2798 def reverse(self):
2798 2799 """reverse the expected iteration order"""
2799 2800 raise NotImplementedError()
2800 2801
2801 2802 def sort(self, reverse=True):
2802 2803 """get the set to iterate in an ascending or descending order"""
2803 2804 raise NotImplementedError()
2804 2805
2805 2806 def __and__(self, other):
2806 2807 """Returns a new object with the intersection of the two collections.
2807 2808
2808 2809 This is part of the mandatory API for smartset."""
2809 2810 if isinstance(other, fullreposet):
2810 2811 return self
2811 2812 return self.filter(other.__contains__, cache=False)
2812 2813
2813 2814 def __add__(self, other):
2814 2815 """Returns a new object with the union of the two collections.
2815 2816
2816 2817 This is part of the mandatory API for smartset."""
2817 2818 return addset(self, other)
2818 2819
2819 2820 def __sub__(self, other):
2820 2821 """Returns a new object with the substraction of the two collections.
2821 2822
2822 2823 This is part of the mandatory API for smartset."""
2823 2824 c = other.__contains__
2824 2825 return self.filter(lambda r: not c(r), cache=False)
2825 2826
2826 2827 def filter(self, condition, cache=True):
2827 2828 """Returns this smartset filtered by condition as a new smartset.
2828 2829
2829 2830 `condition` is a callable which takes a revision number and returns a
2830 2831 boolean.
2831 2832
2832 2833 This is part of the mandatory API for smartset."""
2833 2834 # builtin cannot be cached. but do not needs to
2834 2835 if cache and util.safehasattr(condition, 'func_code'):
2835 2836 condition = util.cachefunc(condition)
2836 2837 return filteredset(self, condition)
2837 2838
2838 2839 class baseset(abstractsmartset):
2839 2840 """Basic data structure that represents a revset and contains the basic
2840 2841 operation that it should be able to perform.
2841 2842
2842 2843 Every method in this class should be implemented by any smartset class.
2843 2844 """
2844 2845 def __init__(self, data=()):
2845 2846 if not isinstance(data, list):
2846 2847 data = list(data)
2847 2848 self._list = data
2848 2849 self._ascending = None
2849 2850
2850 2851 @util.propertycache
2851 2852 def _set(self):
2852 2853 return set(self._list)
2853 2854
2854 2855 @util.propertycache
2855 2856 def _asclist(self):
2856 2857 asclist = self._list[:]
2857 2858 asclist.sort()
2858 2859 return asclist
2859 2860
2860 2861 def __iter__(self):
2861 2862 if self._ascending is None:
2862 2863 return iter(self._list)
2863 2864 elif self._ascending:
2864 2865 return iter(self._asclist)
2865 2866 else:
2866 2867 return reversed(self._asclist)
2867 2868
2868 2869 def fastasc(self):
2869 2870 return iter(self._asclist)
2870 2871
2871 2872 def fastdesc(self):
2872 2873 return reversed(self._asclist)
2873 2874
2874 2875 @util.propertycache
2875 2876 def __contains__(self):
2876 2877 return self._set.__contains__
2877 2878
2878 2879 def __nonzero__(self):
2879 2880 return bool(self._list)
2880 2881
2881 2882 def sort(self, reverse=False):
2882 2883 self._ascending = not bool(reverse)
2883 2884
2884 2885 def reverse(self):
2885 2886 if self._ascending is None:
2886 2887 self._list.reverse()
2887 2888 else:
2888 2889 self._ascending = not self._ascending
2889 2890
2890 2891 def __len__(self):
2891 2892 return len(self._list)
2892 2893
2893 2894 def isascending(self):
2894 2895 """Returns True if the collection is ascending order, False if not.
2895 2896
2896 2897 This is part of the mandatory API for smartset."""
2897 2898 if len(self) <= 1:
2898 2899 return True
2899 2900 return self._ascending is not None and self._ascending
2900 2901
2901 2902 def isdescending(self):
2902 2903 """Returns True if the collection is descending order, False if not.
2903 2904
2904 2905 This is part of the mandatory API for smartset."""
2905 2906 if len(self) <= 1:
2906 2907 return True
2907 2908 return self._ascending is not None and not self._ascending
2908 2909
2909 2910 def first(self):
2910 2911 if self:
2911 2912 if self._ascending is None:
2912 2913 return self._list[0]
2913 2914 elif self._ascending:
2914 2915 return self._asclist[0]
2915 2916 else:
2916 2917 return self._asclist[-1]
2917 2918 return None
2918 2919
2919 2920 def last(self):
2920 2921 if self:
2921 2922 if self._ascending is None:
2922 2923 return self._list[-1]
2923 2924 elif self._ascending:
2924 2925 return self._asclist[-1]
2925 2926 else:
2926 2927 return self._asclist[0]
2927 2928 return None
2928 2929
2929 2930 def __repr__(self):
2930 2931 d = {None: '', False: '-', True: '+'}[self._ascending]
2931 2932 return '<%s%s %r>' % (type(self).__name__, d, self._list)
2932 2933
2933 2934 class filteredset(abstractsmartset):
2934 2935 """Duck type for baseset class which iterates lazily over the revisions in
2935 2936 the subset and contains a function which tests for membership in the
2936 2937 revset
2937 2938 """
2938 2939 def __init__(self, subset, condition=lambda x: True):
2939 2940 """
2940 2941 condition: a function that decide whether a revision in the subset
2941 2942 belongs to the revset or not.
2942 2943 """
2943 2944 self._subset = subset
2944 2945 self._condition = condition
2945 2946 self._cache = {}
2946 2947
2947 2948 def __contains__(self, x):
2948 2949 c = self._cache
2949 2950 if x not in c:
2950 2951 v = c[x] = x in self._subset and self._condition(x)
2951 2952 return v
2952 2953 return c[x]
2953 2954
2954 2955 def __iter__(self):
2955 2956 return self._iterfilter(self._subset)
2956 2957
2957 2958 def _iterfilter(self, it):
2958 2959 cond = self._condition
2959 2960 for x in it:
2960 2961 if cond(x):
2961 2962 yield x
2962 2963
2963 2964 @property
2964 2965 def fastasc(self):
2965 2966 it = self._subset.fastasc
2966 2967 if it is None:
2967 2968 return None
2968 2969 return lambda: self._iterfilter(it())
2969 2970
2970 2971 @property
2971 2972 def fastdesc(self):
2972 2973 it = self._subset.fastdesc
2973 2974 if it is None:
2974 2975 return None
2975 2976 return lambda: self._iterfilter(it())
2976 2977
2977 2978 def __nonzero__(self):
2978 2979 for r in self:
2979 2980 return True
2980 2981 return False
2981 2982
2982 2983 def __len__(self):
2983 2984 # Basic implementation to be changed in future patches.
2984 2985 l = baseset([r for r in self])
2985 2986 return len(l)
2986 2987
2987 2988 def sort(self, reverse=False):
2988 2989 self._subset.sort(reverse=reverse)
2989 2990
2990 2991 def reverse(self):
2991 2992 self._subset.reverse()
2992 2993
2993 2994 def isascending(self):
2994 2995 return self._subset.isascending()
2995 2996
2996 2997 def isdescending(self):
2997 2998 return self._subset.isdescending()
2998 2999
2999 3000 def first(self):
3000 3001 for x in self:
3001 3002 return x
3002 3003 return None
3003 3004
3004 3005 def last(self):
3005 3006 it = None
3006 3007 if self._subset.isascending:
3007 3008 it = self.fastdesc
3008 3009 elif self._subset.isdescending:
3009 3010 it = self.fastdesc
3010 3011 if it is None:
3011 3012 # slowly consume everything. This needs improvement
3012 3013 it = lambda: reversed(list(self))
3013 3014 for x in it():
3014 3015 return x
3015 3016 return None
3016 3017
3017 3018 def __repr__(self):
3018 3019 return '<%s %r>' % (type(self).__name__, self._subset)
3019 3020
3020 3021 # this function will be removed, or merged to addset or orset, when
3021 3022 # - scmutil.revrange() can be rewritten to not combine calculated smartsets
3022 3023 # - or addset can handle more than two sets without balanced tree
3023 3024 def _combinesets(subsets):
3024 3025 """Create balanced tree of addsets representing union of given sets"""
3025 3026 if not subsets:
3026 3027 return baseset()
3027 3028 if len(subsets) == 1:
3028 3029 return subsets[0]
3029 3030 p = len(subsets) // 2
3030 3031 xs = _combinesets(subsets[:p])
3031 3032 ys = _combinesets(subsets[p:])
3032 3033 return addset(xs, ys)
3033 3034
3034 3035 def _iterordered(ascending, iter1, iter2):
3035 3036 """produce an ordered iteration from two iterators with the same order
3036 3037
3037 3038 The ascending is used to indicated the iteration direction.
3038 3039 """
3039 3040 choice = max
3040 3041 if ascending:
3041 3042 choice = min
3042 3043
3043 3044 val1 = None
3044 3045 val2 = None
3045 3046 try:
3046 3047 # Consume both iterators in an ordered way until one is empty
3047 3048 while True:
3048 3049 if val1 is None:
3049 3050 val1 = iter1.next()
3050 3051 if val2 is None:
3051 3052 val2 = iter2.next()
3052 3053 next = choice(val1, val2)
3053 3054 yield next
3054 3055 if val1 == next:
3055 3056 val1 = None
3056 3057 if val2 == next:
3057 3058 val2 = None
3058 3059 except StopIteration:
3059 3060 # Flush any remaining values and consume the other one
3060 3061 it = iter2
3061 3062 if val1 is not None:
3062 3063 yield val1
3063 3064 it = iter1
3064 3065 elif val2 is not None:
3065 3066 # might have been equality and both are empty
3066 3067 yield val2
3067 3068 for val in it:
3068 3069 yield val
3069 3070
3070 3071 class addset(abstractsmartset):
3071 3072 """Represent the addition of two sets
3072 3073
3073 3074 Wrapper structure for lazily adding two structures without losing much
3074 3075 performance on the __contains__ method
3075 3076
3076 3077 If the ascending attribute is set, that means the two structures are
3077 3078 ordered in either an ascending or descending way. Therefore, we can add
3078 3079 them maintaining the order by iterating over both at the same time
3079 3080
3080 3081 >>> xs = baseset([0, 3, 2])
3081 3082 >>> ys = baseset([5, 2, 4])
3082 3083
3083 3084 >>> rs = addset(xs, ys)
3084 3085 >>> bool(rs), 0 in rs, 1 in rs, 5 in rs, rs.first(), rs.last()
3085 3086 (True, True, False, True, 0, 4)
3086 3087 >>> rs = addset(xs, baseset([]))
3087 3088 >>> bool(rs), 0 in rs, 1 in rs, rs.first(), rs.last()
3088 3089 (True, True, False, 0, 2)
3089 3090 >>> rs = addset(baseset([]), baseset([]))
3090 3091 >>> bool(rs), 0 in rs, rs.first(), rs.last()
3091 3092 (False, False, None, None)
3092 3093
3093 3094 iterate unsorted:
3094 3095 >>> rs = addset(xs, ys)
3095 3096 >>> [x for x in rs] # without _genlist
3096 3097 [0, 3, 2, 5, 4]
3097 3098 >>> assert not rs._genlist
3098 3099 >>> len(rs)
3099 3100 5
3100 3101 >>> [x for x in rs] # with _genlist
3101 3102 [0, 3, 2, 5, 4]
3102 3103 >>> assert rs._genlist
3103 3104
3104 3105 iterate ascending:
3105 3106 >>> rs = addset(xs, ys, ascending=True)
3106 3107 >>> [x for x in rs], [x for x in rs.fastasc()] # without _asclist
3107 3108 ([0, 2, 3, 4, 5], [0, 2, 3, 4, 5])
3108 3109 >>> assert not rs._asclist
3109 3110 >>> len(rs)
3110 3111 5
3111 3112 >>> [x for x in rs], [x for x in rs.fastasc()]
3112 3113 ([0, 2, 3, 4, 5], [0, 2, 3, 4, 5])
3113 3114 >>> assert rs._asclist
3114 3115
3115 3116 iterate descending:
3116 3117 >>> rs = addset(xs, ys, ascending=False)
3117 3118 >>> [x for x in rs], [x for x in rs.fastdesc()] # without _asclist
3118 3119 ([5, 4, 3, 2, 0], [5, 4, 3, 2, 0])
3119 3120 >>> assert not rs._asclist
3120 3121 >>> len(rs)
3121 3122 5
3122 3123 >>> [x for x in rs], [x for x in rs.fastdesc()]
3123 3124 ([5, 4, 3, 2, 0], [5, 4, 3, 2, 0])
3124 3125 >>> assert rs._asclist
3125 3126
3126 3127 iterate ascending without fastasc:
3127 3128 >>> rs = addset(xs, generatorset(ys), ascending=True)
3128 3129 >>> assert rs.fastasc is None
3129 3130 >>> [x for x in rs]
3130 3131 [0, 2, 3, 4, 5]
3131 3132
3132 3133 iterate descending without fastdesc:
3133 3134 >>> rs = addset(generatorset(xs), ys, ascending=False)
3134 3135 >>> assert rs.fastdesc is None
3135 3136 >>> [x for x in rs]
3136 3137 [5, 4, 3, 2, 0]
3137 3138 """
3138 3139 def __init__(self, revs1, revs2, ascending=None):
3139 3140 self._r1 = revs1
3140 3141 self._r2 = revs2
3141 3142 self._iter = None
3142 3143 self._ascending = ascending
3143 3144 self._genlist = None
3144 3145 self._asclist = None
3145 3146
3146 3147 def __len__(self):
3147 3148 return len(self._list)
3148 3149
3149 3150 def __nonzero__(self):
3150 3151 return bool(self._r1) or bool(self._r2)
3151 3152
3152 3153 @util.propertycache
3153 3154 def _list(self):
3154 3155 if not self._genlist:
3155 3156 self._genlist = baseset(iter(self))
3156 3157 return self._genlist
3157 3158
3158 3159 def __iter__(self):
3159 3160 """Iterate over both collections without repeating elements
3160 3161
3161 3162 If the ascending attribute is not set, iterate over the first one and
3162 3163 then over the second one checking for membership on the first one so we
3163 3164 dont yield any duplicates.
3164 3165
3165 3166 If the ascending attribute is set, iterate over both collections at the
3166 3167 same time, yielding only one value at a time in the given order.
3167 3168 """
3168 3169 if self._ascending is None:
3169 3170 if self._genlist:
3170 3171 return iter(self._genlist)
3171 3172 def arbitraryordergen():
3172 3173 for r in self._r1:
3173 3174 yield r
3174 3175 inr1 = self._r1.__contains__
3175 3176 for r in self._r2:
3176 3177 if not inr1(r):
3177 3178 yield r
3178 3179 return arbitraryordergen()
3179 3180 # try to use our own fast iterator if it exists
3180 3181 self._trysetasclist()
3181 3182 if self._ascending:
3182 3183 attr = 'fastasc'
3183 3184 else:
3184 3185 attr = 'fastdesc'
3185 3186 it = getattr(self, attr)
3186 3187 if it is not None:
3187 3188 return it()
3188 3189 # maybe half of the component supports fast
3189 3190 # get iterator for _r1
3190 3191 iter1 = getattr(self._r1, attr)
3191 3192 if iter1 is None:
3192 3193 # let's avoid side effect (not sure it matters)
3193 3194 iter1 = iter(sorted(self._r1, reverse=not self._ascending))
3194 3195 else:
3195 3196 iter1 = iter1()
3196 3197 # get iterator for _r2
3197 3198 iter2 = getattr(self._r2, attr)
3198 3199 if iter2 is None:
3199 3200 # let's avoid side effect (not sure it matters)
3200 3201 iter2 = iter(sorted(self._r2, reverse=not self._ascending))
3201 3202 else:
3202 3203 iter2 = iter2()
3203 3204 return _iterordered(self._ascending, iter1, iter2)
3204 3205
3205 3206 def _trysetasclist(self):
3206 3207 """populate the _asclist attribute if possible and necessary"""
3207 3208 if self._genlist is not None and self._asclist is None:
3208 3209 self._asclist = sorted(self._genlist)
3209 3210
3210 3211 @property
3211 3212 def fastasc(self):
3212 3213 self._trysetasclist()
3213 3214 if self._asclist is not None:
3214 3215 return self._asclist.__iter__
3215 3216 iter1 = self._r1.fastasc
3216 3217 iter2 = self._r2.fastasc
3217 3218 if None in (iter1, iter2):
3218 3219 return None
3219 3220 return lambda: _iterordered(True, iter1(), iter2())
3220 3221
3221 3222 @property
3222 3223 def fastdesc(self):
3223 3224 self._trysetasclist()
3224 3225 if self._asclist is not None:
3225 3226 return self._asclist.__reversed__
3226 3227 iter1 = self._r1.fastdesc
3227 3228 iter2 = self._r2.fastdesc
3228 3229 if None in (iter1, iter2):
3229 3230 return None
3230 3231 return lambda: _iterordered(False, iter1(), iter2())
3231 3232
3232 3233 def __contains__(self, x):
3233 3234 return x in self._r1 or x in self._r2
3234 3235
3235 3236 def sort(self, reverse=False):
3236 3237 """Sort the added set
3237 3238
3238 3239 For this we use the cached list with all the generated values and if we
3239 3240 know they are ascending or descending we can sort them in a smart way.
3240 3241 """
3241 3242 self._ascending = not reverse
3242 3243
3243 3244 def isascending(self):
3244 3245 return self._ascending is not None and self._ascending
3245 3246
3246 3247 def isdescending(self):
3247 3248 return self._ascending is not None and not self._ascending
3248 3249
3249 3250 def reverse(self):
3250 3251 if self._ascending is None:
3251 3252 self._list.reverse()
3252 3253 else:
3253 3254 self._ascending = not self._ascending
3254 3255
3255 3256 def first(self):
3256 3257 for x in self:
3257 3258 return x
3258 3259 return None
3259 3260
3260 3261 def last(self):
3261 3262 self.reverse()
3262 3263 val = self.first()
3263 3264 self.reverse()
3264 3265 return val
3265 3266
3266 3267 def __repr__(self):
3267 3268 d = {None: '', False: '-', True: '+'}[self._ascending]
3268 3269 return '<%s%s %r, %r>' % (type(self).__name__, d, self._r1, self._r2)
3269 3270
3270 3271 class generatorset(abstractsmartset):
3271 3272 """Wrap a generator for lazy iteration
3272 3273
3273 3274 Wrapper structure for generators that provides lazy membership and can
3274 3275 be iterated more than once.
3275 3276 When asked for membership it generates values until either it finds the
3276 3277 requested one or has gone through all the elements in the generator
3277 3278 """
3278 3279 def __init__(self, gen, iterasc=None):
3279 3280 """
3280 3281 gen: a generator producing the values for the generatorset.
3281 3282 """
3282 3283 self._gen = gen
3283 3284 self._asclist = None
3284 3285 self._cache = {}
3285 3286 self._genlist = []
3286 3287 self._finished = False
3287 3288 self._ascending = True
3288 3289 if iterasc is not None:
3289 3290 if iterasc:
3290 3291 self.fastasc = self._iterator
3291 3292 self.__contains__ = self._asccontains
3292 3293 else:
3293 3294 self.fastdesc = self._iterator
3294 3295 self.__contains__ = self._desccontains
3295 3296
3296 3297 def __nonzero__(self):
3297 3298 # Do not use 'for r in self' because it will enforce the iteration
3298 3299 # order (default ascending), possibly unrolling a whole descending
3299 3300 # iterator.
3300 3301 if self._genlist:
3301 3302 return True
3302 3303 for r in self._consumegen():
3303 3304 return True
3304 3305 return False
3305 3306
3306 3307 def __contains__(self, x):
3307 3308 if x in self._cache:
3308 3309 return self._cache[x]
3309 3310
3310 3311 # Use new values only, as existing values would be cached.
3311 3312 for l in self._consumegen():
3312 3313 if l == x:
3313 3314 return True
3314 3315
3315 3316 self._cache[x] = False
3316 3317 return False
3317 3318
3318 3319 def _asccontains(self, x):
3319 3320 """version of contains optimised for ascending generator"""
3320 3321 if x in self._cache:
3321 3322 return self._cache[x]
3322 3323
3323 3324 # Use new values only, as existing values would be cached.
3324 3325 for l in self._consumegen():
3325 3326 if l == x:
3326 3327 return True
3327 3328 if l > x:
3328 3329 break
3329 3330
3330 3331 self._cache[x] = False
3331 3332 return False
3332 3333
3333 3334 def _desccontains(self, x):
3334 3335 """version of contains optimised for descending generator"""
3335 3336 if x in self._cache:
3336 3337 return self._cache[x]
3337 3338
3338 3339 # Use new values only, as existing values would be cached.
3339 3340 for l in self._consumegen():
3340 3341 if l == x:
3341 3342 return True
3342 3343 if l < x:
3343 3344 break
3344 3345
3345 3346 self._cache[x] = False
3346 3347 return False
3347 3348
3348 3349 def __iter__(self):
3349 3350 if self._ascending:
3350 3351 it = self.fastasc
3351 3352 else:
3352 3353 it = self.fastdesc
3353 3354 if it is not None:
3354 3355 return it()
3355 3356 # we need to consume the iterator
3356 3357 for x in self._consumegen():
3357 3358 pass
3358 3359 # recall the same code
3359 3360 return iter(self)
3360 3361
3361 3362 def _iterator(self):
3362 3363 if self._finished:
3363 3364 return iter(self._genlist)
3364 3365
3365 3366 # We have to use this complex iteration strategy to allow multiple
3366 3367 # iterations at the same time. We need to be able to catch revision
3367 3368 # removed from _consumegen and added to genlist in another instance.
3368 3369 #
3369 3370 # Getting rid of it would provide an about 15% speed up on this
3370 3371 # iteration.
3371 3372 genlist = self._genlist
3372 3373 nextrev = self._consumegen().next
3373 3374 _len = len # cache global lookup
3374 3375 def gen():
3375 3376 i = 0
3376 3377 while True:
3377 3378 if i < _len(genlist):
3378 3379 yield genlist[i]
3379 3380 else:
3380 3381 yield nextrev()
3381 3382 i += 1
3382 3383 return gen()
3383 3384
3384 3385 def _consumegen(self):
3385 3386 cache = self._cache
3386 3387 genlist = self._genlist.append
3387 3388 for item in self._gen:
3388 3389 cache[item] = True
3389 3390 genlist(item)
3390 3391 yield item
3391 3392 if not self._finished:
3392 3393 self._finished = True
3393 3394 asc = self._genlist[:]
3394 3395 asc.sort()
3395 3396 self._asclist = asc
3396 3397 self.fastasc = asc.__iter__
3397 3398 self.fastdesc = asc.__reversed__
3398 3399
3399 3400 def __len__(self):
3400 3401 for x in self._consumegen():
3401 3402 pass
3402 3403 return len(self._genlist)
3403 3404
3404 3405 def sort(self, reverse=False):
3405 3406 self._ascending = not reverse
3406 3407
3407 3408 def reverse(self):
3408 3409 self._ascending = not self._ascending
3409 3410
3410 3411 def isascending(self):
3411 3412 return self._ascending
3412 3413
3413 3414 def isdescending(self):
3414 3415 return not self._ascending
3415 3416
3416 3417 def first(self):
3417 3418 if self._ascending:
3418 3419 it = self.fastasc
3419 3420 else:
3420 3421 it = self.fastdesc
3421 3422 if it is None:
3422 3423 # we need to consume all and try again
3423 3424 for x in self._consumegen():
3424 3425 pass
3425 3426 return self.first()
3426 3427 return next(it(), None)
3427 3428
3428 3429 def last(self):
3429 3430 if self._ascending:
3430 3431 it = self.fastdesc
3431 3432 else:
3432 3433 it = self.fastasc
3433 3434 if it is None:
3434 3435 # we need to consume all and try again
3435 3436 for x in self._consumegen():
3436 3437 pass
3437 3438 return self.first()
3438 3439 return next(it(), None)
3439 3440
3440 3441 def __repr__(self):
3441 3442 d = {False: '-', True: '+'}[self._ascending]
3442 3443 return '<%s%s>' % (type(self).__name__, d)
3443 3444
3444 3445 class spanset(abstractsmartset):
3445 3446 """Duck type for baseset class which represents a range of revisions and
3446 3447 can work lazily and without having all the range in memory
3447 3448
3448 3449 Note that spanset(x, y) behave almost like xrange(x, y) except for two
3449 3450 notable points:
3450 3451 - when x < y it will be automatically descending,
3451 3452 - revision filtered with this repoview will be skipped.
3452 3453
3453 3454 """
3454 3455 def __init__(self, repo, start=0, end=None):
3455 3456 """
3456 3457 start: first revision included the set
3457 3458 (default to 0)
3458 3459 end: first revision excluded (last+1)
3459 3460 (default to len(repo)
3460 3461
3461 3462 Spanset will be descending if `end` < `start`.
3462 3463 """
3463 3464 if end is None:
3464 3465 end = len(repo)
3465 3466 self._ascending = start <= end
3466 3467 if not self._ascending:
3467 3468 start, end = end + 1, start +1
3468 3469 self._start = start
3469 3470 self._end = end
3470 3471 self._hiddenrevs = repo.changelog.filteredrevs
3471 3472
3472 3473 def sort(self, reverse=False):
3473 3474 self._ascending = not reverse
3474 3475
3475 3476 def reverse(self):
3476 3477 self._ascending = not self._ascending
3477 3478
3478 3479 def _iterfilter(self, iterrange):
3479 3480 s = self._hiddenrevs
3480 3481 for r in iterrange:
3481 3482 if r not in s:
3482 3483 yield r
3483 3484
3484 3485 def __iter__(self):
3485 3486 if self._ascending:
3486 3487 return self.fastasc()
3487 3488 else:
3488 3489 return self.fastdesc()
3489 3490
3490 3491 def fastasc(self):
3491 3492 iterrange = xrange(self._start, self._end)
3492 3493 if self._hiddenrevs:
3493 3494 return self._iterfilter(iterrange)
3494 3495 return iter(iterrange)
3495 3496
3496 3497 def fastdesc(self):
3497 3498 iterrange = xrange(self._end - 1, self._start - 1, -1)
3498 3499 if self._hiddenrevs:
3499 3500 return self._iterfilter(iterrange)
3500 3501 return iter(iterrange)
3501 3502
3502 3503 def __contains__(self, rev):
3503 3504 hidden = self._hiddenrevs
3504 3505 return ((self._start <= rev < self._end)
3505 3506 and not (hidden and rev in hidden))
3506 3507
3507 3508 def __nonzero__(self):
3508 3509 for r in self:
3509 3510 return True
3510 3511 return False
3511 3512
3512 3513 def __len__(self):
3513 3514 if not self._hiddenrevs:
3514 3515 return abs(self._end - self._start)
3515 3516 else:
3516 3517 count = 0
3517 3518 start = self._start
3518 3519 end = self._end
3519 3520 for rev in self._hiddenrevs:
3520 3521 if (end < rev <= start) or (start <= rev < end):
3521 3522 count += 1
3522 3523 return abs(self._end - self._start) - count
3523 3524
3524 3525 def isascending(self):
3525 3526 return self._ascending
3526 3527
3527 3528 def isdescending(self):
3528 3529 return not self._ascending
3529 3530
3530 3531 def first(self):
3531 3532 if self._ascending:
3532 3533 it = self.fastasc
3533 3534 else:
3534 3535 it = self.fastdesc
3535 3536 for x in it():
3536 3537 return x
3537 3538 return None
3538 3539
3539 3540 def last(self):
3540 3541 if self._ascending:
3541 3542 it = self.fastdesc
3542 3543 else:
3543 3544 it = self.fastasc
3544 3545 for x in it():
3545 3546 return x
3546 3547 return None
3547 3548
3548 3549 def __repr__(self):
3549 3550 d = {False: '-', True: '+'}[self._ascending]
3550 3551 return '<%s%s %d:%d>' % (type(self).__name__, d,
3551 3552 self._start, self._end - 1)
3552 3553
3553 3554 class fullreposet(spanset):
3554 3555 """a set containing all revisions in the repo
3555 3556
3556 3557 This class exists to host special optimization and magic to handle virtual
3557 3558 revisions such as "null".
3558 3559 """
3559 3560
3560 3561 def __init__(self, repo):
3561 3562 super(fullreposet, self).__init__(repo)
3562 3563
3563 3564 def __and__(self, other):
3564 3565 """As self contains the whole repo, all of the other set should also be
3565 3566 in self. Therefore `self & other = other`.
3566 3567
3567 3568 This boldly assumes the other contains valid revs only.
3568 3569 """
3569 3570 # other not a smartset, make is so
3570 3571 if not util.safehasattr(other, 'isascending'):
3571 3572 # filter out hidden revision
3572 3573 # (this boldly assumes all smartset are pure)
3573 3574 #
3574 3575 # `other` was used with "&", let's assume this is a set like
3575 3576 # object.
3576 3577 other = baseset(other - self._hiddenrevs)
3577 3578
3578 3579 # XXX As fullreposet is also used as bootstrap, this is wrong.
3579 3580 #
3580 3581 # With a giveme312() revset returning [3,1,2], this makes
3581 3582 # 'hg log -r "giveme312()"' -> 1, 2, 3 (wrong)
3582 3583 # We cannot just drop it because other usage still need to sort it:
3583 3584 # 'hg log -r "all() and giveme312()"' -> 1, 2, 3 (right)
3584 3585 #
3585 3586 # There is also some faulty revset implementations that rely on it
3586 3587 # (eg: children as of its state in e8075329c5fb)
3587 3588 #
3588 3589 # When we fix the two points above we can move this into the if clause
3589 3590 other.sort(reverse=self.isdescending())
3590 3591 return other
3591 3592
3592 3593 def prettyformatset(revs):
3593 3594 lines = []
3594 3595 rs = repr(revs)
3595 3596 p = 0
3596 3597 while p < len(rs):
3597 3598 q = rs.find('<', p + 1)
3598 3599 if q < 0:
3599 3600 q = len(rs)
3600 3601 l = rs.count('<', 0, p) - rs.count('>', 0, p)
3601 3602 assert l >= 0
3602 3603 lines.append((l, rs[p:q].rstrip()))
3603 3604 p = q
3604 3605 return '\n'.join(' ' * l + s for l, s in lines)
3605 3606
3606 3607 # tell hggettext to extract docstrings from these functions:
3607 3608 i18nfunctions = symbols.values()
General Comments 0
You need to be logged in to leave comments. Login now