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