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