##// END OF EJS Templates
revset: drop an outdated comment...
marmoute -
r47728:1e2f2c78 default
parent child Browse files
Show More
@@ -1,2814 +1,2813 b''
1 1 # revset.py - revision set queries for mercurial
2 2 #
3 3 # Copyright 2010 Olivia Mackall <olivia@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 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 1858 if dest:
1859 # ui.getpath() explicitly tests for None, not just a boolean
1860 1859 dests = [dest]
1861 1860 else:
1862 1861 dests = []
1863 1862 missing = set()
1864 1863 for path in urlutil.get_push_paths(repo, repo.ui, dests):
1865 1864 dest = path.pushloc or path.loc
1866 1865 branches = path.branch, []
1867 1866
1868 1867 revs, checkout = hg.addbranchrevs(repo, repo, branches, [])
1869 1868 if revs:
1870 1869 revs = [repo.lookup(rev) for rev in revs]
1871 1870 other = hg.peer(repo, {}, dest)
1872 1871 try:
1873 1872 repo.ui.pushbuffer()
1874 1873 outgoing = discovery.findcommonoutgoing(repo, other, onlyheads=revs)
1875 1874 repo.ui.popbuffer()
1876 1875 finally:
1877 1876 other.close()
1878 1877 missing.update(outgoing.missing)
1879 1878 cl = repo.changelog
1880 1879 o = {cl.rev(r) for r in missing}
1881 1880 return subset & o
1882 1881
1883 1882
1884 1883 @predicate(b'p1([set])', safe=True)
1885 1884 def p1(repo, subset, x):
1886 1885 """First parent of changesets in set, or the working directory."""
1887 1886 if x is None:
1888 1887 p = repo[x].p1().rev()
1889 1888 if p >= 0:
1890 1889 return subset & baseset([p])
1891 1890 return baseset()
1892 1891
1893 1892 ps = set()
1894 1893 cl = repo.changelog
1895 1894 for r in getset(repo, fullreposet(repo), x):
1896 1895 try:
1897 1896 ps.add(cl.parentrevs(r)[0])
1898 1897 except error.WdirUnsupported:
1899 1898 ps.add(repo[r].p1().rev())
1900 1899 ps -= {nullrev}
1901 1900 # XXX we should turn this into a baseset instead of a set, smartset may do
1902 1901 # some optimizations from the fact this is a baseset.
1903 1902 return subset & ps
1904 1903
1905 1904
1906 1905 @predicate(b'p2([set])', safe=True)
1907 1906 def p2(repo, subset, x):
1908 1907 """Second parent of changesets in set, or the working directory."""
1909 1908 if x is None:
1910 1909 ps = repo[x].parents()
1911 1910 try:
1912 1911 p = ps[1].rev()
1913 1912 if p >= 0:
1914 1913 return subset & baseset([p])
1915 1914 return baseset()
1916 1915 except IndexError:
1917 1916 return baseset()
1918 1917
1919 1918 ps = set()
1920 1919 cl = repo.changelog
1921 1920 for r in getset(repo, fullreposet(repo), x):
1922 1921 try:
1923 1922 ps.add(cl.parentrevs(r)[1])
1924 1923 except error.WdirUnsupported:
1925 1924 parents = repo[r].parents()
1926 1925 if len(parents) == 2:
1927 1926 ps.add(parents[1])
1928 1927 ps -= {nullrev}
1929 1928 # XXX we should turn this into a baseset instead of a set, smartset may do
1930 1929 # some optimizations from the fact this is a baseset.
1931 1930 return subset & ps
1932 1931
1933 1932
1934 1933 def parentpost(repo, subset, x, order):
1935 1934 return p1(repo, subset, x)
1936 1935
1937 1936
1938 1937 @predicate(b'parents([set])', safe=True)
1939 1938 def parents(repo, subset, x):
1940 1939 """
1941 1940 The set of all parents for all changesets in set, or the working directory.
1942 1941 """
1943 1942 if x is None:
1944 1943 ps = {p.rev() for p in repo[x].parents()}
1945 1944 else:
1946 1945 ps = set()
1947 1946 cl = repo.changelog
1948 1947 up = ps.update
1949 1948 parentrevs = cl.parentrevs
1950 1949 for r in getset(repo, fullreposet(repo), x):
1951 1950 try:
1952 1951 up(parentrevs(r))
1953 1952 except error.WdirUnsupported:
1954 1953 up(p.rev() for p in repo[r].parents())
1955 1954 ps -= {nullrev}
1956 1955 return subset & ps
1957 1956
1958 1957
1959 1958 def _phase(repo, subset, *targets):
1960 1959 """helper to select all rev in <targets> phases"""
1961 1960 return repo._phasecache.getrevset(repo, targets, subset)
1962 1961
1963 1962
1964 1963 @predicate(b'_phase(idx)', safe=True)
1965 1964 def phase(repo, subset, x):
1966 1965 l = getargs(x, 1, 1, b"_phase requires one argument")
1967 1966 target = getinteger(l[0], b"_phase expects a number")
1968 1967 return _phase(repo, subset, target)
1969 1968
1970 1969
1971 1970 @predicate(b'draft()', safe=True)
1972 1971 def draft(repo, subset, x):
1973 1972 """Changeset in draft phase."""
1974 1973 # i18n: "draft" is a keyword
1975 1974 getargs(x, 0, 0, _(b"draft takes no arguments"))
1976 1975 target = phases.draft
1977 1976 return _phase(repo, subset, target)
1978 1977
1979 1978
1980 1979 @predicate(b'secret()', safe=True)
1981 1980 def secret(repo, subset, x):
1982 1981 """Changeset in secret phase."""
1983 1982 # i18n: "secret" is a keyword
1984 1983 getargs(x, 0, 0, _(b"secret takes no arguments"))
1985 1984 target = phases.secret
1986 1985 return _phase(repo, subset, target)
1987 1986
1988 1987
1989 1988 @predicate(b'stack([revs])', safe=True)
1990 1989 def stack(repo, subset, x):
1991 1990 """Experimental revset for the stack of changesets or working directory
1992 1991 parent. (EXPERIMENTAL)
1993 1992 """
1994 1993 if x is None:
1995 1994 stacks = stackmod.getstack(repo)
1996 1995 else:
1997 1996 stacks = smartset.baseset([])
1998 1997 for revision in getset(repo, fullreposet(repo), x):
1999 1998 currentstack = stackmod.getstack(repo, revision)
2000 1999 stacks = stacks + currentstack
2001 2000
2002 2001 return subset & stacks
2003 2002
2004 2003
2005 2004 def parentspec(repo, subset, x, n, order):
2006 2005 """``set^0``
2007 2006 The set.
2008 2007 ``set^1`` (or ``set^``), ``set^2``
2009 2008 First or second parent, respectively, of all changesets in set.
2010 2009 """
2011 2010 try:
2012 2011 n = int(n[1])
2013 2012 if n not in (0, 1, 2):
2014 2013 raise ValueError
2015 2014 except (TypeError, ValueError):
2016 2015 raise error.ParseError(_(b"^ expects a number 0, 1, or 2"))
2017 2016 ps = set()
2018 2017 cl = repo.changelog
2019 2018 for r in getset(repo, fullreposet(repo), x):
2020 2019 if n == 0:
2021 2020 ps.add(r)
2022 2021 elif n == 1:
2023 2022 try:
2024 2023 ps.add(cl.parentrevs(r)[0])
2025 2024 except error.WdirUnsupported:
2026 2025 ps.add(repo[r].p1().rev())
2027 2026 else:
2028 2027 try:
2029 2028 parents = cl.parentrevs(r)
2030 2029 if parents[1] != nullrev:
2031 2030 ps.add(parents[1])
2032 2031 except error.WdirUnsupported:
2033 2032 parents = repo[r].parents()
2034 2033 if len(parents) == 2:
2035 2034 ps.add(parents[1].rev())
2036 2035 return subset & ps
2037 2036
2038 2037
2039 2038 @predicate(b'present(set)', safe=True, takeorder=True)
2040 2039 def present(repo, subset, x, order):
2041 2040 """An empty set, if any revision in set isn't found; otherwise,
2042 2041 all revisions in set.
2043 2042
2044 2043 If any of specified revisions is not present in the local repository,
2045 2044 the query is normally aborted. But this predicate allows the query
2046 2045 to continue even in such cases.
2047 2046 """
2048 2047 try:
2049 2048 return getset(repo, subset, x, order)
2050 2049 except error.RepoLookupError:
2051 2050 return baseset()
2052 2051
2053 2052
2054 2053 # for internal use
2055 2054 @predicate(b'_notpublic', safe=True)
2056 2055 def _notpublic(repo, subset, x):
2057 2056 getargs(x, 0, 0, b"_notpublic takes no arguments")
2058 2057 return _phase(repo, subset, phases.draft, phases.secret)
2059 2058
2060 2059
2061 2060 # for internal use
2062 2061 @predicate(b'_phaseandancestors(phasename, set)', safe=True)
2063 2062 def _phaseandancestors(repo, subset, x):
2064 2063 # equivalent to (phasename() & ancestors(set)) but more efficient
2065 2064 # phasename could be one of 'draft', 'secret', or '_notpublic'
2066 2065 args = getargs(x, 2, 2, b"_phaseandancestors requires two arguments")
2067 2066 phasename = getsymbol(args[0])
2068 2067 s = getset(repo, fullreposet(repo), args[1])
2069 2068
2070 2069 draft = phases.draft
2071 2070 secret = phases.secret
2072 2071 phasenamemap = {
2073 2072 b'_notpublic': draft,
2074 2073 b'draft': draft, # follow secret's ancestors
2075 2074 b'secret': secret,
2076 2075 }
2077 2076 if phasename not in phasenamemap:
2078 2077 raise error.ParseError(b'%r is not a valid phasename' % phasename)
2079 2078
2080 2079 minimalphase = phasenamemap[phasename]
2081 2080 getphase = repo._phasecache.phase
2082 2081
2083 2082 def cutfunc(rev):
2084 2083 return getphase(repo, rev) < minimalphase
2085 2084
2086 2085 revs = dagop.revancestors(repo, s, cutfunc=cutfunc)
2087 2086
2088 2087 if phasename == b'draft': # need to remove secret changesets
2089 2088 revs = revs.filter(lambda r: getphase(repo, r) == draft)
2090 2089 return subset & revs
2091 2090
2092 2091
2093 2092 @predicate(b'public()', safe=True)
2094 2093 def public(repo, subset, x):
2095 2094 """Changeset in public phase."""
2096 2095 # i18n: "public" is a keyword
2097 2096 getargs(x, 0, 0, _(b"public takes no arguments"))
2098 2097 return _phase(repo, subset, phases.public)
2099 2098
2100 2099
2101 2100 @predicate(b'remote([id [,path]])', safe=False)
2102 2101 def remote(repo, subset, x):
2103 2102 """Local revision that corresponds to the given identifier in a
2104 2103 remote repository, if present. Here, the '.' identifier is a
2105 2104 synonym for the current local branch.
2106 2105 """
2107 2106
2108 2107 from . import hg # avoid start-up nasties
2109 2108
2110 2109 # i18n: "remote" is a keyword
2111 2110 l = getargs(x, 0, 2, _(b"remote takes zero, one, or two arguments"))
2112 2111
2113 2112 q = b'.'
2114 2113 if len(l) > 0:
2115 2114 # i18n: "remote" is a keyword
2116 2115 q = getstring(l[0], _(b"remote requires a string id"))
2117 2116 if q == b'.':
2118 2117 q = repo[b'.'].branch()
2119 2118
2120 2119 dest = b''
2121 2120 if len(l) > 1:
2122 2121 # i18n: "remote" is a keyword
2123 2122 dest = getstring(l[1], _(b"remote requires a repository path"))
2124 2123 if not dest:
2125 2124 dest = b'default'
2126 2125 dest, branches = urlutil.get_unique_pull_path(
2127 2126 b'remote', repo, repo.ui, dest
2128 2127 )
2129 2128
2130 2129 other = hg.peer(repo, {}, dest)
2131 2130 n = other.lookup(q)
2132 2131 if n in repo:
2133 2132 r = repo[n].rev()
2134 2133 if r in subset:
2135 2134 return baseset([r])
2136 2135 return baseset()
2137 2136
2138 2137
2139 2138 @predicate(b'removes(pattern)', safe=True, weight=30)
2140 2139 def removes(repo, subset, x):
2141 2140 """Changesets which remove files matching pattern.
2142 2141
2143 2142 The pattern without explicit kind like ``glob:`` is expected to be
2144 2143 relative to the current directory and match against a file or a
2145 2144 directory.
2146 2145 """
2147 2146 # i18n: "removes" is a keyword
2148 2147 pat = getstring(x, _(b"removes requires a pattern"))
2149 2148 return checkstatus(repo, subset, pat, 'removed')
2150 2149
2151 2150
2152 2151 @predicate(b'rev(number)', safe=True)
2153 2152 def rev(repo, subset, x):
2154 2153 """Revision with the given numeric identifier."""
2155 2154 try:
2156 2155 return _rev(repo, subset, x)
2157 2156 except error.RepoLookupError:
2158 2157 return baseset()
2159 2158
2160 2159
2161 2160 @predicate(b'_rev(number)', safe=True)
2162 2161 def _rev(repo, subset, x):
2163 2162 # internal version of "rev(x)" that raise error if "x" is invalid
2164 2163 # i18n: "rev" is a keyword
2165 2164 l = getargs(x, 1, 1, _(b"rev requires one argument"))
2166 2165 try:
2167 2166 # i18n: "rev" is a keyword
2168 2167 l = int(getstring(l[0], _(b"rev requires a number")))
2169 2168 except (TypeError, ValueError):
2170 2169 # i18n: "rev" is a keyword
2171 2170 raise error.ParseError(_(b"rev expects a number"))
2172 2171 if l not in _virtualrevs:
2173 2172 try:
2174 2173 repo.changelog.node(l) # check that the rev exists
2175 2174 except IndexError:
2176 2175 raise error.RepoLookupError(_(b"unknown revision '%d'") % l)
2177 2176 return subset & baseset([l])
2178 2177
2179 2178
2180 2179 @predicate(b'revset(set)', safe=True, takeorder=True)
2181 2180 def revsetpredicate(repo, subset, x, order):
2182 2181 """Strictly interpret the content as a revset.
2183 2182
2184 2183 The content of this special predicate will be strictly interpreted as a
2185 2184 revset. For example, ``revset(id(0))`` will be interpreted as "id(0)"
2186 2185 without possible ambiguity with a "id(0)" bookmark or tag.
2187 2186 """
2188 2187 return getset(repo, subset, x, order)
2189 2188
2190 2189
2191 2190 @predicate(b'matching(revision [, field])', safe=True)
2192 2191 def matching(repo, subset, x):
2193 2192 """Changesets in which a given set of fields match the set of fields in the
2194 2193 selected revision or set.
2195 2194
2196 2195 To match more than one field pass the list of fields to match separated
2197 2196 by spaces (e.g. ``author description``).
2198 2197
2199 2198 Valid fields are most regular revision fields and some special fields.
2200 2199
2201 2200 Regular revision fields are ``description``, ``author``, ``branch``,
2202 2201 ``date``, ``files``, ``phase``, ``parents``, ``substate``, ``user``
2203 2202 and ``diff``.
2204 2203 Note that ``author`` and ``user`` are synonyms. ``diff`` refers to the
2205 2204 contents of the revision. Two revisions matching their ``diff`` will
2206 2205 also match their ``files``.
2207 2206
2208 2207 Special fields are ``summary`` and ``metadata``:
2209 2208 ``summary`` matches the first line of the description.
2210 2209 ``metadata`` is equivalent to matching ``description user date``
2211 2210 (i.e. it matches the main metadata fields).
2212 2211
2213 2212 ``metadata`` is the default field which is used when no fields are
2214 2213 specified. You can match more than one field at a time.
2215 2214 """
2216 2215 # i18n: "matching" is a keyword
2217 2216 l = getargs(x, 1, 2, _(b"matching takes 1 or 2 arguments"))
2218 2217
2219 2218 revs = getset(repo, fullreposet(repo), l[0])
2220 2219
2221 2220 fieldlist = [b'metadata']
2222 2221 if len(l) > 1:
2223 2222 fieldlist = getstring(
2224 2223 l[1],
2225 2224 # i18n: "matching" is a keyword
2226 2225 _(b"matching requires a string as its second argument"),
2227 2226 ).split()
2228 2227
2229 2228 # Make sure that there are no repeated fields,
2230 2229 # expand the 'special' 'metadata' field type
2231 2230 # and check the 'files' whenever we check the 'diff'
2232 2231 fields = []
2233 2232 for field in fieldlist:
2234 2233 if field == b'metadata':
2235 2234 fields += [b'user', b'description', b'date']
2236 2235 elif field == b'diff':
2237 2236 # a revision matching the diff must also match the files
2238 2237 # since matching the diff is very costly, make sure to
2239 2238 # also match the files first
2240 2239 fields += [b'files', b'diff']
2241 2240 else:
2242 2241 if field == b'author':
2243 2242 field = b'user'
2244 2243 fields.append(field)
2245 2244 fields = set(fields)
2246 2245 if b'summary' in fields and b'description' in fields:
2247 2246 # If a revision matches its description it also matches its summary
2248 2247 fields.discard(b'summary')
2249 2248
2250 2249 # We may want to match more than one field
2251 2250 # Not all fields take the same amount of time to be matched
2252 2251 # Sort the selected fields in order of increasing matching cost
2253 2252 fieldorder = [
2254 2253 b'phase',
2255 2254 b'parents',
2256 2255 b'user',
2257 2256 b'date',
2258 2257 b'branch',
2259 2258 b'summary',
2260 2259 b'files',
2261 2260 b'description',
2262 2261 b'substate',
2263 2262 b'diff',
2264 2263 ]
2265 2264
2266 2265 def fieldkeyfunc(f):
2267 2266 try:
2268 2267 return fieldorder.index(f)
2269 2268 except ValueError:
2270 2269 # assume an unknown field is very costly
2271 2270 return len(fieldorder)
2272 2271
2273 2272 fields = list(fields)
2274 2273 fields.sort(key=fieldkeyfunc)
2275 2274
2276 2275 # Each field will be matched with its own "getfield" function
2277 2276 # which will be added to the getfieldfuncs array of functions
2278 2277 getfieldfuncs = []
2279 2278 _funcs = {
2280 2279 b'user': lambda r: repo[r].user(),
2281 2280 b'branch': lambda r: repo[r].branch(),
2282 2281 b'date': lambda r: repo[r].date(),
2283 2282 b'description': lambda r: repo[r].description(),
2284 2283 b'files': lambda r: repo[r].files(),
2285 2284 b'parents': lambda r: repo[r].parents(),
2286 2285 b'phase': lambda r: repo[r].phase(),
2287 2286 b'substate': lambda r: repo[r].substate,
2288 2287 b'summary': lambda r: repo[r].description().splitlines()[0],
2289 2288 b'diff': lambda r: list(
2290 2289 repo[r].diff(opts=diffutil.diffallopts(repo.ui, {b'git': True}))
2291 2290 ),
2292 2291 }
2293 2292 for info in fields:
2294 2293 getfield = _funcs.get(info, None)
2295 2294 if getfield is None:
2296 2295 raise error.ParseError(
2297 2296 # i18n: "matching" is a keyword
2298 2297 _(b"unexpected field name passed to matching: %s")
2299 2298 % info
2300 2299 )
2301 2300 getfieldfuncs.append(getfield)
2302 2301 # convert the getfield array of functions into a "getinfo" function
2303 2302 # which returns an array of field values (or a single value if there
2304 2303 # is only one field to match)
2305 2304 getinfo = lambda r: [f(r) for f in getfieldfuncs]
2306 2305
2307 2306 def matches(x):
2308 2307 for rev in revs:
2309 2308 target = getinfo(rev)
2310 2309 match = True
2311 2310 for n, f in enumerate(getfieldfuncs):
2312 2311 if target[n] != f(x):
2313 2312 match = False
2314 2313 if match:
2315 2314 return True
2316 2315 return False
2317 2316
2318 2317 return subset.filter(matches, condrepr=(b'<matching%r %r>', fields, revs))
2319 2318
2320 2319
2321 2320 @predicate(b'reverse(set)', safe=True, takeorder=True, weight=0)
2322 2321 def reverse(repo, subset, x, order):
2323 2322 """Reverse order of set."""
2324 2323 l = getset(repo, subset, x, order)
2325 2324 if order == defineorder:
2326 2325 l.reverse()
2327 2326 return l
2328 2327
2329 2328
2330 2329 @predicate(b'roots(set)', safe=True)
2331 2330 def roots(repo, subset, x):
2332 2331 """Changesets in set with no parent changeset in set."""
2333 2332 s = getset(repo, fullreposet(repo), x)
2334 2333 parents = repo.changelog.parentrevs
2335 2334
2336 2335 def filter(r):
2337 2336 for p in parents(r):
2338 2337 if 0 <= p and p in s:
2339 2338 return False
2340 2339 return True
2341 2340
2342 2341 return subset & s.filter(filter, condrepr=b'<roots>')
2343 2342
2344 2343
2345 2344 _sortkeyfuncs = {
2346 2345 b'rev': scmutil.intrev,
2347 2346 b'branch': lambda c: c.branch(),
2348 2347 b'desc': lambda c: c.description(),
2349 2348 b'user': lambda c: c.user(),
2350 2349 b'author': lambda c: c.user(),
2351 2350 b'date': lambda c: c.date()[0],
2352 2351 b'node': scmutil.binnode,
2353 2352 }
2354 2353
2355 2354
2356 2355 def _getsortargs(x):
2357 2356 """Parse sort options into (set, [(key, reverse)], opts)"""
2358 2357 args = getargsdict(x, b'sort', b'set keys topo.firstbranch')
2359 2358 if b'set' not in args:
2360 2359 # i18n: "sort" is a keyword
2361 2360 raise error.ParseError(_(b'sort requires one or two arguments'))
2362 2361 keys = b"rev"
2363 2362 if b'keys' in args:
2364 2363 # i18n: "sort" is a keyword
2365 2364 keys = getstring(args[b'keys'], _(b"sort spec must be a string"))
2366 2365
2367 2366 keyflags = []
2368 2367 for k in keys.split():
2369 2368 fk = k
2370 2369 reverse = k.startswith(b'-')
2371 2370 if reverse:
2372 2371 k = k[1:]
2373 2372 if k not in _sortkeyfuncs and k != b'topo':
2374 2373 raise error.ParseError(
2375 2374 _(b"unknown sort key %r") % pycompat.bytestr(fk)
2376 2375 )
2377 2376 keyflags.append((k, reverse))
2378 2377
2379 2378 if len(keyflags) > 1 and any(k == b'topo' for k, reverse in keyflags):
2380 2379 # i18n: "topo" is a keyword
2381 2380 raise error.ParseError(
2382 2381 _(b'topo sort order cannot be combined with other sort keys')
2383 2382 )
2384 2383
2385 2384 opts = {}
2386 2385 if b'topo.firstbranch' in args:
2387 2386 if any(k == b'topo' for k, reverse in keyflags):
2388 2387 opts[b'topo.firstbranch'] = args[b'topo.firstbranch']
2389 2388 else:
2390 2389 # i18n: "topo" and "topo.firstbranch" are keywords
2391 2390 raise error.ParseError(
2392 2391 _(
2393 2392 b'topo.firstbranch can only be used '
2394 2393 b'when using the topo sort key'
2395 2394 )
2396 2395 )
2397 2396
2398 2397 return args[b'set'], keyflags, opts
2399 2398
2400 2399
2401 2400 @predicate(
2402 2401 b'sort(set[, [-]key... [, ...]])', safe=True, takeorder=True, weight=10
2403 2402 )
2404 2403 def sort(repo, subset, x, order):
2405 2404 """Sort set by keys. The default sort order is ascending, specify a key
2406 2405 as ``-key`` to sort in descending order.
2407 2406
2408 2407 The keys can be:
2409 2408
2410 2409 - ``rev`` for the revision number,
2411 2410 - ``branch`` for the branch name,
2412 2411 - ``desc`` for the commit message (description),
2413 2412 - ``user`` for user name (``author`` can be used as an alias),
2414 2413 - ``date`` for the commit date
2415 2414 - ``topo`` for a reverse topographical sort
2416 2415 - ``node`` the nodeid of the revision
2417 2416
2418 2417 The ``topo`` sort order cannot be combined with other sort keys. This sort
2419 2418 takes one optional argument, ``topo.firstbranch``, which takes a revset that
2420 2419 specifies what topographical branches to prioritize in the sort.
2421 2420
2422 2421 """
2423 2422 s, keyflags, opts = _getsortargs(x)
2424 2423 revs = getset(repo, subset, s, order)
2425 2424
2426 2425 if not keyflags or order != defineorder:
2427 2426 return revs
2428 2427 if len(keyflags) == 1 and keyflags[0][0] == b"rev":
2429 2428 revs.sort(reverse=keyflags[0][1])
2430 2429 return revs
2431 2430 elif keyflags[0][0] == b"topo":
2432 2431 firstbranch = ()
2433 2432 if b'topo.firstbranch' in opts:
2434 2433 firstbranch = getset(repo, subset, opts[b'topo.firstbranch'])
2435 2434 revs = baseset(
2436 2435 dagop.toposort(revs, repo.changelog.parentrevs, firstbranch),
2437 2436 istopo=True,
2438 2437 )
2439 2438 if keyflags[0][1]:
2440 2439 revs.reverse()
2441 2440 return revs
2442 2441
2443 2442 # sort() is guaranteed to be stable
2444 2443 ctxs = [repo[r] for r in revs]
2445 2444 for k, reverse in reversed(keyflags):
2446 2445 ctxs.sort(key=_sortkeyfuncs[k], reverse=reverse)
2447 2446 return baseset([c.rev() for c in ctxs])
2448 2447
2449 2448
2450 2449 @predicate(b'subrepo([pattern])')
2451 2450 def subrepo(repo, subset, x):
2452 2451 """Changesets that add, modify or remove the given subrepo. If no subrepo
2453 2452 pattern is named, any subrepo changes are returned.
2454 2453 """
2455 2454 # i18n: "subrepo" is a keyword
2456 2455 args = getargs(x, 0, 1, _(b'subrepo takes at most one argument'))
2457 2456 pat = None
2458 2457 if len(args) != 0:
2459 2458 pat = getstring(args[0], _(b"subrepo requires a pattern"))
2460 2459
2461 2460 m = matchmod.exact([b'.hgsubstate'])
2462 2461
2463 2462 def submatches(names):
2464 2463 k, p, m = stringutil.stringmatcher(pat)
2465 2464 for name in names:
2466 2465 if m(name):
2467 2466 yield name
2468 2467
2469 2468 def matches(x):
2470 2469 c = repo[x]
2471 2470 s = repo.status(c.p1().node(), c.node(), match=m)
2472 2471
2473 2472 if pat is None:
2474 2473 return s.added or s.modified or s.removed
2475 2474
2476 2475 if s.added:
2477 2476 return any(submatches(c.substate.keys()))
2478 2477
2479 2478 if s.modified:
2480 2479 subs = set(c.p1().substate.keys())
2481 2480 subs.update(c.substate.keys())
2482 2481
2483 2482 for path in submatches(subs):
2484 2483 if c.p1().substate.get(path) != c.substate.get(path):
2485 2484 return True
2486 2485
2487 2486 if s.removed:
2488 2487 return any(submatches(c.p1().substate.keys()))
2489 2488
2490 2489 return False
2491 2490
2492 2491 return subset.filter(matches, condrepr=(b'<subrepo %r>', pat))
2493 2492
2494 2493
2495 2494 def _mapbynodefunc(repo, s, f):
2496 2495 """(repo, smartset, [node] -> [node]) -> smartset
2497 2496
2498 2497 Helper method to map a smartset to another smartset given a function only
2499 2498 talking about nodes. Handles converting between rev numbers and nodes, and
2500 2499 filtering.
2501 2500 """
2502 2501 cl = repo.unfiltered().changelog
2503 2502 torev = cl.index.get_rev
2504 2503 tonode = cl.node
2505 2504 result = {torev(n) for n in f(tonode(r) for r in s)}
2506 2505 result.discard(None)
2507 2506 return smartset.baseset(result - repo.changelog.filteredrevs)
2508 2507
2509 2508
2510 2509 @predicate(b'successors(set)', safe=True)
2511 2510 def successors(repo, subset, x):
2512 2511 """All successors for set, including the given set themselves.
2513 2512 (EXPERIMENTAL)"""
2514 2513 s = getset(repo, fullreposet(repo), x)
2515 2514 f = lambda nodes: obsutil.allsuccessors(repo.obsstore, nodes)
2516 2515 d = _mapbynodefunc(repo, s, f)
2517 2516 return subset & d
2518 2517
2519 2518
2520 2519 def _substringmatcher(pattern, casesensitive=True):
2521 2520 kind, pattern, matcher = stringutil.stringmatcher(
2522 2521 pattern, casesensitive=casesensitive
2523 2522 )
2524 2523 if kind == b'literal':
2525 2524 if not casesensitive:
2526 2525 pattern = encoding.lower(pattern)
2527 2526 matcher = lambda s: pattern in encoding.lower(s)
2528 2527 else:
2529 2528 matcher = lambda s: pattern in s
2530 2529 return kind, pattern, matcher
2531 2530
2532 2531
2533 2532 @predicate(b'tag([name])', safe=True)
2534 2533 def tag(repo, subset, x):
2535 2534 """The specified tag by name, or all tagged revisions if no name is given.
2536 2535
2537 2536 Pattern matching is supported for `name`. See
2538 2537 :hg:`help revisions.patterns`.
2539 2538 """
2540 2539 # i18n: "tag" is a keyword
2541 2540 args = getargs(x, 0, 1, _(b"tag takes one or no arguments"))
2542 2541 cl = repo.changelog
2543 2542 if args:
2544 2543 pattern = getstring(
2545 2544 args[0],
2546 2545 # i18n: "tag" is a keyword
2547 2546 _(b'the argument to tag must be a string'),
2548 2547 )
2549 2548 kind, pattern, matcher = stringutil.stringmatcher(pattern)
2550 2549 if kind == b'literal':
2551 2550 # avoid resolving all tags
2552 2551 tn = repo._tagscache.tags.get(pattern, None)
2553 2552 if tn is None:
2554 2553 raise error.RepoLookupError(
2555 2554 _(b"tag '%s' does not exist") % pattern
2556 2555 )
2557 2556 s = {repo[tn].rev()}
2558 2557 else:
2559 2558 s = {cl.rev(n) for t, n in repo.tagslist() if matcher(t)}
2560 2559 else:
2561 2560 s = {cl.rev(n) for t, n in repo.tagslist() if t != b'tip'}
2562 2561 return subset & s
2563 2562
2564 2563
2565 2564 @predicate(b'tagged', safe=True)
2566 2565 def tagged(repo, subset, x):
2567 2566 return tag(repo, subset, x)
2568 2567
2569 2568
2570 2569 @predicate(b'orphan()', safe=True)
2571 2570 def orphan(repo, subset, x):
2572 2571 """Non-obsolete changesets with obsolete ancestors. (EXPERIMENTAL)"""
2573 2572 # i18n: "orphan" is a keyword
2574 2573 getargs(x, 0, 0, _(b"orphan takes no arguments"))
2575 2574 orphan = obsmod.getrevs(repo, b'orphan')
2576 2575 return subset & orphan
2577 2576
2578 2577
2579 2578 @predicate(b'unstable()', safe=True)
2580 2579 def unstable(repo, subset, x):
2581 2580 """Changesets with instabilities. (EXPERIMENTAL)"""
2582 2581 # i18n: "unstable" is a keyword
2583 2582 getargs(x, 0, 0, b'unstable takes no arguments')
2584 2583 _unstable = set()
2585 2584 _unstable.update(obsmod.getrevs(repo, b'orphan'))
2586 2585 _unstable.update(obsmod.getrevs(repo, b'phasedivergent'))
2587 2586 _unstable.update(obsmod.getrevs(repo, b'contentdivergent'))
2588 2587 return subset & baseset(_unstable)
2589 2588
2590 2589
2591 2590 @predicate(b'user(string)', safe=True, weight=10)
2592 2591 def user(repo, subset, x):
2593 2592 """User name contains string. The match is case-insensitive.
2594 2593
2595 2594 Pattern matching is supported for `string`. See
2596 2595 :hg:`help revisions.patterns`.
2597 2596 """
2598 2597 return author(repo, subset, x)
2599 2598
2600 2599
2601 2600 @predicate(b'wdir()', safe=True, weight=0)
2602 2601 def wdir(repo, subset, x):
2603 2602 """Working directory. (EXPERIMENTAL)"""
2604 2603 # i18n: "wdir" is a keyword
2605 2604 getargs(x, 0, 0, _(b"wdir takes no arguments"))
2606 2605 if wdirrev in subset or isinstance(subset, fullreposet):
2607 2606 return baseset([wdirrev])
2608 2607 return baseset()
2609 2608
2610 2609
2611 2610 def _orderedlist(repo, subset, x):
2612 2611 s = getstring(x, b"internal error")
2613 2612 if not s:
2614 2613 return baseset()
2615 2614 # remove duplicates here. it's difficult for caller to deduplicate sets
2616 2615 # because different symbols can point to the same rev.
2617 2616 cl = repo.changelog
2618 2617 ls = []
2619 2618 seen = set()
2620 2619 for t in s.split(b'\0'):
2621 2620 try:
2622 2621 # fast path for integer revision
2623 2622 r = int(t)
2624 2623 if (b'%d' % r) != t or r not in cl:
2625 2624 raise ValueError
2626 2625 revs = [r]
2627 2626 except ValueError:
2628 2627 revs = stringset(repo, subset, t, defineorder)
2629 2628
2630 2629 for r in revs:
2631 2630 if r in seen:
2632 2631 continue
2633 2632 if (
2634 2633 r in subset
2635 2634 or r in _virtualrevs
2636 2635 and isinstance(subset, fullreposet)
2637 2636 ):
2638 2637 ls.append(r)
2639 2638 seen.add(r)
2640 2639 return baseset(ls)
2641 2640
2642 2641
2643 2642 # for internal use
2644 2643 @predicate(b'_list', safe=True, takeorder=True)
2645 2644 def _list(repo, subset, x, order):
2646 2645 if order == followorder:
2647 2646 # slow path to take the subset order
2648 2647 return subset & _orderedlist(repo, fullreposet(repo), x)
2649 2648 else:
2650 2649 return _orderedlist(repo, subset, x)
2651 2650
2652 2651
2653 2652 def _orderedintlist(repo, subset, x):
2654 2653 s = getstring(x, b"internal error")
2655 2654 if not s:
2656 2655 return baseset()
2657 2656 ls = [int(r) for r in s.split(b'\0')]
2658 2657 s = subset
2659 2658 return baseset([r for r in ls if r in s])
2660 2659
2661 2660
2662 2661 # for internal use
2663 2662 @predicate(b'_intlist', safe=True, takeorder=True, weight=0)
2664 2663 def _intlist(repo, subset, x, order):
2665 2664 if order == followorder:
2666 2665 # slow path to take the subset order
2667 2666 return subset & _orderedintlist(repo, fullreposet(repo), x)
2668 2667 else:
2669 2668 return _orderedintlist(repo, subset, x)
2670 2669
2671 2670
2672 2671 def _orderedhexlist(repo, subset, x):
2673 2672 s = getstring(x, b"internal error")
2674 2673 if not s:
2675 2674 return baseset()
2676 2675 cl = repo.changelog
2677 2676 ls = [cl.rev(bin(r)) for r in s.split(b'\0')]
2678 2677 s = subset
2679 2678 return baseset([r for r in ls if r in s])
2680 2679
2681 2680
2682 2681 # for internal use
2683 2682 @predicate(b'_hexlist', safe=True, takeorder=True)
2684 2683 def _hexlist(repo, subset, x, order):
2685 2684 if order == followorder:
2686 2685 # slow path to take the subset order
2687 2686 return subset & _orderedhexlist(repo, fullreposet(repo), x)
2688 2687 else:
2689 2688 return _orderedhexlist(repo, subset, x)
2690 2689
2691 2690
2692 2691 methods = {
2693 2692 b"range": rangeset,
2694 2693 b"rangeall": rangeall,
2695 2694 b"rangepre": rangepre,
2696 2695 b"rangepost": rangepost,
2697 2696 b"dagrange": dagrange,
2698 2697 b"string": stringset,
2699 2698 b"symbol": stringset,
2700 2699 b"and": andset,
2701 2700 b"andsmally": andsmallyset,
2702 2701 b"or": orset,
2703 2702 b"not": notset,
2704 2703 b"difference": differenceset,
2705 2704 b"relation": relationset,
2706 2705 b"relsubscript": relsubscriptset,
2707 2706 b"subscript": subscriptset,
2708 2707 b"list": listset,
2709 2708 b"keyvalue": keyvaluepair,
2710 2709 b"func": func,
2711 2710 b"ancestor": ancestorspec,
2712 2711 b"parent": parentspec,
2713 2712 b"parentpost": parentpost,
2714 2713 b"smartset": rawsmartset,
2715 2714 }
2716 2715
2717 2716 relations = {
2718 2717 b"g": generationsrel,
2719 2718 b"generations": generationsrel,
2720 2719 }
2721 2720
2722 2721 subscriptrelations = {
2723 2722 b"g": generationssubrel,
2724 2723 b"generations": generationssubrel,
2725 2724 }
2726 2725
2727 2726
2728 2727 def lookupfn(repo):
2729 2728 def fn(symbol):
2730 2729 try:
2731 2730 return scmutil.isrevsymbol(repo, symbol)
2732 2731 except error.AmbiguousPrefixLookupError:
2733 2732 raise error.InputError(
2734 2733 b'ambiguous revision identifier: %s' % symbol
2735 2734 )
2736 2735
2737 2736 return fn
2738 2737
2739 2738
2740 2739 def match(ui, spec, lookup=None):
2741 2740 """Create a matcher for a single revision spec"""
2742 2741 return matchany(ui, [spec], lookup=lookup)
2743 2742
2744 2743
2745 2744 def matchany(ui, specs, lookup=None, localalias=None):
2746 2745 """Create a matcher that will include any revisions matching one of the
2747 2746 given specs
2748 2747
2749 2748 If lookup function is not None, the parser will first attempt to handle
2750 2749 old-style ranges, which may contain operator characters.
2751 2750
2752 2751 If localalias is not None, it is a dict {name: definitionstring}. It takes
2753 2752 precedence over [revsetalias] config section.
2754 2753 """
2755 2754 if not specs:
2756 2755
2757 2756 def mfunc(repo, subset=None):
2758 2757 return baseset()
2759 2758
2760 2759 return mfunc
2761 2760 if not all(specs):
2762 2761 raise error.ParseError(_(b"empty query"))
2763 2762 if len(specs) == 1:
2764 2763 tree = revsetlang.parse(specs[0], lookup)
2765 2764 else:
2766 2765 tree = (
2767 2766 b'or',
2768 2767 (b'list',) + tuple(revsetlang.parse(s, lookup) for s in specs),
2769 2768 )
2770 2769
2771 2770 aliases = []
2772 2771 warn = None
2773 2772 if ui:
2774 2773 aliases.extend(ui.configitems(b'revsetalias'))
2775 2774 warn = ui.warn
2776 2775 if localalias:
2777 2776 aliases.extend(localalias.items())
2778 2777 if aliases:
2779 2778 tree = revsetlang.expandaliases(tree, aliases, warn=warn)
2780 2779 tree = revsetlang.foldconcat(tree)
2781 2780 tree = revsetlang.analyze(tree)
2782 2781 tree = revsetlang.optimize(tree)
2783 2782 return makematcher(tree)
2784 2783
2785 2784
2786 2785 def makematcher(tree):
2787 2786 """Create a matcher from an evaluatable tree"""
2788 2787
2789 2788 def mfunc(repo, subset=None, order=None):
2790 2789 if order is None:
2791 2790 if subset is None:
2792 2791 order = defineorder # 'x'
2793 2792 else:
2794 2793 order = followorder # 'subset & x'
2795 2794 if subset is None:
2796 2795 subset = fullreposet(repo)
2797 2796 return getset(repo, subset, tree, order)
2798 2797
2799 2798 return mfunc
2800 2799
2801 2800
2802 2801 def loadpredicate(ui, extname, registrarobj):
2803 2802 """Load revset predicates from specified registrarobj"""
2804 2803 for name, func in pycompat.iteritems(registrarobj._table):
2805 2804 symbols[name] = func
2806 2805 if func._safe:
2807 2806 safesymbols.add(name)
2808 2807
2809 2808
2810 2809 # load built-in predicates explicitly to setup safesymbols
2811 2810 loadpredicate(None, None, predicate)
2812 2811
2813 2812 # tell hggettext to extract docstrings from these functions:
2814 2813 i18nfunctions = symbols.values()
General Comments 0
You need to be logged in to leave comments. Login now