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