##// END OF EJS Templates
revset: move order constants from revsetlang...
Yuya Nishihara -
r34018:96f249dc default
parent child Browse files
Show More
@@ -1,2154 +1,2191 b''
1 1 # revset.py - revision set queries for mercurial
2 2 #
3 3 # Copyright 2010 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import re
11 11
12 12 from .i18n import _
13 13 from . import (
14 14 dagop,
15 15 destutil,
16 16 encoding,
17 17 error,
18 18 hbisect,
19 19 match as matchmod,
20 20 node,
21 21 obsolete as obsmod,
22 22 obsutil,
23 23 pathutil,
24 24 phases,
25 25 registrar,
26 26 repoview,
27 27 revsetlang,
28 28 scmutil,
29 29 smartset,
30 30 util,
31 31 )
32 32
33 33 # helpers for processing parsed tree
34 34 getsymbol = revsetlang.getsymbol
35 35 getstring = revsetlang.getstring
36 36 getinteger = revsetlang.getinteger
37 37 getboolean = revsetlang.getboolean
38 38 getlist = revsetlang.getlist
39 39 getrange = revsetlang.getrange
40 40 getargs = revsetlang.getargs
41 41 getargsdict = revsetlang.getargsdict
42 42
43 # constants used as an argument of match() and matchany()
44 anyorder = revsetlang.anyorder
45 defineorder = revsetlang.defineorder
46 followorder = revsetlang.followorder
47
48 43 baseset = smartset.baseset
49 44 generatorset = smartset.generatorset
50 45 spanset = smartset.spanset
51 46 fullreposet = smartset.fullreposet
52 47
48 # Constants for ordering requirement, used in getset():
49 #
50 # If 'define', any nested functions and operations MAY change the ordering of
51 # the entries in the set (but if changes the ordering, it MUST ALWAYS change
52 # it). If 'follow', any nested functions and operations MUST take the ordering
53 # specified by the first operand to the '&' operator.
54 #
55 # For instance,
56 #
57 # X & (Y | Z)
58 # ^ ^^^^^^^
59 # | follow
60 # define
61 #
62 # will be evaluated as 'or(y(x()), z(x()))', where 'x()' can change the order
63 # of the entries in the set, but 'y()', 'z()' and 'or()' shouldn't.
64 #
65 # 'any' means the order doesn't matter. For instance,
66 #
67 # (X & Y) | ancestors(Z)
68 # ^ ^
69 # any any
70 #
71 # For 'X & Y', 'X' decides order so the order of 'Y' does not matter. For
72 # 'ancestors(Z)', Z's order does not matter since 'ancestors' does not care
73 # about the order of its argument.
74 #
75 # Currently, most revsets do not care about the order, so 'define' is
76 # equivalent to 'follow' for them, and the resulting order is based on the
77 # 'subset' parameter passed down to them:
78 #
79 # m = revset.match(..., order=defineorder)
80 # m(repo, subset)
81 # ^^^^^^
82 # For most revsets, 'define' means using the order this subset provides
83 #
84 # There are a few revsets that always redefine the order if 'define' is
85 # specified: 'sort(X)', 'reverse(X)', 'x:y'.
86 anyorder = 'any' # don't care the order, could be even random-shuffled
87 defineorder = 'define' # ALWAYS redefine, or ALWAYS follow the current order
88 followorder = 'follow' # MUST follow the current order
89
53 90 # helpers
54 91
55 92 def getset(repo, subset, x, order=defineorder):
56 93 if not x:
57 94 raise error.ParseError(_("missing argument"))
58 95 return methods[x[0]](repo, subset, *x[1:], order=order)
59 96
60 97 def _getrevsource(repo, r):
61 98 extra = repo[r].extra()
62 99 for label in ('source', 'transplant_source', 'rebase_source'):
63 100 if label in extra:
64 101 try:
65 102 return repo[extra[label]].rev()
66 103 except error.RepoLookupError:
67 104 pass
68 105 return None
69 106
70 107 # operator methods
71 108
72 109 def stringset(repo, subset, x, order):
73 110 x = scmutil.intrev(repo[x])
74 111 if (x in subset
75 112 or x == node.nullrev and isinstance(subset, fullreposet)):
76 113 return baseset([x])
77 114 return baseset()
78 115
79 116 def rangeset(repo, subset, x, y, order):
80 117 m = getset(repo, fullreposet(repo), x)
81 118 n = getset(repo, fullreposet(repo), y)
82 119
83 120 if not m or not n:
84 121 return baseset()
85 122 return _makerangeset(repo, subset, m.first(), n.last(), order)
86 123
87 124 def rangeall(repo, subset, x, order):
88 125 assert x is None
89 126 return _makerangeset(repo, subset, 0, len(repo) - 1, order)
90 127
91 128 def rangepre(repo, subset, y, order):
92 129 # ':y' can't be rewritten to '0:y' since '0' may be hidden
93 130 n = getset(repo, fullreposet(repo), y)
94 131 if not n:
95 132 return baseset()
96 133 return _makerangeset(repo, subset, 0, n.last(), order)
97 134
98 135 def rangepost(repo, subset, x, order):
99 136 m = getset(repo, fullreposet(repo), x)
100 137 if not m:
101 138 return baseset()
102 139 return _makerangeset(repo, subset, m.first(), len(repo) - 1, order)
103 140
104 141 def _makerangeset(repo, subset, m, n, order):
105 142 if m == n:
106 143 r = baseset([m])
107 144 elif n == node.wdirrev:
108 145 r = spanset(repo, m, len(repo)) + baseset([n])
109 146 elif m == node.wdirrev:
110 147 r = baseset([m]) + spanset(repo, len(repo) - 1, n - 1)
111 148 elif m < n:
112 149 r = spanset(repo, m, n + 1)
113 150 else:
114 151 r = spanset(repo, m, n - 1)
115 152
116 153 if order == defineorder:
117 154 return r & subset
118 155 else:
119 156 # carrying the sorting over when possible would be more efficient
120 157 return subset & r
121 158
122 159 def dagrange(repo, subset, x, y, order):
123 160 r = fullreposet(repo)
124 161 xs = dagop.reachableroots(repo, getset(repo, r, x), getset(repo, r, y),
125 162 includepath=True)
126 163 return subset & xs
127 164
128 165 def andset(repo, subset, x, y, order):
129 166 if order == anyorder:
130 167 yorder = anyorder
131 168 else:
132 169 yorder = followorder
133 170 return getset(repo, getset(repo, subset, x, order), y, yorder)
134 171
135 172 def flipandset(repo, subset, y, x, order):
136 173 # 'flipand(y, x)' is equivalent to 'and(x, y)', but faster when y is small
137 174 if order == anyorder:
138 175 yorder = anyorder
139 176 else:
140 177 yorder = followorder
141 178 return getset(repo, getset(repo, subset, y, yorder), x, order)
142 179
143 180 def differenceset(repo, subset, x, y, order):
144 181 return getset(repo, subset, x, order) - getset(repo, subset, y, anyorder)
145 182
146 183 def _orsetlist(repo, subset, xs, order):
147 184 assert xs
148 185 if len(xs) == 1:
149 186 return getset(repo, subset, xs[0], order)
150 187 p = len(xs) // 2
151 188 a = _orsetlist(repo, subset, xs[:p], order)
152 189 b = _orsetlist(repo, subset, xs[p:], order)
153 190 return a + b
154 191
155 192 def orset(repo, subset, x, order):
156 193 xs = getlist(x)
157 194 if order == followorder:
158 195 # slow path to take the subset order
159 196 return subset & _orsetlist(repo, fullreposet(repo), xs, anyorder)
160 197 else:
161 198 return _orsetlist(repo, subset, xs, order)
162 199
163 200 def notset(repo, subset, x, order):
164 201 return subset - getset(repo, subset, x, anyorder)
165 202
166 203 def relationset(repo, subset, x, y, order):
167 204 raise error.ParseError(_("can't use a relation in this context"))
168 205
169 206 def relsubscriptset(repo, subset, x, y, z, order):
170 207 # this is pretty basic implementation of 'x#y[z]' operator, still
171 208 # experimental so undocumented. see the wiki for further ideas.
172 209 # https://www.mercurial-scm.org/wiki/RevsetOperatorPlan
173 210 rel = getsymbol(y)
174 211 n = getinteger(z, _("relation subscript must be an integer"))
175 212
176 213 # TODO: perhaps this should be a table of relation functions
177 214 if rel in ('g', 'generations'):
178 215 # TODO: support range, rewrite tests, and drop startdepth argument
179 216 # from ancestors() and descendants() predicates
180 217 if n <= 0:
181 218 n = -n
182 219 return _ancestors(repo, subset, x, startdepth=n, stopdepth=n + 1)
183 220 else:
184 221 return _descendants(repo, subset, x, startdepth=n, stopdepth=n + 1)
185 222
186 223 raise error.UnknownIdentifier(rel, ['generations'])
187 224
188 225 def subscriptset(repo, subset, x, y, order):
189 226 raise error.ParseError(_("can't use a subscript in this context"))
190 227
191 228 def listset(repo, subset, *xs, **opts):
192 229 raise error.ParseError(_("can't use a list in this context"),
193 230 hint=_('see hg help "revsets.x or y"'))
194 231
195 232 def keyvaluepair(repo, subset, k, v, order):
196 233 raise error.ParseError(_("can't use a key-value pair in this context"))
197 234
198 235 def func(repo, subset, a, b, order):
199 236 f = getsymbol(a)
200 237 if f in symbols:
201 238 func = symbols[f]
202 239 if getattr(func, '_takeorder', False):
203 240 return func(repo, subset, b, order)
204 241 return func(repo, subset, b)
205 242
206 243 keep = lambda fn: getattr(fn, '__doc__', None) is not None
207 244
208 245 syms = [s for (s, fn) in symbols.items() if keep(fn)]
209 246 raise error.UnknownIdentifier(f, syms)
210 247
211 248 # functions
212 249
213 250 # symbols are callables like:
214 251 # fn(repo, subset, x)
215 252 # with:
216 253 # repo - current repository instance
217 254 # subset - of revisions to be examined
218 255 # x - argument in tree form
219 256 symbols = {}
220 257
221 258 # symbols which can't be used for a DoS attack for any given input
222 259 # (e.g. those which accept regexes as plain strings shouldn't be included)
223 260 # functions that just return a lot of changesets (like all) don't count here
224 261 safesymbols = set()
225 262
226 263 predicate = registrar.revsetpredicate()
227 264
228 265 @predicate('_destupdate')
229 266 def _destupdate(repo, subset, x):
230 267 # experimental revset for update destination
231 268 args = getargsdict(x, 'limit', 'clean')
232 269 return subset & baseset([destutil.destupdate(repo, **args)[0]])
233 270
234 271 @predicate('_destmerge')
235 272 def _destmerge(repo, subset, x):
236 273 # experimental revset for merge destination
237 274 sourceset = None
238 275 if x is not None:
239 276 sourceset = getset(repo, fullreposet(repo), x)
240 277 return subset & baseset([destutil.destmerge(repo, sourceset=sourceset)])
241 278
242 279 @predicate('adds(pattern)', safe=True)
243 280 def adds(repo, subset, x):
244 281 """Changesets that add a file matching pattern.
245 282
246 283 The pattern without explicit kind like ``glob:`` is expected to be
247 284 relative to the current directory and match against a file or a
248 285 directory.
249 286 """
250 287 # i18n: "adds" is a keyword
251 288 pat = getstring(x, _("adds requires a pattern"))
252 289 return checkstatus(repo, subset, pat, 1)
253 290
254 291 @predicate('ancestor(*changeset)', safe=True)
255 292 def ancestor(repo, subset, x):
256 293 """A greatest common ancestor of the changesets.
257 294
258 295 Accepts 0 or more changesets.
259 296 Will return empty list when passed no args.
260 297 Greatest common ancestor of a single changeset is that changeset.
261 298 """
262 299 # i18n: "ancestor" is a keyword
263 300 l = getlist(x)
264 301 rl = fullreposet(repo)
265 302 anc = None
266 303
267 304 # (getset(repo, rl, i) for i in l) generates a list of lists
268 305 for revs in (getset(repo, rl, i) for i in l):
269 306 for r in revs:
270 307 if anc is None:
271 308 anc = repo[r]
272 309 else:
273 310 anc = anc.ancestor(repo[r])
274 311
275 312 if anc is not None and anc.rev() in subset:
276 313 return baseset([anc.rev()])
277 314 return baseset()
278 315
279 316 def _ancestors(repo, subset, x, followfirst=False, startdepth=None,
280 317 stopdepth=None):
281 318 heads = getset(repo, fullreposet(repo), x)
282 319 if not heads:
283 320 return baseset()
284 321 s = dagop.revancestors(repo, heads, followfirst, startdepth, stopdepth)
285 322 return subset & s
286 323
287 324 @predicate('ancestors(set[, depth])', safe=True)
288 325 def ancestors(repo, subset, x):
289 326 """Changesets that are ancestors of changesets in set, including the
290 327 given changesets themselves.
291 328
292 329 If depth is specified, the result only includes changesets up to
293 330 the specified generation.
294 331 """
295 332 # startdepth is for internal use only until we can decide the UI
296 333 args = getargsdict(x, 'ancestors', 'set depth startdepth')
297 334 if 'set' not in args:
298 335 # i18n: "ancestors" is a keyword
299 336 raise error.ParseError(_('ancestors takes at least 1 argument'))
300 337 startdepth = stopdepth = None
301 338 if 'startdepth' in args:
302 339 n = getinteger(args['startdepth'],
303 340 "ancestors expects an integer startdepth")
304 341 if n < 0:
305 342 raise error.ParseError("negative startdepth")
306 343 startdepth = n
307 344 if 'depth' in args:
308 345 # i18n: "ancestors" is a keyword
309 346 n = getinteger(args['depth'], _("ancestors expects an integer depth"))
310 347 if n < 0:
311 348 raise error.ParseError(_("negative depth"))
312 349 stopdepth = n + 1
313 350 return _ancestors(repo, subset, args['set'],
314 351 startdepth=startdepth, stopdepth=stopdepth)
315 352
316 353 @predicate('_firstancestors', safe=True)
317 354 def _firstancestors(repo, subset, x):
318 355 # ``_firstancestors(set)``
319 356 # Like ``ancestors(set)`` but follows only the first parents.
320 357 return _ancestors(repo, subset, x, followfirst=True)
321 358
322 359 def _childrenspec(repo, subset, x, n, order):
323 360 """Changesets that are the Nth child of a changeset
324 361 in set.
325 362 """
326 363 cs = set()
327 364 for r in getset(repo, fullreposet(repo), x):
328 365 for i in range(n):
329 366 c = repo[r].children()
330 367 if len(c) == 0:
331 368 break
332 369 if len(c) > 1:
333 370 raise error.RepoLookupError(
334 371 _("revision in set has more than one child"))
335 372 r = c[0].rev()
336 373 else:
337 374 cs.add(r)
338 375 return subset & cs
339 376
340 377 def ancestorspec(repo, subset, x, n, order):
341 378 """``set~n``
342 379 Changesets that are the Nth ancestor (first parents only) of a changeset
343 380 in set.
344 381 """
345 382 n = getinteger(n, _("~ expects a number"))
346 383 if n < 0:
347 384 # children lookup
348 385 return _childrenspec(repo, subset, x, -n, order)
349 386 ps = set()
350 387 cl = repo.changelog
351 388 for r in getset(repo, fullreposet(repo), x):
352 389 for i in range(n):
353 390 try:
354 391 r = cl.parentrevs(r)[0]
355 392 except error.WdirUnsupported:
356 393 r = repo[r].parents()[0].rev()
357 394 ps.add(r)
358 395 return subset & ps
359 396
360 397 @predicate('author(string)', safe=True)
361 398 def author(repo, subset, x):
362 399 """Alias for ``user(string)``.
363 400 """
364 401 # i18n: "author" is a keyword
365 402 n = getstring(x, _("author requires a string"))
366 403 kind, pattern, matcher = _substringmatcher(n, casesensitive=False)
367 404 return subset.filter(lambda x: matcher(repo[x].user()),
368 405 condrepr=('<user %r>', n))
369 406
370 407 @predicate('bisect(string)', safe=True)
371 408 def bisect(repo, subset, x):
372 409 """Changesets marked in the specified bisect status:
373 410
374 411 - ``good``, ``bad``, ``skip``: csets explicitly marked as good/bad/skip
375 412 - ``goods``, ``bads`` : csets topologically good/bad
376 413 - ``range`` : csets taking part in the bisection
377 414 - ``pruned`` : csets that are goods, bads or skipped
378 415 - ``untested`` : csets whose fate is yet unknown
379 416 - ``ignored`` : csets ignored due to DAG topology
380 417 - ``current`` : the cset currently being bisected
381 418 """
382 419 # i18n: "bisect" is a keyword
383 420 status = getstring(x, _("bisect requires a string")).lower()
384 421 state = set(hbisect.get(repo, status))
385 422 return subset & state
386 423
387 424 # Backward-compatibility
388 425 # - no help entry so that we do not advertise it any more
389 426 @predicate('bisected', safe=True)
390 427 def bisected(repo, subset, x):
391 428 return bisect(repo, subset, x)
392 429
393 430 @predicate('bookmark([name])', safe=True)
394 431 def bookmark(repo, subset, x):
395 432 """The named bookmark or all bookmarks.
396 433
397 434 Pattern matching is supported for `name`. See :hg:`help revisions.patterns`.
398 435 """
399 436 # i18n: "bookmark" is a keyword
400 437 args = getargs(x, 0, 1, _('bookmark takes one or no arguments'))
401 438 if args:
402 439 bm = getstring(args[0],
403 440 # i18n: "bookmark" is a keyword
404 441 _('the argument to bookmark must be a string'))
405 442 kind, pattern, matcher = util.stringmatcher(bm)
406 443 bms = set()
407 444 if kind == 'literal':
408 445 bmrev = repo._bookmarks.get(pattern, None)
409 446 if not bmrev:
410 447 raise error.RepoLookupError(_("bookmark '%s' does not exist")
411 448 % pattern)
412 449 bms.add(repo[bmrev].rev())
413 450 else:
414 451 matchrevs = set()
415 452 for name, bmrev in repo._bookmarks.iteritems():
416 453 if matcher(name):
417 454 matchrevs.add(bmrev)
418 455 if not matchrevs:
419 456 raise error.RepoLookupError(_("no bookmarks exist"
420 457 " that match '%s'") % pattern)
421 458 for bmrev in matchrevs:
422 459 bms.add(repo[bmrev].rev())
423 460 else:
424 461 bms = {repo[r].rev() for r in repo._bookmarks.values()}
425 462 bms -= {node.nullrev}
426 463 return subset & bms
427 464
428 465 @predicate('branch(string or set)', safe=True)
429 466 def branch(repo, subset, x):
430 467 """
431 468 All changesets belonging to the given branch or the branches of the given
432 469 changesets.
433 470
434 471 Pattern matching is supported for `string`. See
435 472 :hg:`help revisions.patterns`.
436 473 """
437 474 getbi = repo.revbranchcache().branchinfo
438 475 def getbranch(r):
439 476 try:
440 477 return getbi(r)[0]
441 478 except error.WdirUnsupported:
442 479 return repo[r].branch()
443 480
444 481 try:
445 482 b = getstring(x, '')
446 483 except error.ParseError:
447 484 # not a string, but another revspec, e.g. tip()
448 485 pass
449 486 else:
450 487 kind, pattern, matcher = util.stringmatcher(b)
451 488 if kind == 'literal':
452 489 # note: falls through to the revspec case if no branch with
453 490 # this name exists and pattern kind is not specified explicitly
454 491 if pattern in repo.branchmap():
455 492 return subset.filter(lambda r: matcher(getbranch(r)),
456 493 condrepr=('<branch %r>', b))
457 494 if b.startswith('literal:'):
458 495 raise error.RepoLookupError(_("branch '%s' does not exist")
459 496 % pattern)
460 497 else:
461 498 return subset.filter(lambda r: matcher(getbranch(r)),
462 499 condrepr=('<branch %r>', b))
463 500
464 501 s = getset(repo, fullreposet(repo), x)
465 502 b = set()
466 503 for r in s:
467 504 b.add(getbranch(r))
468 505 c = s.__contains__
469 506 return subset.filter(lambda r: c(r) or getbranch(r) in b,
470 507 condrepr=lambda: '<branch %r>' % sorted(b))
471 508
472 509 @predicate('bumped()', safe=True)
473 510 def bumped(repo, subset, x):
474 511 msg = ("'bumped()' is deprecated, "
475 512 "use 'phasedivergent()'")
476 513 repo.ui.deprecwarn(msg, '4.4')
477 514
478 515 return phasedivergent(repo, subset, x)
479 516
480 517 @predicate('phasedivergent()', safe=True)
481 518 def phasedivergent(repo, subset, x):
482 519 """Mutable changesets marked as successors of public changesets.
483 520
484 521 Only non-public and non-obsolete changesets can be `phasedivergent`.
485 522 (EXPERIMENTAL)
486 523 """
487 524 # i18n: "phasedivergent" is a keyword
488 525 getargs(x, 0, 0, _("phasedivergent takes no arguments"))
489 526 phasedivergent = obsmod.getrevs(repo, 'phasedivergent')
490 527 return subset & phasedivergent
491 528
492 529 @predicate('bundle()', safe=True)
493 530 def bundle(repo, subset, x):
494 531 """Changesets in the bundle.
495 532
496 533 Bundle must be specified by the -R option."""
497 534
498 535 try:
499 536 bundlerevs = repo.changelog.bundlerevs
500 537 except AttributeError:
501 538 raise error.Abort(_("no bundle provided - specify with -R"))
502 539 return subset & bundlerevs
503 540
504 541 def checkstatus(repo, subset, pat, field):
505 542 hasset = matchmod.patkind(pat) == 'set'
506 543
507 544 mcache = [None]
508 545 def matches(x):
509 546 c = repo[x]
510 547 if not mcache[0] or hasset:
511 548 mcache[0] = matchmod.match(repo.root, repo.getcwd(), [pat], ctx=c)
512 549 m = mcache[0]
513 550 fname = None
514 551 if not m.anypats() and len(m.files()) == 1:
515 552 fname = m.files()[0]
516 553 if fname is not None:
517 554 if fname not in c.files():
518 555 return False
519 556 else:
520 557 for f in c.files():
521 558 if m(f):
522 559 break
523 560 else:
524 561 return False
525 562 files = repo.status(c.p1().node(), c.node())[field]
526 563 if fname is not None:
527 564 if fname in files:
528 565 return True
529 566 else:
530 567 for f in files:
531 568 if m(f):
532 569 return True
533 570
534 571 return subset.filter(matches, condrepr=('<status[%r] %r>', field, pat))
535 572
536 573 def _children(repo, subset, parentset):
537 574 if not parentset:
538 575 return baseset()
539 576 cs = set()
540 577 pr = repo.changelog.parentrevs
541 578 minrev = parentset.min()
542 579 nullrev = node.nullrev
543 580 for r in subset:
544 581 if r <= minrev:
545 582 continue
546 583 p1, p2 = pr(r)
547 584 if p1 in parentset:
548 585 cs.add(r)
549 586 if p2 != nullrev and p2 in parentset:
550 587 cs.add(r)
551 588 return baseset(cs)
552 589
553 590 @predicate('children(set)', safe=True)
554 591 def children(repo, subset, x):
555 592 """Child changesets of changesets in set.
556 593 """
557 594 s = getset(repo, fullreposet(repo), x)
558 595 cs = _children(repo, subset, s)
559 596 return subset & cs
560 597
561 598 @predicate('closed()', safe=True)
562 599 def closed(repo, subset, x):
563 600 """Changeset is closed.
564 601 """
565 602 # i18n: "closed" is a keyword
566 603 getargs(x, 0, 0, _("closed takes no arguments"))
567 604 return subset.filter(lambda r: repo[r].closesbranch(),
568 605 condrepr='<branch closed>')
569 606
570 607 @predicate('contains(pattern)')
571 608 def contains(repo, subset, x):
572 609 """The revision's manifest contains a file matching pattern (but might not
573 610 modify it). See :hg:`help patterns` for information about file patterns.
574 611
575 612 The pattern without explicit kind like ``glob:`` is expected to be
576 613 relative to the current directory and match against a file exactly
577 614 for efficiency.
578 615 """
579 616 # i18n: "contains" is a keyword
580 617 pat = getstring(x, _("contains requires a pattern"))
581 618
582 619 def matches(x):
583 620 if not matchmod.patkind(pat):
584 621 pats = pathutil.canonpath(repo.root, repo.getcwd(), pat)
585 622 if pats in repo[x]:
586 623 return True
587 624 else:
588 625 c = repo[x]
589 626 m = matchmod.match(repo.root, repo.getcwd(), [pat], ctx=c)
590 627 for f in c.manifest():
591 628 if m(f):
592 629 return True
593 630 return False
594 631
595 632 return subset.filter(matches, condrepr=('<contains %r>', pat))
596 633
597 634 @predicate('converted([id])', safe=True)
598 635 def converted(repo, subset, x):
599 636 """Changesets converted from the given identifier in the old repository if
600 637 present, or all converted changesets if no identifier is specified.
601 638 """
602 639
603 640 # There is exactly no chance of resolving the revision, so do a simple
604 641 # string compare and hope for the best
605 642
606 643 rev = None
607 644 # i18n: "converted" is a keyword
608 645 l = getargs(x, 0, 1, _('converted takes one or no arguments'))
609 646 if l:
610 647 # i18n: "converted" is a keyword
611 648 rev = getstring(l[0], _('converted requires a revision'))
612 649
613 650 def _matchvalue(r):
614 651 source = repo[r].extra().get('convert_revision', None)
615 652 return source is not None and (rev is None or source.startswith(rev))
616 653
617 654 return subset.filter(lambda r: _matchvalue(r),
618 655 condrepr=('<converted %r>', rev))
619 656
620 657 @predicate('date(interval)', safe=True)
621 658 def date(repo, subset, x):
622 659 """Changesets within the interval, see :hg:`help dates`.
623 660 """
624 661 # i18n: "date" is a keyword
625 662 ds = getstring(x, _("date requires a string"))
626 663 dm = util.matchdate(ds)
627 664 return subset.filter(lambda x: dm(repo[x].date()[0]),
628 665 condrepr=('<date %r>', ds))
629 666
630 667 @predicate('desc(string)', safe=True)
631 668 def desc(repo, subset, x):
632 669 """Search commit message for string. The match is case-insensitive.
633 670
634 671 Pattern matching is supported for `string`. See
635 672 :hg:`help revisions.patterns`.
636 673 """
637 674 # i18n: "desc" is a keyword
638 675 ds = getstring(x, _("desc requires a string"))
639 676
640 677 kind, pattern, matcher = _substringmatcher(ds, casesensitive=False)
641 678
642 679 return subset.filter(lambda r: matcher(repo[r].description()),
643 680 condrepr=('<desc %r>', ds))
644 681
645 682 def _descendants(repo, subset, x, followfirst=False, startdepth=None,
646 683 stopdepth=None):
647 684 roots = getset(repo, fullreposet(repo), x)
648 685 if not roots:
649 686 return baseset()
650 687 s = dagop.revdescendants(repo, roots, followfirst, startdepth, stopdepth)
651 688 return subset & s
652 689
653 690 @predicate('descendants(set[, depth])', safe=True)
654 691 def descendants(repo, subset, x):
655 692 """Changesets which are descendants of changesets in set, including the
656 693 given changesets themselves.
657 694
658 695 If depth is specified, the result only includes changesets up to
659 696 the specified generation.
660 697 """
661 698 # startdepth is for internal use only until we can decide the UI
662 699 args = getargsdict(x, 'descendants', 'set depth startdepth')
663 700 if 'set' not in args:
664 701 # i18n: "descendants" is a keyword
665 702 raise error.ParseError(_('descendants takes at least 1 argument'))
666 703 startdepth = stopdepth = None
667 704 if 'startdepth' in args:
668 705 n = getinteger(args['startdepth'],
669 706 "descendants expects an integer startdepth")
670 707 if n < 0:
671 708 raise error.ParseError("negative startdepth")
672 709 startdepth = n
673 710 if 'depth' in args:
674 711 # i18n: "descendants" is a keyword
675 712 n = getinteger(args['depth'], _("descendants expects an integer depth"))
676 713 if n < 0:
677 714 raise error.ParseError(_("negative depth"))
678 715 stopdepth = n + 1
679 716 return _descendants(repo, subset, args['set'],
680 717 startdepth=startdepth, stopdepth=stopdepth)
681 718
682 719 @predicate('_firstdescendants', safe=True)
683 720 def _firstdescendants(repo, subset, x):
684 721 # ``_firstdescendants(set)``
685 722 # Like ``descendants(set)`` but follows only the first parents.
686 723 return _descendants(repo, subset, x, followfirst=True)
687 724
688 725 @predicate('destination([set])', safe=True)
689 726 def destination(repo, subset, x):
690 727 """Changesets that were created by a graft, transplant or rebase operation,
691 728 with the given revisions specified as the source. Omitting the optional set
692 729 is the same as passing all().
693 730 """
694 731 if x is not None:
695 732 sources = getset(repo, fullreposet(repo), x)
696 733 else:
697 734 sources = fullreposet(repo)
698 735
699 736 dests = set()
700 737
701 738 # subset contains all of the possible destinations that can be returned, so
702 739 # iterate over them and see if their source(s) were provided in the arg set.
703 740 # Even if the immediate src of r is not in the arg set, src's source (or
704 741 # further back) may be. Scanning back further than the immediate src allows
705 742 # transitive transplants and rebases to yield the same results as transitive
706 743 # grafts.
707 744 for r in subset:
708 745 src = _getrevsource(repo, r)
709 746 lineage = None
710 747
711 748 while src is not None:
712 749 if lineage is None:
713 750 lineage = list()
714 751
715 752 lineage.append(r)
716 753
717 754 # The visited lineage is a match if the current source is in the arg
718 755 # set. Since every candidate dest is visited by way of iterating
719 756 # subset, any dests further back in the lineage will be tested by a
720 757 # different iteration over subset. Likewise, if the src was already
721 758 # selected, the current lineage can be selected without going back
722 759 # further.
723 760 if src in sources or src in dests:
724 761 dests.update(lineage)
725 762 break
726 763
727 764 r = src
728 765 src = _getrevsource(repo, r)
729 766
730 767 return subset.filter(dests.__contains__,
731 768 condrepr=lambda: '<destination %r>' % sorted(dests))
732 769
733 770 @predicate('divergent()', safe=True)
734 771 def divergent(repo, subset, x):
735 772 msg = ("'divergent()' is deprecated, "
736 773 "use 'contentdivergent()'")
737 774 repo.ui.deprecwarn(msg, '4.4')
738 775
739 776 return contentdivergent(repo, subset, x)
740 777
741 778 @predicate('contentdivergent()', safe=True)
742 779 def contentdivergent(repo, subset, x):
743 780 """
744 781 Final successors of changesets with an alternative set of final
745 782 successors. (EXPERIMENTAL)
746 783 """
747 784 # i18n: "contentdivergent" is a keyword
748 785 getargs(x, 0, 0, _("contentdivergent takes no arguments"))
749 786 contentdivergent = obsmod.getrevs(repo, 'contentdivergent')
750 787 return subset & contentdivergent
751 788
752 789 @predicate('extinct()', safe=True)
753 790 def extinct(repo, subset, x):
754 791 """Obsolete changesets with obsolete descendants only.
755 792 """
756 793 # i18n: "extinct" is a keyword
757 794 getargs(x, 0, 0, _("extinct takes no arguments"))
758 795 extincts = obsmod.getrevs(repo, 'extinct')
759 796 return subset & extincts
760 797
761 798 @predicate('extra(label, [value])', safe=True)
762 799 def extra(repo, subset, x):
763 800 """Changesets with the given label in the extra metadata, with the given
764 801 optional value.
765 802
766 803 Pattern matching is supported for `value`. See
767 804 :hg:`help revisions.patterns`.
768 805 """
769 806 args = getargsdict(x, 'extra', 'label value')
770 807 if 'label' not in args:
771 808 # i18n: "extra" is a keyword
772 809 raise error.ParseError(_('extra takes at least 1 argument'))
773 810 # i18n: "extra" is a keyword
774 811 label = getstring(args['label'], _('first argument to extra must be '
775 812 'a string'))
776 813 value = None
777 814
778 815 if 'value' in args:
779 816 # i18n: "extra" is a keyword
780 817 value = getstring(args['value'], _('second argument to extra must be '
781 818 'a string'))
782 819 kind, value, matcher = util.stringmatcher(value)
783 820
784 821 def _matchvalue(r):
785 822 extra = repo[r].extra()
786 823 return label in extra and (value is None or matcher(extra[label]))
787 824
788 825 return subset.filter(lambda r: _matchvalue(r),
789 826 condrepr=('<extra[%r] %r>', label, value))
790 827
791 828 @predicate('filelog(pattern)', safe=True)
792 829 def filelog(repo, subset, x):
793 830 """Changesets connected to the specified filelog.
794 831
795 832 For performance reasons, visits only revisions mentioned in the file-level
796 833 filelog, rather than filtering through all changesets (much faster, but
797 834 doesn't include deletes or duplicate changes). For a slower, more accurate
798 835 result, use ``file()``.
799 836
800 837 The pattern without explicit kind like ``glob:`` is expected to be
801 838 relative to the current directory and match against a file exactly
802 839 for efficiency.
803 840
804 841 If some linkrev points to revisions filtered by the current repoview, we'll
805 842 work around it to return a non-filtered value.
806 843 """
807 844
808 845 # i18n: "filelog" is a keyword
809 846 pat = getstring(x, _("filelog requires a pattern"))
810 847 s = set()
811 848 cl = repo.changelog
812 849
813 850 if not matchmod.patkind(pat):
814 851 f = pathutil.canonpath(repo.root, repo.getcwd(), pat)
815 852 files = [f]
816 853 else:
817 854 m = matchmod.match(repo.root, repo.getcwd(), [pat], ctx=repo[None])
818 855 files = (f for f in repo[None] if m(f))
819 856
820 857 for f in files:
821 858 fl = repo.file(f)
822 859 known = {}
823 860 scanpos = 0
824 861 for fr in list(fl):
825 862 fn = fl.node(fr)
826 863 if fn in known:
827 864 s.add(known[fn])
828 865 continue
829 866
830 867 lr = fl.linkrev(fr)
831 868 if lr in cl:
832 869 s.add(lr)
833 870 elif scanpos is not None:
834 871 # lowest matching changeset is filtered, scan further
835 872 # ahead in changelog
836 873 start = max(lr, scanpos) + 1
837 874 scanpos = None
838 875 for r in cl.revs(start):
839 876 # minimize parsing of non-matching entries
840 877 if f in cl.revision(r) and f in cl.readfiles(r):
841 878 try:
842 879 # try to use manifest delta fastpath
843 880 n = repo[r].filenode(f)
844 881 if n not in known:
845 882 if n == fn:
846 883 s.add(r)
847 884 scanpos = r
848 885 break
849 886 else:
850 887 known[n] = r
851 888 except error.ManifestLookupError:
852 889 # deletion in changelog
853 890 continue
854 891
855 892 return subset & s
856 893
857 894 @predicate('first(set, [n])', safe=True, takeorder=True)
858 895 def first(repo, subset, x, order):
859 896 """An alias for limit().
860 897 """
861 898 return limit(repo, subset, x, order)
862 899
863 900 def _follow(repo, subset, x, name, followfirst=False):
864 901 l = getargs(x, 0, 2, _("%s takes no arguments or a pattern "
865 902 "and an optional revset") % name)
866 903 c = repo['.']
867 904 if l:
868 905 x = getstring(l[0], _("%s expected a pattern") % name)
869 906 rev = None
870 907 if len(l) >= 2:
871 908 revs = getset(repo, fullreposet(repo), l[1])
872 909 if len(revs) != 1:
873 910 raise error.RepoLookupError(
874 911 _("%s expected one starting revision") % name)
875 912 rev = revs.last()
876 913 c = repo[rev]
877 914 matcher = matchmod.match(repo.root, repo.getcwd(), [x],
878 915 ctx=repo[rev], default='path')
879 916
880 917 files = c.manifest().walk(matcher)
881 918
882 919 s = set()
883 920 for fname in files:
884 921 fctx = c[fname]
885 922 s = s.union(set(c.rev() for c in fctx.ancestors(followfirst)))
886 923 # include the revision responsible for the most recent version
887 924 s.add(fctx.introrev())
888 925 else:
889 926 s = dagop.revancestors(repo, baseset([c.rev()]), followfirst)
890 927
891 928 return subset & s
892 929
893 930 @predicate('follow([pattern[, startrev]])', safe=True)
894 931 def follow(repo, subset, x):
895 932 """
896 933 An alias for ``::.`` (ancestors of the working directory's first parent).
897 934 If pattern is specified, the histories of files matching given
898 935 pattern in the revision given by startrev are followed, including copies.
899 936 """
900 937 return _follow(repo, subset, x, 'follow')
901 938
902 939 @predicate('_followfirst', safe=True)
903 940 def _followfirst(repo, subset, x):
904 941 # ``followfirst([pattern[, startrev]])``
905 942 # Like ``follow([pattern[, startrev]])`` but follows only the first parent
906 943 # of every revisions or files revisions.
907 944 return _follow(repo, subset, x, '_followfirst', followfirst=True)
908 945
909 946 @predicate('followlines(file, fromline:toline[, startrev=., descend=False])',
910 947 safe=True)
911 948 def followlines(repo, subset, x):
912 949 """Changesets modifying `file` in line range ('fromline', 'toline').
913 950
914 951 Line range corresponds to 'file' content at 'startrev' and should hence be
915 952 consistent with file size. If startrev is not specified, working directory's
916 953 parent is used.
917 954
918 955 By default, ancestors of 'startrev' are returned. If 'descend' is True,
919 956 descendants of 'startrev' are returned though renames are (currently) not
920 957 followed in this direction.
921 958 """
922 959 args = getargsdict(x, 'followlines', 'file *lines startrev descend')
923 960 if len(args['lines']) != 1:
924 961 raise error.ParseError(_("followlines requires a line range"))
925 962
926 963 rev = '.'
927 964 if 'startrev' in args:
928 965 revs = getset(repo, fullreposet(repo), args['startrev'])
929 966 if len(revs) != 1:
930 967 raise error.ParseError(
931 968 # i18n: "followlines" is a keyword
932 969 _("followlines expects exactly one revision"))
933 970 rev = revs.last()
934 971
935 972 pat = getstring(args['file'], _("followlines requires a pattern"))
936 973 if not matchmod.patkind(pat):
937 974 fname = pathutil.canonpath(repo.root, repo.getcwd(), pat)
938 975 else:
939 976 m = matchmod.match(repo.root, repo.getcwd(), [pat], ctx=repo[rev])
940 977 files = [f for f in repo[rev] if m(f)]
941 978 if len(files) != 1:
942 979 # i18n: "followlines" is a keyword
943 980 raise error.ParseError(_("followlines expects exactly one file"))
944 981 fname = files[0]
945 982
946 983 # i18n: "followlines" is a keyword
947 984 lr = getrange(args['lines'][0], _("followlines expects a line range"))
948 985 fromline, toline = [getinteger(a, _("line range bounds must be integers"))
949 986 for a in lr]
950 987 fromline, toline = util.processlinerange(fromline, toline)
951 988
952 989 fctx = repo[rev].filectx(fname)
953 990 descend = False
954 991 if 'descend' in args:
955 992 descend = getboolean(args['descend'],
956 993 # i18n: "descend" is a keyword
957 994 _("descend argument must be a boolean"))
958 995 if descend:
959 996 rs = generatorset(
960 997 (c.rev() for c, _linerange
961 998 in dagop.blockdescendants(fctx, fromline, toline)),
962 999 iterasc=True)
963 1000 else:
964 1001 rs = generatorset(
965 1002 (c.rev() for c, _linerange
966 1003 in dagop.blockancestors(fctx, fromline, toline)),
967 1004 iterasc=False)
968 1005 return subset & rs
969 1006
970 1007 @predicate('all()', safe=True)
971 1008 def getall(repo, subset, x):
972 1009 """All changesets, the same as ``0:tip``.
973 1010 """
974 1011 # i18n: "all" is a keyword
975 1012 getargs(x, 0, 0, _("all takes no arguments"))
976 1013 return subset & spanset(repo) # drop "null" if any
977 1014
978 1015 @predicate('grep(regex)')
979 1016 def grep(repo, subset, x):
980 1017 """Like ``keyword(string)`` but accepts a regex. Use ``grep(r'...')``
981 1018 to ensure special escape characters are handled correctly. Unlike
982 1019 ``keyword(string)``, the match is case-sensitive.
983 1020 """
984 1021 try:
985 1022 # i18n: "grep" is a keyword
986 1023 gr = re.compile(getstring(x, _("grep requires a string")))
987 1024 except re.error as e:
988 1025 raise error.ParseError(_('invalid match pattern: %s') % e)
989 1026
990 1027 def matches(x):
991 1028 c = repo[x]
992 1029 for e in c.files() + [c.user(), c.description()]:
993 1030 if gr.search(e):
994 1031 return True
995 1032 return False
996 1033
997 1034 return subset.filter(matches, condrepr=('<grep %r>', gr.pattern))
998 1035
999 1036 @predicate('_matchfiles', safe=True)
1000 1037 def _matchfiles(repo, subset, x):
1001 1038 # _matchfiles takes a revset list of prefixed arguments:
1002 1039 #
1003 1040 # [p:foo, i:bar, x:baz]
1004 1041 #
1005 1042 # builds a match object from them and filters subset. Allowed
1006 1043 # prefixes are 'p:' for regular patterns, 'i:' for include
1007 1044 # patterns and 'x:' for exclude patterns. Use 'r:' prefix to pass
1008 1045 # a revision identifier, or the empty string to reference the
1009 1046 # working directory, from which the match object is
1010 1047 # initialized. Use 'd:' to set the default matching mode, default
1011 1048 # to 'glob'. At most one 'r:' and 'd:' argument can be passed.
1012 1049
1013 1050 l = getargs(x, 1, -1, "_matchfiles requires at least one argument")
1014 1051 pats, inc, exc = [], [], []
1015 1052 rev, default = None, None
1016 1053 for arg in l:
1017 1054 s = getstring(arg, "_matchfiles requires string arguments")
1018 1055 prefix, value = s[:2], s[2:]
1019 1056 if prefix == 'p:':
1020 1057 pats.append(value)
1021 1058 elif prefix == 'i:':
1022 1059 inc.append(value)
1023 1060 elif prefix == 'x:':
1024 1061 exc.append(value)
1025 1062 elif prefix == 'r:':
1026 1063 if rev is not None:
1027 1064 raise error.ParseError('_matchfiles expected at most one '
1028 1065 'revision')
1029 1066 if value != '': # empty means working directory; leave rev as None
1030 1067 rev = value
1031 1068 elif prefix == 'd:':
1032 1069 if default is not None:
1033 1070 raise error.ParseError('_matchfiles expected at most one '
1034 1071 'default mode')
1035 1072 default = value
1036 1073 else:
1037 1074 raise error.ParseError('invalid _matchfiles prefix: %s' % prefix)
1038 1075 if not default:
1039 1076 default = 'glob'
1040 1077
1041 1078 m = matchmod.match(repo.root, repo.getcwd(), pats, include=inc,
1042 1079 exclude=exc, ctx=repo[rev], default=default)
1043 1080
1044 1081 # This directly read the changelog data as creating changectx for all
1045 1082 # revisions is quite expensive.
1046 1083 getfiles = repo.changelog.readfiles
1047 1084 wdirrev = node.wdirrev
1048 1085 def matches(x):
1049 1086 if x == wdirrev:
1050 1087 files = repo[x].files()
1051 1088 else:
1052 1089 files = getfiles(x)
1053 1090 for f in files:
1054 1091 if m(f):
1055 1092 return True
1056 1093 return False
1057 1094
1058 1095 return subset.filter(matches,
1059 1096 condrepr=('<matchfiles patterns=%r, include=%r '
1060 1097 'exclude=%r, default=%r, rev=%r>',
1061 1098 pats, inc, exc, default, rev))
1062 1099
1063 1100 @predicate('file(pattern)', safe=True)
1064 1101 def hasfile(repo, subset, x):
1065 1102 """Changesets affecting files matched by pattern.
1066 1103
1067 1104 For a faster but less accurate result, consider using ``filelog()``
1068 1105 instead.
1069 1106
1070 1107 This predicate uses ``glob:`` as the default kind of pattern.
1071 1108 """
1072 1109 # i18n: "file" is a keyword
1073 1110 pat = getstring(x, _("file requires a pattern"))
1074 1111 return _matchfiles(repo, subset, ('string', 'p:' + pat))
1075 1112
1076 1113 @predicate('head()', safe=True)
1077 1114 def head(repo, subset, x):
1078 1115 """Changeset is a named branch head.
1079 1116 """
1080 1117 # i18n: "head" is a keyword
1081 1118 getargs(x, 0, 0, _("head takes no arguments"))
1082 1119 hs = set()
1083 1120 cl = repo.changelog
1084 1121 for ls in repo.branchmap().itervalues():
1085 1122 hs.update(cl.rev(h) for h in ls)
1086 1123 return subset & baseset(hs)
1087 1124
1088 1125 @predicate('heads(set)', safe=True)
1089 1126 def heads(repo, subset, x):
1090 1127 """Members of set with no children in set.
1091 1128 """
1092 1129 s = getset(repo, subset, x)
1093 1130 ps = parents(repo, subset, x)
1094 1131 return s - ps
1095 1132
1096 1133 @predicate('hidden()', safe=True)
1097 1134 def hidden(repo, subset, x):
1098 1135 """Hidden changesets.
1099 1136 """
1100 1137 # i18n: "hidden" is a keyword
1101 1138 getargs(x, 0, 0, _("hidden takes no arguments"))
1102 1139 hiddenrevs = repoview.filterrevs(repo, 'visible')
1103 1140 return subset & hiddenrevs
1104 1141
1105 1142 @predicate('keyword(string)', safe=True)
1106 1143 def keyword(repo, subset, x):
1107 1144 """Search commit message, user name, and names of changed files for
1108 1145 string. The match is case-insensitive.
1109 1146
1110 1147 For a regular expression or case sensitive search of these fields, use
1111 1148 ``grep(regex)``.
1112 1149 """
1113 1150 # i18n: "keyword" is a keyword
1114 1151 kw = encoding.lower(getstring(x, _("keyword requires a string")))
1115 1152
1116 1153 def matches(r):
1117 1154 c = repo[r]
1118 1155 return any(kw in encoding.lower(t)
1119 1156 for t in c.files() + [c.user(), c.description()])
1120 1157
1121 1158 return subset.filter(matches, condrepr=('<keyword %r>', kw))
1122 1159
1123 1160 @predicate('limit(set[, n[, offset]])', safe=True, takeorder=True)
1124 1161 def limit(repo, subset, x, order):
1125 1162 """First n members of set, defaulting to 1, starting from offset.
1126 1163 """
1127 1164 args = getargsdict(x, 'limit', 'set n offset')
1128 1165 if 'set' not in args:
1129 1166 # i18n: "limit" is a keyword
1130 1167 raise error.ParseError(_("limit requires one to three arguments"))
1131 1168 # i18n: "limit" is a keyword
1132 1169 lim = getinteger(args.get('n'), _("limit expects a number"), default=1)
1133 1170 if lim < 0:
1134 1171 raise error.ParseError(_("negative number to select"))
1135 1172 # i18n: "limit" is a keyword
1136 1173 ofs = getinteger(args.get('offset'), _("limit expects a number"), default=0)
1137 1174 if ofs < 0:
1138 1175 raise error.ParseError(_("negative offset"))
1139 1176 os = getset(repo, fullreposet(repo), args['set'])
1140 1177 ls = os.slice(ofs, ofs + lim)
1141 1178 if order == followorder and lim > 1:
1142 1179 return subset & ls
1143 1180 return ls & subset
1144 1181
1145 1182 @predicate('last(set, [n])', safe=True, takeorder=True)
1146 1183 def last(repo, subset, x, order):
1147 1184 """Last n members of set, defaulting to 1.
1148 1185 """
1149 1186 # i18n: "last" is a keyword
1150 1187 l = getargs(x, 1, 2, _("last requires one or two arguments"))
1151 1188 lim = 1
1152 1189 if len(l) == 2:
1153 1190 # i18n: "last" is a keyword
1154 1191 lim = getinteger(l[1], _("last expects a number"))
1155 1192 if lim < 0:
1156 1193 raise error.ParseError(_("negative number to select"))
1157 1194 os = getset(repo, fullreposet(repo), l[0])
1158 1195 os.reverse()
1159 1196 ls = os.slice(0, lim)
1160 1197 if order == followorder and lim > 1:
1161 1198 return subset & ls
1162 1199 ls.reverse()
1163 1200 return ls & subset
1164 1201
1165 1202 @predicate('max(set)', safe=True)
1166 1203 def maxrev(repo, subset, x):
1167 1204 """Changeset with highest revision number in set.
1168 1205 """
1169 1206 os = getset(repo, fullreposet(repo), x)
1170 1207 try:
1171 1208 m = os.max()
1172 1209 if m in subset:
1173 1210 return baseset([m], datarepr=('<max %r, %r>', subset, os))
1174 1211 except ValueError:
1175 1212 # os.max() throws a ValueError when the collection is empty.
1176 1213 # Same as python's max().
1177 1214 pass
1178 1215 return baseset(datarepr=('<max %r, %r>', subset, os))
1179 1216
1180 1217 @predicate('merge()', safe=True)
1181 1218 def merge(repo, subset, x):
1182 1219 """Changeset is a merge changeset.
1183 1220 """
1184 1221 # i18n: "merge" is a keyword
1185 1222 getargs(x, 0, 0, _("merge takes no arguments"))
1186 1223 cl = repo.changelog
1187 1224 return subset.filter(lambda r: cl.parentrevs(r)[1] != -1,
1188 1225 condrepr='<merge>')
1189 1226
1190 1227 @predicate('branchpoint()', safe=True)
1191 1228 def branchpoint(repo, subset, x):
1192 1229 """Changesets with more than one child.
1193 1230 """
1194 1231 # i18n: "branchpoint" is a keyword
1195 1232 getargs(x, 0, 0, _("branchpoint takes no arguments"))
1196 1233 cl = repo.changelog
1197 1234 if not subset:
1198 1235 return baseset()
1199 1236 # XXX this should be 'parentset.min()' assuming 'parentset' is a smartset
1200 1237 # (and if it is not, it should.)
1201 1238 baserev = min(subset)
1202 1239 parentscount = [0]*(len(repo) - baserev)
1203 1240 for r in cl.revs(start=baserev + 1):
1204 1241 for p in cl.parentrevs(r):
1205 1242 if p >= baserev:
1206 1243 parentscount[p - baserev] += 1
1207 1244 return subset.filter(lambda r: parentscount[r - baserev] > 1,
1208 1245 condrepr='<branchpoint>')
1209 1246
1210 1247 @predicate('min(set)', safe=True)
1211 1248 def minrev(repo, subset, x):
1212 1249 """Changeset with lowest revision number in set.
1213 1250 """
1214 1251 os = getset(repo, fullreposet(repo), x)
1215 1252 try:
1216 1253 m = os.min()
1217 1254 if m in subset:
1218 1255 return baseset([m], datarepr=('<min %r, %r>', subset, os))
1219 1256 except ValueError:
1220 1257 # os.min() throws a ValueError when the collection is empty.
1221 1258 # Same as python's min().
1222 1259 pass
1223 1260 return baseset(datarepr=('<min %r, %r>', subset, os))
1224 1261
1225 1262 @predicate('modifies(pattern)', safe=True)
1226 1263 def modifies(repo, subset, x):
1227 1264 """Changesets modifying files matched by pattern.
1228 1265
1229 1266 The pattern without explicit kind like ``glob:`` is expected to be
1230 1267 relative to the current directory and match against a file or a
1231 1268 directory.
1232 1269 """
1233 1270 # i18n: "modifies" is a keyword
1234 1271 pat = getstring(x, _("modifies requires a pattern"))
1235 1272 return checkstatus(repo, subset, pat, 0)
1236 1273
1237 1274 @predicate('named(namespace)')
1238 1275 def named(repo, subset, x):
1239 1276 """The changesets in a given namespace.
1240 1277
1241 1278 Pattern matching is supported for `namespace`. See
1242 1279 :hg:`help revisions.patterns`.
1243 1280 """
1244 1281 # i18n: "named" is a keyword
1245 1282 args = getargs(x, 1, 1, _('named requires a namespace argument'))
1246 1283
1247 1284 ns = getstring(args[0],
1248 1285 # i18n: "named" is a keyword
1249 1286 _('the argument to named must be a string'))
1250 1287 kind, pattern, matcher = util.stringmatcher(ns)
1251 1288 namespaces = set()
1252 1289 if kind == 'literal':
1253 1290 if pattern not in repo.names:
1254 1291 raise error.RepoLookupError(_("namespace '%s' does not exist")
1255 1292 % ns)
1256 1293 namespaces.add(repo.names[pattern])
1257 1294 else:
1258 1295 for name, ns in repo.names.iteritems():
1259 1296 if matcher(name):
1260 1297 namespaces.add(ns)
1261 1298 if not namespaces:
1262 1299 raise error.RepoLookupError(_("no namespace exists"
1263 1300 " that match '%s'") % pattern)
1264 1301
1265 1302 names = set()
1266 1303 for ns in namespaces:
1267 1304 for name in ns.listnames(repo):
1268 1305 if name not in ns.deprecated:
1269 1306 names.update(repo[n].rev() for n in ns.nodes(repo, name))
1270 1307
1271 1308 names -= {node.nullrev}
1272 1309 return subset & names
1273 1310
1274 1311 @predicate('id(string)', safe=True)
1275 1312 def node_(repo, subset, x):
1276 1313 """Revision non-ambiguously specified by the given hex string prefix.
1277 1314 """
1278 1315 # i18n: "id" is a keyword
1279 1316 l = getargs(x, 1, 1, _("id requires one argument"))
1280 1317 # i18n: "id" is a keyword
1281 1318 n = getstring(l[0], _("id requires a string"))
1282 1319 if len(n) == 40:
1283 1320 try:
1284 1321 rn = repo.changelog.rev(node.bin(n))
1285 1322 except error.WdirUnsupported:
1286 1323 rn = node.wdirrev
1287 1324 except (LookupError, TypeError):
1288 1325 rn = None
1289 1326 else:
1290 1327 rn = None
1291 1328 try:
1292 1329 pm = repo.changelog._partialmatch(n)
1293 1330 if pm is not None:
1294 1331 rn = repo.changelog.rev(pm)
1295 1332 except error.WdirUnsupported:
1296 1333 rn = node.wdirrev
1297 1334
1298 1335 if rn is None:
1299 1336 return baseset()
1300 1337 result = baseset([rn])
1301 1338 return result & subset
1302 1339
1303 1340 @predicate('obsolete()', safe=True)
1304 1341 def obsolete(repo, subset, x):
1305 1342 """Mutable changeset with a newer version."""
1306 1343 # i18n: "obsolete" is a keyword
1307 1344 getargs(x, 0, 0, _("obsolete takes no arguments"))
1308 1345 obsoletes = obsmod.getrevs(repo, 'obsolete')
1309 1346 return subset & obsoletes
1310 1347
1311 1348 @predicate('only(set, [set])', safe=True)
1312 1349 def only(repo, subset, x):
1313 1350 """Changesets that are ancestors of the first set that are not ancestors
1314 1351 of any other head in the repo. If a second set is specified, the result
1315 1352 is ancestors of the first set that are not ancestors of the second set
1316 1353 (i.e. ::<set1> - ::<set2>).
1317 1354 """
1318 1355 cl = repo.changelog
1319 1356 # i18n: "only" is a keyword
1320 1357 args = getargs(x, 1, 2, _('only takes one or two arguments'))
1321 1358 include = getset(repo, fullreposet(repo), args[0])
1322 1359 if len(args) == 1:
1323 1360 if not include:
1324 1361 return baseset()
1325 1362
1326 1363 descendants = set(dagop.revdescendants(repo, include, False))
1327 1364 exclude = [rev for rev in cl.headrevs()
1328 1365 if not rev in descendants and not rev in include]
1329 1366 else:
1330 1367 exclude = getset(repo, fullreposet(repo), args[1])
1331 1368
1332 1369 results = set(cl.findmissingrevs(common=exclude, heads=include))
1333 1370 # XXX we should turn this into a baseset instead of a set, smartset may do
1334 1371 # some optimizations from the fact this is a baseset.
1335 1372 return subset & results
1336 1373
1337 1374 @predicate('origin([set])', safe=True)
1338 1375 def origin(repo, subset, x):
1339 1376 """
1340 1377 Changesets that were specified as a source for the grafts, transplants or
1341 1378 rebases that created the given revisions. Omitting the optional set is the
1342 1379 same as passing all(). If a changeset created by these operations is itself
1343 1380 specified as a source for one of these operations, only the source changeset
1344 1381 for the first operation is selected.
1345 1382 """
1346 1383 if x is not None:
1347 1384 dests = getset(repo, fullreposet(repo), x)
1348 1385 else:
1349 1386 dests = fullreposet(repo)
1350 1387
1351 1388 def _firstsrc(rev):
1352 1389 src = _getrevsource(repo, rev)
1353 1390 if src is None:
1354 1391 return None
1355 1392
1356 1393 while True:
1357 1394 prev = _getrevsource(repo, src)
1358 1395
1359 1396 if prev is None:
1360 1397 return src
1361 1398 src = prev
1362 1399
1363 1400 o = {_firstsrc(r) for r in dests}
1364 1401 o -= {None}
1365 1402 # XXX we should turn this into a baseset instead of a set, smartset may do
1366 1403 # some optimizations from the fact this is a baseset.
1367 1404 return subset & o
1368 1405
1369 1406 @predicate('outgoing([path])', safe=False)
1370 1407 def outgoing(repo, subset, x):
1371 1408 """Changesets not found in the specified destination repository, or the
1372 1409 default push location.
1373 1410 """
1374 1411 # Avoid cycles.
1375 1412 from . import (
1376 1413 discovery,
1377 1414 hg,
1378 1415 )
1379 1416 # i18n: "outgoing" is a keyword
1380 1417 l = getargs(x, 0, 1, _("outgoing takes one or no arguments"))
1381 1418 # i18n: "outgoing" is a keyword
1382 1419 dest = l and getstring(l[0], _("outgoing requires a repository path")) or ''
1383 1420 dest = repo.ui.expandpath(dest or 'default-push', dest or 'default')
1384 1421 dest, branches = hg.parseurl(dest)
1385 1422 revs, checkout = hg.addbranchrevs(repo, repo, branches, [])
1386 1423 if revs:
1387 1424 revs = [repo.lookup(rev) for rev in revs]
1388 1425 other = hg.peer(repo, {}, dest)
1389 1426 repo.ui.pushbuffer()
1390 1427 outgoing = discovery.findcommonoutgoing(repo, other, onlyheads=revs)
1391 1428 repo.ui.popbuffer()
1392 1429 cl = repo.changelog
1393 1430 o = {cl.rev(r) for r in outgoing.missing}
1394 1431 return subset & o
1395 1432
1396 1433 @predicate('p1([set])', safe=True)
1397 1434 def p1(repo, subset, x):
1398 1435 """First parent of changesets in set, or the working directory.
1399 1436 """
1400 1437 if x is None:
1401 1438 p = repo[x].p1().rev()
1402 1439 if p >= 0:
1403 1440 return subset & baseset([p])
1404 1441 return baseset()
1405 1442
1406 1443 ps = set()
1407 1444 cl = repo.changelog
1408 1445 for r in getset(repo, fullreposet(repo), x):
1409 1446 try:
1410 1447 ps.add(cl.parentrevs(r)[0])
1411 1448 except error.WdirUnsupported:
1412 1449 ps.add(repo[r].parents()[0].rev())
1413 1450 ps -= {node.nullrev}
1414 1451 # XXX we should turn this into a baseset instead of a set, smartset may do
1415 1452 # some optimizations from the fact this is a baseset.
1416 1453 return subset & ps
1417 1454
1418 1455 @predicate('p2([set])', safe=True)
1419 1456 def p2(repo, subset, x):
1420 1457 """Second parent of changesets in set, or the working directory.
1421 1458 """
1422 1459 if x is None:
1423 1460 ps = repo[x].parents()
1424 1461 try:
1425 1462 p = ps[1].rev()
1426 1463 if p >= 0:
1427 1464 return subset & baseset([p])
1428 1465 return baseset()
1429 1466 except IndexError:
1430 1467 return baseset()
1431 1468
1432 1469 ps = set()
1433 1470 cl = repo.changelog
1434 1471 for r in getset(repo, fullreposet(repo), x):
1435 1472 try:
1436 1473 ps.add(cl.parentrevs(r)[1])
1437 1474 except error.WdirUnsupported:
1438 1475 parents = repo[r].parents()
1439 1476 if len(parents) == 2:
1440 1477 ps.add(parents[1])
1441 1478 ps -= {node.nullrev}
1442 1479 # XXX we should turn this into a baseset instead of a set, smartset may do
1443 1480 # some optimizations from the fact this is a baseset.
1444 1481 return subset & ps
1445 1482
1446 1483 def parentpost(repo, subset, x, order):
1447 1484 return p1(repo, subset, x)
1448 1485
1449 1486 @predicate('parents([set])', safe=True)
1450 1487 def parents(repo, subset, x):
1451 1488 """
1452 1489 The set of all parents for all changesets in set, or the working directory.
1453 1490 """
1454 1491 if x is None:
1455 1492 ps = set(p.rev() for p in repo[x].parents())
1456 1493 else:
1457 1494 ps = set()
1458 1495 cl = repo.changelog
1459 1496 up = ps.update
1460 1497 parentrevs = cl.parentrevs
1461 1498 for r in getset(repo, fullreposet(repo), x):
1462 1499 try:
1463 1500 up(parentrevs(r))
1464 1501 except error.WdirUnsupported:
1465 1502 up(p.rev() for p in repo[r].parents())
1466 1503 ps -= {node.nullrev}
1467 1504 return subset & ps
1468 1505
1469 1506 def _phase(repo, subset, *targets):
1470 1507 """helper to select all rev in <targets> phases"""
1471 1508 s = repo._phasecache.getrevset(repo, targets)
1472 1509 return subset & s
1473 1510
1474 1511 @predicate('draft()', safe=True)
1475 1512 def draft(repo, subset, x):
1476 1513 """Changeset in draft phase."""
1477 1514 # i18n: "draft" is a keyword
1478 1515 getargs(x, 0, 0, _("draft takes no arguments"))
1479 1516 target = phases.draft
1480 1517 return _phase(repo, subset, target)
1481 1518
1482 1519 @predicate('secret()', safe=True)
1483 1520 def secret(repo, subset, x):
1484 1521 """Changeset in secret phase."""
1485 1522 # i18n: "secret" is a keyword
1486 1523 getargs(x, 0, 0, _("secret takes no arguments"))
1487 1524 target = phases.secret
1488 1525 return _phase(repo, subset, target)
1489 1526
1490 1527 def parentspec(repo, subset, x, n, order):
1491 1528 """``set^0``
1492 1529 The set.
1493 1530 ``set^1`` (or ``set^``), ``set^2``
1494 1531 First or second parent, respectively, of all changesets in set.
1495 1532 """
1496 1533 try:
1497 1534 n = int(n[1])
1498 1535 if n not in (0, 1, 2):
1499 1536 raise ValueError
1500 1537 except (TypeError, ValueError):
1501 1538 raise error.ParseError(_("^ expects a number 0, 1, or 2"))
1502 1539 ps = set()
1503 1540 cl = repo.changelog
1504 1541 for r in getset(repo, fullreposet(repo), x):
1505 1542 if n == 0:
1506 1543 ps.add(r)
1507 1544 elif n == 1:
1508 1545 try:
1509 1546 ps.add(cl.parentrevs(r)[0])
1510 1547 except error.WdirUnsupported:
1511 1548 ps.add(repo[r].parents()[0].rev())
1512 1549 else:
1513 1550 try:
1514 1551 parents = cl.parentrevs(r)
1515 1552 if parents[1] != node.nullrev:
1516 1553 ps.add(parents[1])
1517 1554 except error.WdirUnsupported:
1518 1555 parents = repo[r].parents()
1519 1556 if len(parents) == 2:
1520 1557 ps.add(parents[1].rev())
1521 1558 return subset & ps
1522 1559
1523 1560 @predicate('present(set)', safe=True, takeorder=True)
1524 1561 def present(repo, subset, x, order):
1525 1562 """An empty set, if any revision in set isn't found; otherwise,
1526 1563 all revisions in set.
1527 1564
1528 1565 If any of specified revisions is not present in the local repository,
1529 1566 the query is normally aborted. But this predicate allows the query
1530 1567 to continue even in such cases.
1531 1568 """
1532 1569 try:
1533 1570 return getset(repo, subset, x, order)
1534 1571 except error.RepoLookupError:
1535 1572 return baseset()
1536 1573
1537 1574 # for internal use
1538 1575 @predicate('_notpublic', safe=True)
1539 1576 def _notpublic(repo, subset, x):
1540 1577 getargs(x, 0, 0, "_notpublic takes no arguments")
1541 1578 return _phase(repo, subset, phases.draft, phases.secret)
1542 1579
1543 1580 @predicate('public()', safe=True)
1544 1581 def public(repo, subset, x):
1545 1582 """Changeset in public phase."""
1546 1583 # i18n: "public" is a keyword
1547 1584 getargs(x, 0, 0, _("public takes no arguments"))
1548 1585 phase = repo._phasecache.phase
1549 1586 target = phases.public
1550 1587 condition = lambda r: phase(repo, r) == target
1551 1588 return subset.filter(condition, condrepr=('<phase %r>', target),
1552 1589 cache=False)
1553 1590
1554 1591 @predicate('remote([id [,path]])', safe=False)
1555 1592 def remote(repo, subset, x):
1556 1593 """Local revision that corresponds to the given identifier in a
1557 1594 remote repository, if present. Here, the '.' identifier is a
1558 1595 synonym for the current local branch.
1559 1596 """
1560 1597
1561 1598 from . import hg # avoid start-up nasties
1562 1599 # i18n: "remote" is a keyword
1563 1600 l = getargs(x, 0, 2, _("remote takes zero, one, or two arguments"))
1564 1601
1565 1602 q = '.'
1566 1603 if len(l) > 0:
1567 1604 # i18n: "remote" is a keyword
1568 1605 q = getstring(l[0], _("remote requires a string id"))
1569 1606 if q == '.':
1570 1607 q = repo['.'].branch()
1571 1608
1572 1609 dest = ''
1573 1610 if len(l) > 1:
1574 1611 # i18n: "remote" is a keyword
1575 1612 dest = getstring(l[1], _("remote requires a repository path"))
1576 1613 dest = repo.ui.expandpath(dest or 'default')
1577 1614 dest, branches = hg.parseurl(dest)
1578 1615 revs, checkout = hg.addbranchrevs(repo, repo, branches, [])
1579 1616 if revs:
1580 1617 revs = [repo.lookup(rev) for rev in revs]
1581 1618 other = hg.peer(repo, {}, dest)
1582 1619 n = other.lookup(q)
1583 1620 if n in repo:
1584 1621 r = repo[n].rev()
1585 1622 if r in subset:
1586 1623 return baseset([r])
1587 1624 return baseset()
1588 1625
1589 1626 @predicate('removes(pattern)', safe=True)
1590 1627 def removes(repo, subset, x):
1591 1628 """Changesets which remove files matching pattern.
1592 1629
1593 1630 The pattern without explicit kind like ``glob:`` is expected to be
1594 1631 relative to the current directory and match against a file or a
1595 1632 directory.
1596 1633 """
1597 1634 # i18n: "removes" is a keyword
1598 1635 pat = getstring(x, _("removes requires a pattern"))
1599 1636 return checkstatus(repo, subset, pat, 2)
1600 1637
1601 1638 @predicate('rev(number)', safe=True)
1602 1639 def rev(repo, subset, x):
1603 1640 """Revision with the given numeric identifier.
1604 1641 """
1605 1642 # i18n: "rev" is a keyword
1606 1643 l = getargs(x, 1, 1, _("rev requires one argument"))
1607 1644 try:
1608 1645 # i18n: "rev" is a keyword
1609 1646 l = int(getstring(l[0], _("rev requires a number")))
1610 1647 except (TypeError, ValueError):
1611 1648 # i18n: "rev" is a keyword
1612 1649 raise error.ParseError(_("rev expects a number"))
1613 1650 if l not in repo.changelog and l not in (node.nullrev, node.wdirrev):
1614 1651 return baseset()
1615 1652 return subset & baseset([l])
1616 1653
1617 1654 @predicate('matching(revision [, field])', safe=True)
1618 1655 def matching(repo, subset, x):
1619 1656 """Changesets in which a given set of fields match the set of fields in the
1620 1657 selected revision or set.
1621 1658
1622 1659 To match more than one field pass the list of fields to match separated
1623 1660 by spaces (e.g. ``author description``).
1624 1661
1625 1662 Valid fields are most regular revision fields and some special fields.
1626 1663
1627 1664 Regular revision fields are ``description``, ``author``, ``branch``,
1628 1665 ``date``, ``files``, ``phase``, ``parents``, ``substate``, ``user``
1629 1666 and ``diff``.
1630 1667 Note that ``author`` and ``user`` are synonyms. ``diff`` refers to the
1631 1668 contents of the revision. Two revisions matching their ``diff`` will
1632 1669 also match their ``files``.
1633 1670
1634 1671 Special fields are ``summary`` and ``metadata``:
1635 1672 ``summary`` matches the first line of the description.
1636 1673 ``metadata`` is equivalent to matching ``description user date``
1637 1674 (i.e. it matches the main metadata fields).
1638 1675
1639 1676 ``metadata`` is the default field which is used when no fields are
1640 1677 specified. You can match more than one field at a time.
1641 1678 """
1642 1679 # i18n: "matching" is a keyword
1643 1680 l = getargs(x, 1, 2, _("matching takes 1 or 2 arguments"))
1644 1681
1645 1682 revs = getset(repo, fullreposet(repo), l[0])
1646 1683
1647 1684 fieldlist = ['metadata']
1648 1685 if len(l) > 1:
1649 1686 fieldlist = getstring(l[1],
1650 1687 # i18n: "matching" is a keyword
1651 1688 _("matching requires a string "
1652 1689 "as its second argument")).split()
1653 1690
1654 1691 # Make sure that there are no repeated fields,
1655 1692 # expand the 'special' 'metadata' field type
1656 1693 # and check the 'files' whenever we check the 'diff'
1657 1694 fields = []
1658 1695 for field in fieldlist:
1659 1696 if field == 'metadata':
1660 1697 fields += ['user', 'description', 'date']
1661 1698 elif field == 'diff':
1662 1699 # a revision matching the diff must also match the files
1663 1700 # since matching the diff is very costly, make sure to
1664 1701 # also match the files first
1665 1702 fields += ['files', 'diff']
1666 1703 else:
1667 1704 if field == 'author':
1668 1705 field = 'user'
1669 1706 fields.append(field)
1670 1707 fields = set(fields)
1671 1708 if 'summary' in fields and 'description' in fields:
1672 1709 # If a revision matches its description it also matches its summary
1673 1710 fields.discard('summary')
1674 1711
1675 1712 # We may want to match more than one field
1676 1713 # Not all fields take the same amount of time to be matched
1677 1714 # Sort the selected fields in order of increasing matching cost
1678 1715 fieldorder = ['phase', 'parents', 'user', 'date', 'branch', 'summary',
1679 1716 'files', 'description', 'substate', 'diff']
1680 1717 def fieldkeyfunc(f):
1681 1718 try:
1682 1719 return fieldorder.index(f)
1683 1720 except ValueError:
1684 1721 # assume an unknown field is very costly
1685 1722 return len(fieldorder)
1686 1723 fields = list(fields)
1687 1724 fields.sort(key=fieldkeyfunc)
1688 1725
1689 1726 # Each field will be matched with its own "getfield" function
1690 1727 # which will be added to the getfieldfuncs array of functions
1691 1728 getfieldfuncs = []
1692 1729 _funcs = {
1693 1730 'user': lambda r: repo[r].user(),
1694 1731 'branch': lambda r: repo[r].branch(),
1695 1732 'date': lambda r: repo[r].date(),
1696 1733 'description': lambda r: repo[r].description(),
1697 1734 'files': lambda r: repo[r].files(),
1698 1735 'parents': lambda r: repo[r].parents(),
1699 1736 'phase': lambda r: repo[r].phase(),
1700 1737 'substate': lambda r: repo[r].substate,
1701 1738 'summary': lambda r: repo[r].description().splitlines()[0],
1702 1739 'diff': lambda r: list(repo[r].diff(git=True),)
1703 1740 }
1704 1741 for info in fields:
1705 1742 getfield = _funcs.get(info, None)
1706 1743 if getfield is None:
1707 1744 raise error.ParseError(
1708 1745 # i18n: "matching" is a keyword
1709 1746 _("unexpected field name passed to matching: %s") % info)
1710 1747 getfieldfuncs.append(getfield)
1711 1748 # convert the getfield array of functions into a "getinfo" function
1712 1749 # which returns an array of field values (or a single value if there
1713 1750 # is only one field to match)
1714 1751 getinfo = lambda r: [f(r) for f in getfieldfuncs]
1715 1752
1716 1753 def matches(x):
1717 1754 for rev in revs:
1718 1755 target = getinfo(rev)
1719 1756 match = True
1720 1757 for n, f in enumerate(getfieldfuncs):
1721 1758 if target[n] != f(x):
1722 1759 match = False
1723 1760 if match:
1724 1761 return True
1725 1762 return False
1726 1763
1727 1764 return subset.filter(matches, condrepr=('<matching%r %r>', fields, revs))
1728 1765
1729 1766 @predicate('reverse(set)', safe=True, takeorder=True)
1730 1767 def reverse(repo, subset, x, order):
1731 1768 """Reverse order of set.
1732 1769 """
1733 1770 l = getset(repo, subset, x, order)
1734 1771 if order == defineorder:
1735 1772 l.reverse()
1736 1773 return l
1737 1774
1738 1775 @predicate('roots(set)', safe=True)
1739 1776 def roots(repo, subset, x):
1740 1777 """Changesets in set with no parent changeset in set.
1741 1778 """
1742 1779 s = getset(repo, fullreposet(repo), x)
1743 1780 parents = repo.changelog.parentrevs
1744 1781 def filter(r):
1745 1782 for p in parents(r):
1746 1783 if 0 <= p and p in s:
1747 1784 return False
1748 1785 return True
1749 1786 return subset & s.filter(filter, condrepr='<roots>')
1750 1787
1751 1788 _sortkeyfuncs = {
1752 1789 'rev': lambda c: c.rev(),
1753 1790 'branch': lambda c: c.branch(),
1754 1791 'desc': lambda c: c.description(),
1755 1792 'user': lambda c: c.user(),
1756 1793 'author': lambda c: c.user(),
1757 1794 'date': lambda c: c.date()[0],
1758 1795 }
1759 1796
1760 1797 def _getsortargs(x):
1761 1798 """Parse sort options into (set, [(key, reverse)], opts)"""
1762 1799 args = getargsdict(x, 'sort', 'set keys topo.firstbranch')
1763 1800 if 'set' not in args:
1764 1801 # i18n: "sort" is a keyword
1765 1802 raise error.ParseError(_('sort requires one or two arguments'))
1766 1803 keys = "rev"
1767 1804 if 'keys' in args:
1768 1805 # i18n: "sort" is a keyword
1769 1806 keys = getstring(args['keys'], _("sort spec must be a string"))
1770 1807
1771 1808 keyflags = []
1772 1809 for k in keys.split():
1773 1810 fk = k
1774 1811 reverse = (k[0] == '-')
1775 1812 if reverse:
1776 1813 k = k[1:]
1777 1814 if k not in _sortkeyfuncs and k != 'topo':
1778 1815 raise error.ParseError(_("unknown sort key %r") % fk)
1779 1816 keyflags.append((k, reverse))
1780 1817
1781 1818 if len(keyflags) > 1 and any(k == 'topo' for k, reverse in keyflags):
1782 1819 # i18n: "topo" is a keyword
1783 1820 raise error.ParseError(_('topo sort order cannot be combined '
1784 1821 'with other sort keys'))
1785 1822
1786 1823 opts = {}
1787 1824 if 'topo.firstbranch' in args:
1788 1825 if any(k == 'topo' for k, reverse in keyflags):
1789 1826 opts['topo.firstbranch'] = args['topo.firstbranch']
1790 1827 else:
1791 1828 # i18n: "topo" and "topo.firstbranch" are keywords
1792 1829 raise error.ParseError(_('topo.firstbranch can only be used '
1793 1830 'when using the topo sort key'))
1794 1831
1795 1832 return args['set'], keyflags, opts
1796 1833
1797 1834 @predicate('sort(set[, [-]key... [, ...]])', safe=True, takeorder=True)
1798 1835 def sort(repo, subset, x, order):
1799 1836 """Sort set by keys. The default sort order is ascending, specify a key
1800 1837 as ``-key`` to sort in descending order.
1801 1838
1802 1839 The keys can be:
1803 1840
1804 1841 - ``rev`` for the revision number,
1805 1842 - ``branch`` for the branch name,
1806 1843 - ``desc`` for the commit message (description),
1807 1844 - ``user`` for user name (``author`` can be used as an alias),
1808 1845 - ``date`` for the commit date
1809 1846 - ``topo`` for a reverse topographical sort
1810 1847
1811 1848 The ``topo`` sort order cannot be combined with other sort keys. This sort
1812 1849 takes one optional argument, ``topo.firstbranch``, which takes a revset that
1813 1850 specifies what topographical branches to prioritize in the sort.
1814 1851
1815 1852 """
1816 1853 s, keyflags, opts = _getsortargs(x)
1817 1854 revs = getset(repo, subset, s, order)
1818 1855
1819 1856 if not keyflags or order != defineorder:
1820 1857 return revs
1821 1858 if len(keyflags) == 1 and keyflags[0][0] == "rev":
1822 1859 revs.sort(reverse=keyflags[0][1])
1823 1860 return revs
1824 1861 elif keyflags[0][0] == "topo":
1825 1862 firstbranch = ()
1826 1863 if 'topo.firstbranch' in opts:
1827 1864 firstbranch = getset(repo, subset, opts['topo.firstbranch'])
1828 1865 revs = baseset(dagop.toposort(revs, repo.changelog.parentrevs,
1829 1866 firstbranch),
1830 1867 istopo=True)
1831 1868 if keyflags[0][1]:
1832 1869 revs.reverse()
1833 1870 return revs
1834 1871
1835 1872 # sort() is guaranteed to be stable
1836 1873 ctxs = [repo[r] for r in revs]
1837 1874 for k, reverse in reversed(keyflags):
1838 1875 ctxs.sort(key=_sortkeyfuncs[k], reverse=reverse)
1839 1876 return baseset([c.rev() for c in ctxs])
1840 1877
1841 1878 @predicate('subrepo([pattern])')
1842 1879 def subrepo(repo, subset, x):
1843 1880 """Changesets that add, modify or remove the given subrepo. If no subrepo
1844 1881 pattern is named, any subrepo changes are returned.
1845 1882 """
1846 1883 # i18n: "subrepo" is a keyword
1847 1884 args = getargs(x, 0, 1, _('subrepo takes at most one argument'))
1848 1885 pat = None
1849 1886 if len(args) != 0:
1850 1887 pat = getstring(args[0], _("subrepo requires a pattern"))
1851 1888
1852 1889 m = matchmod.exact(repo.root, repo.root, ['.hgsubstate'])
1853 1890
1854 1891 def submatches(names):
1855 1892 k, p, m = util.stringmatcher(pat)
1856 1893 for name in names:
1857 1894 if m(name):
1858 1895 yield name
1859 1896
1860 1897 def matches(x):
1861 1898 c = repo[x]
1862 1899 s = repo.status(c.p1().node(), c.node(), match=m)
1863 1900
1864 1901 if pat is None:
1865 1902 return s.added or s.modified or s.removed
1866 1903
1867 1904 if s.added:
1868 1905 return any(submatches(c.substate.keys()))
1869 1906
1870 1907 if s.modified:
1871 1908 subs = set(c.p1().substate.keys())
1872 1909 subs.update(c.substate.keys())
1873 1910
1874 1911 for path in submatches(subs):
1875 1912 if c.p1().substate.get(path) != c.substate.get(path):
1876 1913 return True
1877 1914
1878 1915 if s.removed:
1879 1916 return any(submatches(c.p1().substate.keys()))
1880 1917
1881 1918 return False
1882 1919
1883 1920 return subset.filter(matches, condrepr=('<subrepo %r>', pat))
1884 1921
1885 1922 def _mapbynodefunc(repo, s, f):
1886 1923 """(repo, smartset, [node] -> [node]) -> smartset
1887 1924
1888 1925 Helper method to map a smartset to another smartset given a function only
1889 1926 talking about nodes. Handles converting between rev numbers and nodes, and
1890 1927 filtering.
1891 1928 """
1892 1929 cl = repo.unfiltered().changelog
1893 1930 torev = cl.rev
1894 1931 tonode = cl.node
1895 1932 nodemap = cl.nodemap
1896 1933 result = set(torev(n) for n in f(tonode(r) for r in s) if n in nodemap)
1897 1934 return smartset.baseset(result - repo.changelog.filteredrevs)
1898 1935
1899 1936 @predicate('successors(set)', safe=True)
1900 1937 def successors(repo, subset, x):
1901 1938 """All successors for set, including the given set themselves"""
1902 1939 s = getset(repo, fullreposet(repo), x)
1903 1940 f = lambda nodes: obsutil.allsuccessors(repo.obsstore, nodes)
1904 1941 d = _mapbynodefunc(repo, s, f)
1905 1942 return subset & d
1906 1943
1907 1944 def _substringmatcher(pattern, casesensitive=True):
1908 1945 kind, pattern, matcher = util.stringmatcher(pattern,
1909 1946 casesensitive=casesensitive)
1910 1947 if kind == 'literal':
1911 1948 if not casesensitive:
1912 1949 pattern = encoding.lower(pattern)
1913 1950 matcher = lambda s: pattern in encoding.lower(s)
1914 1951 else:
1915 1952 matcher = lambda s: pattern in s
1916 1953 return kind, pattern, matcher
1917 1954
1918 1955 @predicate('tag([name])', safe=True)
1919 1956 def tag(repo, subset, x):
1920 1957 """The specified tag by name, or all tagged revisions if no name is given.
1921 1958
1922 1959 Pattern matching is supported for `name`. See
1923 1960 :hg:`help revisions.patterns`.
1924 1961 """
1925 1962 # i18n: "tag" is a keyword
1926 1963 args = getargs(x, 0, 1, _("tag takes one or no arguments"))
1927 1964 cl = repo.changelog
1928 1965 if args:
1929 1966 pattern = getstring(args[0],
1930 1967 # i18n: "tag" is a keyword
1931 1968 _('the argument to tag must be a string'))
1932 1969 kind, pattern, matcher = util.stringmatcher(pattern)
1933 1970 if kind == 'literal':
1934 1971 # avoid resolving all tags
1935 1972 tn = repo._tagscache.tags.get(pattern, None)
1936 1973 if tn is None:
1937 1974 raise error.RepoLookupError(_("tag '%s' does not exist")
1938 1975 % pattern)
1939 1976 s = {repo[tn].rev()}
1940 1977 else:
1941 1978 s = {cl.rev(n) for t, n in repo.tagslist() if matcher(t)}
1942 1979 else:
1943 1980 s = {cl.rev(n) for t, n in repo.tagslist() if t != 'tip'}
1944 1981 return subset & s
1945 1982
1946 1983 @predicate('tagged', safe=True)
1947 1984 def tagged(repo, subset, x):
1948 1985 return tag(repo, subset, x)
1949 1986
1950 1987 @predicate('unstable()', safe=True)
1951 1988 def unstable(repo, subset, x):
1952 1989 msg = ("'unstable()' is deprecated, "
1953 1990 "use 'orphan()'")
1954 1991 repo.ui.deprecwarn(msg, '4.4')
1955 1992
1956 1993 return orphan(repo, subset, x)
1957 1994
1958 1995 @predicate('orphan()', safe=True)
1959 1996 def orphan(repo, subset, x):
1960 1997 """Non-obsolete changesets with obsolete ancestors. (EXPERIMENTAL)
1961 1998 """
1962 1999 # i18n: "orphan" is a keyword
1963 2000 getargs(x, 0, 0, _("orphan takes no arguments"))
1964 2001 orphan = obsmod.getrevs(repo, 'orphan')
1965 2002 return subset & orphan
1966 2003
1967 2004
1968 2005 @predicate('user(string)', safe=True)
1969 2006 def user(repo, subset, x):
1970 2007 """User name contains string. The match is case-insensitive.
1971 2008
1972 2009 Pattern matching is supported for `string`. See
1973 2010 :hg:`help revisions.patterns`.
1974 2011 """
1975 2012 return author(repo, subset, x)
1976 2013
1977 2014 @predicate('wdir()', safe=True)
1978 2015 def wdir(repo, subset, x):
1979 2016 """Working directory. (EXPERIMENTAL)"""
1980 2017 # i18n: "wdir" is a keyword
1981 2018 getargs(x, 0, 0, _("wdir takes no arguments"))
1982 2019 if node.wdirrev in subset or isinstance(subset, fullreposet):
1983 2020 return baseset([node.wdirrev])
1984 2021 return baseset()
1985 2022
1986 2023 def _orderedlist(repo, subset, x):
1987 2024 s = getstring(x, "internal error")
1988 2025 if not s:
1989 2026 return baseset()
1990 2027 # remove duplicates here. it's difficult for caller to deduplicate sets
1991 2028 # because different symbols can point to the same rev.
1992 2029 cl = repo.changelog
1993 2030 ls = []
1994 2031 seen = set()
1995 2032 for t in s.split('\0'):
1996 2033 try:
1997 2034 # fast path for integer revision
1998 2035 r = int(t)
1999 2036 if str(r) != t or r not in cl:
2000 2037 raise ValueError
2001 2038 revs = [r]
2002 2039 except ValueError:
2003 2040 revs = stringset(repo, subset, t, defineorder)
2004 2041
2005 2042 for r in revs:
2006 2043 if r in seen:
2007 2044 continue
2008 2045 if (r in subset
2009 2046 or r == node.nullrev and isinstance(subset, fullreposet)):
2010 2047 ls.append(r)
2011 2048 seen.add(r)
2012 2049 return baseset(ls)
2013 2050
2014 2051 # for internal use
2015 2052 @predicate('_list', safe=True, takeorder=True)
2016 2053 def _list(repo, subset, x, order):
2017 2054 if order == followorder:
2018 2055 # slow path to take the subset order
2019 2056 return subset & _orderedlist(repo, fullreposet(repo), x)
2020 2057 else:
2021 2058 return _orderedlist(repo, subset, x)
2022 2059
2023 2060 def _orderedintlist(repo, subset, x):
2024 2061 s = getstring(x, "internal error")
2025 2062 if not s:
2026 2063 return baseset()
2027 2064 ls = [int(r) for r in s.split('\0')]
2028 2065 s = subset
2029 2066 return baseset([r for r in ls if r in s])
2030 2067
2031 2068 # for internal use
2032 2069 @predicate('_intlist', safe=True, takeorder=True)
2033 2070 def _intlist(repo, subset, x, order):
2034 2071 if order == followorder:
2035 2072 # slow path to take the subset order
2036 2073 return subset & _orderedintlist(repo, fullreposet(repo), x)
2037 2074 else:
2038 2075 return _orderedintlist(repo, subset, x)
2039 2076
2040 2077 def _orderedhexlist(repo, subset, x):
2041 2078 s = getstring(x, "internal error")
2042 2079 if not s:
2043 2080 return baseset()
2044 2081 cl = repo.changelog
2045 2082 ls = [cl.rev(node.bin(r)) for r in s.split('\0')]
2046 2083 s = subset
2047 2084 return baseset([r for r in ls if r in s])
2048 2085
2049 2086 # for internal use
2050 2087 @predicate('_hexlist', safe=True, takeorder=True)
2051 2088 def _hexlist(repo, subset, x, order):
2052 2089 if order == followorder:
2053 2090 # slow path to take the subset order
2054 2091 return subset & _orderedhexlist(repo, fullreposet(repo), x)
2055 2092 else:
2056 2093 return _orderedhexlist(repo, subset, x)
2057 2094
2058 2095 methods = {
2059 2096 "range": rangeset,
2060 2097 "rangeall": rangeall,
2061 2098 "rangepre": rangepre,
2062 2099 "rangepost": rangepost,
2063 2100 "dagrange": dagrange,
2064 2101 "string": stringset,
2065 2102 "symbol": stringset,
2066 2103 "and": andset,
2067 2104 "flipand": flipandset,
2068 2105 "or": orset,
2069 2106 "not": notset,
2070 2107 "difference": differenceset,
2071 2108 "relation": relationset,
2072 2109 "relsubscript": relsubscriptset,
2073 2110 "subscript": subscriptset,
2074 2111 "list": listset,
2075 2112 "keyvalue": keyvaluepair,
2076 2113 "func": func,
2077 2114 "ancestor": ancestorspec,
2078 2115 "parent": parentspec,
2079 2116 "parentpost": parentpost,
2080 2117 }
2081 2118
2082 2119 def posttreebuilthook(tree, repo):
2083 2120 # hook for extensions to execute code on the optimized tree
2084 2121 pass
2085 2122
2086 2123 def match(ui, spec, repo=None, order=defineorder):
2087 2124 """Create a matcher for a single revision spec
2088 2125
2089 2126 If order=followorder, a matcher takes the ordering specified by the input
2090 2127 set.
2091 2128 """
2092 2129 return matchany(ui, [spec], repo=repo, order=order)
2093 2130
2094 2131 def matchany(ui, specs, repo=None, order=defineorder, localalias=None):
2095 2132 """Create a matcher that will include any revisions matching one of the
2096 2133 given specs
2097 2134
2098 2135 If order=followorder, a matcher takes the ordering specified by the input
2099 2136 set.
2100 2137
2101 2138 If localalias is not None, it is a dict {name: definitionstring}. It takes
2102 2139 precedence over [revsetalias] config section.
2103 2140 """
2104 2141 if not specs:
2105 2142 def mfunc(repo, subset=None):
2106 2143 return baseset()
2107 2144 return mfunc
2108 2145 if not all(specs):
2109 2146 raise error.ParseError(_("empty query"))
2110 2147 lookup = None
2111 2148 if repo:
2112 2149 lookup = repo.__contains__
2113 2150 if len(specs) == 1:
2114 2151 tree = revsetlang.parse(specs[0], lookup)
2115 2152 else:
2116 2153 tree = ('or',
2117 2154 ('list',) + tuple(revsetlang.parse(s, lookup) for s in specs))
2118 2155
2119 2156 aliases = []
2120 2157 warn = None
2121 2158 if ui:
2122 2159 aliases.extend(ui.configitems('revsetalias'))
2123 2160 warn = ui.warn
2124 2161 if localalias:
2125 2162 aliases.extend(localalias.items())
2126 2163 if aliases:
2127 2164 tree = revsetlang.expandaliases(tree, aliases, warn=warn)
2128 2165 tree = revsetlang.foldconcat(tree)
2129 2166 tree = revsetlang.analyze(tree)
2130 2167 tree = revsetlang.optimize(tree)
2131 2168 posttreebuilthook(tree, repo)
2132 2169 return makematcher(tree, order)
2133 2170
2134 2171 def makematcher(tree, order=defineorder):
2135 2172 """Create a matcher from an evaluatable tree"""
2136 2173 def mfunc(repo, subset=None):
2137 2174 if subset is None:
2138 2175 subset = fullreposet(repo)
2139 2176 return getset(repo, subset, tree, order)
2140 2177 return mfunc
2141 2178
2142 2179 def loadpredicate(ui, extname, registrarobj):
2143 2180 """Load revset predicates from specified registrarobj
2144 2181 """
2145 2182 for name, func in registrarobj._table.iteritems():
2146 2183 symbols[name] = func
2147 2184 if func._safe:
2148 2185 safesymbols.add(name)
2149 2186
2150 2187 # load built-in predicates explicitly to setup safesymbols
2151 2188 loadpredicate(None, None, predicate)
2152 2189
2153 2190 # tell hggettext to extract docstrings from these functions:
2154 2191 i18nfunctions = symbols.values()
@@ -1,713 +1,671 b''
1 1 # revsetlang.py - parser, tokenizer and utility for revision set language
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 string
11 11
12 12 from .i18n import _
13 13 from . import (
14 14 error,
15 15 node,
16 16 parser,
17 17 pycompat,
18 18 util,
19 19 )
20 20
21 21 elements = {
22 22 # token-type: binding-strength, primary, prefix, infix, suffix
23 23 "(": (21, None, ("group", 1, ")"), ("func", 1, ")"), None),
24 24 "[": (21, None, None, ("subscript", 1, "]"), None),
25 25 "#": (21, None, None, ("relation", 21), None),
26 26 "##": (20, None, None, ("_concat", 20), None),
27 27 "~": (18, None, None, ("ancestor", 18), None),
28 28 "^": (18, None, None, ("parent", 18), "parentpost"),
29 29 "-": (5, None, ("negate", 19), ("minus", 5), None),
30 30 "::": (17, None, ("dagrangepre", 17), ("dagrange", 17), "dagrangepost"),
31 31 "..": (17, None, ("dagrangepre", 17), ("dagrange", 17), "dagrangepost"),
32 32 ":": (15, "rangeall", ("rangepre", 15), ("range", 15), "rangepost"),
33 33 "not": (10, None, ("not", 10), None, None),
34 34 "!": (10, None, ("not", 10), None, None),
35 35 "and": (5, None, None, ("and", 5), None),
36 36 "&": (5, None, None, ("and", 5), None),
37 37 "%": (5, None, None, ("only", 5), "onlypost"),
38 38 "or": (4, None, None, ("or", 4), None),
39 39 "|": (4, None, None, ("or", 4), None),
40 40 "+": (4, None, None, ("or", 4), None),
41 41 "=": (3, None, None, ("keyvalue", 3), None),
42 42 ",": (2, None, None, ("list", 2), None),
43 43 ")": (0, None, None, None, None),
44 44 "]": (0, None, None, None, None),
45 45 "symbol": (0, "symbol", None, None, None),
46 46 "string": (0, "string", None, None, None),
47 47 "end": (0, None, None, None, None),
48 48 }
49 49
50 50 keywords = {'and', 'or', 'not'}
51 51
52 52 _quoteletters = {'"', "'"}
53 53 _simpleopletters = set(pycompat.iterbytestr("()[]#:=,-|&+!~^%"))
54 54
55 55 # default set of valid characters for the initial letter of symbols
56 56 _syminitletters = set(pycompat.iterbytestr(
57 57 string.ascii_letters.encode('ascii') +
58 58 string.digits.encode('ascii') +
59 59 '._@')) | set(map(pycompat.bytechr, xrange(128, 256)))
60 60
61 61 # default set of valid characters for non-initial letters of symbols
62 62 _symletters = _syminitletters | set(pycompat.iterbytestr('-/'))
63 63
64 64 def tokenize(program, lookup=None, syminitletters=None, symletters=None):
65 65 '''
66 66 Parse a revset statement into a stream of tokens
67 67
68 68 ``syminitletters`` is the set of valid characters for the initial
69 69 letter of symbols.
70 70
71 71 By default, character ``c`` is recognized as valid for initial
72 72 letter of symbols, if ``c.isalnum() or c in '._@' or ord(c) > 127``.
73 73
74 74 ``symletters`` is the set of valid characters for non-initial
75 75 letters of symbols.
76 76
77 77 By default, character ``c`` is recognized as valid for non-initial
78 78 letters of symbols, if ``c.isalnum() or c in '-._/@' or ord(c) > 127``.
79 79
80 80 Check that @ is a valid unquoted token character (issue3686):
81 81 >>> list(tokenize("@::"))
82 82 [('symbol', '@', 0), ('::', None, 1), ('end', None, 3)]
83 83
84 84 '''
85 85 program = pycompat.bytestr(program)
86 86 if syminitletters is None:
87 87 syminitletters = _syminitletters
88 88 if symletters is None:
89 89 symletters = _symletters
90 90
91 91 if program and lookup:
92 92 # attempt to parse old-style ranges first to deal with
93 93 # things like old-tag which contain query metacharacters
94 94 parts = program.split(':', 1)
95 95 if all(lookup(sym) for sym in parts if sym):
96 96 if parts[0]:
97 97 yield ('symbol', parts[0], 0)
98 98 if len(parts) > 1:
99 99 s = len(parts[0])
100 100 yield (':', None, s)
101 101 if parts[1]:
102 102 yield ('symbol', parts[1], s + 1)
103 103 yield ('end', None, len(program))
104 104 return
105 105
106 106 pos, l = 0, len(program)
107 107 while pos < l:
108 108 c = program[pos]
109 109 if c.isspace(): # skip inter-token whitespace
110 110 pass
111 111 elif c == ':' and program[pos:pos + 2] == '::': # look ahead carefully
112 112 yield ('::', None, pos)
113 113 pos += 1 # skip ahead
114 114 elif c == '.' and program[pos:pos + 2] == '..': # look ahead carefully
115 115 yield ('..', None, pos)
116 116 pos += 1 # skip ahead
117 117 elif c == '#' and program[pos:pos + 2] == '##': # look ahead carefully
118 118 yield ('##', None, pos)
119 119 pos += 1 # skip ahead
120 120 elif c in _simpleopletters: # handle simple operators
121 121 yield (c, None, pos)
122 122 elif (c in _quoteletters or c == 'r' and
123 123 program[pos:pos + 2] in ("r'", 'r"')): # handle quoted strings
124 124 if c == 'r':
125 125 pos += 1
126 126 c = program[pos]
127 127 decode = lambda x: x
128 128 else:
129 129 decode = parser.unescapestr
130 130 pos += 1
131 131 s = pos
132 132 while pos < l: # find closing quote
133 133 d = program[pos]
134 134 if d == '\\': # skip over escaped characters
135 135 pos += 2
136 136 continue
137 137 if d == c:
138 138 yield ('string', decode(program[s:pos]), s)
139 139 break
140 140 pos += 1
141 141 else:
142 142 raise error.ParseError(_("unterminated string"), s)
143 143 # gather up a symbol/keyword
144 144 elif c in syminitletters:
145 145 s = pos
146 146 pos += 1
147 147 while pos < l: # find end of symbol
148 148 d = program[pos]
149 149 if d not in symletters:
150 150 break
151 151 if d == '.' and program[pos - 1] == '.': # special case for ..
152 152 pos -= 1
153 153 break
154 154 pos += 1
155 155 sym = program[s:pos]
156 156 if sym in keywords: # operator keywords
157 157 yield (sym, None, s)
158 158 elif '-' in sym:
159 159 # some jerk gave us foo-bar-baz, try to check if it's a symbol
160 160 if lookup and lookup(sym):
161 161 # looks like a real symbol
162 162 yield ('symbol', sym, s)
163 163 else:
164 164 # looks like an expression
165 165 parts = sym.split('-')
166 166 for p in parts[:-1]:
167 167 if p: # possible consecutive -
168 168 yield ('symbol', p, s)
169 169 s += len(p)
170 170 yield ('-', None, pos)
171 171 s += 1
172 172 if parts[-1]: # possible trailing -
173 173 yield ('symbol', parts[-1], s)
174 174 else:
175 175 yield ('symbol', sym, s)
176 176 pos -= 1
177 177 else:
178 178 raise error.ParseError(_("syntax error in revset '%s'") %
179 179 program, pos)
180 180 pos += 1
181 181 yield ('end', None, pos)
182 182
183 183 # helpers
184 184
185 185 _notset = object()
186 186
187 187 def getsymbol(x):
188 188 if x and x[0] == 'symbol':
189 189 return x[1]
190 190 raise error.ParseError(_('not a symbol'))
191 191
192 192 def getstring(x, err):
193 193 if x and (x[0] == 'string' or x[0] == 'symbol'):
194 194 return x[1]
195 195 raise error.ParseError(err)
196 196
197 197 def getinteger(x, err, default=_notset):
198 198 if not x and default is not _notset:
199 199 return default
200 200 try:
201 201 return int(getstring(x, err))
202 202 except ValueError:
203 203 raise error.ParseError(err)
204 204
205 205 def getboolean(x, err):
206 206 value = util.parsebool(getsymbol(x))
207 207 if value is not None:
208 208 return value
209 209 raise error.ParseError(err)
210 210
211 211 def getlist(x):
212 212 if not x:
213 213 return []
214 214 if x[0] == 'list':
215 215 return list(x[1:])
216 216 return [x]
217 217
218 218 def getrange(x, err):
219 219 if not x:
220 220 raise error.ParseError(err)
221 221 op = x[0]
222 222 if op == 'range':
223 223 return x[1], x[2]
224 224 elif op == 'rangepre':
225 225 return None, x[1]
226 226 elif op == 'rangepost':
227 227 return x[1], None
228 228 elif op == 'rangeall':
229 229 return None, None
230 230 raise error.ParseError(err)
231 231
232 232 def getargs(x, min, max, err):
233 233 l = getlist(x)
234 234 if len(l) < min or (max >= 0 and len(l) > max):
235 235 raise error.ParseError(err)
236 236 return l
237 237
238 238 def getargsdict(x, funcname, keys):
239 239 return parser.buildargsdict(getlist(x), funcname, parser.splitargspec(keys),
240 240 keyvaluenode='keyvalue', keynode='symbol')
241 241
242 242 def _isnamedfunc(x, funcname):
243 243 """Check if given tree matches named function"""
244 244 return x and x[0] == 'func' and getsymbol(x[1]) == funcname
245 245
246 246 def _isposargs(x, n):
247 247 """Check if given tree is n-length list of positional arguments"""
248 248 l = getlist(x)
249 249 return len(l) == n and all(y and y[0] != 'keyvalue' for y in l)
250 250
251 251 def _matchnamedfunc(x, funcname):
252 252 """Return args tree if given tree matches named function; otherwise None
253 253
254 254 This can't be used for testing a nullary function since its args tree
255 255 is also None. Use _isnamedfunc() instead.
256 256 """
257 257 if not _isnamedfunc(x, funcname):
258 258 return
259 259 return x[2]
260 260
261 # Constants for ordering requirement, used in getset():
262 #
263 # If 'define', any nested functions and operations MAY change the ordering of
264 # the entries in the set (but if changes the ordering, it MUST ALWAYS change
265 # it). If 'follow', any nested functions and operations MUST take the ordering
266 # specified by the first operand to the '&' operator.
267 #
268 # For instance,
269 #
270 # X & (Y | Z)
271 # ^ ^^^^^^^
272 # | follow
273 # define
274 #
275 # will be evaluated as 'or(y(x()), z(x()))', where 'x()' can change the order
276 # of the entries in the set, but 'y()', 'z()' and 'or()' shouldn't.
277 #
278 # 'any' means the order doesn't matter. For instance,
279 #
280 # (X & Y) | ancestors(Z)
281 # ^ ^
282 # any any
283 #
284 # For 'X & Y', 'X' decides order so the order of 'Y' does not matter. For
285 # 'ancestors(Z)', Z's order does not matter since 'ancestors' does not care
286 # about the order of its argument.
287 #
288 # Currently, most revsets do not care about the order, so 'define' is
289 # equivalent to 'follow' for them, and the resulting order is based on the
290 # 'subset' parameter passed down to them:
291 #
292 # m = revset.match(..., order=defineorder)
293 # m(repo, subset)
294 # ^^^^^^
295 # For most revsets, 'define' means using the order this subset provides
296 #
297 # There are a few revsets that always redefine the order if 'define' is
298 # specified: 'sort(X)', 'reverse(X)', 'x:y'.
299 anyorder = 'any' # don't care the order, could be even random-shuffled
300 defineorder = 'define' # ALWAYS redefine, or ALWAYS follow the current order
301 followorder = 'follow' # MUST follow the current order
302
303 261 def _matchonly(revs, bases):
304 262 """
305 263 >>> f = lambda *args: _matchonly(*map(parse, args))
306 264 >>> f('ancestors(A)', 'not ancestors(B)')
307 265 ('list', ('symbol', 'A'), ('symbol', 'B'))
308 266 """
309 267 ta = _matchnamedfunc(revs, 'ancestors')
310 268 tb = bases and bases[0] == 'not' and _matchnamedfunc(bases[1], 'ancestors')
311 269 if _isposargs(ta, 1) and _isposargs(tb, 1):
312 270 return ('list', ta, tb)
313 271
314 272 def _fixops(x):
315 273 """Rewrite raw parsed tree to resolve ambiguous syntax which cannot be
316 274 handled well by our simple top-down parser"""
317 275 if not isinstance(x, tuple):
318 276 return x
319 277
320 278 op = x[0]
321 279 if op == 'parent':
322 280 # x^:y means (x^) : y, not x ^ (:y)
323 281 # x^: means (x^) :, not x ^ (:)
324 282 post = ('parentpost', x[1])
325 283 if x[2][0] == 'dagrangepre':
326 284 return _fixops(('dagrange', post, x[2][1]))
327 285 elif x[2][0] == 'rangepre':
328 286 return _fixops(('range', post, x[2][1]))
329 287 elif x[2][0] == 'rangeall':
330 288 return _fixops(('rangepost', post))
331 289 elif op == 'or':
332 290 # make number of arguments deterministic:
333 291 # x + y + z -> (or x y z) -> (or (list x y z))
334 292 return (op, _fixops(('list',) + x[1:]))
335 293 elif op == 'subscript' and x[1][0] == 'relation':
336 294 # x#y[z] ternary
337 295 return _fixops(('relsubscript', x[1][1], x[1][2], x[2]))
338 296
339 297 return (op,) + tuple(_fixops(y) for y in x[1:])
340 298
341 299 def _analyze(x):
342 300 if x is None:
343 301 return x
344 302
345 303 op = x[0]
346 304 if op == 'minus':
347 305 return _analyze(('and', x[1], ('not', x[2])))
348 306 elif op == 'only':
349 307 t = ('func', ('symbol', 'only'), ('list', x[1], x[2]))
350 308 return _analyze(t)
351 309 elif op == 'onlypost':
352 310 return _analyze(('func', ('symbol', 'only'), x[1]))
353 311 elif op == 'dagrangepre':
354 312 return _analyze(('func', ('symbol', 'ancestors'), x[1]))
355 313 elif op == 'dagrangepost':
356 314 return _analyze(('func', ('symbol', 'descendants'), x[1]))
357 315 elif op == 'negate':
358 316 s = getstring(x[1], _("can't negate that"))
359 317 return _analyze(('string', '-' + s))
360 318 elif op in ('string', 'symbol'):
361 319 return x
362 320 elif op == 'rangeall':
363 321 return (op, None)
364 322 elif op in {'or', 'not', 'rangepre', 'rangepost', 'parentpost'}:
365 323 return (op, _analyze(x[1]))
366 324 elif op == 'group':
367 325 return _analyze(x[1])
368 326 elif op in {'and', 'dagrange', 'range', 'parent', 'ancestor', 'relation',
369 327 'subscript'}:
370 328 ta = _analyze(x[1])
371 329 tb = _analyze(x[2])
372 330 return (op, ta, tb)
373 331 elif op == 'relsubscript':
374 332 ta = _analyze(x[1])
375 333 tb = _analyze(x[2])
376 334 tc = _analyze(x[3])
377 335 return (op, ta, tb, tc)
378 336 elif op == 'list':
379 337 return (op,) + tuple(_analyze(y) for y in x[1:])
380 338 elif op == 'keyvalue':
381 339 return (op, x[1], _analyze(x[2]))
382 340 elif op == 'func':
383 341 return (op, x[1], _analyze(x[2]))
384 342 raise ValueError('invalid operator %r' % op)
385 343
386 344 def analyze(x):
387 345 """Transform raw parsed tree to evaluatable tree which can be fed to
388 346 optimize() or getset()
389 347
390 348 All pseudo operations should be mapped to real operations or functions
391 349 defined in methods or symbols table respectively.
392 350 """
393 351 return _analyze(x)
394 352
395 353 def _optimize(x, small):
396 354 if x is None:
397 355 return 0, x
398 356
399 357 smallbonus = 1
400 358 if small:
401 359 smallbonus = .5
402 360
403 361 op = x[0]
404 362 if op in ('string', 'symbol'):
405 363 return smallbonus, x # single revisions are small
406 364 elif op == 'and':
407 365 wa, ta = _optimize(x[1], True)
408 366 wb, tb = _optimize(x[2], True)
409 367 w = min(wa, wb)
410 368
411 369 # (::x and not ::y)/(not ::y and ::x) have a fast path
412 370 tm = _matchonly(ta, tb) or _matchonly(tb, ta)
413 371 if tm:
414 372 return w, ('func', ('symbol', 'only'), tm)
415 373
416 374 if tb is not None and tb[0] == 'not':
417 375 return wa, ('difference', ta, tb[1])
418 376 if wa > wb:
419 377 return w, ('flipand', tb, ta)
420 378 return w, (op, ta, tb)
421 379 elif op == 'or':
422 380 # fast path for machine-generated expression, that is likely to have
423 381 # lots of trivial revisions: 'a + b + c()' to '_list(a b) + c()'
424 382 ws, ts, ss = [], [], []
425 383 def flushss():
426 384 if not ss:
427 385 return
428 386 if len(ss) == 1:
429 387 w, t = ss[0]
430 388 else:
431 389 s = '\0'.join(t[1] for w, t in ss)
432 390 y = ('func', ('symbol', '_list'), ('string', s))
433 391 w, t = _optimize(y, False)
434 392 ws.append(w)
435 393 ts.append(t)
436 394 del ss[:]
437 395 for y in getlist(x[1]):
438 396 w, t = _optimize(y, False)
439 397 if t is not None and (t[0] == 'string' or t[0] == 'symbol'):
440 398 ss.append((w, t))
441 399 continue
442 400 flushss()
443 401 ws.append(w)
444 402 ts.append(t)
445 403 flushss()
446 404 if len(ts) == 1:
447 405 return ws[0], ts[0] # 'or' operation is fully optimized out
448 406 return max(ws), (op, ('list',) + tuple(ts))
449 407 elif op == 'not':
450 408 # Optimize not public() to _notpublic() because we have a fast version
451 409 if x[1][:3] == ('func', ('symbol', 'public'), None):
452 410 newsym = ('func', ('symbol', '_notpublic'), None)
453 411 o = _optimize(newsym, not small)
454 412 return o[0], o[1]
455 413 else:
456 414 o = _optimize(x[1], not small)
457 415 return o[0], (op, o[1])
458 416 elif op == 'rangeall':
459 417 return smallbonus, x
460 418 elif op in ('rangepre', 'rangepost', 'parentpost'):
461 419 o = _optimize(x[1], small)
462 420 return o[0], (op, o[1])
463 421 elif op in ('dagrange', 'range'):
464 422 wa, ta = _optimize(x[1], small)
465 423 wb, tb = _optimize(x[2], small)
466 424 return wa + wb, (op, ta, tb)
467 425 elif op in ('parent', 'ancestor', 'relation', 'subscript'):
468 426 w, t = _optimize(x[1], small)
469 427 return w, (op, t, x[2])
470 428 elif op == 'relsubscript':
471 429 w, t = _optimize(x[1], small)
472 430 return w, (op, t, x[2], x[3])
473 431 elif op == 'list':
474 432 ws, ts = zip(*(_optimize(y, small) for y in x[1:]))
475 433 return sum(ws), (op,) + ts
476 434 elif op == 'keyvalue':
477 435 w, t = _optimize(x[2], small)
478 436 return w, (op, x[1], t)
479 437 elif op == 'func':
480 438 f = getsymbol(x[1])
481 439 wa, ta = _optimize(x[2], small)
482 440 if f in ('author', 'branch', 'closed', 'date', 'desc', 'file', 'grep',
483 441 'keyword', 'outgoing', 'user', 'destination'):
484 442 w = 10 # slow
485 443 elif f in ('modifies', 'adds', 'removes'):
486 444 w = 30 # slower
487 445 elif f == "contains":
488 446 w = 100 # very slow
489 447 elif f == "ancestor":
490 448 w = 1 * smallbonus
491 449 elif f in ('reverse', 'limit', 'first', 'wdir', '_intlist'):
492 450 w = 0
493 451 elif f == "sort":
494 452 w = 10 # assume most sorts look at changelog
495 453 else:
496 454 w = 1
497 455 return w + wa, (op, x[1], ta)
498 456 raise ValueError('invalid operator %r' % op)
499 457
500 458 def optimize(tree):
501 459 """Optimize evaluatable tree
502 460
503 461 All pseudo operations should be transformed beforehand.
504 462 """
505 463 _weight, newtree = _optimize(tree, small=True)
506 464 return newtree
507 465
508 466 # the set of valid characters for the initial letter of symbols in
509 467 # alias declarations and definitions
510 468 _aliassyminitletters = _syminitletters | set(pycompat.sysstr('$'))
511 469
512 470 def _parsewith(spec, lookup=None, syminitletters=None):
513 471 """Generate a parse tree of given spec with given tokenizing options
514 472
515 473 >>> _parsewith('foo($1)', syminitletters=_aliassyminitletters)
516 474 ('func', ('symbol', 'foo'), ('symbol', '$1'))
517 475 >>> _parsewith('$1')
518 476 Traceback (most recent call last):
519 477 ...
520 478 ParseError: ("syntax error in revset '$1'", 0)
521 479 >>> _parsewith('foo bar')
522 480 Traceback (most recent call last):
523 481 ...
524 482 ParseError: ('invalid token', 4)
525 483 """
526 484 p = parser.parser(elements)
527 485 tree, pos = p.parse(tokenize(spec, lookup=lookup,
528 486 syminitletters=syminitletters))
529 487 if pos != len(spec):
530 488 raise error.ParseError(_('invalid token'), pos)
531 489 return _fixops(parser.simplifyinfixops(tree, ('list', 'or')))
532 490
533 491 class _aliasrules(parser.basealiasrules):
534 492 """Parsing and expansion rule set of revset aliases"""
535 493 _section = _('revset alias')
536 494
537 495 @staticmethod
538 496 def _parse(spec):
539 497 """Parse alias declaration/definition ``spec``
540 498
541 499 This allows symbol names to use also ``$`` as an initial letter
542 500 (for backward compatibility), and callers of this function should
543 501 examine whether ``$`` is used also for unexpected symbols or not.
544 502 """
545 503 return _parsewith(spec, syminitletters=_aliassyminitletters)
546 504
547 505 @staticmethod
548 506 def _trygetfunc(tree):
549 507 if tree[0] == 'func' and tree[1][0] == 'symbol':
550 508 return tree[1][1], getlist(tree[2])
551 509
552 510 def expandaliases(tree, aliases, warn=None):
553 511 """Expand aliases in a tree, aliases is a list of (name, value) tuples"""
554 512 aliases = _aliasrules.buildmap(aliases)
555 513 tree = _aliasrules.expand(aliases, tree)
556 514 # warn about problematic (but not referred) aliases
557 515 if warn is not None:
558 516 for name, alias in sorted(aliases.iteritems()):
559 517 if alias.error and not alias.warned:
560 518 warn(_('warning: %s\n') % (alias.error))
561 519 alias.warned = True
562 520 return tree
563 521
564 522 def foldconcat(tree):
565 523 """Fold elements to be concatenated by `##`
566 524 """
567 525 if not isinstance(tree, tuple) or tree[0] in ('string', 'symbol'):
568 526 return tree
569 527 if tree[0] == '_concat':
570 528 pending = [tree]
571 529 l = []
572 530 while pending:
573 531 e = pending.pop()
574 532 if e[0] == '_concat':
575 533 pending.extend(reversed(e[1:]))
576 534 elif e[0] in ('string', 'symbol'):
577 535 l.append(e[1])
578 536 else:
579 537 msg = _("\"##\" can't concatenate \"%s\" element") % (e[0])
580 538 raise error.ParseError(msg)
581 539 return ('string', ''.join(l))
582 540 else:
583 541 return tuple(foldconcat(t) for t in tree)
584 542
585 543 def parse(spec, lookup=None):
586 544 return _parsewith(spec, lookup=lookup)
587 545
588 546 def _quote(s):
589 547 r"""Quote a value in order to make it safe for the revset engine.
590 548
591 549 >>> _quote('asdf')
592 550 "'asdf'"
593 551 >>> _quote("asdf'\"")
594 552 '\'asdf\\\'"\''
595 553 >>> _quote('asdf\'')
596 554 "'asdf\\''"
597 555 >>> _quote(1)
598 556 "'1'"
599 557 """
600 558 return "'%s'" % util.escapestr(pycompat.bytestr(s))
601 559
602 560 def formatspec(expr, *args):
603 561 '''
604 562 This is a convenience function for using revsets internally, and
605 563 escapes arguments appropriately. Aliases are intentionally ignored
606 564 so that intended expression behavior isn't accidentally subverted.
607 565
608 566 Supported arguments:
609 567
610 568 %r = revset expression, parenthesized
611 569 %d = int(arg), no quoting
612 570 %s = string(arg), escaped and single-quoted
613 571 %b = arg.branch(), escaped and single-quoted
614 572 %n = hex(arg), single-quoted
615 573 %% = a literal '%'
616 574
617 575 Prefixing the type with 'l' specifies a parenthesized list of that type.
618 576
619 577 >>> formatspec('%r:: and %lr', '10 or 11', ("this()", "that()"))
620 578 '(10 or 11):: and ((this()) or (that()))'
621 579 >>> formatspec('%d:: and not %d::', 10, 20)
622 580 '10:: and not 20::'
623 581 >>> formatspec('%ld or %ld', [], [1])
624 582 "_list('') or 1"
625 583 >>> formatspec('keyword(%s)', 'foo\\xe9')
626 584 "keyword('foo\\\\xe9')"
627 585 >>> b = lambda: 'default'
628 586 >>> b.branch = b
629 587 >>> formatspec('branch(%b)', b)
630 588 "branch('default')"
631 589 >>> formatspec('root(%ls)', ['a', 'b', 'c', 'd'])
632 590 "root(_list('a\\x00b\\x00c\\x00d'))"
633 591 '''
634 592
635 593 def argtype(c, arg):
636 594 if c == 'd':
637 595 return '%d' % int(arg)
638 596 elif c == 's':
639 597 return _quote(arg)
640 598 elif c == 'r':
641 599 parse(arg) # make sure syntax errors are confined
642 600 return '(%s)' % arg
643 601 elif c == 'n':
644 602 return _quote(node.hex(arg))
645 603 elif c == 'b':
646 604 return _quote(arg.branch())
647 605
648 606 def listexp(s, t):
649 607 l = len(s)
650 608 if l == 0:
651 609 return "_list('')"
652 610 elif l == 1:
653 611 return argtype(t, s[0])
654 612 elif t == 'd':
655 613 return "_intlist('%s')" % "\0".join('%d' % int(a) for a in s)
656 614 elif t == 's':
657 615 return "_list('%s')" % "\0".join(s)
658 616 elif t == 'n':
659 617 return "_hexlist('%s')" % "\0".join(node.hex(a) for a in s)
660 618 elif t == 'b':
661 619 return "_list('%s')" % "\0".join(a.branch() for a in s)
662 620
663 621 m = l // 2
664 622 return '(%s or %s)' % (listexp(s[:m], t), listexp(s[m:], t))
665 623
666 624 expr = pycompat.bytestr(expr)
667 625 ret = ''
668 626 pos = 0
669 627 arg = 0
670 628 while pos < len(expr):
671 629 c = expr[pos]
672 630 if c == '%':
673 631 pos += 1
674 632 d = expr[pos]
675 633 if d == '%':
676 634 ret += d
677 635 elif d in 'dsnbr':
678 636 ret += argtype(d, args[arg])
679 637 arg += 1
680 638 elif d == 'l':
681 639 # a list of some type
682 640 pos += 1
683 641 d = expr[pos]
684 642 ret += listexp(list(args[arg]), d)
685 643 arg += 1
686 644 else:
687 645 raise error.Abort(_('unexpected revspec format character %s')
688 646 % d)
689 647 else:
690 648 ret += c
691 649 pos += 1
692 650
693 651 return ret
694 652
695 653 def prettyformat(tree):
696 654 return parser.prettyformat(tree, ('string', 'symbol'))
697 655
698 656 def depth(tree):
699 657 if isinstance(tree, tuple):
700 658 return max(map(depth, tree)) + 1
701 659 else:
702 660 return 0
703 661
704 662 def funcsused(tree):
705 663 if not isinstance(tree, tuple) or tree[0] in ('string', 'symbol'):
706 664 return set()
707 665 else:
708 666 funcs = set()
709 667 for s in tree[1:]:
710 668 funcs |= funcsused(s)
711 669 if tree[0] == 'func':
712 670 funcs.add(tree[1][1])
713 671 return funcs
General Comments 0
You need to be logged in to leave comments. Login now