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