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