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