1# revset.py - revision set queries for mercurial 2# 3# Copyright 2010 Olivia Mackall <olivia@selenic.com> 4# 5# This software may be used and distributed according to the terms of the 6# GNU General Public License version 2 or any later version. 7 8from __future__ import absolute_import 9 10import re 11 12from .i18n import _ 13from .pycompat import getattr 14from .node import ( 15 bin, 16 nullrev, 17 wdirrev, 18) 19from . import ( 20 dagop, 21 destutil, 22 diffutil, 23 encoding, 24 error, 25 grep as grepmod, 26 hbisect, 27 match as matchmod, 28 obsolete as obsmod, 29 obsutil, 30 pathutil, 31 phases, 32 pycompat, 33 registrar, 34 repoview, 35 revsetlang, 36 scmutil, 37 smartset, 38 stack as stackmod, 39 util, 40) 41from .utils import ( 42 dateutil, 43 stringutil, 44 urlutil, 45) 46 47# helpers for processing parsed tree 48getsymbol = revsetlang.getsymbol 49getstring = revsetlang.getstring 50getinteger = revsetlang.getinteger 51getboolean = revsetlang.getboolean 52getlist = revsetlang.getlist 53getintrange = revsetlang.getintrange 54getargs = revsetlang.getargs 55getargsdict = revsetlang.getargsdict 56 57baseset = smartset.baseset 58generatorset = smartset.generatorset 59spanset = smartset.spanset 60fullreposet = smartset.fullreposet 61 62# revisions not included in all(), but populated if specified 63_virtualrevs = (nullrev, wdirrev) 64 65# Constants for ordering requirement, used in getset(): 66# 67# If 'define', any nested functions and operations MAY change the ordering of 68# the entries in the set (but if changes the ordering, it MUST ALWAYS change 69# it). If 'follow', any nested functions and operations MUST take the ordering 70# specified by the first operand to the '&' operator. 71# 72# For instance, 73# 74# X & (Y | Z) 75# ^ ^^^^^^^ 76# | follow 77# define 78# 79# will be evaluated as 'or(y(x()), z(x()))', where 'x()' can change the order 80# of the entries in the set, but 'y()', 'z()' and 'or()' shouldn't. 81# 82# 'any' means the order doesn't matter. For instance, 83# 84# (X & !Y) | ancestors(Z) 85# ^ ^ 86# any any 87# 88# For 'X & !Y', 'X' decides the order and 'Y' is subtracted from 'X', so the 89# order of 'Y' does not matter. For 'ancestors(Z)', Z's order does not matter 90# since 'ancestors' does not care about the order of its argument. 91# 92# Currently, most revsets do not care about the order, so 'define' is 93# equivalent to 'follow' for them, and the resulting order is based on the 94# 'subset' parameter passed down to them: 95# 96# m = revset.match(...) 97# m(repo, subset, order=defineorder) 98# ^^^^^^ 99# For most revsets, 'define' means using the order this subset provides 100# 101# There are a few revsets that always redefine the order if 'define' is 102# specified: 'sort(X)', 'reverse(X)', 'x:y'. 103anyorder = b'any' # don't care the order, could be even random-shuffled 104defineorder = b'define' # ALWAYS redefine, or ALWAYS follow the current order 105followorder = b'follow' # MUST follow the current order 106 107# helpers 108 109 110def getset(repo, subset, x, order=defineorder): 111 if not x: 112 raise error.ParseError(_(b"missing argument")) 113 return methods[x[0]](repo, subset, *x[1:], order=order) 114 115 116def _getrevsource(repo, r): 117 extra = repo[r].extra() 118 for label in (b'source', b'transplant_source', b'rebase_source'): 119 if label in extra: 120 try: 121 return repo[extra[label]].rev() 122 except error.RepoLookupError: 123 pass 124 return None 125 126 127def _sortedb(xs): 128 return sorted(pycompat.rapply(pycompat.maybebytestr, xs)) 129 130 131# operator methods 132 133 134def stringset(repo, subset, x, order): 135 if not x: 136 raise error.ParseError(_(b"empty string is not a valid revision")) 137 x = scmutil.intrev(scmutil.revsymbol(repo, x)) 138 if x in subset or x in _virtualrevs and isinstance(subset, fullreposet): 139 return baseset([x]) 140 return baseset() 141 142 143def rawsmartset(repo, subset, x, order): 144 """argument is already a smartset, use that directly""" 145 if order == followorder: 146 return subset & x 147 else: 148 return x & subset 149 150 151def rangeset(repo, subset, x, y, order): 152 m = getset(repo, fullreposet(repo), x) 153 n = getset(repo, fullreposet(repo), y) 154 155 if not m or not n: 156 return baseset() 157 return _makerangeset(repo, subset, m.first(), n.last(), order) 158 159 160def rangeall(repo, subset, x, order): 161 assert x is None 162 return _makerangeset(repo, subset, 0, repo.changelog.tiprev(), order) 163 164 165def rangepre(repo, subset, y, order): 166 # ':y' can't be rewritten to '0:y' since '0' may be hidden 167 n = getset(repo, fullreposet(repo), y) 168 if not n: 169 return baseset() 170 return _makerangeset(repo, subset, 0, n.last(), order) 171 172 173def rangepost(repo, subset, x, order): 174 m = getset(repo, fullreposet(repo), x) 175 if not m: 176 return baseset() 177 return _makerangeset( 178 repo, subset, m.first(), repo.changelog.tiprev(), order 179 ) 180 181 182def _makerangeset(repo, subset, m, n, order): 183 if m == n: 184 r = baseset([m]) 185 elif n == wdirrev: 186 r = spanset(repo, m, len(repo)) + baseset([n]) 187 elif m == wdirrev: 188 r = baseset([m]) + spanset(repo, repo.changelog.tiprev(), n - 1) 189 elif m < n: 190 r = spanset(repo, m, n + 1) 191 else: 192 r = spanset(repo, m, n - 1) 193 194 if order == defineorder: 195 return r & subset 196 else: 197 # carrying the sorting over when possible would be more efficient 198 return subset & r 199 200 201def dagrange(repo, subset, x, y, order): 202 r = fullreposet(repo) 203 xs = dagop.reachableroots( 204 repo, getset(repo, r, x), getset(repo, r, y), includepath=True 205 ) 206 return subset & xs 207 208 209def andset(repo, subset, x, y, order): 210 if order == anyorder: 211 yorder = anyorder 212 else: 213 yorder = followorder 214 return getset(repo, getset(repo, subset, x, order), y, yorder) 215 216 217def andsmallyset(repo, subset, x, y, order): 218 # 'andsmally(x, y)' is equivalent to 'and(x, y)', but faster when y is small 219 if order == anyorder: 220 yorder = anyorder 221 else: 222 yorder = followorder 223 return getset(repo, getset(repo, subset, y, yorder), x, order) 224 225 226def differenceset(repo, subset, x, y, order): 227 return getset(repo, subset, x, order) - getset(repo, subset, y, anyorder) 228 229 230def _orsetlist(repo, subset, xs, order): 231 assert xs 232 if len(xs) == 1: 233 return getset(repo, subset, xs[0], order) 234 p = len(xs) // 2 235 a = _orsetlist(repo, subset, xs[:p], order) 236 b = _orsetlist(repo, subset, xs[p:], order) 237 return a + b 238 239 240def orset(repo, subset, x, order): 241 xs = getlist(x) 242 if not xs: 243 return baseset() 244 if order == followorder: 245 # slow path to take the subset order 246 return subset & _orsetlist(repo, fullreposet(repo), xs, anyorder) 247 else: 248 return _orsetlist(repo, subset, xs, order) 249 250 251def notset(repo, subset, x, order): 252 return subset - getset(repo, subset, x, anyorder) 253 254 255def relationset(repo, subset, x, y, order): 256 # this is pretty basic implementation of 'x#y' operator, still 257 # experimental so undocumented. see the wiki for further ideas. 258 # https://www.mercurial-scm.org/wiki/RevsetOperatorPlan 259 rel = getsymbol(y) 260 if rel in relations: 261 return relations[rel](repo, subset, x, rel, order) 262 263 relnames = [r for r in relations.keys() if len(r) > 1] 264 raise error.UnknownIdentifier(rel, relnames) 265 266 267def _splitrange(a, b): 268 """Split range with bounds a and b into two ranges at 0 and return two 269 tuples of numbers for use as startdepth and stopdepth arguments of 270 revancestors and revdescendants. 271 272 >>> _splitrange(-10, -5) # [-10:-5] 273 ((5, 11), (None, None)) 274 >>> _splitrange(5, 10) # [5:10] 275 ((None, None), (5, 11)) 276 >>> _splitrange(-10, 10) # [-10:10] 277 ((0, 11), (0, 11)) 278 >>> _splitrange(-10, 0) # [-10:0] 279 ((0, 11), (None, None)) 280 >>> _splitrange(0, 10) # [0:10] 281 ((None, None), (0, 11)) 282 >>> _splitrange(0, 0) # [0:0] 283 ((0, 1), (None, None)) 284 >>> _splitrange(1, -1) # [1:-1] 285 ((None, None), (None, None)) 286 """ 287 ancdepths = (None, None) 288 descdepths = (None, None) 289 if a == b == 0: 290 ancdepths = (0, 1) 291 if a < 0: 292 ancdepths = (-min(b, 0), -a + 1) 293 if b > 0: 294 descdepths = (max(a, 0), b + 1) 295 return ancdepths, descdepths 296 297 298def generationsrel(repo, subset, x, rel, order): 299 z = (b'rangeall', None) 300 return generationssubrel(repo, subset, x, rel, z, order) 301 302 303def generationssubrel(repo, subset, x, rel, z, order): 304 # TODO: rewrite tests, and drop startdepth argument from ancestors() and 305 # descendants() predicates 306 a, b = getintrange( 307 z, 308 _(b'relation subscript must be an integer or a range'), 309 _(b'relation subscript bounds must be integers'), 310 deffirst=-(dagop.maxlogdepth - 1), 311 deflast=+(dagop.maxlogdepth - 1), 312 ) 313 (ancstart, ancstop), (descstart, descstop) = _splitrange(a, b) 314 315 if ancstart is None and descstart is None: 316 return baseset() 317 318 revs = getset(repo, fullreposet(repo), x) 319 if not revs: 320 return baseset() 321 322 if ancstart is not None and descstart is not None: 323 s = dagop.revancestors(repo, revs, False, ancstart, ancstop) 324 s += dagop.revdescendants(repo, revs, False, descstart, descstop) 325 elif ancstart is not None: 326 s = dagop.revancestors(repo, revs, False, ancstart, ancstop) 327 elif descstart is not None: 328 s = dagop.revdescendants(repo, revs, False, descstart, descstop) 329 330 return subset & s 331 332 333def relsubscriptset(repo, subset, x, y, z, order): 334 # this is pretty basic implementation of 'x#y[z]' operator, still 335 # experimental so undocumented. see the wiki for further ideas. 336 # https://www.mercurial-scm.org/wiki/RevsetOperatorPlan 337 rel = getsymbol(y) 338 if rel in subscriptrelations: 339 return subscriptrelations[rel](repo, subset, x, rel, z, order) 340 341 relnames = [r for r in subscriptrelations.keys() if len(r) > 1] 342 raise error.UnknownIdentifier(rel, relnames) 343 344 345def subscriptset(repo, subset, x, y, order): 346 raise error.ParseError(_(b"can't use a subscript in this context")) 347 348 349def listset(repo, subset, *xs, **opts): 350 raise error.ParseError( 351 _(b"can't use a list in this context"), 352 hint=_(b'see \'hg help "revsets.x or y"\''), 353 ) 354 355 356def keyvaluepair(repo, subset, k, v, order): 357 raise error.ParseError(_(b"can't use a key-value pair in this context")) 358 359 360def func(repo, subset, a, b, order): 361 f = getsymbol(a) 362 if f in symbols: 363 func = symbols[f] 364 if getattr(func, '_takeorder', False): 365 return func(repo, subset, b, order) 366 return func(repo, subset, b) 367 368 keep = lambda fn: getattr(fn, '__doc__', None) is not None 369 370 syms = [s for (s, fn) in symbols.items() if keep(fn)] 371 raise error.UnknownIdentifier(f, syms) 372 373 374# functions 375 376# symbols are callables like: 377# fn(repo, subset, x) 378# with: 379# repo - current repository instance 380# subset - of revisions to be examined 381# x - argument in tree form 382symbols = revsetlang.symbols 383 384# symbols which can't be used for a DoS attack for any given input 385# (e.g. those which accept regexes as plain strings shouldn't be included) 386# functions that just return a lot of changesets (like all) don't count here 387safesymbols = set() 388 389predicate = registrar.revsetpredicate() 390 391 392@predicate(b'_destupdate') 393def _destupdate(repo, subset, x): 394 # experimental revset for update destination 395 args = getargsdict(x, b'limit', b'clean') 396 return subset & baseset( 397 [destutil.destupdate(repo, **pycompat.strkwargs(args))[0]] 398 ) 399 400 401@predicate(b'_destmerge') 402def _destmerge(repo, subset, x): 403 # experimental revset for merge destination 404 sourceset = None 405 if x is not None: 406 sourceset = getset(repo, fullreposet(repo), x) 407 return subset & baseset([destutil.destmerge(repo, sourceset=sourceset)]) 408 409 410@predicate(b'adds(pattern)', safe=True, weight=30) 411def adds(repo, subset, x): 412 """Changesets that add a file matching pattern. 413 414 The pattern without explicit kind like ``glob:`` is expected to be 415 relative to the current directory and match against a file or a 416 directory. 417 """ 418 # i18n: "adds" is a keyword 419 pat = getstring(x, _(b"adds requires a pattern")) 420 return checkstatus(repo, subset, pat, 'added') 421 422 423@predicate(b'ancestor(*changeset)', safe=True, weight=0.5) 424def ancestor(repo, subset, x): 425 """A greatest common ancestor of the changesets. 426 427 Accepts 0 or more changesets. 428 Will return empty list when passed no args. 429 Greatest common ancestor of a single changeset is that changeset. 430 """ 431 reviter = iter(orset(repo, fullreposet(repo), x, order=anyorder)) 432 try: 433 anc = repo[next(reviter)] 434 except StopIteration: 435 return baseset() 436 for r in reviter: 437 anc = anc.ancestor(repo[r]) 438 439 r = scmutil.intrev(anc) 440 if r in subset: 441 return baseset([r]) 442 return baseset() 443 444 445def _ancestors( 446 repo, subset, x, followfirst=False, startdepth=None, stopdepth=None 447): 448 heads = getset(repo, fullreposet(repo), x) 449 if not heads: 450 return baseset() 451 s = dagop.revancestors(repo, heads, followfirst, startdepth, stopdepth) 452 return subset & s 453 454 455@predicate(b'ancestors(set[, depth])', safe=True) 456def ancestors(repo, subset, x): 457 """Changesets that are ancestors of changesets in set, including the 458 given changesets themselves. 459 460 If depth is specified, the result only includes changesets up to 461 the specified generation. 462 """ 463 # startdepth is for internal use only until we can decide the UI 464 args = getargsdict(x, b'ancestors', b'set depth startdepth') 465 if b'set' not in args: 466 # i18n: "ancestors" is a keyword 467 raise error.ParseError(_(b'ancestors takes at least 1 argument')) 468 startdepth = stopdepth = None 469 if b'startdepth' in args: 470 n = getinteger( 471 args[b'startdepth'], b"ancestors expects an integer startdepth" 472 ) 473 if n < 0: 474 raise error.ParseError(b"negative startdepth") 475 startdepth = n 476 if b'depth' in args: 477 # i18n: "ancestors" is a keyword 478 n = getinteger(args[b'depth'], _(b"ancestors expects an integer depth")) 479 if n < 0: 480 raise error.ParseError(_(b"negative depth")) 481 stopdepth = n + 1 482 return _ancestors( 483 repo, subset, args[b'set'], startdepth=startdepth, stopdepth=stopdepth 484 ) 485 486 487@predicate(b'_firstancestors', safe=True) 488def _firstancestors(repo, subset, x): 489 # ``_firstancestors(set)`` 490 # Like ``ancestors(set)`` but follows only the first parents. 491 return _ancestors(repo, subset, x, followfirst=True) 492 493 494def _childrenspec(repo, subset, x, n, order): 495 """Changesets that are the Nth child of a changeset 496 in set. 497 """ 498 cs = set() 499 for r in getset(repo, fullreposet(repo), x): 500 for i in range(n): 501 c = repo[r].children() 502 if len(c) == 0: 503 break 504 if len(c) > 1: 505 raise error.RepoLookupError( 506 _(b"revision in set has more than one child") 507 ) 508 r = c[0].rev() 509 else: 510 cs.add(r) 511 return subset & cs 512 513 514def ancestorspec(repo, subset, x, n, order): 515 """``set~n`` 516 Changesets that are the Nth ancestor (first parents only) of a changeset 517 in set. 518 """ 519 n = getinteger(n, _(b"~ expects a number")) 520 if n < 0: 521 # children lookup 522 return _childrenspec(repo, subset, x, -n, order) 523 ps = set() 524 cl = repo.changelog 525 for r in getset(repo, fullreposet(repo), x): 526 for i in range(n): 527 try: 528 r = cl.parentrevs(r)[0] 529 except error.WdirUnsupported: 530 r = repo[r].p1().rev() 531 ps.add(r) 532 return subset & ps 533 534 535@predicate(b'author(string)', safe=True, weight=10) 536def author(repo, subset, x): 537 """Alias for ``user(string)``.""" 538 # i18n: "author" is a keyword 539 n = getstring(x, _(b"author requires a string")) 540 kind, pattern, matcher = _substringmatcher(n, casesensitive=False) 541 return subset.filter( 542 lambda x: matcher(repo[x].user()), condrepr=(b'<user %r>', n) 543 ) 544 545 546@predicate(b'bisect(string)', safe=True) 547def bisect(repo, subset, x): 548 """Changesets marked in the specified bisect status: 549 550 - ``good``, ``bad``, ``skip``: csets explicitly marked as good/bad/skip 551 - ``goods``, ``bads`` : csets topologically good/bad 552 - ``range`` : csets taking part in the bisection 553 - ``pruned`` : csets that are goods, bads or skipped 554 - ``untested`` : csets whose fate is yet unknown 555 - ``ignored`` : csets ignored due to DAG topology 556 - ``current`` : the cset currently being bisected 557 """ 558 # i18n: "bisect" is a keyword 559 status = getstring(x, _(b"bisect requires a string")).lower() 560 state = set(hbisect.get(repo, status)) 561 return subset & state 562 563 564# Backward-compatibility 565# - no help entry so that we do not advertise it any more 566@predicate(b'bisected', safe=True) 567def bisected(repo, subset, x): 568 return bisect(repo, subset, x) 569 570 571@predicate(b'bookmark([name])', safe=True) 572def bookmark(repo, subset, x): 573 """The named bookmark or all bookmarks. 574 575 Pattern matching is supported for `name`. See :hg:`help revisions.patterns`. 576 """ 577 # i18n: "bookmark" is a keyword 578 args = getargs(x, 0, 1, _(b'bookmark takes one or no arguments')) 579 if args: 580 bm = getstring( 581 args[0], 582 # i18n: "bookmark" is a keyword 583 _(b'the argument to bookmark must be a string'), 584 ) 585 kind, pattern, matcher = stringutil.stringmatcher(bm) 586 bms = set() 587 if kind == b'literal': 588 if bm == pattern: 589 pattern = repo._bookmarks.expandname(pattern) 590 bmrev = repo._bookmarks.get(pattern, None) 591 if not bmrev: 592 raise error.RepoLookupError( 593 _(b"bookmark '%s' does not exist") % pattern 594 ) 595 bms.add(repo[bmrev].rev()) 596 else: 597 matchrevs = set() 598 for name, bmrev in pycompat.iteritems(repo._bookmarks): 599 if matcher(name): 600 matchrevs.add(bmrev) 601 for bmrev in matchrevs: 602 bms.add(repo[bmrev].rev()) 603 else: 604 bms = {repo[r].rev() for r in repo._bookmarks.values()} 605 bms -= {nullrev} 606 return subset & bms 607 608 609@predicate(b'branch(string or set)', safe=True, weight=10) 610def branch(repo, subset, x): 611 """ 612 All changesets belonging to the given branch or the branches of the given 613 changesets. 614 615 Pattern matching is supported for `string`. See 616 :hg:`help revisions.patterns`. 617 """ 618 getbi = repo.revbranchcache().branchinfo 619 620 def getbranch(r): 621 try: 622 return getbi(r)[0] 623 except error.WdirUnsupported: 624 return repo[r].branch() 625 626 try: 627 b = getstring(x, b'') 628 except error.ParseError: 629 # not a string, but another revspec, e.g. tip() 630 pass 631 else: 632 kind, pattern, matcher = stringutil.stringmatcher(b) 633 if kind == b'literal': 634 # note: falls through to the revspec case if no branch with 635 # this name exists and pattern kind is not specified explicitly 636 if repo.branchmap().hasbranch(pattern): 637 return subset.filter( 638 lambda r: matcher(getbranch(r)), 639 condrepr=(b'<branch %r>', b), 640 ) 641 if b.startswith(b'literal:'): 642 raise error.RepoLookupError( 643 _(b"branch '%s' does not exist") % pattern 644 ) 645 else: 646 return subset.filter( 647 lambda r: matcher(getbranch(r)), condrepr=(b'<branch %r>', b) 648 ) 649 650 s = getset(repo, fullreposet(repo), x) 651 b = set() 652 for r in s: 653 b.add(getbranch(r)) 654 c = s.__contains__ 655 return subset.filter( 656 lambda r: c(r) or getbranch(r) in b, 657 condrepr=lambda: b'<branch %r>' % _sortedb(b), 658 ) 659 660 661@predicate(b'phasedivergent()', safe=True) 662def phasedivergent(repo, subset, x): 663 """Mutable changesets marked as successors of public changesets. 664 665 Only non-public and non-obsolete changesets can be `phasedivergent`. 666 (EXPERIMENTAL) 667 """ 668 # i18n: "phasedivergent" is a keyword 669 getargs(x, 0, 0, _(b"phasedivergent takes no arguments")) 670 phasedivergent = obsmod.getrevs(repo, b'phasedivergent') 671 return subset & phasedivergent 672 673 674@predicate(b'bundle()', safe=True) 675def bundle(repo, subset, x): 676 """Changesets in the bundle. 677 678 Bundle must be specified by the -R option.""" 679 680 try: 681 bundlerevs = repo.changelog.bundlerevs 682 except AttributeError: 683 raise error.Abort(_(b"no bundle provided - specify with -R")) 684 return subset & bundlerevs 685 686 687def checkstatus(repo, subset, pat, field): 688 """Helper for status-related revsets (adds, removes, modifies). 689 The field parameter says which kind is desired. 690 """ 691 hasset = matchmod.patkind(pat) == b'set' 692 693 mcache = [None] 694 695 def matches(x): 696 c = repo[x] 697 if not mcache[0] or hasset: 698 mcache[0] = matchmod.match(repo.root, repo.getcwd(), [pat], ctx=c) 699 m = mcache[0] 700 fname = None 701 702 assert m is not None # help pytype 703 if not m.anypats() and len(m.files()) == 1: 704 fname = m.files()[0] 705 if fname is not None: 706 if fname not in c.files(): 707 return False 708 else: 709 if not any(m(f) for f in c.files()): 710 return False 711 files = getattr(repo.status(c.p1().node(), c.node()), field) 712 if fname is not None: 713 if fname in files: 714 return True 715 else: 716 if any(m(f) for f in files): 717 return True 718 719 return subset.filter( 720 matches, condrepr=(b'<status.%s %r>', pycompat.sysbytes(field), pat) 721 ) 722 723 724def _children(repo, subset, parentset): 725 if not parentset: 726 return baseset() 727 cs = set() 728 pr = repo.changelog.parentrevs 729 minrev = parentset.min() 730 for r in subset: 731 if r <= minrev: 732 continue 733 p1, p2 = pr(r) 734 if p1 in parentset: 735 cs.add(r) 736 if p2 != nullrev and p2 in parentset: 737 cs.add(r) 738 return baseset(cs) 739 740 741@predicate(b'children(set)', safe=True) 742def children(repo, subset, x): 743 """Child changesets of changesets in set.""" 744 s = getset(repo, fullreposet(repo), x) 745 cs = _children(repo, subset, s) 746 return subset & cs 747 748 749@predicate(b'closed()', safe=True, weight=10) 750def closed(repo, subset, x): 751 """Changeset is closed.""" 752 # i18n: "closed" is a keyword 753 getargs(x, 0, 0, _(b"closed takes no arguments")) 754 return subset.filter( 755 lambda r: repo[r].closesbranch(), condrepr=b'<branch closed>' 756 ) 757 758 759# for internal use 760@predicate(b'_commonancestorheads(set)', safe=True) 761def _commonancestorheads(repo, subset, x): 762 # This is an internal method is for quickly calculating "heads(::x and 763 # ::y)" 764 765 # These greatest common ancestors are the same ones that the consensus bid 766 # merge will find. 767 startrevs = getset(repo, fullreposet(repo), x, order=anyorder) 768 769 ancs = repo.changelog._commonancestorsheads(*list(startrevs)) 770 return subset & baseset(ancs) 771 772 773@predicate(b'commonancestors(set)', safe=True) 774def commonancestors(repo, subset, x): 775 """Changesets that are ancestors of every changeset in set.""" 776 startrevs = getset(repo, fullreposet(repo), x, order=anyorder) 777 if not startrevs: 778 return baseset() 779 for r in startrevs: 780 subset &= dagop.revancestors(repo, baseset([r])) 781 return subset 782 783 784@predicate(b'conflictlocal()', safe=True) 785def conflictlocal(repo, subset, x): 786 """The local side of the merge, if currently in an unresolved merge. 787 788 "merge" here includes merge conflicts from e.g. 'hg rebase' or 'hg graft'. 789 """ 790 getargs(x, 0, 0, _(b"conflictlocal takes no arguments")) 791 from . import mergestate as mergestatemod 792 793 mergestate = mergestatemod.mergestate.read(repo) 794 if mergestate.active() and repo.changelog.hasnode(mergestate.local): 795 return subset & {repo.changelog.rev(mergestate.local)} 796 797 return baseset() 798 799 800@predicate(b'conflictother()', safe=True) 801def conflictother(repo, subset, x): 802 """The other side of the merge, if currently in an unresolved merge. 803 804 "merge" here includes merge conflicts from e.g. 'hg rebase' or 'hg graft'. 805 """ 806 getargs(x, 0, 0, _(b"conflictother takes no arguments")) 807 from . import mergestate as mergestatemod 808 809 mergestate = mergestatemod.mergestate.read(repo) 810 if mergestate.active() and repo.changelog.hasnode(mergestate.other): 811 return subset & {repo.changelog.rev(mergestate.other)} 812 813 return baseset() 814 815 816@predicate(b'contains(pattern)', weight=100) 817def contains(repo, subset, x): 818 """The revision's manifest contains a file matching pattern (but might not 819 modify it). See :hg:`help patterns` for information about file patterns. 820 821 The pattern without explicit kind like ``glob:`` is expected to be 822 relative to the current directory and match against a file exactly 823 for efficiency. 824 """ 825 # i18n: "contains" is a keyword 826 pat = getstring(x, _(b"contains requires a pattern")) 827 828 def matches(x): 829 if not matchmod.patkind(pat): 830 pats = pathutil.canonpath(repo.root, repo.getcwd(), pat) 831 if pats in repo[x]: 832 return True 833 else: 834 c = repo[x] 835 m = matchmod.match(repo.root, repo.getcwd(), [pat], ctx=c) 836 for f in c.manifest(): 837 if m(f): 838 return True 839 return False 840 841 return subset.filter(matches, condrepr=(b'<contains %r>', pat)) 842 843 844@predicate(b'converted([id])', safe=True) 845def converted(repo, subset, x): 846 """Changesets converted from the given identifier in the old repository if 847 present, or all converted changesets if no identifier is specified. 848 """ 849 850 # There is exactly no chance of resolving the revision, so do a simple 851 # string compare and hope for the best 852 853 rev = None 854 # i18n: "converted" is a keyword 855 l = getargs(x, 0, 1, _(b'converted takes one or no arguments')) 856 if l: 857 # i18n: "converted" is a keyword 858 rev = getstring(l[0], _(b'converted requires a revision')) 859 860 def _matchvalue(r): 861 source = repo[r].extra().get(b'convert_revision', None) 862 return source is not None and (rev is None or source.startswith(rev)) 863 864 return subset.filter( 865 lambda r: _matchvalue(r), condrepr=(b'<converted %r>', rev) 866 ) 867 868 869@predicate(b'date(interval)', safe=True, weight=10) 870def date(repo, subset, x): 871 """Changesets within the interval, see :hg:`help dates`.""" 872 # i18n: "date" is a keyword 873 ds = getstring(x, _(b"date requires a string")) 874 dm = dateutil.matchdate(ds) 875 return subset.filter( 876 lambda x: dm(repo[x].date()[0]), condrepr=(b'<date %r>', ds) 877 ) 878 879 880@predicate(b'desc(string)', safe=True, weight=10) 881def desc(repo, subset, x): 882 """Search commit message for string. The match is case-insensitive. 883 884 Pattern matching is supported for `string`. See 885 :hg:`help revisions.patterns`. 886 """ 887 # i18n: "desc" is a keyword 888 ds = getstring(x, _(b"desc requires a string")) 889 890 kind, pattern, matcher = _substringmatcher(ds, casesensitive=False) 891 892 return subset.filter( 893 lambda r: matcher(repo[r].description()), condrepr=(b'<desc %r>', ds) 894 ) 895 896 897def _descendants( 898 repo, subset, x, followfirst=False, startdepth=None, stopdepth=None 899): 900 roots = getset(repo, fullreposet(repo), x) 901 if not roots: 902 return baseset() 903 s = dagop.revdescendants(repo, roots, followfirst, startdepth, stopdepth) 904 return subset & s 905 906 907@predicate(b'descendants(set[, depth])', safe=True) 908def descendants(repo, subset, x): 909 """Changesets which are descendants of changesets in set, including the 910 given changesets themselves. 911 912 If depth is specified, the result only includes changesets up to 913 the specified generation. 914 """ 915 # startdepth is for internal use only until we can decide the UI 916 args = getargsdict(x, b'descendants', b'set depth startdepth') 917 if b'set' not in args: 918 # i18n: "descendants" is a keyword 919 raise error.ParseError(_(b'descendants takes at least 1 argument')) 920 startdepth = stopdepth = None 921 if b'startdepth' in args: 922 n = getinteger( 923 args[b'startdepth'], b"descendants expects an integer startdepth" 924 ) 925 if n < 0: 926 raise error.ParseError(b"negative startdepth") 927 startdepth = n 928 if b'depth' in args: 929 # i18n: "descendants" is a keyword 930 n = getinteger( 931 args[b'depth'], _(b"descendants expects an integer depth") 932 ) 933 if n < 0: 934 raise error.ParseError(_(b"negative depth")) 935 stopdepth = n + 1 936 return _descendants( 937 repo, subset, args[b'set'], startdepth=startdepth, stopdepth=stopdepth 938 ) 939 940 941@predicate(b'_firstdescendants', safe=True) 942def _firstdescendants(repo, subset, x): 943 # ``_firstdescendants(set)`` 944 # Like ``descendants(set)`` but follows only the first parents. 945 return _descendants(repo, subset, x, followfirst=True) 946 947 948@predicate(b'destination([set])', safe=True, weight=10) 949def destination(repo, subset, x): 950 """Changesets that were created by a graft, transplant or rebase operation, 951 with the given revisions specified as the source. Omitting the optional set 952 is the same as passing all(). 953 """ 954 if x is not None: 955 sources = getset(repo, fullreposet(repo), x) 956 else: 957 sources = fullreposet(repo) 958 959 dests = set() 960 961 # subset contains all of the possible destinations that can be returned, so 962 # iterate over them and see if their source(s) were provided in the arg set. 963 # Even if the immediate src of r is not in the arg set, src's source (or 964 # further back) may be. Scanning back further than the immediate src allows 965 # transitive transplants and rebases to yield the same results as transitive 966 # grafts. 967 for r in subset: 968 src = _getrevsource(repo, r) 969 lineage = None 970 971 while src is not None: 972 if lineage is None: 973 lineage = list() 974 975 lineage.append(r) 976 977 # The visited lineage is a match if the current source is in the arg 978 # set. Since every candidate dest is visited by way of iterating 979 # subset, any dests further back in the lineage will be tested by a 980 # different iteration over subset. Likewise, if the src was already 981 # selected, the current lineage can be selected without going back 982 # further. 983 if src in sources or src in dests: 984 dests.update(lineage) 985 break 986 987 r = src 988 src = _getrevsource(repo, r) 989 990 return subset.filter( 991 dests.__contains__, 992 condrepr=lambda: b'<destination %r>' % _sortedb(dests), 993 ) 994 995 996@predicate(b'diffcontains(pattern)', weight=110) 997def diffcontains(repo, subset, x): 998 """Search revision differences for when the pattern was added or removed. 999 1000 The pattern may be a substring literal or a regular expression. See 1001 :hg:`help revisions.patterns`. 1002 """ 1003 args = getargsdict(x, b'diffcontains', b'pattern') 1004 if b'pattern' not in args: 1005 # i18n: "diffcontains" is a keyword 1006 raise error.ParseError(_(b'diffcontains takes at least 1 argument')) 1007 1008 pattern = getstring( 1009 args[b'pattern'], _(b'diffcontains requires a string pattern') 1010 ) 1011 regexp = stringutil.substringregexp(pattern, re.M) 1012 1013 # TODO: add support for file pattern and --follow. For example, 1014 # diffcontains(pattern[, set]) where set may be file(pattern) or 1015 # follow(pattern), and we'll eventually add a support for narrowing 1016 # files by revset? 1017 fmatch = matchmod.always() 1018 1019 def makefilematcher(ctx): 1020 return fmatch 1021 1022 # TODO: search in a windowed way 1023 searcher = grepmod.grepsearcher(repo.ui, repo, regexp, diff=True) 1024 1025 def testdiff(rev): 1026 # consume the generator to discard revfiles/matches cache 1027 found = False 1028 for fn, ctx, pstates, states in searcher.searchfiles( 1029 baseset([rev]), makefilematcher 1030 ): 1031 if next(grepmod.difflinestates(pstates, states), None): 1032 found = True 1033 return found 1034 1035 return subset.filter(testdiff, condrepr=(b'<diffcontains %r>', pattern)) 1036 1037 1038@predicate(b'contentdivergent()', safe=True) 1039def contentdivergent(repo, subset, x): 1040 """ 1041 Final successors of changesets with an alternative set of final 1042 successors. (EXPERIMENTAL) 1043 """ 1044 # i18n: "contentdivergent" is a keyword 1045 getargs(x, 0, 0, _(b"contentdivergent takes no arguments")) 1046 contentdivergent = obsmod.getrevs(repo, b'contentdivergent') 1047 return subset & contentdivergent 1048 1049 1050@predicate(b'expectsize(set[, size])', safe=True, takeorder=True) 1051def expectsize(repo, subset, x, order): 1052 """Return the given revset if size matches the revset size. 1053 Abort if the revset doesn't expect given size. 1054 size can either be an integer range or an integer. 1055 1056 For example, ``expectsize(0:1, 3:5)`` will abort as revset size is 2 and 1057 2 is not between 3 and 5 inclusive.""" 1058 1059 args = getargsdict(x, b'expectsize', b'set size') 1060 minsize = 0 1061 maxsize = len(repo) + 1 1062 err = b'' 1063 if b'size' not in args or b'set' not in args: 1064 raise error.ParseError(_(b'invalid set of arguments')) 1065 minsize, maxsize = getintrange( 1066 args[b'size'], 1067 _(b'expectsize requires a size range or a positive integer'), 1068 _(b'size range bounds must be integers'), 1069 minsize, 1070 maxsize, 1071 ) 1072 if minsize < 0 or maxsize < 0: 1073 raise error.ParseError(_(b'negative size')) 1074 rev = getset(repo, fullreposet(repo), args[b'set'], order=order) 1075 if minsize != maxsize and (len(rev) < minsize or len(rev) > maxsize): 1076 err = _(b'revset size mismatch. expected between %d and %d, got %d') % ( 1077 minsize, 1078 maxsize, 1079 len(rev), 1080 ) 1081 elif minsize == maxsize and len(rev) != minsize: 1082 err = _(b'revset size mismatch. expected %d, got %d') % ( 1083 minsize, 1084 len(rev), 1085 ) 1086 if err: 1087 raise error.RepoLookupError(err) 1088 if order == followorder: 1089 return subset & rev 1090 else: 1091 return rev & subset 1092 1093 1094@predicate(b'extdata(source)', safe=False, weight=100) 1095def extdata(repo, subset, x): 1096 """Changesets in the specified extdata source. (EXPERIMENTAL)""" 1097 # i18n: "extdata" is a keyword 1098 args = getargsdict(x, b'extdata', b'source') 1099 source = getstring( 1100 args.get(b'source'), 1101 # i18n: "extdata" is a keyword 1102 _(b'extdata takes at least 1 string argument'), 1103 ) 1104 data = scmutil.extdatasource(repo, source) 1105 return subset & baseset(data) 1106 1107 1108@predicate(b'extinct()', safe=True) 1109def extinct(repo, subset, x): 1110 """Obsolete changesets with obsolete descendants only. (EXPERIMENTAL)""" 1111 # i18n: "extinct" is a keyword 1112 getargs(x, 0, 0, _(b"extinct takes no arguments")) 1113 extincts = obsmod.getrevs(repo, b'extinct') 1114 return subset & extincts 1115 1116 1117@predicate(b'extra(label, [value])', safe=True) 1118def extra(repo, subset, x): 1119 """Changesets with the given label in the extra metadata, with the given 1120 optional value. 1121 1122 Pattern matching is supported for `value`. See 1123 :hg:`help revisions.patterns`. 1124 """ 1125 args = getargsdict(x, b'extra', b'label value') 1126 if b'label' not in args: 1127 # i18n: "extra" is a keyword 1128 raise error.ParseError(_(b'extra takes at least 1 argument')) 1129 # i18n: "extra" is a keyword 1130 label = getstring( 1131 args[b'label'], _(b'first argument to extra must be a string') 1132 ) 1133 value = None 1134 1135 if b'value' in args: 1136 # i18n: "extra" is a keyword 1137 value = getstring( 1138 args[b'value'], _(b'second argument to extra must be a string') 1139 ) 1140 kind, value, matcher = stringutil.stringmatcher(value) 1141 1142 def _matchvalue(r): 1143 extra = repo[r].extra() 1144 return label in extra and (value is None or matcher(extra[label])) 1145 1146 return subset.filter( 1147 lambda r: _matchvalue(r), condrepr=(b'<extra[%r] %r>', label, value) 1148 ) 1149 1150 1151@predicate(b'filelog(pattern)', safe=True) 1152def filelog(repo, subset, x): 1153 """Changesets connected to the specified filelog. 1154 1155 For performance reasons, visits only revisions mentioned in the file-level 1156 filelog, rather than filtering through all changesets (much faster, but 1157 doesn't include deletes or duplicate changes). For a slower, more accurate 1158 result, use ``file()``. 1159 1160 The pattern without explicit kind like ``glob:`` is expected to be 1161 relative to the current directory and match against a file exactly 1162 for efficiency. 1163 """ 1164 1165 # i18n: "filelog" is a keyword 1166 pat = getstring(x, _(b"filelog requires a pattern")) 1167 s = set() 1168 cl = repo.changelog 1169 1170 if not matchmod.patkind(pat): 1171 f = pathutil.canonpath(repo.root, repo.getcwd(), pat) 1172 files = [f] 1173 else: 1174 m = matchmod.match(repo.root, repo.getcwd(), [pat], ctx=repo[None]) 1175 files = (f for f in repo[None] if m(f)) 1176 1177 for f in files: 1178 fl = repo.file(f) 1179 known = {} 1180 scanpos = 0 1181 for fr in list(fl): 1182 fn = fl.node(fr) 1183 if fn in known: 1184 s.add(known[fn]) 1185 continue 1186 1187 lr = fl.linkrev(fr) 1188 if lr in cl: 1189 s.add(lr) 1190 elif scanpos is not None: 1191 # lowest matching changeset is filtered, scan further 1192 # ahead in changelog 1193 start = max(lr, scanpos) + 1 1194 scanpos = None 1195 for r in cl.revs(start): 1196 # minimize parsing of non-matching entries 1197 if f in cl.revision(r) and f in cl.readfiles(r): 1198 try: 1199 # try to use manifest delta fastpath 1200 n = repo[r].filenode(f) 1201 if n not in known: 1202 if n == fn: 1203 s.add(r) 1204 scanpos = r 1205 break 1206 else: 1207 known[n] = r 1208 except error.ManifestLookupError: 1209 # deletion in changelog 1210 continue 1211 1212 return subset & s 1213 1214 1215@predicate(b'first(set, [n])', safe=True, takeorder=True, weight=0) 1216def first(repo, subset, x, order): 1217 """An alias for limit().""" 1218 return limit(repo, subset, x, order) 1219 1220 1221def _follow(repo, subset, x, name, followfirst=False): 1222 args = getargsdict(x, name, b'file startrev') 1223 revs = None 1224 if b'startrev' in args: 1225 revs = getset(repo, fullreposet(repo), args[b'startrev']) 1226 if b'file' in args: 1227 x = getstring(args[b'file'], _(b"%s expected a pattern") % name) 1228 if revs is None: 1229 revs = [None] 1230 fctxs = [] 1231 for r in revs: 1232 ctx = mctx = repo[r] 1233 if r is None: 1234 ctx = repo[b'.'] 1235 m = matchmod.match( 1236 repo.root, repo.getcwd(), [x], ctx=mctx, default=b'path' 1237 ) 1238 fctxs.extend(ctx[f].introfilectx() for f in ctx.manifest().walk(m)) 1239 s = dagop.filerevancestors(fctxs, followfirst) 1240 else: 1241 if revs is None: 1242 revs = baseset([repo[b'.'].rev()]) 1243 s = dagop.revancestors(repo, revs, followfirst) 1244 1245 return subset & s 1246 1247 1248@predicate(b'follow([file[, startrev]])', safe=True) 1249def follow(repo, subset, x): 1250 """ 1251 An alias for ``::.`` (ancestors of the working directory's first parent). 1252 If file pattern is specified, the histories of files matching given 1253 pattern in the revision given by startrev are followed, including copies. 1254 """ 1255 return _follow(repo, subset, x, b'follow') 1256 1257 1258@predicate(b'_followfirst', safe=True) 1259def _followfirst(repo, subset, x): 1260 # ``followfirst([file[, startrev]])`` 1261 # Like ``follow([file[, startrev]])`` but follows only the first parent 1262 # of every revisions or files revisions. 1263 return _follow(repo, subset, x, b'_followfirst', followfirst=True) 1264 1265 1266@predicate( 1267 b'followlines(file, fromline:toline[, startrev=., descend=False])', 1268 safe=True, 1269) 1270def followlines(repo, subset, x): 1271 """Changesets modifying `file` in line range ('fromline', 'toline'). 1272 1273 Line range corresponds to 'file' content at 'startrev' and should hence be 1274 consistent with file size. If startrev is not specified, working directory's 1275 parent is used. 1276 1277 By default, ancestors of 'startrev' are returned. If 'descend' is True, 1278 descendants of 'startrev' are returned though renames are (currently) not 1279 followed in this direction. 1280 """ 1281 args = getargsdict(x, b'followlines', b'file *lines startrev descend') 1282 if len(args[b'lines']) != 1: 1283 raise error.ParseError(_(b"followlines requires a line range")) 1284 1285 rev = b'.' 1286 if b'startrev' in args: 1287 revs = getset(repo, fullreposet(repo), args[b'startrev']) 1288 if len(revs) != 1: 1289 raise error.ParseError( 1290 # i18n: "followlines" is a keyword 1291 _(b"followlines expects exactly one revision") 1292 ) 1293 rev = revs.last() 1294 1295 pat = getstring(args[b'file'], _(b"followlines requires a pattern")) 1296 # i18n: "followlines" is a keyword 1297 msg = _(b"followlines expects exactly one file") 1298 fname = scmutil.parsefollowlinespattern(repo, rev, pat, msg) 1299 fromline, toline = util.processlinerange( 1300 *getintrange( 1301 args[b'lines'][0], 1302 # i18n: "followlines" is a keyword 1303 _(b"followlines expects a line number or a range"), 1304 _(b"line range bounds must be integers"), 1305 ) 1306 ) 1307 1308 fctx = repo[rev].filectx(fname) 1309 descend = False 1310 if b'descend' in args: 1311 descend = getboolean( 1312 args[b'descend'], 1313 # i18n: "descend" is a keyword 1314 _(b"descend argument must be a boolean"), 1315 ) 1316 if descend: 1317 rs = generatorset( 1318 ( 1319 c.rev() 1320 for c, _linerange in dagop.blockdescendants( 1321 fctx, fromline, toline 1322 ) 1323 ), 1324 iterasc=True, 1325 ) 1326 else: 1327 rs = generatorset( 1328 ( 1329 c.rev() 1330 for c, _linerange in dagop.blockancestors( 1331 fctx, fromline, toline 1332 ) 1333 ), 1334 iterasc=False, 1335 ) 1336 return subset & rs 1337 1338 1339@predicate(b'nodefromfile(path)') 1340def nodefromfile(repo, subset, x): 1341 """ 1342 An alias for ``::.`` (ancestors of the working directory's first parent). 1343 If file pattern is specified, the histories of files matching given 1344 pattern in the revision given by startrev are followed, including copies. 1345 """ 1346 path = getstring(x, _(b"nodefromfile require a file path")) 1347 listed_rev = set() 1348 try: 1349 with pycompat.open(path, 'rb') as f: 1350 for line in f: 1351 n = line.strip() 1352 rn = _node(repo, n) 1353 if rn is not None: 1354 listed_rev.add(rn) 1355 except IOError as exc: 1356 m = _(b'cannot open nodes file "%s": %s') 1357 m %= (path, encoding.strtolocal(exc.strerror)) 1358 raise error.Abort(m) 1359 return subset & baseset(listed_rev) 1360 1361 1362@predicate(b'all()', safe=True) 1363def getall(repo, subset, x): 1364 """All changesets, the same as ``0:tip``.""" 1365 # i18n: "all" is a keyword 1366 getargs(x, 0, 0, _(b"all takes no arguments")) 1367 return subset & spanset(repo) # drop "null" if any 1368 1369 1370@predicate(b'grep(regex)', weight=10) 1371def grep(repo, subset, x): 1372 """Like ``keyword(string)`` but accepts a regex. Use ``grep(r'...')`` 1373 to ensure special escape characters are handled correctly. Unlike 1374 ``keyword(string)``, the match is case-sensitive. 1375 """ 1376 try: 1377 # i18n: "grep" is a keyword 1378 gr = re.compile(getstring(x, _(b"grep requires a string"))) 1379 except re.error as e: 1380 raise error.ParseError( 1381 _(b'invalid match pattern: %s') % stringutil.forcebytestr(e) 1382 ) 1383 1384 def matches(x): 1385 c = repo[x] 1386 for e in c.files() + [c.user(), c.description()]: 1387 if gr.search(e): 1388 return True 1389 return False 1390 1391 return subset.filter(matches, condrepr=(b'<grep %r>', gr.pattern)) 1392 1393 1394@predicate(b'_matchfiles', safe=True) 1395def _matchfiles(repo, subset, x): 1396 # _matchfiles takes a revset list of prefixed arguments: 1397 # 1398 # [p:foo, i:bar, x:baz] 1399 # 1400 # builds a match object from them and filters subset. Allowed 1401 # prefixes are 'p:' for regular patterns, 'i:' for include 1402 # patterns and 'x:' for exclude patterns. Use 'r:' prefix to pass 1403 # a revision identifier, or the empty string to reference the 1404 # working directory, from which the match object is 1405 # initialized. Use 'd:' to set the default matching mode, default 1406 # to 'glob'. At most one 'r:' and 'd:' argument can be passed. 1407 1408 l = getargs(x, 1, -1, b"_matchfiles requires at least one argument") 1409 pats, inc, exc = [], [], [] 1410 rev, default = None, None 1411 for arg in l: 1412 s = getstring(arg, b"_matchfiles requires string arguments") 1413 prefix, value = s[:2], s[2:] 1414 if prefix == b'p:': 1415 pats.append(value) 1416 elif prefix == b'i:': 1417 inc.append(value) 1418 elif prefix == b'x:': 1419 exc.append(value) 1420 elif prefix == b'r:': 1421 if rev is not None: 1422 raise error.ParseError( 1423 b'_matchfiles expected at most one revision' 1424 ) 1425 if value == b'': # empty means working directory 1426 rev = wdirrev 1427 else: 1428 rev = value 1429 elif prefix == b'd:': 1430 if default is not None: 1431 raise error.ParseError( 1432 b'_matchfiles expected at most one default mode' 1433 ) 1434 default = value 1435 else: 1436 raise error.ParseError(b'invalid _matchfiles prefix: %s' % prefix) 1437 if not default: 1438 default = b'glob' 1439 hasset = any(matchmod.patkind(p) == b'set' for p in pats + inc + exc) 1440 1441 mcache = [None] 1442 1443 # This directly read the changelog data as creating changectx for all 1444 # revisions is quite expensive. 1445 getfiles = repo.changelog.readfiles 1446 1447 def matches(x): 1448 if x == wdirrev: 1449 files = repo[x].files() 1450 else: 1451 files = getfiles(x) 1452 1453 if not mcache[0] or (hasset and rev is None): 1454 r = x if rev is None else rev 1455 mcache[0] = matchmod.match( 1456 repo.root, 1457 repo.getcwd(), 1458 pats, 1459 include=inc, 1460 exclude=exc, 1461 ctx=repo[r], 1462 default=default, 1463 ) 1464 m = mcache[0] 1465 1466 for f in files: 1467 if m(f): 1468 return True 1469 return False 1470 1471 return subset.filter( 1472 matches, 1473 condrepr=( 1474 b'<matchfiles patterns=%r, include=%r ' 1475 b'exclude=%r, default=%r, rev=%r>', 1476 pats, 1477 inc, 1478 exc, 1479 default, 1480 rev, 1481 ), 1482 ) 1483 1484 1485@predicate(b'file(pattern)', safe=True, weight=10) 1486def hasfile(repo, subset, x): 1487 """Changesets affecting files matched by pattern. 1488 1489 For a faster but less accurate result, consider using ``filelog()`` 1490 instead. 1491 1492 This predicate uses ``glob:`` as the default kind of pattern. 1493 """ 1494 # i18n: "file" is a keyword 1495 pat = getstring(x, _(b"file requires a pattern")) 1496 return _matchfiles(repo, subset, (b'string', b'p:' + pat)) 1497 1498 1499@predicate(b'head()', safe=True) 1500def head(repo, subset, x): 1501 """Changeset is a named branch head.""" 1502 # i18n: "head" is a keyword 1503 getargs(x, 0, 0, _(b"head takes no arguments")) 1504 hs = set() 1505 cl = repo.changelog 1506 for ls in repo.branchmap().iterheads(): 1507 hs.update(cl.rev(h) for h in ls) 1508 return subset & baseset(hs) 1509 1510 1511@predicate(b'heads(set)', safe=True, takeorder=True) 1512def heads(repo, subset, x, order): 1513 """Members of set with no children in set.""" 1514 # argument set should never define order 1515 if order == defineorder: 1516 order = followorder 1517 inputset = getset(repo, fullreposet(repo), x, order=order) 1518 wdirparents = None 1519 if wdirrev in inputset: 1520 # a bit slower, but not common so good enough for now 1521 wdirparents = [p.rev() for p in repo[None].parents()] 1522 inputset = set(inputset) 1523 inputset.discard(wdirrev) 1524 heads = repo.changelog.headrevs(inputset) 1525 if wdirparents is not None: 1526 heads.difference_update(wdirparents) 1527 heads.add(wdirrev) 1528 heads = baseset(heads) 1529 return subset & heads 1530 1531 1532@predicate(b'hidden()', safe=True) 1533def hidden(repo, subset, x): 1534 """Hidden changesets.""" 1535 # i18n: "hidden" is a keyword 1536 getargs(x, 0, 0, _(b"hidden takes no arguments")) 1537 hiddenrevs = repoview.filterrevs(repo, b'visible') 1538 return subset & hiddenrevs 1539 1540 1541@predicate(b'keyword(string)', safe=True, weight=10) 1542def keyword(repo, subset, x): 1543 """Search commit message, user name, and names of changed files for 1544 string. The match is case-insensitive. 1545 1546 For a regular expression or case sensitive search of these fields, use 1547 ``grep(regex)``. 1548 """ 1549 # i18n: "keyword" is a keyword 1550 kw = encoding.lower(getstring(x, _(b"keyword requires a string"))) 1551 1552 def matches(r): 1553 c = repo[r] 1554 return any( 1555 kw in encoding.lower(t) 1556 for t in c.files() + [c.user(), c.description()] 1557 ) 1558 1559 return subset.filter(matches, condrepr=(b'<keyword %r>', kw)) 1560 1561 1562@predicate(b'limit(set[, n[, offset]])', safe=True, takeorder=True, weight=0) 1563def limit(repo, subset, x, order): 1564 """First n members of set, defaulting to 1, starting from offset.""" 1565 args = getargsdict(x, b'limit', b'set n offset') 1566 if b'set' not in args: 1567 # i18n: "limit" is a keyword 1568 raise error.ParseError(_(b"limit requires one to three arguments")) 1569 # i18n: "limit" is a keyword 1570 lim = getinteger(args.get(b'n'), _(b"limit expects a number"), default=1) 1571 if lim < 0: 1572 raise error.ParseError(_(b"negative number to select")) 1573 # i18n: "limit" is a keyword 1574 ofs = getinteger( 1575 args.get(b'offset'), _(b"limit expects a number"), default=0 1576 ) 1577 if ofs < 0: 1578 raise error.ParseError(_(b"negative offset")) 1579 os = getset(repo, fullreposet(repo), args[b'set']) 1580 ls = os.slice(ofs, ofs + lim) 1581 if order == followorder and lim > 1: 1582 return subset & ls 1583 return ls & subset 1584 1585 1586@predicate(b'last(set, [n])', safe=True, takeorder=True) 1587def last(repo, subset, x, order): 1588 """Last n members of set, defaulting to 1.""" 1589 # i18n: "last" is a keyword 1590 l = getargs(x, 1, 2, _(b"last requires one or two arguments")) 1591 lim = 1 1592 if len(l) == 2: 1593 # i18n: "last" is a keyword 1594 lim = getinteger(l[1], _(b"last expects a number")) 1595 if lim < 0: 1596 raise error.ParseError(_(b"negative number to select")) 1597 os = getset(repo, fullreposet(repo), l[0]) 1598 os.reverse() 1599 ls = os.slice(0, lim) 1600 if order == followorder and lim > 1: 1601 return subset & ls 1602 ls.reverse() 1603 return ls & subset 1604 1605 1606@predicate(b'max(set)', safe=True) 1607def maxrev(repo, subset, x): 1608 """Changeset with highest revision number in set.""" 1609 os = getset(repo, fullreposet(repo), x) 1610 try: 1611 m = os.max() 1612 if m in subset: 1613 return baseset([m], datarepr=(b'<max %r, %r>', subset, os)) 1614 except ValueError: 1615 # os.max() throws a ValueError when the collection is empty. 1616 # Same as python's max(). 1617 pass 1618 return baseset(datarepr=(b'<max %r, %r>', subset, os)) 1619 1620 1621@predicate(b'merge()', safe=True) 1622def merge(repo, subset, x): 1623 """Changeset is a merge changeset.""" 1624 # i18n: "merge" is a keyword 1625 getargs(x, 0, 0, _(b"merge takes no arguments")) 1626 cl = repo.changelog 1627 1628 def ismerge(r): 1629 try: 1630 return cl.parentrevs(r)[1] != nullrev 1631 except error.WdirUnsupported: 1632 return bool(repo[r].p2()) 1633 1634 return subset.filter(ismerge, condrepr=b'<merge>') 1635 1636 1637@predicate(b'branchpoint()', safe=True) 1638def branchpoint(repo, subset, x): 1639 """Changesets with more than one child.""" 1640 # i18n: "branchpoint" is a keyword 1641 getargs(x, 0, 0, _(b"branchpoint takes no arguments")) 1642 cl = repo.changelog 1643 if not subset: 1644 return baseset() 1645 # XXX this should be 'parentset.min()' assuming 'parentset' is a smartset 1646 # (and if it is not, it should.) 1647 baserev = min(subset) 1648 parentscount = [0] * (len(repo) - baserev) 1649 for r in cl.revs(start=baserev + 1): 1650 for p in cl.parentrevs(r): 1651 if p >= baserev: 1652 parentscount[p - baserev] += 1 1653 return subset.filter( 1654 lambda r: parentscount[r - baserev] > 1, condrepr=b'<branchpoint>' 1655 ) 1656 1657 1658@predicate(b'min(set)', safe=True) 1659def minrev(repo, subset, x): 1660 """Changeset with lowest revision number in set.""" 1661 os = getset(repo, fullreposet(repo), x) 1662 try: 1663 m = os.min() 1664 if m in subset: 1665 return baseset([m], datarepr=(b'<min %r, %r>', subset, os)) 1666 except ValueError: 1667 # os.min() throws a ValueError when the collection is empty. 1668 # Same as python's min(). 1669 pass 1670 return baseset(datarepr=(b'<min %r, %r>', subset, os)) 1671 1672 1673@predicate(b'modifies(pattern)', safe=True, weight=30) 1674def modifies(repo, subset, x): 1675 """Changesets modifying files matched by pattern. 1676 1677 The pattern without explicit kind like ``glob:`` is expected to be 1678 relative to the current directory and match against a file or a 1679 directory. 1680 """ 1681 # i18n: "modifies" is a keyword 1682 pat = getstring(x, _(b"modifies requires a pattern")) 1683 return checkstatus(repo, subset, pat, 'modified') 1684 1685 1686@predicate(b'named(namespace)') 1687def named(repo, subset, x): 1688 """The changesets in a given namespace. 1689 1690 Pattern matching is supported for `namespace`. See 1691 :hg:`help revisions.patterns`. 1692 """ 1693 # i18n: "named" is a keyword 1694 args = getargs(x, 1, 1, _(b'named requires a namespace argument')) 1695 1696 ns = getstring( 1697 args[0], 1698 # i18n: "named" is a keyword 1699 _(b'the argument to named must be a string'), 1700 ) 1701 kind, pattern, matcher = stringutil.stringmatcher(ns) 1702 namespaces = set() 1703 if kind == b'literal': 1704 if pattern not in repo.names: 1705 raise error.RepoLookupError( 1706 _(b"namespace '%s' does not exist") % ns 1707 ) 1708 namespaces.add(repo.names[pattern]) 1709 else: 1710 for name, ns in pycompat.iteritems(repo.names): 1711 if matcher(name): 1712 namespaces.add(ns) 1713 1714 names = set() 1715 for ns in namespaces: 1716 for name in ns.listnames(repo): 1717 if name not in ns.deprecated: 1718 names.update(repo[n].rev() for n in ns.nodes(repo, name)) 1719 1720 names -= {nullrev} 1721 return subset & names 1722 1723 1724def _node(repo, n): 1725 """process a node input""" 1726 rn = None 1727 if len(n) == 2 * repo.nodeconstants.nodelen: 1728 try: 1729 rn = repo.changelog.rev(bin(n)) 1730 except error.WdirUnsupported: 1731 rn = wdirrev 1732 except (LookupError, TypeError): 1733 rn = None 1734 else: 1735 try: 1736 pm = scmutil.resolvehexnodeidprefix(repo, n) 1737 if pm is not None: 1738 rn = repo.changelog.rev(pm) 1739 except LookupError: 1740 pass 1741 except error.WdirUnsupported: 1742 rn = wdirrev 1743 return rn 1744 1745 1746@predicate(b'id(string)', safe=True) 1747def node_(repo, subset, x): 1748 """Revision non-ambiguously specified by the given hex string prefix.""" 1749 # i18n: "id" is a keyword 1750 l = getargs(x, 1, 1, _(b"id requires one argument")) 1751 # i18n: "id" is a keyword 1752 n = getstring(l[0], _(b"id requires a string")) 1753 rn = _node(repo, n) 1754 1755 if rn is None: 1756 return baseset() 1757 result = baseset([rn]) 1758 return result & subset 1759 1760 1761@predicate(b'none()', safe=True) 1762def none(repo, subset, x): 1763 """No changesets.""" 1764 # i18n: "none" is a keyword 1765 getargs(x, 0, 0, _(b"none takes no arguments")) 1766 return baseset() 1767 1768 1769@predicate(b'obsolete()', safe=True) 1770def obsolete(repo, subset, x): 1771 """Mutable changeset with a newer version. (EXPERIMENTAL)""" 1772 # i18n: "obsolete" is a keyword 1773 getargs(x, 0, 0, _(b"obsolete takes no arguments")) 1774 obsoletes = obsmod.getrevs(repo, b'obsolete') 1775 return subset & obsoletes 1776 1777 1778@predicate(b'only(set, [set])', safe=True) 1779def only(repo, subset, x): 1780 """Changesets that are ancestors of the first set that are not ancestors 1781 of any other head in the repo. If a second set is specified, the result 1782 is ancestors of the first set that are not ancestors of the second set 1783 (i.e. ::<set1> - ::<set2>). 1784 """ 1785 cl = repo.changelog 1786 # i18n: "only" is a keyword 1787 args = getargs(x, 1, 2, _(b'only takes one or two arguments')) 1788 include = getset(repo, fullreposet(repo), args[0]) 1789 if len(args) == 1: 1790 if not include: 1791 return baseset() 1792 1793 descendants = set(dagop.revdescendants(repo, include, False)) 1794 exclude = [ 1795 rev 1796 for rev in cl.headrevs() 1797 if not rev in descendants and not rev in include 1798 ] 1799 else: 1800 exclude = getset(repo, fullreposet(repo), args[1]) 1801 1802 results = set(cl.findmissingrevs(common=exclude, heads=include)) 1803 # XXX we should turn this into a baseset instead of a set, smartset may do 1804 # some optimizations from the fact this is a baseset. 1805 return subset & results 1806 1807 1808@predicate(b'origin([set])', safe=True) 1809def origin(repo, subset, x): 1810 """ 1811 Changesets that were specified as a source for the grafts, transplants or 1812 rebases that created the given revisions. Omitting the optional set is the 1813 same as passing all(). If a changeset created by these operations is itself 1814 specified as a source for one of these operations, only the source changeset 1815 for the first operation is selected. 1816 """ 1817 if x is not None: 1818 dests = getset(repo, fullreposet(repo), x) 1819 else: 1820 dests = fullreposet(repo) 1821 1822 def _firstsrc(rev): 1823 src = _getrevsource(repo, rev) 1824 if src is None: 1825 return None 1826 1827 while True: 1828 prev = _getrevsource(repo, src) 1829 1830 if prev is None: 1831 return src 1832 src = prev 1833 1834 o = {_firstsrc(r) for r in dests} 1835 o -= {None} 1836 # XXX we should turn this into a baseset instead of a set, smartset may do 1837 # some optimizations from the fact this is a baseset. 1838 return subset & o 1839 1840 1841@predicate(b'outgoing([path])', safe=False, weight=10) 1842def outgoing(repo, subset, x): 1843 """Changesets not found in the specified destination repository, or the 1844 default push location. 1845 1846 If the location resolve to multiple repositories, the union of all 1847 outgoing changeset will be used. 1848 """ 1849 # Avoid cycles. 1850 from . import ( 1851 discovery, 1852 hg, 1853 ) 1854 1855 # i18n: "outgoing" is a keyword 1856 l = getargs(x, 0, 1, _(b"outgoing takes one or no arguments")) 1857 # i18n: "outgoing" is a keyword 1858 dest = ( 1859 l and getstring(l[0], _(b"outgoing requires a repository path")) or b'' 1860 ) 1861 if dest: 1862 dests = [dest] 1863 else: 1864 dests = [] 1865 missing = set() 1866 for path in urlutil.get_push_paths(repo, repo.ui, dests): 1867 dest = path.pushloc or path.loc 1868 branches = path.branch, [] 1869 1870 revs, checkout = hg.addbranchrevs(repo, repo, branches, []) 1871 if revs: 1872 revs = [repo.lookup(rev) for rev in revs] 1873 other = hg.peer(repo, {}, dest) 1874 try: 1875 with repo.ui.silent(): 1876 outgoing = discovery.findcommonoutgoing( 1877 repo, other, onlyheads=revs 1878 ) 1879 finally: 1880 other.close() 1881 missing.update(outgoing.missing) 1882 cl = repo.changelog 1883 o = {cl.rev(r) for r in missing} 1884 return subset & o 1885 1886 1887@predicate(b'p1([set])', safe=True) 1888def p1(repo, subset, x): 1889 """First parent of changesets in set, or the working directory.""" 1890 if x is None: 1891 p = repo[x].p1().rev() 1892 if p >= 0: 1893 return subset & baseset([p]) 1894 return baseset() 1895 1896 ps = set() 1897 cl = repo.changelog 1898 for r in getset(repo, fullreposet(repo), x): 1899 try: 1900 ps.add(cl.parentrevs(r)[0]) 1901 except error.WdirUnsupported: 1902 ps.add(repo[r].p1().rev()) 1903 ps -= {nullrev} 1904 # XXX we should turn this into a baseset instead of a set, smartset may do 1905 # some optimizations from the fact this is a baseset. 1906 return subset & ps 1907 1908 1909@predicate(b'p2([set])', safe=True) 1910def p2(repo, subset, x): 1911 """Second parent of changesets in set, or the working directory.""" 1912 if x is None: 1913 ps = repo[x].parents() 1914 try: 1915 p = ps[1].rev() 1916 if p >= 0: 1917 return subset & baseset([p]) 1918 return baseset() 1919 except IndexError: 1920 return baseset() 1921 1922 ps = set() 1923 cl = repo.changelog 1924 for r in getset(repo, fullreposet(repo), x): 1925 try: 1926 ps.add(cl.parentrevs(r)[1]) 1927 except error.WdirUnsupported: 1928 parents = repo[r].parents() 1929 if len(parents) == 2: 1930 ps.add(parents[1]) 1931 ps -= {nullrev} 1932 # XXX we should turn this into a baseset instead of a set, smartset may do 1933 # some optimizations from the fact this is a baseset. 1934 return subset & ps 1935 1936 1937def parentpost(repo, subset, x, order): 1938 return p1(repo, subset, x) 1939 1940 1941@predicate(b'parents([set])', safe=True) 1942def parents(repo, subset, x): 1943 """ 1944 The set of all parents for all changesets in set, or the working directory. 1945 """ 1946 if x is None: 1947 ps = {p.rev() for p in repo[x].parents()} 1948 else: 1949 ps = set() 1950 cl = repo.changelog 1951 up = ps.update 1952 parentrevs = cl.parentrevs 1953 for r in getset(repo, fullreposet(repo), x): 1954 try: 1955 up(parentrevs(r)) 1956 except error.WdirUnsupported: 1957 up(p.rev() for p in repo[r].parents()) 1958 ps -= {nullrev} 1959 return subset & ps 1960 1961 1962def _phase(repo, subset, *targets): 1963 """helper to select all rev in <targets> phases""" 1964 return repo._phasecache.getrevset(repo, targets, subset) 1965 1966 1967@predicate(b'_phase(idx)', safe=True) 1968def phase(repo, subset, x): 1969 l = getargs(x, 1, 1, b"_phase requires one argument") 1970 target = getinteger(l[0], b"_phase expects a number") 1971 return _phase(repo, subset, target) 1972 1973 1974@predicate(b'draft()', safe=True) 1975def draft(repo, subset, x): 1976 """Changeset in draft phase.""" 1977 # i18n: "draft" is a keyword 1978 getargs(x, 0, 0, _(b"draft takes no arguments")) 1979 target = phases.draft 1980 return _phase(repo, subset, target) 1981 1982 1983@predicate(b'secret()', safe=True) 1984def secret(repo, subset, x): 1985 """Changeset in secret phase.""" 1986 # i18n: "secret" is a keyword 1987 getargs(x, 0, 0, _(b"secret takes no arguments")) 1988 target = phases.secret 1989 return _phase(repo, subset, target) 1990 1991 1992@predicate(b'stack([revs])', safe=True) 1993def stack(repo, subset, x): 1994 """Experimental revset for the stack of changesets or working directory 1995 parent. (EXPERIMENTAL) 1996 """ 1997 if x is None: 1998 stacks = stackmod.getstack(repo) 1999 else: 2000 stacks = smartset.baseset([]) 2001 for revision in getset(repo, fullreposet(repo), x): 2002 currentstack = stackmod.getstack(repo, revision) 2003 stacks = stacks + currentstack 2004 2005 return subset & stacks 2006 2007 2008def parentspec(repo, subset, x, n, order): 2009 """``set^0`` 2010 The set. 2011 ``set^1`` (or ``set^``), ``set^2`` 2012 First or second parent, respectively, of all changesets in set. 2013 """ 2014 try: 2015 n = int(n[1]) 2016 if n not in (0, 1, 2): 2017 raise ValueError 2018 except (TypeError, ValueError): 2019 raise error.ParseError(_(b"^ expects a number 0, 1, or 2")) 2020 ps = set() 2021 cl = repo.changelog 2022 for r in getset(repo, fullreposet(repo), x): 2023 if n == 0: 2024 ps.add(r) 2025 elif n == 1: 2026 try: 2027 ps.add(cl.parentrevs(r)[0]) 2028 except error.WdirUnsupported: 2029 ps.add(repo[r].p1().rev()) 2030 else: 2031 try: 2032 parents = cl.parentrevs(r) 2033 if parents[1] != nullrev: 2034 ps.add(parents[1]) 2035 except error.WdirUnsupported: 2036 parents = repo[r].parents() 2037 if len(parents) == 2: 2038 ps.add(parents[1].rev()) 2039 return subset & ps 2040 2041 2042@predicate(b'present(set)', safe=True, takeorder=True) 2043def present(repo, subset, x, order): 2044 """An empty set, if any revision in set isn't found; otherwise, 2045 all revisions in set. 2046 2047 If any of specified revisions is not present in the local repository, 2048 the query is normally aborted. But this predicate allows the query 2049 to continue even in such cases. 2050 """ 2051 try: 2052 return getset(repo, subset, x, order) 2053 except error.RepoLookupError: 2054 return baseset() 2055 2056 2057# for internal use 2058@predicate(b'_notpublic', safe=True) 2059def _notpublic(repo, subset, x): 2060 getargs(x, 0, 0, b"_notpublic takes no arguments") 2061 return _phase(repo, subset, phases.draft, phases.secret) 2062 2063 2064# for internal use 2065@predicate(b'_phaseandancestors(phasename, set)', safe=True) 2066def _phaseandancestors(repo, subset, x): 2067 # equivalent to (phasename() & ancestors(set)) but more efficient 2068 # phasename could be one of 'draft', 'secret', or '_notpublic' 2069 args = getargs(x, 2, 2, b"_phaseandancestors requires two arguments") 2070 phasename = getsymbol(args[0]) 2071 s = getset(repo, fullreposet(repo), args[1]) 2072 2073 draft = phases.draft 2074 secret = phases.secret 2075 phasenamemap = { 2076 b'_notpublic': draft, 2077 b'draft': draft, # follow secret's ancestors 2078 b'secret': secret, 2079 } 2080 if phasename not in phasenamemap: 2081 raise error.ParseError(b'%r is not a valid phasename' % phasename) 2082 2083 minimalphase = phasenamemap[phasename] 2084 getphase = repo._phasecache.phase 2085 2086 def cutfunc(rev): 2087 return getphase(repo, rev) < minimalphase 2088 2089 revs = dagop.revancestors(repo, s, cutfunc=cutfunc) 2090 2091 if phasename == b'draft': # need to remove secret changesets 2092 revs = revs.filter(lambda r: getphase(repo, r) == draft) 2093 return subset & revs 2094 2095 2096@predicate(b'public()', safe=True) 2097def public(repo, subset, x): 2098 """Changeset in public phase.""" 2099 # i18n: "public" is a keyword 2100 getargs(x, 0, 0, _(b"public takes no arguments")) 2101 return _phase(repo, subset, phases.public) 2102 2103 2104@predicate(b'remote([id [,path]])', safe=False) 2105def remote(repo, subset, x): 2106 """Local revision that corresponds to the given identifier in a 2107 remote repository, if present. Here, the '.' identifier is a 2108 synonym for the current local branch. 2109 """ 2110 2111 from . import hg # avoid start-up nasties 2112 2113 # i18n: "remote" is a keyword 2114 l = getargs(x, 0, 2, _(b"remote takes zero, one, or two arguments")) 2115 2116 q = b'.' 2117 if len(l) > 0: 2118 # i18n: "remote" is a keyword 2119 q = getstring(l[0], _(b"remote requires a string id")) 2120 if q == b'.': 2121 q = repo[b'.'].branch() 2122 2123 dest = b'' 2124 if len(l) > 1: 2125 # i18n: "remote" is a keyword 2126 dest = getstring(l[1], _(b"remote requires a repository path")) 2127 if not dest: 2128 dest = b'default' 2129 dest, branches = urlutil.get_unique_pull_path( 2130 b'remote', repo, repo.ui, dest 2131 ) 2132 2133 other = hg.peer(repo, {}, dest) 2134 n = other.lookup(q) 2135 if n in repo: 2136 r = repo[n].rev() 2137 if r in subset: 2138 return baseset([r]) 2139 return baseset() 2140 2141 2142@predicate(b'removes(pattern)', safe=True, weight=30) 2143def removes(repo, subset, x): 2144 """Changesets which remove files matching pattern. 2145 2146 The pattern without explicit kind like ``glob:`` is expected to be 2147 relative to the current directory and match against a file or a 2148 directory. 2149 """ 2150 # i18n: "removes" is a keyword 2151 pat = getstring(x, _(b"removes requires a pattern")) 2152 return checkstatus(repo, subset, pat, 'removed') 2153 2154 2155@predicate(b'rev(number)', safe=True) 2156def rev(repo, subset, x): 2157 """Revision with the given numeric identifier.""" 2158 try: 2159 return _rev(repo, subset, x) 2160 except error.RepoLookupError: 2161 return baseset() 2162 2163 2164@predicate(b'_rev(number)', safe=True) 2165def _rev(repo, subset, x): 2166 # internal version of "rev(x)" that raise error if "x" is invalid 2167 # i18n: "rev" is a keyword 2168 l = getargs(x, 1, 1, _(b"rev requires one argument")) 2169 try: 2170 # i18n: "rev" is a keyword 2171 l = int(getstring(l[0], _(b"rev requires a number"))) 2172 except (TypeError, ValueError): 2173 # i18n: "rev" is a keyword 2174 raise error.ParseError(_(b"rev expects a number")) 2175 if l not in _virtualrevs: 2176 try: 2177 repo.changelog.node(l) # check that the rev exists 2178 except IndexError: 2179 raise error.RepoLookupError(_(b"unknown revision '%d'") % l) 2180 return subset & baseset([l]) 2181 2182 2183@predicate(b'revset(set)', safe=True, takeorder=True) 2184def revsetpredicate(repo, subset, x, order): 2185 """Strictly interpret the content as a revset. 2186 2187 The content of this special predicate will be strictly interpreted as a 2188 revset. For example, ``revset(id(0))`` will be interpreted as "id(0)" 2189 without possible ambiguity with a "id(0)" bookmark or tag. 2190 """ 2191 return getset(repo, subset, x, order) 2192 2193 2194@predicate(b'matching(revision [, field])', safe=True) 2195def matching(repo, subset, x): 2196 """Changesets in which a given set of fields match the set of fields in the 2197 selected revision or set. 2198 2199 To match more than one field pass the list of fields to match separated 2200 by spaces (e.g. ``author description``). 2201 2202 Valid fields are most regular revision fields and some special fields. 2203 2204 Regular revision fields are ``description``, ``author``, ``branch``, 2205 ``date``, ``files``, ``phase``, ``parents``, ``substate``, ``user`` 2206 and ``diff``. 2207 Note that ``author`` and ``user`` are synonyms. ``diff`` refers to the 2208 contents of the revision. Two revisions matching their ``diff`` will 2209 also match their ``files``. 2210 2211 Special fields are ``summary`` and ``metadata``: 2212 ``summary`` matches the first line of the description. 2213 ``metadata`` is equivalent to matching ``description user date`` 2214 (i.e. it matches the main metadata fields). 2215 2216 ``metadata`` is the default field which is used when no fields are 2217 specified. You can match more than one field at a time. 2218 """ 2219 # i18n: "matching" is a keyword 2220 l = getargs(x, 1, 2, _(b"matching takes 1 or 2 arguments")) 2221 2222 revs = getset(repo, fullreposet(repo), l[0]) 2223 2224 fieldlist = [b'metadata'] 2225 if len(l) > 1: 2226 fieldlist = getstring( 2227 l[1], 2228 # i18n: "matching" is a keyword 2229 _(b"matching requires a string as its second argument"), 2230 ).split() 2231 2232 # Make sure that there are no repeated fields, 2233 # expand the 'special' 'metadata' field type 2234 # and check the 'files' whenever we check the 'diff' 2235 fields = [] 2236 for field in fieldlist: 2237 if field == b'metadata': 2238 fields += [b'user', b'description', b'date'] 2239 elif field == b'diff': 2240 # a revision matching the diff must also match the files 2241 # since matching the diff is very costly, make sure to 2242 # also match the files first 2243 fields += [b'files', b'diff'] 2244 else: 2245 if field == b'author': 2246 field = b'user' 2247 fields.append(field) 2248 fields = set(fields) 2249 if b'summary' in fields and b'description' in fields: 2250 # If a revision matches its description it also matches its summary 2251 fields.discard(b'summary') 2252 2253 # We may want to match more than one field 2254 # Not all fields take the same amount of time to be matched 2255 # Sort the selected fields in order of increasing matching cost 2256 fieldorder = [ 2257 b'phase', 2258 b'parents', 2259 b'user', 2260 b'date', 2261 b'branch', 2262 b'summary', 2263 b'files', 2264 b'description', 2265 b'substate', 2266 b'diff', 2267 ] 2268 2269 def fieldkeyfunc(f): 2270 try: 2271 return fieldorder.index(f) 2272 except ValueError: 2273 # assume an unknown field is very costly 2274 return len(fieldorder) 2275 2276 fields = list(fields) 2277 fields.sort(key=fieldkeyfunc) 2278 2279 # Each field will be matched with its own "getfield" function 2280 # which will be added to the getfieldfuncs array of functions 2281 getfieldfuncs = [] 2282 _funcs = { 2283 b'user': lambda r: repo[r].user(), 2284 b'branch': lambda r: repo[r].branch(), 2285 b'date': lambda r: repo[r].date(), 2286 b'description': lambda r: repo[r].description(), 2287 b'files': lambda r: repo[r].files(), 2288 b'parents': lambda r: repo[r].parents(), 2289 b'phase': lambda r: repo[r].phase(), 2290 b'substate': lambda r: repo[r].substate, 2291 b'summary': lambda r: repo[r].description().splitlines()[0], 2292 b'diff': lambda r: list( 2293 repo[r].diff(opts=diffutil.diffallopts(repo.ui, {b'git': True})) 2294 ), 2295 } 2296 for info in fields: 2297 getfield = _funcs.get(info, None) 2298 if getfield is None: 2299 raise error.ParseError( 2300 # i18n: "matching" is a keyword 2301 _(b"unexpected field name passed to matching: %s") 2302 % info 2303 ) 2304 getfieldfuncs.append(getfield) 2305 # convert the getfield array of functions into a "getinfo" function 2306 # which returns an array of field values (or a single value if there 2307 # is only one field to match) 2308 getinfo = lambda r: [f(r) for f in getfieldfuncs] 2309 2310 def matches(x): 2311 for rev in revs: 2312 target = getinfo(rev) 2313 match = True 2314 for n, f in enumerate(getfieldfuncs): 2315 if target[n] != f(x): 2316 match = False 2317 if match: 2318 return True 2319 return False 2320 2321 return subset.filter(matches, condrepr=(b'<matching%r %r>', fields, revs)) 2322 2323 2324@predicate(b'reverse(set)', safe=True, takeorder=True, weight=0) 2325def reverse(repo, subset, x, order): 2326 """Reverse order of set.""" 2327 l = getset(repo, subset, x, order) 2328 if order == defineorder: 2329 l.reverse() 2330 return l 2331 2332 2333@predicate(b'roots(set)', safe=True) 2334def roots(repo, subset, x): 2335 """Changesets in set with no parent changeset in set.""" 2336 s = getset(repo, fullreposet(repo), x) 2337 parents = repo.changelog.parentrevs 2338 2339 def filter(r): 2340 for p in parents(r): 2341 if 0 <= p and p in s: 2342 return False 2343 return True 2344 2345 return subset & s.filter(filter, condrepr=b'<roots>') 2346 2347 2348_sortkeyfuncs = { 2349 b'rev': scmutil.intrev, 2350 b'branch': lambda c: c.branch(), 2351 b'desc': lambda c: c.description(), 2352 b'user': lambda c: c.user(), 2353 b'author': lambda c: c.user(), 2354 b'date': lambda c: c.date()[0], 2355 b'node': scmutil.binnode, 2356} 2357 2358 2359def _getsortargs(x): 2360 """Parse sort options into (set, [(key, reverse)], opts)""" 2361 args = getargsdict(x, b'sort', b'set keys topo.firstbranch') 2362 if b'set' not in args: 2363 # i18n: "sort" is a keyword 2364 raise error.ParseError(_(b'sort requires one or two arguments')) 2365 keys = b"rev" 2366 if b'keys' in args: 2367 # i18n: "sort" is a keyword 2368 keys = getstring(args[b'keys'], _(b"sort spec must be a string")) 2369 2370 keyflags = [] 2371 for k in keys.split(): 2372 fk = k 2373 reverse = k.startswith(b'-') 2374 if reverse: 2375 k = k[1:] 2376 if k not in _sortkeyfuncs and k != b'topo': 2377 raise error.ParseError( 2378 _(b"unknown sort key %r") % pycompat.bytestr(fk) 2379 ) 2380 keyflags.append((k, reverse)) 2381 2382 if len(keyflags) > 1 and any(k == b'topo' for k, reverse in keyflags): 2383 # i18n: "topo" is a keyword 2384 raise error.ParseError( 2385 _(b'topo sort order cannot be combined with other sort keys') 2386 ) 2387 2388 opts = {} 2389 if b'topo.firstbranch' in args: 2390 if any(k == b'topo' for k, reverse in keyflags): 2391 opts[b'topo.firstbranch'] = args[b'topo.firstbranch'] 2392 else: 2393 # i18n: "topo" and "topo.firstbranch" are keywords 2394 raise error.ParseError( 2395 _( 2396 b'topo.firstbranch can only be used ' 2397 b'when using the topo sort key' 2398 ) 2399 ) 2400 2401 return args[b'set'], keyflags, opts 2402 2403 2404@predicate( 2405 b'sort(set[, [-]key... [, ...]])', safe=True, takeorder=True, weight=10 2406) 2407def sort(repo, subset, x, order): 2408 """Sort set by keys. The default sort order is ascending, specify a key 2409 as ``-key`` to sort in descending order. 2410 2411 The keys can be: 2412 2413 - ``rev`` for the revision number, 2414 - ``branch`` for the branch name, 2415 - ``desc`` for the commit message (description), 2416 - ``user`` for user name (``author`` can be used as an alias), 2417 - ``date`` for the commit date 2418 - ``topo`` for a reverse topographical sort 2419 - ``node`` the nodeid of the revision 2420 2421 The ``topo`` sort order cannot be combined with other sort keys. This sort 2422 takes one optional argument, ``topo.firstbranch``, which takes a revset that 2423 specifies what topographical branches to prioritize in the sort. 2424 2425 """ 2426 s, keyflags, opts = _getsortargs(x) 2427 revs = getset(repo, subset, s, order) 2428 2429 if not keyflags or order != defineorder: 2430 return revs 2431 if len(keyflags) == 1 and keyflags[0][0] == b"rev": 2432 revs.sort(reverse=keyflags[0][1]) 2433 return revs 2434 elif keyflags[0][0] == b"topo": 2435 firstbranch = () 2436 if b'topo.firstbranch' in opts: 2437 firstbranch = getset(repo, subset, opts[b'topo.firstbranch']) 2438 revs = baseset( 2439 dagop.toposort(revs, repo.changelog.parentrevs, firstbranch), 2440 istopo=True, 2441 ) 2442 if keyflags[0][1]: 2443 revs.reverse() 2444 return revs 2445 2446 # sort() is guaranteed to be stable 2447 ctxs = [repo[r] for r in revs] 2448 for k, reverse in reversed(keyflags): 2449 ctxs.sort(key=_sortkeyfuncs[k], reverse=reverse) 2450 return baseset([c.rev() for c in ctxs]) 2451 2452 2453@predicate(b'subrepo([pattern])') 2454def subrepo(repo, subset, x): 2455 """Changesets that add, modify or remove the given subrepo. If no subrepo 2456 pattern is named, any subrepo changes are returned. 2457 """ 2458 # i18n: "subrepo" is a keyword 2459 args = getargs(x, 0, 1, _(b'subrepo takes at most one argument')) 2460 pat = None 2461 if len(args) != 0: 2462 pat = getstring(args[0], _(b"subrepo requires a pattern")) 2463 2464 m = matchmod.exact([b'.hgsubstate']) 2465 2466 def submatches(names): 2467 k, p, m = stringutil.stringmatcher(pat) 2468 for name in names: 2469 if m(name): 2470 yield name 2471 2472 def matches(x): 2473 c = repo[x] 2474 s = repo.status(c.p1().node(), c.node(), match=m) 2475 2476 if pat is None: 2477 return s.added or s.modified or s.removed 2478 2479 if s.added: 2480 return any(submatches(c.substate.keys())) 2481 2482 if s.modified: 2483 subs = set(c.p1().substate.keys()) 2484 subs.update(c.substate.keys()) 2485 2486 for path in submatches(subs): 2487 if c.p1().substate.get(path) != c.substate.get(path): 2488 return True 2489 2490 if s.removed: 2491 return any(submatches(c.p1().substate.keys())) 2492 2493 return False 2494 2495 return subset.filter(matches, condrepr=(b'<subrepo %r>', pat)) 2496 2497 2498def _mapbynodefunc(repo, s, f): 2499 """(repo, smartset, [node] -> [node]) -> smartset 2500 2501 Helper method to map a smartset to another smartset given a function only 2502 talking about nodes. Handles converting between rev numbers and nodes, and 2503 filtering. 2504 """ 2505 cl = repo.unfiltered().changelog 2506 torev = cl.index.get_rev 2507 tonode = cl.node 2508 result = {torev(n) for n in f(tonode(r) for r in s)} 2509 result.discard(None) 2510 return smartset.baseset(result - repo.changelog.filteredrevs) 2511 2512 2513@predicate(b'successors(set)', safe=True) 2514def successors(repo, subset, x): 2515 """All successors for set, including the given set themselves. 2516 (EXPERIMENTAL)""" 2517 s = getset(repo, fullreposet(repo), x) 2518 f = lambda nodes: obsutil.allsuccessors(repo.obsstore, nodes) 2519 d = _mapbynodefunc(repo, s, f) 2520 return subset & d 2521 2522 2523def _substringmatcher(pattern, casesensitive=True): 2524 kind, pattern, matcher = stringutil.stringmatcher( 2525 pattern, casesensitive=casesensitive 2526 ) 2527 if kind == b'literal': 2528 if not casesensitive: 2529 pattern = encoding.lower(pattern) 2530 matcher = lambda s: pattern in encoding.lower(s) 2531 else: 2532 matcher = lambda s: pattern in s 2533 return kind, pattern, matcher 2534 2535 2536@predicate(b'tag([name])', safe=True) 2537def tag(repo, subset, x): 2538 """The specified tag by name, or all tagged revisions if no name is given. 2539 2540 Pattern matching is supported for `name`. See 2541 :hg:`help revisions.patterns`. 2542 """ 2543 # i18n: "tag" is a keyword 2544 args = getargs(x, 0, 1, _(b"tag takes one or no arguments")) 2545 cl = repo.changelog 2546 if args: 2547 pattern = getstring( 2548 args[0], 2549 # i18n: "tag" is a keyword 2550 _(b'the argument to tag must be a string'), 2551 ) 2552 kind, pattern, matcher = stringutil.stringmatcher(pattern) 2553 if kind == b'literal': 2554 # avoid resolving all tags 2555 tn = repo._tagscache.tags.get(pattern, None) 2556 if tn is None: 2557 raise error.RepoLookupError( 2558 _(b"tag '%s' does not exist") % pattern 2559 ) 2560 s = {repo[tn].rev()} 2561 else: 2562 s = {cl.rev(n) for t, n in repo.tagslist() if matcher(t)} 2563 else: 2564 s = {cl.rev(n) for t, n in repo.tagslist() if t != b'tip'} 2565 return subset & s 2566 2567 2568@predicate(b'tagged', safe=True) 2569def tagged(repo, subset, x): 2570 return tag(repo, subset, x) 2571 2572 2573@predicate(b'orphan()', safe=True) 2574def orphan(repo, subset, x): 2575 """Non-obsolete changesets with obsolete ancestors. (EXPERIMENTAL)""" 2576 # i18n: "orphan" is a keyword 2577 getargs(x, 0, 0, _(b"orphan takes no arguments")) 2578 orphan = obsmod.getrevs(repo, b'orphan') 2579 return subset & orphan 2580 2581 2582@predicate(b'unstable()', safe=True) 2583def unstable(repo, subset, x): 2584 """Changesets with instabilities. (EXPERIMENTAL)""" 2585 # i18n: "unstable" is a keyword 2586 getargs(x, 0, 0, b'unstable takes no arguments') 2587 _unstable = set() 2588 _unstable.update(obsmod.getrevs(repo, b'orphan')) 2589 _unstable.update(obsmod.getrevs(repo, b'phasedivergent')) 2590 _unstable.update(obsmod.getrevs(repo, b'contentdivergent')) 2591 return subset & baseset(_unstable) 2592 2593 2594@predicate(b'user(string)', safe=True, weight=10) 2595def user(repo, subset, x): 2596 """User name contains string. The match is case-insensitive. 2597 2598 Pattern matching is supported for `string`. See 2599 :hg:`help revisions.patterns`. 2600 """ 2601 return author(repo, subset, x) 2602 2603 2604@predicate(b'wdir()', safe=True, weight=0) 2605def wdir(repo, subset, x): 2606 """Working directory. (EXPERIMENTAL)""" 2607 # i18n: "wdir" is a keyword 2608 getargs(x, 0, 0, _(b"wdir takes no arguments")) 2609 if wdirrev in subset or isinstance(subset, fullreposet): 2610 return baseset([wdirrev]) 2611 return baseset() 2612 2613 2614def _orderedlist(repo, subset, x): 2615 s = getstring(x, b"internal error") 2616 if not s: 2617 return baseset() 2618 # remove duplicates here. it's difficult for caller to deduplicate sets 2619 # because different symbols can point to the same rev. 2620 cl = repo.changelog 2621 ls = [] 2622 seen = set() 2623 for t in s.split(b'\0'): 2624 try: 2625 # fast path for integer revision 2626 r = int(t) 2627 if (b'%d' % r) != t or r not in cl: 2628 raise ValueError 2629 revs = [r] 2630 except ValueError: 2631 revs = stringset(repo, subset, t, defineorder) 2632 2633 for r in revs: 2634 if r in seen: 2635 continue 2636 if ( 2637 r in subset 2638 or r in _virtualrevs 2639 and isinstance(subset, fullreposet) 2640 ): 2641 ls.append(r) 2642 seen.add(r) 2643 return baseset(ls) 2644 2645 2646# for internal use 2647@predicate(b'_list', safe=True, takeorder=True) 2648def _list(repo, subset, x, order): 2649 if order == followorder: 2650 # slow path to take the subset order 2651 return subset & _orderedlist(repo, fullreposet(repo), x) 2652 else: 2653 return _orderedlist(repo, subset, x) 2654 2655 2656def _orderedintlist(repo, subset, x): 2657 s = getstring(x, b"internal error") 2658 if not s: 2659 return baseset() 2660 ls = [int(r) for r in s.split(b'\0')] 2661 s = subset 2662 return baseset([r for r in ls if r in s]) 2663 2664 2665# for internal use 2666@predicate(b'_intlist', safe=True, takeorder=True, weight=0) 2667def _intlist(repo, subset, x, order): 2668 if order == followorder: 2669 # slow path to take the subset order 2670 return subset & _orderedintlist(repo, fullreposet(repo), x) 2671 else: 2672 return _orderedintlist(repo, subset, x) 2673 2674 2675def _orderedhexlist(repo, subset, x): 2676 s = getstring(x, b"internal error") 2677 if not s: 2678 return baseset() 2679 cl = repo.changelog 2680 ls = [cl.rev(bin(r)) for r in s.split(b'\0')] 2681 s = subset 2682 return baseset([r for r in ls if r in s]) 2683 2684 2685# for internal use 2686@predicate(b'_hexlist', safe=True, takeorder=True) 2687def _hexlist(repo, subset, x, order): 2688 if order == followorder: 2689 # slow path to take the subset order 2690 return subset & _orderedhexlist(repo, fullreposet(repo), x) 2691 else: 2692 return _orderedhexlist(repo, subset, x) 2693 2694 2695methods = { 2696 b"range": rangeset, 2697 b"rangeall": rangeall, 2698 b"rangepre": rangepre, 2699 b"rangepost": rangepost, 2700 b"dagrange": dagrange, 2701 b"string": stringset, 2702 b"symbol": stringset, 2703 b"and": andset, 2704 b"andsmally": andsmallyset, 2705 b"or": orset, 2706 b"not": notset, 2707 b"difference": differenceset, 2708 b"relation": relationset, 2709 b"relsubscript": relsubscriptset, 2710 b"subscript": subscriptset, 2711 b"list": listset, 2712 b"keyvalue": keyvaluepair, 2713 b"func": func, 2714 b"ancestor": ancestorspec, 2715 b"parent": parentspec, 2716 b"parentpost": parentpost, 2717 b"smartset": rawsmartset, 2718} 2719 2720relations = { 2721 b"g": generationsrel, 2722 b"generations": generationsrel, 2723} 2724 2725subscriptrelations = { 2726 b"g": generationssubrel, 2727 b"generations": generationssubrel, 2728} 2729 2730 2731def lookupfn(repo): 2732 def fn(symbol): 2733 try: 2734 return scmutil.isrevsymbol(repo, symbol) 2735 except error.AmbiguousPrefixLookupError: 2736 raise error.InputError( 2737 b'ambiguous revision identifier: %s' % symbol 2738 ) 2739 2740 return fn 2741 2742 2743def match(ui, spec, lookup=None): 2744 """Create a matcher for a single revision spec""" 2745 return matchany(ui, [spec], lookup=lookup) 2746 2747 2748def matchany(ui, specs, lookup=None, localalias=None): 2749 """Create a matcher that will include any revisions matching one of the 2750 given specs 2751 2752 If lookup function is not None, the parser will first attempt to handle 2753 old-style ranges, which may contain operator characters. 2754 2755 If localalias is not None, it is a dict {name: definitionstring}. It takes 2756 precedence over [revsetalias] config section. 2757 """ 2758 if not specs: 2759 2760 def mfunc(repo, subset=None): 2761 return baseset() 2762 2763 return mfunc 2764 if not all(specs): 2765 raise error.ParseError(_(b"empty query")) 2766 if len(specs) == 1: 2767 tree = revsetlang.parse(specs[0], lookup) 2768 else: 2769 tree = ( 2770 b'or', 2771 (b'list',) + tuple(revsetlang.parse(s, lookup) for s in specs), 2772 ) 2773 2774 aliases = [] 2775 warn = None 2776 if ui: 2777 aliases.extend(ui.configitems(b'revsetalias')) 2778 warn = ui.warn 2779 if localalias: 2780 aliases.extend(localalias.items()) 2781 if aliases: 2782 tree = revsetlang.expandaliases(tree, aliases, warn=warn) 2783 tree = revsetlang.foldconcat(tree) 2784 tree = revsetlang.analyze(tree) 2785 tree = revsetlang.optimize(tree) 2786 return makematcher(tree) 2787 2788 2789def makematcher(tree): 2790 """Create a matcher from an evaluatable tree""" 2791 2792 def mfunc(repo, subset=None, order=None): 2793 if order is None: 2794 if subset is None: 2795 order = defineorder # 'x' 2796 else: 2797 order = followorder # 'subset & x' 2798 if subset is None: 2799 subset = fullreposet(repo) 2800 return getset(repo, subset, tree, order) 2801 2802 return mfunc 2803 2804 2805def loadpredicate(ui, extname, registrarobj): 2806 """Load revset predicates from specified registrarobj""" 2807 for name, func in pycompat.iteritems(registrarobj._table): 2808 symbols[name] = func 2809 if func._safe: 2810 safesymbols.add(name) 2811 2812 2813# load built-in predicates explicitly to setup safesymbols 2814loadpredicate(None, None, predicate) 2815 2816# tell hggettext to extract docstrings from these functions: 2817i18nfunctions = symbols.values() 2818