##// END OF EJS Templates
revset: add helper function for matching strings to patterns
Simon King -
r16819:5260a9e9 default
parent child Browse files
Show More
@@ -1,1552 +1,1592
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 import re, collections
9 9 import parser, util, error, discovery, hbisect, phases
10 10 import node
11 11 import bookmarks as bookmarksmod
12 12 import match as matchmod
13 13 from i18n import _
14 14 import encoding
15 15
16 16 def _revancestors(repo, revs, followfirst):
17 17 """Like revlog.ancestors(), but supports followfirst."""
18 18 cut = followfirst and 1 or None
19 19 cl = repo.changelog
20 20 visit = collections.deque(revs)
21 21 seen = set([node.nullrev])
22 22 while visit:
23 23 for parent in cl.parentrevs(visit.popleft())[:cut]:
24 24 if parent not in seen:
25 25 visit.append(parent)
26 26 seen.add(parent)
27 27 yield parent
28 28
29 29 def _revdescendants(repo, revs, followfirst):
30 30 """Like revlog.descendants() but supports followfirst."""
31 31 cut = followfirst and 1 or None
32 32 cl = repo.changelog
33 33 first = min(revs)
34 34 nullrev = node.nullrev
35 35 if first == nullrev:
36 36 # Are there nodes with a null first parent and a non-null
37 37 # second one? Maybe. Do we care? Probably not.
38 38 for i in cl:
39 39 yield i
40 40 return
41 41
42 42 seen = set(revs)
43 43 for i in xrange(first + 1, len(cl)):
44 44 for x in cl.parentrevs(i)[:cut]:
45 45 if x != nullrev and x in seen:
46 46 seen.add(i)
47 47 yield i
48 48 break
49 49
50 50 elements = {
51 51 "(": (20, ("group", 1, ")"), ("func", 1, ")")),
52 52 "~": (18, None, ("ancestor", 18)),
53 53 "^": (18, None, ("parent", 18), ("parentpost", 18)),
54 54 "-": (5, ("negate", 19), ("minus", 5)),
55 55 "::": (17, ("dagrangepre", 17), ("dagrange", 17),
56 56 ("dagrangepost", 17)),
57 57 "..": (17, ("dagrangepre", 17), ("dagrange", 17),
58 58 ("dagrangepost", 17)),
59 59 ":": (15, ("rangepre", 15), ("range", 15), ("rangepost", 15)),
60 60 "not": (10, ("not", 10)),
61 61 "!": (10, ("not", 10)),
62 62 "and": (5, None, ("and", 5)),
63 63 "&": (5, None, ("and", 5)),
64 64 "or": (4, None, ("or", 4)),
65 65 "|": (4, None, ("or", 4)),
66 66 "+": (4, None, ("or", 4)),
67 67 ",": (2, None, ("list", 2)),
68 68 ")": (0, None, None),
69 69 "symbol": (0, ("symbol",), None),
70 70 "string": (0, ("string",), None),
71 71 "end": (0, None, None),
72 72 }
73 73
74 74 keywords = set(['and', 'or', 'not'])
75 75
76 76 def tokenize(program):
77 77 pos, l = 0, len(program)
78 78 while pos < l:
79 79 c = program[pos]
80 80 if c.isspace(): # skip inter-token whitespace
81 81 pass
82 82 elif c == ':' and program[pos:pos + 2] == '::': # look ahead carefully
83 83 yield ('::', None, pos)
84 84 pos += 1 # skip ahead
85 85 elif c == '.' and program[pos:pos + 2] == '..': # look ahead carefully
86 86 yield ('..', None, pos)
87 87 pos += 1 # skip ahead
88 88 elif c in "():,-|&+!~^": # handle simple operators
89 89 yield (c, None, pos)
90 90 elif (c in '"\'' or c == 'r' and
91 91 program[pos:pos + 2] in ("r'", 'r"')): # handle quoted strings
92 92 if c == 'r':
93 93 pos += 1
94 94 c = program[pos]
95 95 decode = lambda x: x
96 96 else:
97 97 decode = lambda x: x.decode('string-escape')
98 98 pos += 1
99 99 s = pos
100 100 while pos < l: # find closing quote
101 101 d = program[pos]
102 102 if d == '\\': # skip over escaped characters
103 103 pos += 2
104 104 continue
105 105 if d == c:
106 106 yield ('string', decode(program[s:pos]), s)
107 107 break
108 108 pos += 1
109 109 else:
110 110 raise error.ParseError(_("unterminated string"), s)
111 111 # gather up a symbol/keyword
112 112 elif c.isalnum() or c in '._' or ord(c) > 127:
113 113 s = pos
114 114 pos += 1
115 115 while pos < l: # find end of symbol
116 116 d = program[pos]
117 117 if not (d.isalnum() or d in "._/" or ord(d) > 127):
118 118 break
119 119 if d == '.' and program[pos - 1] == '.': # special case for ..
120 120 pos -= 1
121 121 break
122 122 pos += 1
123 123 sym = program[s:pos]
124 124 if sym in keywords: # operator keywords
125 125 yield (sym, None, s)
126 126 else:
127 127 yield ('symbol', sym, s)
128 128 pos -= 1
129 129 else:
130 130 raise error.ParseError(_("syntax error"), pos)
131 131 pos += 1
132 132 yield ('end', None, pos)
133 133
134 134 # helpers
135 135
136 136 def getstring(x, err):
137 137 if x and (x[0] == 'string' or x[0] == 'symbol'):
138 138 return x[1]
139 139 raise error.ParseError(err)
140 140
141 141 def getlist(x):
142 142 if not x:
143 143 return []
144 144 if x[0] == 'list':
145 145 return getlist(x[1]) + [x[2]]
146 146 return [x]
147 147
148 148 def getargs(x, min, max, err):
149 149 l = getlist(x)
150 150 if len(l) < min or (max >= 0 and len(l) > max):
151 151 raise error.ParseError(err)
152 152 return l
153 153
154 154 def getset(repo, subset, x):
155 155 if not x:
156 156 raise error.ParseError(_("missing argument"))
157 157 return methods[x[0]](repo, subset, *x[1:])
158 158
159 159 # operator methods
160 160
161 161 def stringset(repo, subset, x):
162 162 x = repo[x].rev()
163 163 if x == -1 and len(subset) == len(repo):
164 164 return [-1]
165 165 if len(subset) == len(repo) or x in subset:
166 166 return [x]
167 167 return []
168 168
169 169 def symbolset(repo, subset, x):
170 170 if x in symbols:
171 171 raise error.ParseError(_("can't use %s here") % x)
172 172 return stringset(repo, subset, x)
173 173
174 174 def rangeset(repo, subset, x, y):
175 175 m = getset(repo, subset, x)
176 176 if not m:
177 177 m = getset(repo, range(len(repo)), x)
178 178
179 179 n = getset(repo, subset, y)
180 180 if not n:
181 181 n = getset(repo, range(len(repo)), y)
182 182
183 183 if not m or not n:
184 184 return []
185 185 m, n = m[0], n[-1]
186 186
187 187 if m < n:
188 188 r = range(m, n + 1)
189 189 else:
190 190 r = range(m, n - 1, -1)
191 191 s = set(subset)
192 192 return [x for x in r if x in s]
193 193
194 194 def andset(repo, subset, x, y):
195 195 return getset(repo, getset(repo, subset, x), y)
196 196
197 197 def orset(repo, subset, x, y):
198 198 xl = getset(repo, subset, x)
199 199 s = set(xl)
200 200 yl = getset(repo, [r for r in subset if r not in s], y)
201 201 return xl + yl
202 202
203 203 def notset(repo, subset, x):
204 204 s = set(getset(repo, subset, x))
205 205 return [r for r in subset if r not in s]
206 206
207 207 def listset(repo, subset, a, b):
208 208 raise error.ParseError(_("can't use a list in this context"))
209 209
210 210 def func(repo, subset, a, b):
211 211 if a[0] == 'symbol' and a[1] in symbols:
212 212 return symbols[a[1]](repo, subset, b)
213 213 raise error.ParseError(_("not a function: %s") % a[1])
214 214
215 215 # functions
216 216
217 217 def adds(repo, subset, x):
218 218 """``adds(pattern)``
219 219 Changesets that add a file matching pattern.
220 220 """
221 221 # i18n: "adds" is a keyword
222 222 pat = getstring(x, _("adds requires a pattern"))
223 223 return checkstatus(repo, subset, pat, 1)
224 224
225 225 def ancestor(repo, subset, x):
226 226 """``ancestor(single, single)``
227 227 Greatest common ancestor of the two changesets.
228 228 """
229 229 # i18n: "ancestor" is a keyword
230 230 l = getargs(x, 2, 2, _("ancestor requires two arguments"))
231 231 r = range(len(repo))
232 232 a = getset(repo, r, l[0])
233 233 b = getset(repo, r, l[1])
234 234 if len(a) != 1 or len(b) != 1:
235 235 # i18n: "ancestor" is a keyword
236 236 raise error.ParseError(_("ancestor arguments must be single revisions"))
237 237 an = [repo[a[0]].ancestor(repo[b[0]]).rev()]
238 238
239 239 return [r for r in an if r in subset]
240 240
241 241 def _ancestors(repo, subset, x, followfirst=False):
242 242 args = getset(repo, range(len(repo)), x)
243 243 if not args:
244 244 return []
245 245 s = set(_revancestors(repo, args, followfirst)) | set(args)
246 246 return [r for r in subset if r in s]
247 247
248 248 def ancestors(repo, subset, x):
249 249 """``ancestors(set)``
250 250 Changesets that are ancestors of a changeset in set.
251 251 """
252 252 return _ancestors(repo, subset, x)
253 253
254 254 def _firstancestors(repo, subset, x):
255 255 # ``_firstancestors(set)``
256 256 # Like ``ancestors(set)`` but follows only the first parents.
257 257 return _ancestors(repo, subset, x, followfirst=True)
258 258
259 259 def ancestorspec(repo, subset, x, n):
260 260 """``set~n``
261 261 Changesets that are the Nth ancestor (first parents only) of a changeset
262 262 in set.
263 263 """
264 264 try:
265 265 n = int(n[1])
266 266 except (TypeError, ValueError):
267 267 raise error.ParseError(_("~ expects a number"))
268 268 ps = set()
269 269 cl = repo.changelog
270 270 for r in getset(repo, subset, x):
271 271 for i in range(n):
272 272 r = cl.parentrevs(r)[0]
273 273 ps.add(r)
274 274 return [r for r in subset if r in ps]
275 275
276 276 def author(repo, subset, x):
277 277 """``author(string)``
278 278 Alias for ``user(string)``.
279 279 """
280 280 # i18n: "author" is a keyword
281 281 n = encoding.lower(getstring(x, _("author requires a string")))
282 282 return [r for r in subset if n in encoding.lower(repo[r].user())]
283 283
284 284 def bisect(repo, subset, x):
285 285 """``bisect(string)``
286 286 Changesets marked in the specified bisect status:
287 287
288 288 - ``good``, ``bad``, ``skip``: csets explicitly marked as good/bad/skip
289 289 - ``goods``, ``bads`` : csets topologicaly good/bad
290 290 - ``range`` : csets taking part in the bisection
291 291 - ``pruned`` : csets that are goods, bads or skipped
292 292 - ``untested`` : csets whose fate is yet unknown
293 293 - ``ignored`` : csets ignored due to DAG topology
294 294 - ``current`` : the cset currently being bisected
295 295 """
296 296 status = getstring(x, _("bisect requires a string")).lower()
297 297 state = set(hbisect.get(repo, status))
298 298 return [r for r in subset if r in state]
299 299
300 300 # Backward-compatibility
301 301 # - no help entry so that we do not advertise it any more
302 302 def bisected(repo, subset, x):
303 303 return bisect(repo, subset, x)
304 304
305 305 def bookmark(repo, subset, x):
306 306 """``bookmark([name])``
307 307 The named bookmark or all bookmarks.
308 308 """
309 309 # i18n: "bookmark" is a keyword
310 310 args = getargs(x, 0, 1, _('bookmark takes one or no arguments'))
311 311 if args:
312 312 bm = getstring(args[0],
313 313 # i18n: "bookmark" is a keyword
314 314 _('the argument to bookmark must be a string'))
315 315 bmrev = bookmarksmod.listbookmarks(repo).get(bm, None)
316 316 if not bmrev:
317 317 raise util.Abort(_("bookmark '%s' does not exist") % bm)
318 318 bmrev = repo[bmrev].rev()
319 319 return [r for r in subset if r == bmrev]
320 320 bms = set([repo[r].rev()
321 321 for r in bookmarksmod.listbookmarks(repo).values()])
322 322 return [r for r in subset if r in bms]
323 323
324 324 def branch(repo, subset, x):
325 325 """``branch(string or set)``
326 326 All changesets belonging to the given branch or the branches of the given
327 327 changesets.
328 328 """
329 329 try:
330 330 b = getstring(x, '')
331 331 if b in repo.branchmap():
332 332 return [r for r in subset if repo[r].branch() == b]
333 333 except error.ParseError:
334 334 # not a string, but another revspec, e.g. tip()
335 335 pass
336 336
337 337 s = getset(repo, range(len(repo)), x)
338 338 b = set()
339 339 for r in s:
340 340 b.add(repo[r].branch())
341 341 s = set(s)
342 342 return [r for r in subset if r in s or repo[r].branch() in b]
343 343
344 344 def checkstatus(repo, subset, pat, field):
345 345 m = None
346 346 s = []
347 347 hasset = matchmod.patkind(pat) == 'set'
348 348 fname = None
349 349 for r in subset:
350 350 c = repo[r]
351 351 if not m or hasset:
352 352 m = matchmod.match(repo.root, repo.getcwd(), [pat], ctx=c)
353 353 if not m.anypats() and len(m.files()) == 1:
354 354 fname = m.files()[0]
355 355 if fname is not None:
356 356 if fname not in c.files():
357 357 continue
358 358 else:
359 359 for f in c.files():
360 360 if m(f):
361 361 break
362 362 else:
363 363 continue
364 364 files = repo.status(c.p1().node(), c.node())[field]
365 365 if fname is not None:
366 366 if fname in files:
367 367 s.append(r)
368 368 else:
369 369 for f in files:
370 370 if m(f):
371 371 s.append(r)
372 372 break
373 373 return s
374 374
375 375 def _children(repo, narrow, parentset):
376 376 cs = set()
377 377 pr = repo.changelog.parentrevs
378 378 for r in narrow:
379 379 for p in pr(r):
380 380 if p in parentset:
381 381 cs.add(r)
382 382 return cs
383 383
384 384 def children(repo, subset, x):
385 385 """``children(set)``
386 386 Child changesets of changesets in set.
387 387 """
388 388 s = set(getset(repo, range(len(repo)), x))
389 389 cs = _children(repo, subset, s)
390 390 return [r for r in subset if r in cs]
391 391
392 392 def closed(repo, subset, x):
393 393 """``closed()``
394 394 Changeset is closed.
395 395 """
396 396 # i18n: "closed" is a keyword
397 397 getargs(x, 0, 0, _("closed takes no arguments"))
398 398 return [r for r in subset if repo[r].closesbranch()]
399 399
400 400 def contains(repo, subset, x):
401 401 """``contains(pattern)``
402 402 Revision contains a file matching pattern. See :hg:`help patterns`
403 403 for information about file patterns.
404 404 """
405 405 # i18n: "contains" is a keyword
406 406 pat = getstring(x, _("contains requires a pattern"))
407 407 m = None
408 408 s = []
409 409 if not matchmod.patkind(pat):
410 410 for r in subset:
411 411 if pat in repo[r]:
412 412 s.append(r)
413 413 else:
414 414 for r in subset:
415 415 c = repo[r]
416 416 if not m or matchmod.patkind(pat) == 'set':
417 417 m = matchmod.match(repo.root, repo.getcwd(), [pat], ctx=c)
418 418 for f in c.manifest():
419 419 if m(f):
420 420 s.append(r)
421 421 break
422 422 return s
423 423
424 424 def date(repo, subset, x):
425 425 """``date(interval)``
426 426 Changesets within the interval, see :hg:`help dates`.
427 427 """
428 428 # i18n: "date" is a keyword
429 429 ds = getstring(x, _("date requires a string"))
430 430 dm = util.matchdate(ds)
431 431 return [r for r in subset if dm(repo[r].date()[0])]
432 432
433 433 def desc(repo, subset, x):
434 434 """``desc(string)``
435 435 Search commit message for string. The match is case-insensitive.
436 436 """
437 437 # i18n: "desc" is a keyword
438 438 ds = encoding.lower(getstring(x, _("desc requires a string")))
439 439 l = []
440 440 for r in subset:
441 441 c = repo[r]
442 442 if ds in encoding.lower(c.description()):
443 443 l.append(r)
444 444 return l
445 445
446 446 def _descendants(repo, subset, x, followfirst=False):
447 447 args = getset(repo, range(len(repo)), x)
448 448 if not args:
449 449 return []
450 450 s = set(_revdescendants(repo, args, followfirst)) | set(args)
451 451 return [r for r in subset if r in s]
452 452
453 453 def descendants(repo, subset, x):
454 454 """``descendants(set)``
455 455 Changesets which are descendants of changesets in set.
456 456 """
457 457 return _descendants(repo, subset, x)
458 458
459 459 def _firstdescendants(repo, subset, x):
460 460 # ``_firstdescendants(set)``
461 461 # Like ``descendants(set)`` but follows only the first parents.
462 462 return _descendants(repo, subset, x, followfirst=True)
463 463
464 464 def draft(repo, subset, x):
465 465 """``draft()``
466 466 Changeset in draft phase."""
467 467 getargs(x, 0, 0, _("draft takes no arguments"))
468 468 pc = repo._phasecache
469 469 return [r for r in subset if pc.phase(repo, r) == phases.draft]
470 470
471 471 def extra(repo, subset, x):
472 472 """``extra(label, [value])``
473 473 Changesets with the given label in the extra metadata, with the given
474 474 optional value."""
475 475
476 476 l = getargs(x, 1, 2, _('extra takes at least 1 and at most 2 arguments'))
477 477 label = getstring(l[0], _('first argument to extra must be a string'))
478 478 value = None
479 479
480 480 if len(l) > 1:
481 481 value = getstring(l[1], _('second argument to extra must be a string'))
482 482
483 483 def _matchvalue(r):
484 484 extra = repo[r].extra()
485 485 return label in extra and (value is None or value == extra[label])
486 486
487 487 return [r for r in subset if _matchvalue(r)]
488 488
489 489 def filelog(repo, subset, x):
490 490 """``filelog(pattern)``
491 491 Changesets connected to the specified filelog.
492 492 """
493 493
494 494 pat = getstring(x, _("filelog requires a pattern"))
495 495 m = matchmod.match(repo.root, repo.getcwd(), [pat], default='relpath',
496 496 ctx=repo[None])
497 497 s = set()
498 498
499 499 if not matchmod.patkind(pat):
500 500 for f in m.files():
501 501 fl = repo.file(f)
502 502 for fr in fl:
503 503 s.add(fl.linkrev(fr))
504 504 else:
505 505 for f in repo[None]:
506 506 if m(f):
507 507 fl = repo.file(f)
508 508 for fr in fl:
509 509 s.add(fl.linkrev(fr))
510 510
511 511 return [r for r in subset if r in s]
512 512
513 513 def first(repo, subset, x):
514 514 """``first(set, [n])``
515 515 An alias for limit().
516 516 """
517 517 return limit(repo, subset, x)
518 518
519 519 def _follow(repo, subset, x, name, followfirst=False):
520 520 l = getargs(x, 0, 1, _("%s takes no arguments or a filename") % name)
521 521 c = repo['.']
522 522 if l:
523 523 x = getstring(l[0], _("%s expected a filename") % name)
524 524 if x in c:
525 525 cx = c[x]
526 526 s = set(ctx.rev() for ctx in cx.ancestors(followfirst=followfirst))
527 527 # include the revision responsible for the most recent version
528 528 s.add(cx.linkrev())
529 529 else:
530 530 return []
531 531 else:
532 532 s = set(_revancestors(repo, [c.rev()], followfirst)) | set([c.rev()])
533 533
534 534 return [r for r in subset if r in s]
535 535
536 536 def follow(repo, subset, x):
537 537 """``follow([file])``
538 538 An alias for ``::.`` (ancestors of the working copy's first parent).
539 539 If a filename is specified, the history of the given file is followed,
540 540 including copies.
541 541 """
542 542 return _follow(repo, subset, x, 'follow')
543 543
544 544 def _followfirst(repo, subset, x):
545 545 # ``followfirst([file])``
546 546 # Like ``follow([file])`` but follows only the first parent of
547 547 # every revision or file revision.
548 548 return _follow(repo, subset, x, '_followfirst', followfirst=True)
549 549
550 550 def getall(repo, subset, x):
551 551 """``all()``
552 552 All changesets, the same as ``0:tip``.
553 553 """
554 554 # i18n: "all" is a keyword
555 555 getargs(x, 0, 0, _("all takes no arguments"))
556 556 return subset
557 557
558 558 def grep(repo, subset, x):
559 559 """``grep(regex)``
560 560 Like ``keyword(string)`` but accepts a regex. Use ``grep(r'...')``
561 561 to ensure special escape characters are handled correctly. Unlike
562 562 ``keyword(string)``, the match is case-sensitive.
563 563 """
564 564 try:
565 565 # i18n: "grep" is a keyword
566 566 gr = re.compile(getstring(x, _("grep requires a string")))
567 567 except re.error, e:
568 568 raise error.ParseError(_('invalid match pattern: %s') % e)
569 569 l = []
570 570 for r in subset:
571 571 c = repo[r]
572 572 for e in c.files() + [c.user(), c.description()]:
573 573 if gr.search(e):
574 574 l.append(r)
575 575 break
576 576 return l
577 577
578 578 def _matchfiles(repo, subset, x):
579 579 # _matchfiles takes a revset list of prefixed arguments:
580 580 #
581 581 # [p:foo, i:bar, x:baz]
582 582 #
583 583 # builds a match object from them and filters subset. Allowed
584 584 # prefixes are 'p:' for regular patterns, 'i:' for include
585 585 # patterns and 'x:' for exclude patterns. Use 'r:' prefix to pass
586 586 # a revision identifier, or the empty string to reference the
587 587 # working directory, from which the match object is
588 588 # initialized. Use 'd:' to set the default matching mode, default
589 589 # to 'glob'. At most one 'r:' and 'd:' argument can be passed.
590 590
591 591 # i18n: "_matchfiles" is a keyword
592 592 l = getargs(x, 1, -1, _("_matchfiles requires at least one argument"))
593 593 pats, inc, exc = [], [], []
594 594 hasset = False
595 595 rev, default = None, None
596 596 for arg in l:
597 597 s = getstring(arg, _("_matchfiles requires string arguments"))
598 598 prefix, value = s[:2], s[2:]
599 599 if prefix == 'p:':
600 600 pats.append(value)
601 601 elif prefix == 'i:':
602 602 inc.append(value)
603 603 elif prefix == 'x:':
604 604 exc.append(value)
605 605 elif prefix == 'r:':
606 606 if rev is not None:
607 607 raise error.ParseError(_('_matchfiles expected at most one '
608 608 'revision'))
609 609 rev = value
610 610 elif prefix == 'd:':
611 611 if default is not None:
612 612 raise error.ParseError(_('_matchfiles expected at most one '
613 613 'default mode'))
614 614 default = value
615 615 else:
616 616 raise error.ParseError(_('invalid _matchfiles prefix: %s') % prefix)
617 617 if not hasset and matchmod.patkind(value) == 'set':
618 618 hasset = True
619 619 if not default:
620 620 default = 'glob'
621 621 m = None
622 622 s = []
623 623 for r in subset:
624 624 c = repo[r]
625 625 if not m or (hasset and rev is None):
626 626 ctx = c
627 627 if rev is not None:
628 628 ctx = repo[rev or None]
629 629 m = matchmod.match(repo.root, repo.getcwd(), pats, include=inc,
630 630 exclude=exc, ctx=ctx, default=default)
631 631 for f in c.files():
632 632 if m(f):
633 633 s.append(r)
634 634 break
635 635 return s
636 636
637 637 def hasfile(repo, subset, x):
638 638 """``file(pattern)``
639 639 Changesets affecting files matched by pattern.
640 640 """
641 641 # i18n: "file" is a keyword
642 642 pat = getstring(x, _("file requires a pattern"))
643 643 return _matchfiles(repo, subset, ('string', 'p:' + pat))
644 644
645 645 def head(repo, subset, x):
646 646 """``head()``
647 647 Changeset is a named branch head.
648 648 """
649 649 # i18n: "head" is a keyword
650 650 getargs(x, 0, 0, _("head takes no arguments"))
651 651 hs = set()
652 652 for b, ls in repo.branchmap().iteritems():
653 653 hs.update(repo[h].rev() for h in ls)
654 654 return [r for r in subset if r in hs]
655 655
656 656 def heads(repo, subset, x):
657 657 """``heads(set)``
658 658 Members of set with no children in set.
659 659 """
660 660 s = getset(repo, subset, x)
661 661 ps = set(parents(repo, subset, x))
662 662 return [r for r in s if r not in ps]
663 663
664 664 def keyword(repo, subset, x):
665 665 """``keyword(string)``
666 666 Search commit message, user name, and names of changed files for
667 667 string. The match is case-insensitive.
668 668 """
669 669 # i18n: "keyword" is a keyword
670 670 kw = encoding.lower(getstring(x, _("keyword requires a string")))
671 671 l = []
672 672 for r in subset:
673 673 c = repo[r]
674 674 t = " ".join(c.files() + [c.user(), c.description()])
675 675 if kw in encoding.lower(t):
676 676 l.append(r)
677 677 return l
678 678
679 679 def limit(repo, subset, x):
680 680 """``limit(set, [n])``
681 681 First n members of set, defaulting to 1.
682 682 """
683 683 # i18n: "limit" is a keyword
684 684 l = getargs(x, 1, 2, _("limit requires one or two arguments"))
685 685 try:
686 686 lim = 1
687 687 if len(l) == 2:
688 688 # i18n: "limit" is a keyword
689 689 lim = int(getstring(l[1], _("limit requires a number")))
690 690 except (TypeError, ValueError):
691 691 # i18n: "limit" is a keyword
692 692 raise error.ParseError(_("limit expects a number"))
693 693 ss = set(subset)
694 694 os = getset(repo, range(len(repo)), l[0])[:lim]
695 695 return [r for r in os if r in ss]
696 696
697 697 def last(repo, subset, x):
698 698 """``last(set, [n])``
699 699 Last n members of set, defaulting to 1.
700 700 """
701 701 # i18n: "last" is a keyword
702 702 l = getargs(x, 1, 2, _("last requires one or two arguments"))
703 703 try:
704 704 lim = 1
705 705 if len(l) == 2:
706 706 # i18n: "last" is a keyword
707 707 lim = int(getstring(l[1], _("last requires a number")))
708 708 except (TypeError, ValueError):
709 709 # i18n: "last" is a keyword
710 710 raise error.ParseError(_("last expects a number"))
711 711 ss = set(subset)
712 712 os = getset(repo, range(len(repo)), l[0])[-lim:]
713 713 return [r for r in os if r in ss]
714 714
715 715 def maxrev(repo, subset, x):
716 716 """``max(set)``
717 717 Changeset with highest revision number in set.
718 718 """
719 719 os = getset(repo, range(len(repo)), x)
720 720 if os:
721 721 m = max(os)
722 722 if m in subset:
723 723 return [m]
724 724 return []
725 725
726 726 def merge(repo, subset, x):
727 727 """``merge()``
728 728 Changeset is a merge changeset.
729 729 """
730 730 # i18n: "merge" is a keyword
731 731 getargs(x, 0, 0, _("merge takes no arguments"))
732 732 cl = repo.changelog
733 733 return [r for r in subset if cl.parentrevs(r)[1] != -1]
734 734
735 735 def minrev(repo, subset, x):
736 736 """``min(set)``
737 737 Changeset with lowest revision number in set.
738 738 """
739 739 os = getset(repo, range(len(repo)), x)
740 740 if os:
741 741 m = min(os)
742 742 if m in subset:
743 743 return [m]
744 744 return []
745 745
746 746 def modifies(repo, subset, x):
747 747 """``modifies(pattern)``
748 748 Changesets modifying files matched by pattern.
749 749 """
750 750 # i18n: "modifies" is a keyword
751 751 pat = getstring(x, _("modifies requires a pattern"))
752 752 return checkstatus(repo, subset, pat, 0)
753 753
754 754 def node_(repo, subset, x):
755 755 """``id(string)``
756 756 Revision non-ambiguously specified by the given hex string prefix.
757 757 """
758 758 # i18n: "id" is a keyword
759 759 l = getargs(x, 1, 1, _("id requires one argument"))
760 760 # i18n: "id" is a keyword
761 761 n = getstring(l[0], _("id requires a string"))
762 762 if len(n) == 40:
763 763 rn = repo[n].rev()
764 764 else:
765 765 rn = None
766 766 pm = repo.changelog._partialmatch(n)
767 767 if pm is not None:
768 768 rn = repo.changelog.rev(pm)
769 769
770 770 return [r for r in subset if r == rn]
771 771
772 772 def outgoing(repo, subset, x):
773 773 """``outgoing([path])``
774 774 Changesets not found in the specified destination repository, or the
775 775 default push location.
776 776 """
777 777 import hg # avoid start-up nasties
778 778 # i18n: "outgoing" is a keyword
779 779 l = getargs(x, 0, 1, _("outgoing takes one or no arguments"))
780 780 # i18n: "outgoing" is a keyword
781 781 dest = l and getstring(l[0], _("outgoing requires a repository path")) or ''
782 782 dest = repo.ui.expandpath(dest or 'default-push', dest or 'default')
783 783 dest, branches = hg.parseurl(dest)
784 784 revs, checkout = hg.addbranchrevs(repo, repo, branches, [])
785 785 if revs:
786 786 revs = [repo.lookup(rev) for rev in revs]
787 787 other = hg.peer(repo, {}, dest)
788 788 repo.ui.pushbuffer()
789 789 outgoing = discovery.findcommonoutgoing(repo, other, onlyheads=revs)
790 790 repo.ui.popbuffer()
791 791 cl = repo.changelog
792 792 o = set([cl.rev(r) for r in outgoing.missing])
793 793 return [r for r in subset if r in o]
794 794
795 795 def p1(repo, subset, x):
796 796 """``p1([set])``
797 797 First parent of changesets in set, or the working directory.
798 798 """
799 799 if x is None:
800 800 p = repo[x].p1().rev()
801 801 return [r for r in subset if r == p]
802 802
803 803 ps = set()
804 804 cl = repo.changelog
805 805 for r in getset(repo, range(len(repo)), x):
806 806 ps.add(cl.parentrevs(r)[0])
807 807 return [r for r in subset if r in ps]
808 808
809 809 def p2(repo, subset, x):
810 810 """``p2([set])``
811 811 Second parent of changesets in set, or the working directory.
812 812 """
813 813 if x is None:
814 814 ps = repo[x].parents()
815 815 try:
816 816 p = ps[1].rev()
817 817 return [r for r in subset if r == p]
818 818 except IndexError:
819 819 return []
820 820
821 821 ps = set()
822 822 cl = repo.changelog
823 823 for r in getset(repo, range(len(repo)), x):
824 824 ps.add(cl.parentrevs(r)[1])
825 825 return [r for r in subset if r in ps]
826 826
827 827 def parents(repo, subset, x):
828 828 """``parents([set])``
829 829 The set of all parents for all changesets in set, or the working directory.
830 830 """
831 831 if x is None:
832 832 ps = tuple(p.rev() for p in repo[x].parents())
833 833 return [r for r in subset if r in ps]
834 834
835 835 ps = set()
836 836 cl = repo.changelog
837 837 for r in getset(repo, range(len(repo)), x):
838 838 ps.update(cl.parentrevs(r))
839 839 return [r for r in subset if r in ps]
840 840
841 841 def parentspec(repo, subset, x, n):
842 842 """``set^0``
843 843 The set.
844 844 ``set^1`` (or ``set^``), ``set^2``
845 845 First or second parent, respectively, of all changesets in set.
846 846 """
847 847 try:
848 848 n = int(n[1])
849 849 if n not in (0, 1, 2):
850 850 raise ValueError
851 851 except (TypeError, ValueError):
852 852 raise error.ParseError(_("^ expects a number 0, 1, or 2"))
853 853 ps = set()
854 854 cl = repo.changelog
855 855 for r in getset(repo, subset, x):
856 856 if n == 0:
857 857 ps.add(r)
858 858 elif n == 1:
859 859 ps.add(cl.parentrevs(r)[0])
860 860 elif n == 2:
861 861 parents = cl.parentrevs(r)
862 862 if len(parents) > 1:
863 863 ps.add(parents[1])
864 864 return [r for r in subset if r in ps]
865 865
866 866 def present(repo, subset, x):
867 867 """``present(set)``
868 868 An empty set, if any revision in set isn't found; otherwise,
869 869 all revisions in set.
870 870
871 871 If any of specified revisions is not present in the local repository,
872 872 the query is normally aborted. But this predicate allows the query
873 873 to continue even in such cases.
874 874 """
875 875 try:
876 876 return getset(repo, subset, x)
877 877 except error.RepoLookupError:
878 878 return []
879 879
880 880 def public(repo, subset, x):
881 881 """``public()``
882 882 Changeset in public phase."""
883 883 getargs(x, 0, 0, _("public takes no arguments"))
884 884 pc = repo._phasecache
885 885 return [r for r in subset if pc.phase(repo, r) == phases.public]
886 886
887 887 def remote(repo, subset, x):
888 888 """``remote([id [,path]])``
889 889 Local revision that corresponds to the given identifier in a
890 890 remote repository, if present. Here, the '.' identifier is a
891 891 synonym for the current local branch.
892 892 """
893 893
894 894 import hg # avoid start-up nasties
895 895 # i18n: "remote" is a keyword
896 896 l = getargs(x, 0, 2, _("remote takes one, two or no arguments"))
897 897
898 898 q = '.'
899 899 if len(l) > 0:
900 900 # i18n: "remote" is a keyword
901 901 q = getstring(l[0], _("remote requires a string id"))
902 902 if q == '.':
903 903 q = repo['.'].branch()
904 904
905 905 dest = ''
906 906 if len(l) > 1:
907 907 # i18n: "remote" is a keyword
908 908 dest = getstring(l[1], _("remote requires a repository path"))
909 909 dest = repo.ui.expandpath(dest or 'default')
910 910 dest, branches = hg.parseurl(dest)
911 911 revs, checkout = hg.addbranchrevs(repo, repo, branches, [])
912 912 if revs:
913 913 revs = [repo.lookup(rev) for rev in revs]
914 914 other = hg.peer(repo, {}, dest)
915 915 n = other.lookup(q)
916 916 if n in repo:
917 917 r = repo[n].rev()
918 918 if r in subset:
919 919 return [r]
920 920 return []
921 921
922 922 def removes(repo, subset, x):
923 923 """``removes(pattern)``
924 924 Changesets which remove files matching pattern.
925 925 """
926 926 # i18n: "removes" is a keyword
927 927 pat = getstring(x, _("removes requires a pattern"))
928 928 return checkstatus(repo, subset, pat, 2)
929 929
930 930 def rev(repo, subset, x):
931 931 """``rev(number)``
932 932 Revision with the given numeric identifier.
933 933 """
934 934 # i18n: "rev" is a keyword
935 935 l = getargs(x, 1, 1, _("rev requires one argument"))
936 936 try:
937 937 # i18n: "rev" is a keyword
938 938 l = int(getstring(l[0], _("rev requires a number")))
939 939 except (TypeError, ValueError):
940 940 # i18n: "rev" is a keyword
941 941 raise error.ParseError(_("rev expects a number"))
942 942 return [r for r in subset if r == l]
943 943
944 944 def matching(repo, subset, x):
945 945 """``matching(revision [, field])``
946 946 Changesets in which a given set of fields match the set of fields in the
947 947 selected revision or set.
948 948
949 949 To match more than one field pass the list of fields to match separated
950 950 by spaces (e.g. ``author description``).
951 951
952 952 Valid fields are most regular revision fields and some special fields.
953 953
954 954 Regular revision fields are ``description``, ``author``, ``branch``,
955 955 ``date``, ``files``, ``phase``, ``parents``, ``substate`` and ``user``.
956 956 Note that ``author`` and ``user`` are synonyms.
957 957
958 958 Special fields are ``summary`` and ``metadata``:
959 959 ``summary`` matches the first line of the description.
960 960 ``metadata`` is equivalent to matching ``description user date``
961 961 (i.e. it matches the main metadata fields).
962 962
963 963 ``metadata`` is the default field which is used when no fields are
964 964 specified. You can match more than one field at a time.
965 965 """
966 966 l = getargs(x, 1, 2, _("matching takes 1 or 2 arguments"))
967 967
968 968 revs = getset(repo, xrange(len(repo)), l[0])
969 969
970 970 fieldlist = ['metadata']
971 971 if len(l) > 1:
972 972 fieldlist = getstring(l[1],
973 973 _("matching requires a string "
974 974 "as its second argument")).split()
975 975
976 976 # Make sure that there are no repeated fields, and expand the
977 977 # 'special' 'metadata' field type
978 978 fields = []
979 979 for field in fieldlist:
980 980 if field == 'metadata':
981 981 fields += ['user', 'description', 'date']
982 982 else:
983 983 if field == 'author':
984 984 field = 'user'
985 985 fields.append(field)
986 986 fields = set(fields)
987 987 if 'summary' in fields and 'description' in fields:
988 988 # If a revision matches its description it also matches its summary
989 989 fields.discard('summary')
990 990
991 991 # We may want to match more than one field
992 992 # Not all fields take the same amount of time to be matched
993 993 # Sort the selected fields in order of increasing matching cost
994 994 fieldorder = ['phase', 'parents', 'user', 'date', 'branch', 'summary',
995 995 'files', 'description', 'substate']
996 996 def fieldkeyfunc(f):
997 997 try:
998 998 return fieldorder.index(f)
999 999 except ValueError:
1000 1000 # assume an unknown field is very costly
1001 1001 return len(fieldorder)
1002 1002 fields = list(fields)
1003 1003 fields.sort(key=fieldkeyfunc)
1004 1004
1005 1005 # Each field will be matched with its own "getfield" function
1006 1006 # which will be added to the getfieldfuncs array of functions
1007 1007 getfieldfuncs = []
1008 1008 _funcs = {
1009 1009 'user': lambda r: repo[r].user(),
1010 1010 'branch': lambda r: repo[r].branch(),
1011 1011 'date': lambda r: repo[r].date(),
1012 1012 'description': lambda r: repo[r].description(),
1013 1013 'files': lambda r: repo[r].files(),
1014 1014 'parents': lambda r: repo[r].parents(),
1015 1015 'phase': lambda r: repo[r].phase(),
1016 1016 'substate': lambda r: repo[r].substate,
1017 1017 'summary': lambda r: repo[r].description().splitlines()[0],
1018 1018 }
1019 1019 for info in fields:
1020 1020 getfield = _funcs.get(info, None)
1021 1021 if getfield is None:
1022 1022 raise error.ParseError(
1023 1023 _("unexpected field name passed to matching: %s") % info)
1024 1024 getfieldfuncs.append(getfield)
1025 1025 # convert the getfield array of functions into a "getinfo" function
1026 1026 # which returns an array of field values (or a single value if there
1027 1027 # is only one field to match)
1028 1028 getinfo = lambda r: [f(r) for f in getfieldfuncs]
1029 1029
1030 1030 matches = set()
1031 1031 for rev in revs:
1032 1032 target = getinfo(rev)
1033 1033 for r in subset:
1034 1034 match = True
1035 1035 for n, f in enumerate(getfieldfuncs):
1036 1036 if target[n] != f(r):
1037 1037 match = False
1038 1038 break
1039 1039 if match:
1040 1040 matches.add(r)
1041 1041 return [r for r in subset if r in matches]
1042 1042
1043 1043 def reverse(repo, subset, x):
1044 1044 """``reverse(set)``
1045 1045 Reverse order of set.
1046 1046 """
1047 1047 l = getset(repo, subset, x)
1048 1048 l.reverse()
1049 1049 return l
1050 1050
1051 1051 def roots(repo, subset, x):
1052 1052 """``roots(set)``
1053 1053 Changesets in set with no parent changeset in set.
1054 1054 """
1055 1055 s = set(getset(repo, xrange(len(repo)), x))
1056 1056 subset = [r for r in subset if r in s]
1057 1057 cs = _children(repo, subset, s)
1058 1058 return [r for r in subset if r not in cs]
1059 1059
1060 1060 def secret(repo, subset, x):
1061 1061 """``secret()``
1062 1062 Changeset in secret phase."""
1063 1063 getargs(x, 0, 0, _("secret takes no arguments"))
1064 1064 pc = repo._phasecache
1065 1065 return [r for r in subset if pc.phase(repo, r) == phases.secret]
1066 1066
1067 1067 def sort(repo, subset, x):
1068 1068 """``sort(set[, [-]key...])``
1069 1069 Sort set by keys. The default sort order is ascending, specify a key
1070 1070 as ``-key`` to sort in descending order.
1071 1071
1072 1072 The keys can be:
1073 1073
1074 1074 - ``rev`` for the revision number,
1075 1075 - ``branch`` for the branch name,
1076 1076 - ``desc`` for the commit message (description),
1077 1077 - ``user`` for user name (``author`` can be used as an alias),
1078 1078 - ``date`` for the commit date
1079 1079 """
1080 1080 # i18n: "sort" is a keyword
1081 1081 l = getargs(x, 1, 2, _("sort requires one or two arguments"))
1082 1082 keys = "rev"
1083 1083 if len(l) == 2:
1084 1084 keys = getstring(l[1], _("sort spec must be a string"))
1085 1085
1086 1086 s = l[0]
1087 1087 keys = keys.split()
1088 1088 l = []
1089 1089 def invert(s):
1090 1090 return "".join(chr(255 - ord(c)) for c in s)
1091 1091 for r in getset(repo, subset, s):
1092 1092 c = repo[r]
1093 1093 e = []
1094 1094 for k in keys:
1095 1095 if k == 'rev':
1096 1096 e.append(r)
1097 1097 elif k == '-rev':
1098 1098 e.append(-r)
1099 1099 elif k == 'branch':
1100 1100 e.append(c.branch())
1101 1101 elif k == '-branch':
1102 1102 e.append(invert(c.branch()))
1103 1103 elif k == 'desc':
1104 1104 e.append(c.description())
1105 1105 elif k == '-desc':
1106 1106 e.append(invert(c.description()))
1107 1107 elif k in 'user author':
1108 1108 e.append(c.user())
1109 1109 elif k in '-user -author':
1110 1110 e.append(invert(c.user()))
1111 1111 elif k == 'date':
1112 1112 e.append(c.date()[0])
1113 1113 elif k == '-date':
1114 1114 e.append(-c.date()[0])
1115 1115 else:
1116 1116 raise error.ParseError(_("unknown sort key %r") % k)
1117 1117 e.append(r)
1118 1118 l.append(e)
1119 1119 l.sort()
1120 1120 return [e[-1] for e in l]
1121 1121
1122 def _stringmatcher(pattern):
1123 """
1124 accepts a string, possibly starting with 're:' or 'literal:' prefix.
1125 returns the matcher name, pattern, and matcher function.
1126 missing or unknown prefixes are treated as literal matches.
1127
1128 helper for tests:
1129 >>> def test(pattern, *tests):
1130 ... kind, pattern, matcher = _stringmatcher(pattern)
1131 ... return (kind, pattern, [bool(matcher(t)) for t in tests])
1132
1133 exact matching (no prefix):
1134 >>> test('abcdefg', 'abc', 'def', 'abcdefg')
1135 ('literal', 'abcdefg', [False, False, True])
1136
1137 regex matching ('re:' prefix)
1138 >>> test('re:a.+b', 'nomatch', 'fooadef', 'fooadefbar')
1139 ('re', 'a.+b', [False, False, True])
1140
1141 force exact matches ('literal:' prefix)
1142 >>> test('literal:re:foobar', 'foobar', 're:foobar')
1143 ('literal', 're:foobar', [False, True])
1144
1145 unknown prefixes are ignored and treated as literals
1146 >>> test('foo:bar', 'foo', 'bar', 'foo:bar')
1147 ('literal', 'foo:bar', [False, False, True])
1148 """
1149 if pattern.startswith('re:'):
1150 pattern = pattern[3:]
1151 try:
1152 regex = re.compile(pattern)
1153 except re.error, e:
1154 raise error.ParseError(_('invalid regular expression: %s')
1155 % e)
1156 return 're', pattern, regex.search
1157 elif pattern.startswith('literal:'):
1158 pattern = pattern[8:]
1159 return 'literal', pattern, pattern.__eq__
1160
1161
1122 1162 def tag(repo, subset, x):
1123 1163 """``tag([name])``
1124 1164 The specified tag by name, or all tagged revisions if no name is given.
1125 1165 """
1126 1166 # i18n: "tag" is a keyword
1127 1167 args = getargs(x, 0, 1, _("tag takes one or no arguments"))
1128 1168 cl = repo.changelog
1129 1169 if args:
1130 1170 tn = getstring(args[0],
1131 1171 # i18n: "tag" is a keyword
1132 1172 _('the argument to tag must be a string'))
1133 1173 if not repo.tags().get(tn, None):
1134 1174 raise util.Abort(_("tag '%s' does not exist") % tn)
1135 1175 s = set([cl.rev(n) for t, n in repo.tagslist() if t == tn])
1136 1176 else:
1137 1177 s = set([cl.rev(n) for t, n in repo.tagslist() if t != 'tip'])
1138 1178 return [r for r in subset if r in s]
1139 1179
1140 1180 def tagged(repo, subset, x):
1141 1181 return tag(repo, subset, x)
1142 1182
1143 1183 def user(repo, subset, x):
1144 1184 """``user(string)``
1145 1185 User name contains string. The match is case-insensitive.
1146 1186 """
1147 1187 return author(repo, subset, x)
1148 1188
1149 1189 # for internal use
1150 1190 def _list(repo, subset, x):
1151 1191 s = getstring(x, "internal error")
1152 1192 if not s:
1153 1193 return []
1154 1194 if not isinstance(subset, set):
1155 1195 subset = set(subset)
1156 1196 ls = [repo[r].rev() for r in s.split('\0')]
1157 1197 return [r for r in ls if r in subset]
1158 1198
1159 1199 symbols = {
1160 1200 "adds": adds,
1161 1201 "all": getall,
1162 1202 "ancestor": ancestor,
1163 1203 "ancestors": ancestors,
1164 1204 "_firstancestors": _firstancestors,
1165 1205 "author": author,
1166 1206 "bisect": bisect,
1167 1207 "bisected": bisected,
1168 1208 "bookmark": bookmark,
1169 1209 "branch": branch,
1170 1210 "children": children,
1171 1211 "closed": closed,
1172 1212 "contains": contains,
1173 1213 "date": date,
1174 1214 "desc": desc,
1175 1215 "descendants": descendants,
1176 1216 "_firstdescendants": _firstdescendants,
1177 1217 "draft": draft,
1178 1218 "extra": extra,
1179 1219 "file": hasfile,
1180 1220 "filelog": filelog,
1181 1221 "first": first,
1182 1222 "follow": follow,
1183 1223 "_followfirst": _followfirst,
1184 1224 "grep": grep,
1185 1225 "head": head,
1186 1226 "heads": heads,
1187 1227 "id": node_,
1188 1228 "keyword": keyword,
1189 1229 "last": last,
1190 1230 "limit": limit,
1191 1231 "_matchfiles": _matchfiles,
1192 1232 "max": maxrev,
1193 1233 "merge": merge,
1194 1234 "min": minrev,
1195 1235 "modifies": modifies,
1196 1236 "outgoing": outgoing,
1197 1237 "p1": p1,
1198 1238 "p2": p2,
1199 1239 "parents": parents,
1200 1240 "present": present,
1201 1241 "public": public,
1202 1242 "remote": remote,
1203 1243 "removes": removes,
1204 1244 "rev": rev,
1205 1245 "reverse": reverse,
1206 1246 "roots": roots,
1207 1247 "sort": sort,
1208 1248 "secret": secret,
1209 1249 "matching": matching,
1210 1250 "tag": tag,
1211 1251 "tagged": tagged,
1212 1252 "user": user,
1213 1253 "_list": _list,
1214 1254 }
1215 1255
1216 1256 methods = {
1217 1257 "range": rangeset,
1218 1258 "string": stringset,
1219 1259 "symbol": symbolset,
1220 1260 "and": andset,
1221 1261 "or": orset,
1222 1262 "not": notset,
1223 1263 "list": listset,
1224 1264 "func": func,
1225 1265 "ancestor": ancestorspec,
1226 1266 "parent": parentspec,
1227 1267 "parentpost": p1,
1228 1268 }
1229 1269
1230 1270 def optimize(x, small):
1231 1271 if x is None:
1232 1272 return 0, x
1233 1273
1234 1274 smallbonus = 1
1235 1275 if small:
1236 1276 smallbonus = .5
1237 1277
1238 1278 op = x[0]
1239 1279 if op == 'minus':
1240 1280 return optimize(('and', x[1], ('not', x[2])), small)
1241 1281 elif op == 'dagrange':
1242 1282 return optimize(('and', ('func', ('symbol', 'descendants'), x[1]),
1243 1283 ('func', ('symbol', 'ancestors'), x[2])), small)
1244 1284 elif op == 'dagrangepre':
1245 1285 return optimize(('func', ('symbol', 'ancestors'), x[1]), small)
1246 1286 elif op == 'dagrangepost':
1247 1287 return optimize(('func', ('symbol', 'descendants'), x[1]), small)
1248 1288 elif op == 'rangepre':
1249 1289 return optimize(('range', ('string', '0'), x[1]), small)
1250 1290 elif op == 'rangepost':
1251 1291 return optimize(('range', x[1], ('string', 'tip')), small)
1252 1292 elif op == 'negate':
1253 1293 return optimize(('string',
1254 1294 '-' + getstring(x[1], _("can't negate that"))), small)
1255 1295 elif op in 'string symbol negate':
1256 1296 return smallbonus, x # single revisions are small
1257 1297 elif op == 'and' or op == 'dagrange':
1258 1298 wa, ta = optimize(x[1], True)
1259 1299 wb, tb = optimize(x[2], True)
1260 1300 w = min(wa, wb)
1261 1301 if wa > wb:
1262 1302 return w, (op, tb, ta)
1263 1303 return w, (op, ta, tb)
1264 1304 elif op == 'or':
1265 1305 wa, ta = optimize(x[1], False)
1266 1306 wb, tb = optimize(x[2], False)
1267 1307 if wb < wa:
1268 1308 wb, wa = wa, wb
1269 1309 return max(wa, wb), (op, ta, tb)
1270 1310 elif op == 'not':
1271 1311 o = optimize(x[1], not small)
1272 1312 return o[0], (op, o[1])
1273 1313 elif op == 'parentpost':
1274 1314 o = optimize(x[1], small)
1275 1315 return o[0], (op, o[1])
1276 1316 elif op == 'group':
1277 1317 return optimize(x[1], small)
1278 1318 elif op in 'range list parent ancestorspec':
1279 1319 if op == 'parent':
1280 1320 # x^:y means (x^) : y, not x ^ (:y)
1281 1321 post = ('parentpost', x[1])
1282 1322 if x[2][0] == 'dagrangepre':
1283 1323 return optimize(('dagrange', post, x[2][1]), small)
1284 1324 elif x[2][0] == 'rangepre':
1285 1325 return optimize(('range', post, x[2][1]), small)
1286 1326
1287 1327 wa, ta = optimize(x[1], small)
1288 1328 wb, tb = optimize(x[2], small)
1289 1329 return wa + wb, (op, ta, tb)
1290 1330 elif op == 'func':
1291 1331 f = getstring(x[1], _("not a symbol"))
1292 1332 wa, ta = optimize(x[2], small)
1293 1333 if f in ("author branch closed date desc file grep keyword "
1294 1334 "outgoing user"):
1295 1335 w = 10 # slow
1296 1336 elif f in "modifies adds removes":
1297 1337 w = 30 # slower
1298 1338 elif f == "contains":
1299 1339 w = 100 # very slow
1300 1340 elif f == "ancestor":
1301 1341 w = 1 * smallbonus
1302 1342 elif f in "reverse limit first":
1303 1343 w = 0
1304 1344 elif f in "sort":
1305 1345 w = 10 # assume most sorts look at changelog
1306 1346 else:
1307 1347 w = 1
1308 1348 return w + wa, (op, x[1], ta)
1309 1349 return 1, x
1310 1350
1311 1351 _aliasarg = ('func', ('symbol', '_aliasarg'))
1312 1352 def _getaliasarg(tree):
1313 1353 """If tree matches ('func', ('symbol', '_aliasarg'), ('string', X))
1314 1354 return X, None otherwise.
1315 1355 """
1316 1356 if (len(tree) == 3 and tree[:2] == _aliasarg
1317 1357 and tree[2][0] == 'string'):
1318 1358 return tree[2][1]
1319 1359 return None
1320 1360
1321 1361 def _checkaliasarg(tree, known=None):
1322 1362 """Check tree contains no _aliasarg construct or only ones which
1323 1363 value is in known. Used to avoid alias placeholders injection.
1324 1364 """
1325 1365 if isinstance(tree, tuple):
1326 1366 arg = _getaliasarg(tree)
1327 1367 if arg is not None and (not known or arg not in known):
1328 1368 raise error.ParseError(_("not a function: %s") % '_aliasarg')
1329 1369 for t in tree:
1330 1370 _checkaliasarg(t, known)
1331 1371
1332 1372 class revsetalias(object):
1333 1373 funcre = re.compile('^([^(]+)\(([^)]+)\)$')
1334 1374 args = None
1335 1375
1336 1376 def __init__(self, name, value):
1337 1377 '''Aliases like:
1338 1378
1339 1379 h = heads(default)
1340 1380 b($1) = ancestors($1) - ancestors(default)
1341 1381 '''
1342 1382 m = self.funcre.search(name)
1343 1383 if m:
1344 1384 self.name = m.group(1)
1345 1385 self.tree = ('func', ('symbol', m.group(1)))
1346 1386 self.args = [x.strip() for x in m.group(2).split(',')]
1347 1387 for arg in self.args:
1348 1388 # _aliasarg() is an unknown symbol only used separate
1349 1389 # alias argument placeholders from regular strings.
1350 1390 value = value.replace(arg, '_aliasarg(%r)' % (arg,))
1351 1391 else:
1352 1392 self.name = name
1353 1393 self.tree = ('symbol', name)
1354 1394
1355 1395 self.replacement, pos = parse(value)
1356 1396 if pos != len(value):
1357 1397 raise error.ParseError(_('invalid token'), pos)
1358 1398 # Check for placeholder injection
1359 1399 _checkaliasarg(self.replacement, self.args)
1360 1400
1361 1401 def _getalias(aliases, tree):
1362 1402 """If tree looks like an unexpanded alias, return it. Return None
1363 1403 otherwise.
1364 1404 """
1365 1405 if isinstance(tree, tuple) and tree:
1366 1406 if tree[0] == 'symbol' and len(tree) == 2:
1367 1407 name = tree[1]
1368 1408 alias = aliases.get(name)
1369 1409 if alias and alias.args is None and alias.tree == tree:
1370 1410 return alias
1371 1411 if tree[0] == 'func' and len(tree) > 1:
1372 1412 if tree[1][0] == 'symbol' and len(tree[1]) == 2:
1373 1413 name = tree[1][1]
1374 1414 alias = aliases.get(name)
1375 1415 if alias and alias.args is not None and alias.tree == tree[:2]:
1376 1416 return alias
1377 1417 return None
1378 1418
1379 1419 def _expandargs(tree, args):
1380 1420 """Replace _aliasarg instances with the substitution value of the
1381 1421 same name in args, recursively.
1382 1422 """
1383 1423 if not tree or not isinstance(tree, tuple):
1384 1424 return tree
1385 1425 arg = _getaliasarg(tree)
1386 1426 if arg is not None:
1387 1427 return args[arg]
1388 1428 return tuple(_expandargs(t, args) for t in tree)
1389 1429
1390 1430 def _expandaliases(aliases, tree, expanding):
1391 1431 """Expand aliases in tree, recursively.
1392 1432
1393 1433 'aliases' is a dictionary mapping user defined aliases to
1394 1434 revsetalias objects.
1395 1435 """
1396 1436 if not isinstance(tree, tuple):
1397 1437 # Do not expand raw strings
1398 1438 return tree
1399 1439 alias = _getalias(aliases, tree)
1400 1440 if alias is not None:
1401 1441 if alias in expanding:
1402 1442 raise error.ParseError(_('infinite expansion of revset alias "%s" '
1403 1443 'detected') % alias.name)
1404 1444 expanding.append(alias)
1405 1445 result = _expandaliases(aliases, alias.replacement, expanding)
1406 1446 expanding.pop()
1407 1447 if alias.args is not None:
1408 1448 l = getlist(tree[2])
1409 1449 if len(l) != len(alias.args):
1410 1450 raise error.ParseError(
1411 1451 _('invalid number of arguments: %s') % len(l))
1412 1452 l = [_expandaliases(aliases, a, []) for a in l]
1413 1453 result = _expandargs(result, dict(zip(alias.args, l)))
1414 1454 else:
1415 1455 result = tuple(_expandaliases(aliases, t, expanding)
1416 1456 for t in tree)
1417 1457 return result
1418 1458
1419 1459 def findaliases(ui, tree):
1420 1460 _checkaliasarg(tree)
1421 1461 aliases = {}
1422 1462 for k, v in ui.configitems('revsetalias'):
1423 1463 alias = revsetalias(k, v)
1424 1464 aliases[alias.name] = alias
1425 1465 return _expandaliases(aliases, tree, [])
1426 1466
1427 1467 parse = parser.parser(tokenize, elements).parse
1428 1468
1429 1469 def match(ui, spec):
1430 1470 if not spec:
1431 1471 raise error.ParseError(_("empty query"))
1432 1472 tree, pos = parse(spec)
1433 1473 if (pos != len(spec)):
1434 1474 raise error.ParseError(_("invalid token"), pos)
1435 1475 if ui:
1436 1476 tree = findaliases(ui, tree)
1437 1477 weight, tree = optimize(tree, True)
1438 1478 def mfunc(repo, subset):
1439 1479 return getset(repo, subset, tree)
1440 1480 return mfunc
1441 1481
1442 1482 def formatspec(expr, *args):
1443 1483 '''
1444 1484 This is a convenience function for using revsets internally, and
1445 1485 escapes arguments appropriately. Aliases are intentionally ignored
1446 1486 so that intended expression behavior isn't accidentally subverted.
1447 1487
1448 1488 Supported arguments:
1449 1489
1450 1490 %r = revset expression, parenthesized
1451 1491 %d = int(arg), no quoting
1452 1492 %s = string(arg), escaped and single-quoted
1453 1493 %b = arg.branch(), escaped and single-quoted
1454 1494 %n = hex(arg), single-quoted
1455 1495 %% = a literal '%'
1456 1496
1457 1497 Prefixing the type with 'l' specifies a parenthesized list of that type.
1458 1498
1459 1499 >>> formatspec('%r:: and %lr', '10 or 11', ("this()", "that()"))
1460 1500 '(10 or 11):: and ((this()) or (that()))'
1461 1501 >>> formatspec('%d:: and not %d::', 10, 20)
1462 1502 '10:: and not 20::'
1463 1503 >>> formatspec('%ld or %ld', [], [1])
1464 1504 "_list('') or 1"
1465 1505 >>> formatspec('keyword(%s)', 'foo\\xe9')
1466 1506 "keyword('foo\\\\xe9')"
1467 1507 >>> b = lambda: 'default'
1468 1508 >>> b.branch = b
1469 1509 >>> formatspec('branch(%b)', b)
1470 1510 "branch('default')"
1471 1511 >>> formatspec('root(%ls)', ['a', 'b', 'c', 'd'])
1472 1512 "root(_list('a\\x00b\\x00c\\x00d'))"
1473 1513 '''
1474 1514
1475 1515 def quote(s):
1476 1516 return repr(str(s))
1477 1517
1478 1518 def argtype(c, arg):
1479 1519 if c == 'd':
1480 1520 return str(int(arg))
1481 1521 elif c == 's':
1482 1522 return quote(arg)
1483 1523 elif c == 'r':
1484 1524 parse(arg) # make sure syntax errors are confined
1485 1525 return '(%s)' % arg
1486 1526 elif c == 'n':
1487 1527 return quote(node.hex(arg))
1488 1528 elif c == 'b':
1489 1529 return quote(arg.branch())
1490 1530
1491 1531 def listexp(s, t):
1492 1532 l = len(s)
1493 1533 if l == 0:
1494 1534 return "_list('')"
1495 1535 elif l == 1:
1496 1536 return argtype(t, s[0])
1497 1537 elif t == 'd':
1498 1538 return "_list('%s')" % "\0".join(str(int(a)) for a in s)
1499 1539 elif t == 's':
1500 1540 return "_list('%s')" % "\0".join(s)
1501 1541 elif t == 'n':
1502 1542 return "_list('%s')" % "\0".join(node.hex(a) for a in s)
1503 1543 elif t == 'b':
1504 1544 return "_list('%s')" % "\0".join(a.branch() for a in s)
1505 1545
1506 1546 m = l // 2
1507 1547 return '(%s or %s)' % (listexp(s[:m], t), listexp(s[m:], t))
1508 1548
1509 1549 ret = ''
1510 1550 pos = 0
1511 1551 arg = 0
1512 1552 while pos < len(expr):
1513 1553 c = expr[pos]
1514 1554 if c == '%':
1515 1555 pos += 1
1516 1556 d = expr[pos]
1517 1557 if d == '%':
1518 1558 ret += d
1519 1559 elif d in 'dsnbr':
1520 1560 ret += argtype(d, args[arg])
1521 1561 arg += 1
1522 1562 elif d == 'l':
1523 1563 # a list of some type
1524 1564 pos += 1
1525 1565 d = expr[pos]
1526 1566 ret += listexp(list(args[arg]), d)
1527 1567 arg += 1
1528 1568 else:
1529 1569 raise util.Abort('unexpected revspec format character %s' % d)
1530 1570 else:
1531 1571 ret += c
1532 1572 pos += 1
1533 1573
1534 1574 return ret
1535 1575
1536 1576 def prettyformat(tree):
1537 1577 def _prettyformat(tree, level, lines):
1538 1578 if not isinstance(tree, tuple) or tree[0] in ('string', 'symbol'):
1539 1579 lines.append((level, str(tree)))
1540 1580 else:
1541 1581 lines.append((level, '(%s' % tree[0]))
1542 1582 for s in tree[1:]:
1543 1583 _prettyformat(s, level + 1, lines)
1544 1584 lines[-1:] = [(lines[-1][0], lines[-1][1] + ')')]
1545 1585
1546 1586 lines = []
1547 1587 _prettyformat(tree, 0, lines)
1548 1588 output = '\n'.join((' '*l + s) for l, s in lines)
1549 1589 return output
1550 1590
1551 1591 # tell hggettext to extract docstrings from these functions:
1552 1592 i18nfunctions = symbols.values()
General Comments 0
You need to be logged in to leave comments. Login now