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