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