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