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