##// END OF EJS Templates
revsets: preserve ordering with the or operator...
Augie Fackler -
r13932:34f57700 default
parent child Browse files
Show More
@@ -1,844 +1,845
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, help, hbisect
10 10 import bookmarks as bookmarksmod
11 11 import match as matchmod
12 12 from i18n import _
13 13
14 14 elements = {
15 15 "(": (20, ("group", 1, ")"), ("func", 1, ")")),
16 16 "-": (5, ("negate", 19), ("minus", 5)),
17 17 "::": (17, ("dagrangepre", 17), ("dagrange", 17),
18 18 ("dagrangepost", 17)),
19 19 "..": (17, ("dagrangepre", 17), ("dagrange", 17),
20 20 ("dagrangepost", 17)),
21 21 ":": (15, ("rangepre", 15), ("range", 15), ("rangepost", 15)),
22 22 "not": (10, ("not", 10)),
23 23 "!": (10, ("not", 10)),
24 24 "and": (5, None, ("and", 5)),
25 25 "&": (5, None, ("and", 5)),
26 26 "or": (4, None, ("or", 4)),
27 27 "|": (4, None, ("or", 4)),
28 28 "+": (4, None, ("or", 4)),
29 29 ",": (2, None, ("list", 2)),
30 30 ")": (0, None, None),
31 31 "symbol": (0, ("symbol",), None),
32 32 "string": (0, ("string",), None),
33 33 "end": (0, None, None),
34 34 }
35 35
36 36 keywords = set(['and', 'or', 'not'])
37 37
38 38 def tokenize(program):
39 39 pos, l = 0, len(program)
40 40 while pos < l:
41 41 c = program[pos]
42 42 if c.isspace(): # skip inter-token whitespace
43 43 pass
44 44 elif c == ':' and program[pos:pos + 2] == '::': # look ahead carefully
45 45 yield ('::', None, pos)
46 46 pos += 1 # skip ahead
47 47 elif c == '.' and program[pos:pos + 2] == '..': # look ahead carefully
48 48 yield ('..', None, pos)
49 49 pos += 1 # skip ahead
50 50 elif c in "():,-|&+!": # handle simple operators
51 51 yield (c, None, pos)
52 52 elif (c in '"\'' or c == 'r' and
53 53 program[pos:pos + 2] in ("r'", 'r"')): # handle quoted strings
54 54 if c == 'r':
55 55 pos += 1
56 56 c = program[pos]
57 57 decode = lambda x: x
58 58 else:
59 59 decode = lambda x: x.decode('string-escape')
60 60 pos += 1
61 61 s = pos
62 62 while pos < l: # find closing quote
63 63 d = program[pos]
64 64 if d == '\\': # skip over escaped characters
65 65 pos += 2
66 66 continue
67 67 if d == c:
68 68 yield ('string', decode(program[s:pos]), s)
69 69 break
70 70 pos += 1
71 71 else:
72 72 raise error.ParseError(_("unterminated string"), s)
73 73 elif c.isalnum() or c in '._' or ord(c) > 127: # gather up a symbol/keyword
74 74 s = pos
75 75 pos += 1
76 76 while pos < l: # find end of symbol
77 77 d = program[pos]
78 78 if not (d.isalnum() or d in "._" or ord(d) > 127):
79 79 break
80 80 if d == '.' and program[pos - 1] == '.': # special case for ..
81 81 pos -= 1
82 82 break
83 83 pos += 1
84 84 sym = program[s:pos]
85 85 if sym in keywords: # operator keywords
86 86 yield (sym, None, s)
87 87 else:
88 88 yield ('symbol', sym, s)
89 89 pos -= 1
90 90 else:
91 91 raise error.ParseError(_("syntax error"), pos)
92 92 pos += 1
93 93 yield ('end', None, pos)
94 94
95 95 # helpers
96 96
97 97 def getstring(x, err):
98 98 if x and (x[0] == 'string' or x[0] == 'symbol'):
99 99 return x[1]
100 100 raise error.ParseError(err)
101 101
102 102 def getlist(x):
103 103 if not x:
104 104 return []
105 105 if x[0] == 'list':
106 106 return getlist(x[1]) + [x[2]]
107 107 return [x]
108 108
109 109 def getargs(x, min, max, err):
110 110 l = getlist(x)
111 111 if len(l) < min or len(l) > max:
112 112 raise error.ParseError(err)
113 113 return l
114 114
115 115 def getset(repo, subset, x):
116 116 if not x:
117 117 raise error.ParseError(_("missing argument"))
118 118 return methods[x[0]](repo, subset, *x[1:])
119 119
120 120 # operator methods
121 121
122 122 def stringset(repo, subset, x):
123 123 x = repo[x].rev()
124 124 if x == -1 and len(subset) == len(repo):
125 125 return [-1]
126 126 if x in subset:
127 127 return [x]
128 128 return []
129 129
130 130 def symbolset(repo, subset, x):
131 131 if x in symbols:
132 132 raise error.ParseError(_("can't use %s here") % x)
133 133 return stringset(repo, subset, x)
134 134
135 135 def rangeset(repo, subset, x, y):
136 136 m = getset(repo, subset, x)
137 137 if not m:
138 138 m = getset(repo, range(len(repo)), x)
139 139
140 140 n = getset(repo, subset, y)
141 141 if not n:
142 142 n = getset(repo, range(len(repo)), y)
143 143
144 144 if not m or not n:
145 145 return []
146 146 m, n = m[0], n[-1]
147 147
148 148 if m < n:
149 149 r = range(m, n + 1)
150 150 else:
151 151 r = range(m, n - 1, -1)
152 152 s = set(subset)
153 153 return [x for x in r if x in s]
154 154
155 155 def andset(repo, subset, x, y):
156 156 return getset(repo, getset(repo, subset, x), y)
157 157
158 158 def orset(repo, subset, x, y):
159 s = set(getset(repo, subset, x))
160 s |= set(getset(repo, [r for r in subset if r not in s], y))
161 return [r for r in subset if r in s]
159 xl = getset(repo, subset, x)
160 s = set(xl)
161 yl = getset(repo, [r for r in subset if r not in s], y)
162 return xl + yl
162 163
163 164 def notset(repo, subset, x):
164 165 s = set(getset(repo, subset, x))
165 166 return [r for r in subset if r not in s]
166 167
167 168 def listset(repo, subset, a, b):
168 169 raise error.ParseError(_("can't use a list in this context"))
169 170
170 171 def func(repo, subset, a, b):
171 172 if a[0] == 'symbol' and a[1] in symbols:
172 173 return symbols[a[1]](repo, subset, b)
173 174 raise error.ParseError(_("not a function: %s") % a[1])
174 175
175 176 # functions
176 177
177 178 def adds(repo, subset, x):
178 179 """``adds(pattern)``
179 180 Changesets that add a file matching pattern.
180 181 """
181 182 # i18n: "adds" is a keyword
182 183 pat = getstring(x, _("adds requires a pattern"))
183 184 return checkstatus(repo, subset, pat, 1)
184 185
185 186 def ancestor(repo, subset, x):
186 187 """``ancestor(single, single)``
187 188 Greatest common ancestor of the two changesets.
188 189 """
189 190 # i18n: "ancestor" is a keyword
190 191 l = getargs(x, 2, 2, _("ancestor requires two arguments"))
191 192 r = range(len(repo))
192 193 a = getset(repo, r, l[0])
193 194 b = getset(repo, r, l[1])
194 195 if len(a) != 1 or len(b) != 1:
195 196 # i18n: "ancestor" is a keyword
196 197 raise error.ParseError(_("ancestor arguments must be single revisions"))
197 198 an = [repo[a[0]].ancestor(repo[b[0]]).rev()]
198 199
199 200 return [r for r in an if r in subset]
200 201
201 202 def ancestors(repo, subset, x):
202 203 """``ancestors(set)``
203 204 Changesets that are ancestors of a changeset in set.
204 205 """
205 206 args = getset(repo, range(len(repo)), x)
206 207 if not args:
207 208 return []
208 209 s = set(repo.changelog.ancestors(*args)) | set(args)
209 210 return [r for r in subset if r in s]
210 211
211 212 def author(repo, subset, x):
212 213 """``author(string)``
213 214 Alias for ``user(string)``.
214 215 """
215 216 # i18n: "author" is a keyword
216 217 n = getstring(x, _("author requires a string")).lower()
217 218 return [r for r in subset if n in repo[r].user().lower()]
218 219
219 220 def bisected(repo, subset, x):
220 221 """``bisected(string)``
221 222 Changesets marked in the specified bisect state (good, bad, skip).
222 223 """
223 224 state = getstring(x, _("bisect requires a string")).lower()
224 225 if state not in ('good', 'bad', 'skip', 'unknown'):
225 226 raise ParseError(_('invalid bisect state'))
226 227 marked = set(repo.changelog.rev(n) for n in hbisect.load_state(repo)[state])
227 228 return [r for r in subset if r in marked]
228 229
229 230 def bookmark(repo, subset, x):
230 231 """``bookmark([name])``
231 232 The named bookmark or all bookmarks.
232 233 """
233 234 # i18n: "bookmark" is a keyword
234 235 args = getargs(x, 0, 1, _('bookmark takes one or no arguments'))
235 236 if args:
236 237 bm = getstring(args[0],
237 238 # i18n: "bookmark" is a keyword
238 239 _('the argument to bookmark must be a string'))
239 240 bmrev = bookmarksmod.listbookmarks(repo).get(bm, None)
240 241 if not bmrev:
241 242 raise util.Abort(_("bookmark '%s' does not exist") % bm)
242 243 bmrev = repo[bmrev].rev()
243 244 return [r for r in subset if r == bmrev]
244 245 bms = set([repo[r].rev()
245 246 for r in bookmarksmod.listbookmarks(repo).values()])
246 247 return [r for r in subset if r in bms]
247 248
248 249 def branch(repo, subset, x):
249 250 """``branch(string or set)``
250 251 All changesets belonging to the given branch or the branches of the given
251 252 changesets.
252 253 """
253 254 try:
254 255 b = getstring(x, '')
255 256 if b in repo.branchmap():
256 257 return [r for r in subset if repo[r].branch() == b]
257 258 except error.ParseError:
258 259 # not a string, but another revspec, e.g. tip()
259 260 pass
260 261
261 262 s = getset(repo, range(len(repo)), x)
262 263 b = set()
263 264 for r in s:
264 265 b.add(repo[r].branch())
265 266 s = set(s)
266 267 return [r for r in subset if r in s or repo[r].branch() in b]
267 268
268 269 def checkstatus(repo, subset, pat, field):
269 270 m = matchmod.match(repo.root, repo.getcwd(), [pat])
270 271 s = []
271 272 fast = (m.files() == [pat])
272 273 for r in subset:
273 274 c = repo[r]
274 275 if fast:
275 276 if pat not in c.files():
276 277 continue
277 278 else:
278 279 for f in c.files():
279 280 if m(f):
280 281 break
281 282 else:
282 283 continue
283 284 files = repo.status(c.p1().node(), c.node())[field]
284 285 if fast:
285 286 if pat in files:
286 287 s.append(r)
287 288 else:
288 289 for f in files:
289 290 if m(f):
290 291 s.append(r)
291 292 break
292 293 return s
293 294
294 295 def children(repo, subset, x):
295 296 """``children(set)``
296 297 Child changesets of changesets in set.
297 298 """
298 299 cs = set()
299 300 cl = repo.changelog
300 301 s = set(getset(repo, range(len(repo)), x))
301 302 for r in xrange(0, len(repo)):
302 303 for p in cl.parentrevs(r):
303 304 if p in s:
304 305 cs.add(r)
305 306 return [r for r in subset if r in cs]
306 307
307 308 def closed(repo, subset, x):
308 309 """``closed()``
309 310 Changeset is closed.
310 311 """
311 312 # i18n: "closed" is a keyword
312 313 getargs(x, 0, 0, _("closed takes no arguments"))
313 314 return [r for r in subset if repo[r].extra().get('close')]
314 315
315 316 def contains(repo, subset, x):
316 317 """``contains(pattern)``
317 318 Revision contains pattern.
318 319 """
319 320 # i18n: "contains" is a keyword
320 321 pat = getstring(x, _("contains requires a pattern"))
321 322 m = matchmod.match(repo.root, repo.getcwd(), [pat])
322 323 s = []
323 324 if m.files() == [pat]:
324 325 for r in subset:
325 326 if pat in repo[r]:
326 327 s.append(r)
327 328 else:
328 329 for r in subset:
329 330 for f in repo[r].manifest():
330 331 if m(f):
331 332 s.append(r)
332 333 break
333 334 return s
334 335
335 336 def date(repo, subset, x):
336 337 """``date(interval)``
337 338 Changesets within the interval, see :hg:`help dates`.
338 339 """
339 340 # i18n: "date" is a keyword
340 341 ds = getstring(x, _("date requires a string"))
341 342 dm = util.matchdate(ds)
342 343 return [r for r in subset if dm(repo[r].date()[0])]
343 344
344 345 def descendants(repo, subset, x):
345 346 """``descendants(set)``
346 347 Changesets which are descendants of changesets in set.
347 348 """
348 349 args = getset(repo, range(len(repo)), x)
349 350 if not args:
350 351 return []
351 352 s = set(repo.changelog.descendants(*args)) | set(args)
352 353 return [r for r in subset if r in s]
353 354
354 355 def follow(repo, subset, x):
355 356 """``follow()``
356 357 An alias for ``::.`` (ancestors of the working copy's first parent).
357 358 """
358 359 # i18n: "follow" is a keyword
359 360 getargs(x, 0, 0, _("follow takes no arguments"))
360 361 p = repo['.'].rev()
361 362 s = set(repo.changelog.ancestors(p)) | set([p])
362 363 return [r for r in subset if r in s]
363 364
364 365 def getall(repo, subset, x):
365 366 """``all()``
366 367 All changesets, the same as ``0:tip``.
367 368 """
368 369 # i18n: "all" is a keyword
369 370 getargs(x, 0, 0, _("all takes no arguments"))
370 371 return subset
371 372
372 373 def grep(repo, subset, x):
373 374 """``grep(regex)``
374 375 Like ``keyword(string)`` but accepts a regex. Use ``grep(r'...')``
375 376 to ensure special escape characters are handled correctly.
376 377 """
377 378 try:
378 379 # i18n: "grep" is a keyword
379 380 gr = re.compile(getstring(x, _("grep requires a string")))
380 381 except re.error, e:
381 382 raise error.ParseError(_('invalid match pattern: %s') % e)
382 383 l = []
383 384 for r in subset:
384 385 c = repo[r]
385 386 for e in c.files() + [c.user(), c.description()]:
386 387 if gr.search(e):
387 388 l.append(r)
388 389 break
389 390 return l
390 391
391 392 def hasfile(repo, subset, x):
392 393 """``file(pattern)``
393 394 Changesets affecting files matched by pattern.
394 395 """
395 396 # i18n: "file" is a keyword
396 397 pat = getstring(x, _("file requires a pattern"))
397 398 m = matchmod.match(repo.root, repo.getcwd(), [pat])
398 399 s = []
399 400 for r in subset:
400 401 for f in repo[r].files():
401 402 if m(f):
402 403 s.append(r)
403 404 break
404 405 return s
405 406
406 407 def head(repo, subset, x):
407 408 """``head()``
408 409 Changeset is a named branch head.
409 410 """
410 411 # i18n: "head" is a keyword
411 412 getargs(x, 0, 0, _("head takes no arguments"))
412 413 hs = set()
413 414 for b, ls in repo.branchmap().iteritems():
414 415 hs.update(repo[h].rev() for h in ls)
415 416 return [r for r in subset if r in hs]
416 417
417 418 def heads(repo, subset, x):
418 419 """``heads(set)``
419 420 Members of set with no children in set.
420 421 """
421 422 s = getset(repo, subset, x)
422 423 ps = set(parents(repo, subset, x))
423 424 return [r for r in s if r not in ps]
424 425
425 426 def keyword(repo, subset, x):
426 427 """``keyword(string)``
427 428 Search commit message, user name, and names of changed files for
428 429 string.
429 430 """
430 431 # i18n: "keyword" is a keyword
431 432 kw = getstring(x, _("keyword requires a string")).lower()
432 433 l = []
433 434 for r in subset:
434 435 c = repo[r]
435 436 t = " ".join(c.files() + [c.user(), c.description()])
436 437 if kw in t.lower():
437 438 l.append(r)
438 439 return l
439 440
440 441 def limit(repo, subset, x):
441 442 """``limit(set, n)``
442 443 First n members of set.
443 444 """
444 445 # i18n: "limit" is a keyword
445 446 l = getargs(x, 2, 2, _("limit requires two arguments"))
446 447 try:
447 448 # i18n: "limit" is a keyword
448 449 lim = int(getstring(l[1], _("limit requires a number")))
449 450 except ValueError:
450 451 # i18n: "limit" is a keyword
451 452 raise error.ParseError(_("limit expects a number"))
452 453 return getset(repo, subset, l[0])[:lim]
453 454
454 455 def maxrev(repo, subset, x):
455 456 """``max(set)``
456 457 Changeset with highest revision number in set.
457 458 """
458 459 s = getset(repo, subset, x)
459 460 if s:
460 461 m = max(s)
461 462 if m in subset:
462 463 return [m]
463 464 return []
464 465
465 466 def merge(repo, subset, x):
466 467 """``merge()``
467 468 Changeset is a merge changeset.
468 469 """
469 470 # i18n: "merge" is a keyword
470 471 getargs(x, 0, 0, _("merge takes no arguments"))
471 472 cl = repo.changelog
472 473 return [r for r in subset if cl.parentrevs(r)[1] != -1]
473 474
474 475 def minrev(repo, subset, x):
475 476 """``min(set)``
476 477 Changeset with lowest revision number in set.
477 478 """
478 479 s = getset(repo, subset, x)
479 480 if s:
480 481 m = min(s)
481 482 if m in subset:
482 483 return [m]
483 484 return []
484 485
485 486 def modifies(repo, subset, x):
486 487 """``modifies(pattern)``
487 488 Changesets modifying files matched by pattern.
488 489 """
489 490 # i18n: "modifies" is a keyword
490 491 pat = getstring(x, _("modifies requires a pattern"))
491 492 return checkstatus(repo, subset, pat, 0)
492 493
493 494 def node(repo, subset, x):
494 495 """``id(string)``
495 496 Revision non-ambiguously specified by the given hex string prefix.
496 497 """
497 498 # i18n: "id" is a keyword
498 499 l = getargs(x, 1, 1, _("id requires one argument"))
499 500 # i18n: "id" is a keyword
500 501 n = getstring(l[0], _("id requires a string"))
501 502 if len(n) == 40:
502 503 rn = repo[n].rev()
503 504 else:
504 505 rn = repo.changelog.rev(repo.changelog._partialmatch(n))
505 506 return [r for r in subset if r == rn]
506 507
507 508 def outgoing(repo, subset, x):
508 509 """``outgoing([path])``
509 510 Changesets not found in the specified destination repository, or the
510 511 default push location.
511 512 """
512 513 import hg # avoid start-up nasties
513 514 # i18n: "outgoing" is a keyword
514 515 l = getargs(x, 0, 1, _("outgoing requires a repository path"))
515 516 # i18n: "outgoing" is a keyword
516 517 dest = l and getstring(l[0], _("outgoing requires a repository path")) or ''
517 518 dest = repo.ui.expandpath(dest or 'default-push', dest or 'default')
518 519 dest, branches = hg.parseurl(dest)
519 520 revs, checkout = hg.addbranchrevs(repo, repo, branches, [])
520 521 if revs:
521 522 revs = [repo.lookup(rev) for rev in revs]
522 523 other = hg.repository(hg.remoteui(repo, {}), dest)
523 524 repo.ui.pushbuffer()
524 525 o = discovery.findoutgoing(repo, other)
525 526 repo.ui.popbuffer()
526 527 cl = repo.changelog
527 528 o = set([cl.rev(r) for r in repo.changelog.nodesbetween(o, revs)[0]])
528 529 return [r for r in subset if r in o]
529 530
530 531 def p1(repo, subset, x):
531 532 """``p1([set])``
532 533 First parent of changesets in set, or the working directory.
533 534 """
534 535 if x is None:
535 536 p = repo[x].p1().rev()
536 537 return [r for r in subset if r == p]
537 538
538 539 ps = set()
539 540 cl = repo.changelog
540 541 for r in getset(repo, range(len(repo)), x):
541 542 ps.add(cl.parentrevs(r)[0])
542 543 return [r for r in subset if r in ps]
543 544
544 545 def p2(repo, subset, x):
545 546 """``p2([set])``
546 547 Second parent of changesets in set, or the working directory.
547 548 """
548 549 if x is None:
549 550 ps = repo[x].parents()
550 551 try:
551 552 p = ps[1].rev()
552 553 return [r for r in subset if r == p]
553 554 except IndexError:
554 555 return []
555 556
556 557 ps = set()
557 558 cl = repo.changelog
558 559 for r in getset(repo, range(len(repo)), x):
559 560 ps.add(cl.parentrevs(r)[1])
560 561 return [r for r in subset if r in ps]
561 562
562 563 def parents(repo, subset, x):
563 564 """``parents([set])``
564 565 The set of all parents for all changesets in set, or the working directory.
565 566 """
566 567 if x is None:
567 568 ps = tuple(p.rev() for p in repo[x].parents())
568 569 return [r for r in subset if r in ps]
569 570
570 571 ps = set()
571 572 cl = repo.changelog
572 573 for r in getset(repo, range(len(repo)), x):
573 574 ps.update(cl.parentrevs(r))
574 575 return [r for r in subset if r in ps]
575 576
576 577 def present(repo, subset, x):
577 578 """``present(set)``
578 579 An empty set, if any revision in set isn't found; otherwise,
579 580 all revisions in set.
580 581 """
581 582 try:
582 583 return getset(repo, subset, x)
583 584 except error.RepoLookupError:
584 585 return []
585 586
586 587 def removes(repo, subset, x):
587 588 """``removes(pattern)``
588 589 Changesets which remove files matching pattern.
589 590 """
590 591 # i18n: "removes" is a keyword
591 592 pat = getstring(x, _("removes requires a pattern"))
592 593 return checkstatus(repo, subset, pat, 2)
593 594
594 595 def rev(repo, subset, x):
595 596 """``rev(number)``
596 597 Revision with the given numeric identifier.
597 598 """
598 599 # i18n: "rev" is a keyword
599 600 l = getargs(x, 1, 1, _("rev requires one argument"))
600 601 try:
601 602 # i18n: "rev" is a keyword
602 603 l = int(getstring(l[0], _("rev requires a number")))
603 604 except ValueError:
604 605 # i18n: "rev" is a keyword
605 606 raise error.ParseError(_("rev expects a number"))
606 607 return [r for r in subset if r == l]
607 608
608 609 def reverse(repo, subset, x):
609 610 """``reverse(set)``
610 611 Reverse order of set.
611 612 """
612 613 l = getset(repo, subset, x)
613 614 l.reverse()
614 615 return l
615 616
616 617 def roots(repo, subset, x):
617 618 """``roots(set)``
618 619 Changesets with no parent changeset in set.
619 620 """
620 621 s = getset(repo, subset, x)
621 622 cs = set(children(repo, subset, x))
622 623 return [r for r in s if r not in cs]
623 624
624 625 def sort(repo, subset, x):
625 626 """``sort(set[, [-]key...])``
626 627 Sort set by keys. The default sort order is ascending, specify a key
627 628 as ``-key`` to sort in descending order.
628 629
629 630 The keys can be:
630 631
631 632 - ``rev`` for the revision number,
632 633 - ``branch`` for the branch name,
633 634 - ``desc`` for the commit message (description),
634 635 - ``user`` for user name (``author`` can be used as an alias),
635 636 - ``date`` for the commit date
636 637 """
637 638 # i18n: "sort" is a keyword
638 639 l = getargs(x, 1, 2, _("sort requires one or two arguments"))
639 640 keys = "rev"
640 641 if len(l) == 2:
641 642 keys = getstring(l[1], _("sort spec must be a string"))
642 643
643 644 s = l[0]
644 645 keys = keys.split()
645 646 l = []
646 647 def invert(s):
647 648 return "".join(chr(255 - ord(c)) for c in s)
648 649 for r in getset(repo, subset, s):
649 650 c = repo[r]
650 651 e = []
651 652 for k in keys:
652 653 if k == 'rev':
653 654 e.append(r)
654 655 elif k == '-rev':
655 656 e.append(-r)
656 657 elif k == 'branch':
657 658 e.append(c.branch())
658 659 elif k == '-branch':
659 660 e.append(invert(c.branch()))
660 661 elif k == 'desc':
661 662 e.append(c.description())
662 663 elif k == '-desc':
663 664 e.append(invert(c.description()))
664 665 elif k in 'user author':
665 666 e.append(c.user())
666 667 elif k in '-user -author':
667 668 e.append(invert(c.user()))
668 669 elif k == 'date':
669 670 e.append(c.date()[0])
670 671 elif k == '-date':
671 672 e.append(-c.date()[0])
672 673 else:
673 674 raise error.ParseError(_("unknown sort key %r") % k)
674 675 e.append(r)
675 676 l.append(e)
676 677 l.sort()
677 678 return [e[-1] for e in l]
678 679
679 680 def tag(repo, subset, x):
680 681 """``tag(name)``
681 682 The specified tag by name, or all tagged revisions if no name is given.
682 683 """
683 684 # i18n: "tag" is a keyword
684 685 args = getargs(x, 0, 1, _("tag takes one or no arguments"))
685 686 cl = repo.changelog
686 687 if args:
687 688 tn = getstring(args[0],
688 689 # i18n: "tag" is a keyword
689 690 _('the argument to tag must be a string'))
690 691 if not repo.tags().get(tn, None):
691 692 raise util.Abort(_("tag '%s' does not exist") % tn)
692 693 s = set([cl.rev(n) for t, n in repo.tagslist() if t == tn])
693 694 else:
694 695 s = set([cl.rev(n) for t, n in repo.tagslist() if t != 'tip'])
695 696 return [r for r in subset if r in s]
696 697
697 698 def tagged(repo, subset, x):
698 699 return tag(repo, subset, x)
699 700
700 701 def user(repo, subset, x):
701 702 """``user(string)``
702 703 User name is string.
703 704 """
704 705 return author(repo, subset, x)
705 706
706 707 symbols = {
707 708 "adds": adds,
708 709 "all": getall,
709 710 "ancestor": ancestor,
710 711 "ancestors": ancestors,
711 712 "author": author,
712 713 "bisected": bisected,
713 714 "bookmark": bookmark,
714 715 "branch": branch,
715 716 "children": children,
716 717 "closed": closed,
717 718 "contains": contains,
718 719 "date": date,
719 720 "descendants": descendants,
720 721 "file": hasfile,
721 722 "follow": follow,
722 723 "grep": grep,
723 724 "head": head,
724 725 "heads": heads,
725 726 "keyword": keyword,
726 727 "limit": limit,
727 728 "max": maxrev,
728 729 "min": minrev,
729 730 "merge": merge,
730 731 "modifies": modifies,
731 732 "id": node,
732 733 "outgoing": outgoing,
733 734 "p1": p1,
734 735 "p2": p2,
735 736 "parents": parents,
736 737 "present": present,
737 738 "removes": removes,
738 739 "reverse": reverse,
739 740 "rev": rev,
740 741 "roots": roots,
741 742 "sort": sort,
742 743 "tag": tag,
743 744 "tagged": tagged,
744 745 "user": user,
745 746 }
746 747
747 748 methods = {
748 749 "range": rangeset,
749 750 "string": stringset,
750 751 "symbol": symbolset,
751 752 "and": andset,
752 753 "or": orset,
753 754 "not": notset,
754 755 "list": listset,
755 756 "func": func,
756 757 }
757 758
758 759 def optimize(x, small):
759 760 if x is None:
760 761 return 0, x
761 762
762 763 smallbonus = 1
763 764 if small:
764 765 smallbonus = .5
765 766
766 767 op = x[0]
767 768 if op == 'minus':
768 769 return optimize(('and', x[1], ('not', x[2])), small)
769 770 elif op == 'dagrange':
770 771 return optimize(('and', ('func', ('symbol', 'descendants'), x[1]),
771 772 ('func', ('symbol', 'ancestors'), x[2])), small)
772 773 elif op == 'dagrangepre':
773 774 return optimize(('func', ('symbol', 'ancestors'), x[1]), small)
774 775 elif op == 'dagrangepost':
775 776 return optimize(('func', ('symbol', 'descendants'), x[1]), small)
776 777 elif op == 'rangepre':
777 778 return optimize(('range', ('string', '0'), x[1]), small)
778 779 elif op == 'rangepost':
779 780 return optimize(('range', x[1], ('string', 'tip')), small)
780 781 elif op == 'negate':
781 782 return optimize(('string',
782 783 '-' + getstring(x[1], _("can't negate that"))), small)
783 784 elif op in 'string symbol negate':
784 785 return smallbonus, x # single revisions are small
785 786 elif op == 'and' or op == 'dagrange':
786 787 wa, ta = optimize(x[1], True)
787 788 wb, tb = optimize(x[2], True)
788 789 w = min(wa, wb)
789 790 if wa > wb:
790 791 return w, (op, tb, ta)
791 792 return w, (op, ta, tb)
792 793 elif op == 'or':
793 794 wa, ta = optimize(x[1], False)
794 795 wb, tb = optimize(x[2], False)
795 796 if wb < wa:
796 797 wb, wa = wa, wb
797 798 return max(wa, wb), (op, ta, tb)
798 799 elif op == 'not':
799 800 o = optimize(x[1], not small)
800 801 return o[0], (op, o[1])
801 802 elif op == 'group':
802 803 return optimize(x[1], small)
803 804 elif op in 'range list':
804 805 wa, ta = optimize(x[1], small)
805 806 wb, tb = optimize(x[2], small)
806 807 return wa + wb, (op, ta, tb)
807 808 elif op == 'func':
808 809 f = getstring(x[1], _("not a symbol"))
809 810 wa, ta = optimize(x[2], small)
810 811 if f in "grep date user author keyword branch file outgoing closed":
811 812 w = 10 # slow
812 813 elif f in "modifies adds removes":
813 814 w = 30 # slower
814 815 elif f == "contains":
815 816 w = 100 # very slow
816 817 elif f == "ancestor":
817 818 w = 1 * smallbonus
818 819 elif f in "reverse limit":
819 820 w = 0
820 821 elif f in "sort":
821 822 w = 10 # assume most sorts look at changelog
822 823 else:
823 824 w = 1
824 825 return w + wa, (op, x[1], ta)
825 826 return 1, x
826 827
827 828 parse = parser.parser(tokenize, elements).parse
828 829
829 830 def match(spec):
830 831 if not spec:
831 832 raise error.ParseError(_("empty query"))
832 833 tree, pos = parse(spec)
833 834 if (pos != len(spec)):
834 835 raise error.ParseError("invalid token", pos)
835 836 weight, tree = optimize(tree, True)
836 837 def mfunc(repo, subset):
837 838 return getset(repo, subset, tree)
838 839 return mfunc
839 840
840 841 def makedoc(topic, doc):
841 842 return help.makeitemsdoc(topic, doc, '.. predicatesmarker', symbols)
842 843
843 844 # tell hggettext to extract docstrings from these functions:
844 845 i18nfunctions = symbols.values()
@@ -1,371 +1,376
1 1 $ HGENCODING=utf-8
2 2 $ export HGENCODING
3 3
4 4 $ try() {
5 5 > hg debugrevspec --debug $@
6 6 > }
7 7
8 8 $ log() {
9 9 > hg log --template '{rev}\n' -r "$1"
10 10 > }
11 11
12 12 $ hg init repo
13 13 $ cd repo
14 14
15 15 $ echo a > a
16 16 $ hg branch a
17 17 marked working directory as branch a
18 18 $ hg ci -Aqm0
19 19
20 20 $ echo b > b
21 21 $ hg branch b
22 22 marked working directory as branch b
23 23 $ hg ci -Aqm1
24 24
25 25 $ rm a
26 26 $ hg branch a-b-c-
27 27 marked working directory as branch a-b-c-
28 28 $ hg ci -Aqm2 -u Bob
29 29
30 30 $ hg co 1
31 31 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
32 32 $ hg branch +a+b+c+
33 33 marked working directory as branch +a+b+c+
34 34 $ hg ci -Aqm3
35 35
36 36 $ hg co 2 # interleave
37 37 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
38 38 $ echo bb > b
39 39 $ hg branch -- -a-b-c-
40 40 marked working directory as branch -a-b-c-
41 41 $ hg ci -Aqm4 -d "May 12 2005"
42 42
43 43 $ hg co 3
44 44 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
45 45 $ hg branch /a/b/c/
46 46 marked working directory as branch /a/b/c/
47 47 $ hg ci -Aqm"5 bug"
48 48
49 49 $ hg merge 4
50 50 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
51 51 (branch merge, don't forget to commit)
52 52 $ hg branch _a_b_c_
53 53 marked working directory as branch _a_b_c_
54 54 $ hg ci -Aqm"6 issue619"
55 55
56 56 $ hg branch .a.b.c.
57 57 marked working directory as branch .a.b.c.
58 58 $ hg ci -Aqm7
59 59
60 60 $ hg branch all
61 61 marked working directory as branch all
62 62 $ hg ci --close-branch -Aqm8
63 63 abort: can only close branch heads
64 64 [255]
65 65
66 66 $ hg co 4
67 67 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
68 68 $ hg branch é
69 69 marked working directory as branch \xc3\xa9 (esc)
70 70 $ hg ci -Aqm9
71 71
72 72 $ hg tag -r6 1.0
73 73
74 74 $ hg clone --quiet -U -r 7 . ../remote1
75 75 $ hg clone --quiet -U -r 8 . ../remote2
76 76 $ echo "[paths]" >> .hg/hgrc
77 77 $ echo "default = ../remote1" >> .hg/hgrc
78 78
79 79 names that should work without quoting
80 80
81 81 $ try a
82 82 ('symbol', 'a')
83 83 0
84 84 $ try b-a
85 85 ('minus', ('symbol', 'b'), ('symbol', 'a'))
86 86 1
87 87 $ try _a_b_c_
88 88 ('symbol', '_a_b_c_')
89 89 6
90 90 $ try _a_b_c_-a
91 91 ('minus', ('symbol', '_a_b_c_'), ('symbol', 'a'))
92 92 6
93 93 $ try .a.b.c.
94 94 ('symbol', '.a.b.c.')
95 95 7
96 96 $ try .a.b.c.-a
97 97 ('minus', ('symbol', '.a.b.c.'), ('symbol', 'a'))
98 98 7
99 99 $ try -- '-a-b-c-' # complains
100 100 hg: parse error at 7: not a prefix: end
101 101 [255]
102 102 $ log -a-b-c- # succeeds with fallback
103 103 4
104 104 $ try -- -a-b-c--a # complains
105 105 ('minus', ('minus', ('minus', ('negate', ('symbol', 'a')), ('symbol', 'b')), ('symbol', 'c')), ('negate', ('symbol', 'a')))
106 106 abort: unknown revision '-a'!
107 107 [255]
108 108 $ try é
109 109 ('symbol', '\xc3\xa9')
110 110 9
111 111
112 112 quoting needed
113 113
114 114 $ try '"-a-b-c-"-a'
115 115 ('minus', ('string', '-a-b-c-'), ('symbol', 'a'))
116 116 4
117 117
118 118 $ log '1 or 2'
119 119 1
120 120 2
121 121 $ log '1|2'
122 122 1
123 123 2
124 124 $ log '1 and 2'
125 125 $ log '1&2'
126 126 $ try '1&2|3' # precedence - and is higher
127 127 ('or', ('and', ('symbol', '1'), ('symbol', '2')), ('symbol', '3'))
128 128 3
129 129 $ try '1|2&3'
130 130 ('or', ('symbol', '1'), ('and', ('symbol', '2'), ('symbol', '3')))
131 131 1
132 132 $ try '1&2&3' # associativity
133 133 ('and', ('and', ('symbol', '1'), ('symbol', '2')), ('symbol', '3'))
134 134 $ try '1|(2|3)'
135 135 ('or', ('symbol', '1'), ('group', ('or', ('symbol', '2'), ('symbol', '3'))))
136 136 1
137 137 2
138 138 3
139 139 $ log '1.0' # tag
140 140 6
141 141 $ log 'a' # branch
142 142 0
143 143 $ log '2785f51ee'
144 144 0
145 145 $ log 'date(2005)'
146 146 4
147 147 $ log 'date(this is a test)'
148 148 hg: parse error at 10: unexpected token: symbol
149 149 [255]
150 150 $ log 'date()'
151 151 hg: parse error: date requires a string
152 152 [255]
153 153 $ log 'date'
154 154 hg: parse error: can't use date here
155 155 [255]
156 156 $ log 'date('
157 157 hg: parse error at 5: not a prefix: end
158 158 [255]
159 159 $ log 'date(tip)'
160 160 abort: invalid date: 'tip'
161 161 [255]
162 162 $ log '"date"'
163 163 abort: unknown revision 'date'!
164 164 [255]
165 165 $ log 'date(2005) and 1::'
166 166 4
167 167
168 168 $ log 'ancestor(1)'
169 169 hg: parse error: ancestor requires two arguments
170 170 [255]
171 171 $ log 'ancestor(4,5)'
172 172 1
173 173 $ log 'ancestor(4,5) and 4'
174 174 $ log 'ancestors(5)'
175 175 0
176 176 1
177 177 3
178 178 5
179 179 $ log 'author(bob)'
180 180 2
181 181 $ log 'branch(é)'
182 182 8
183 183 9
184 184 $ log 'children(ancestor(4,5))'
185 185 2
186 186 3
187 187 $ log 'closed()'
188 188 $ log 'contains(a)'
189 189 0
190 190 1
191 191 3
192 192 5
193 193 $ log 'descendants(2 or 3)'
194 194 2
195 195 3
196 196 4
197 197 5
198 198 6
199 199 7
200 200 8
201 201 9
202 202 $ log 'file(b)'
203 203 1
204 204 4
205 205 $ log 'follow()'
206 206 0
207 207 1
208 208 2
209 209 4
210 210 8
211 211 9
212 212 $ log 'grep("issue\d+")'
213 213 6
214 214 $ try 'grep("(")' # invalid regular expression
215 215 ('func', ('symbol', 'grep'), ('string', '('))
216 216 hg: parse error: invalid match pattern: unbalanced parenthesis
217 217 [255]
218 218 $ try 'grep("\bissue\d+")'
219 219 ('func', ('symbol', 'grep'), ('string', '\x08issue\\d+'))
220 220 $ try 'grep(r"\bissue\d+")'
221 221 ('func', ('symbol', 'grep'), ('string', '\\bissue\\d+'))
222 222 6
223 223 $ try 'grep(r"\")'
224 224 hg: parse error at 7: unterminated string
225 225 [255]
226 226 $ log 'head()'
227 227 0
228 228 1
229 229 2
230 230 3
231 231 4
232 232 5
233 233 6
234 234 7
235 235 9
236 236 $ log 'heads(6::)'
237 237 7
238 238 $ log 'keyword(issue)'
239 239 6
240 240 $ log 'limit(head(), 1)'
241 241 0
242 242 $ log 'max(contains(a))'
243 243 5
244 244 $ log 'min(contains(a))'
245 245 0
246 246 $ log 'merge()'
247 247 6
248 248 $ log 'modifies(b)'
249 249 4
250 250 $ log 'id(5)'
251 251 2
252 252 $ log 'outgoing()'
253 253 8
254 254 9
255 255 $ log 'outgoing("../remote1")'
256 256 8
257 257 9
258 258 $ log 'outgoing("../remote2")'
259 259 3
260 260 5
261 261 6
262 262 7
263 263 9
264 264 $ log 'p1(merge())'
265 265 5
266 266 $ log 'p2(merge())'
267 267 4
268 268 $ log 'parents(merge())'
269 269 4
270 270 5
271 271 $ log 'removes(a)'
272 272 2
273 273 6
274 274 $ log 'roots(all())'
275 275 0
276 276 $ log 'reverse(2 or 3 or 4 or 5)'
277 277 5
278 278 4
279 279 3
280 280 2
281 281 $ log 'rev(5)'
282 282 5
283 283 $ log 'sort(limit(reverse(all()), 3))'
284 284 7
285 285 8
286 286 9
287 287 $ log 'sort(2 or 3 or 4 or 5, date)'
288 288 2
289 289 3
290 290 5
291 291 4
292 292 $ log 'tagged()'
293 293 6
294 294 $ log 'tag()'
295 295 6
296 296 $ log 'tag(1.0)'
297 297 6
298 298 $ log 'tag(tip)'
299 299 9
300 300 $ log 'tag(unknown)'
301 301 abort: tag 'unknown' does not exist
302 302 [255]
303 303 $ log 'branch(unknown)'
304 304 abort: unknown revision 'unknown'!
305 305 [255]
306 306 $ log 'user(bob)'
307 307 2
308 308
309 309 $ log '4::8'
310 310 4
311 311 8
312 312 $ log '4:8'
313 313 4
314 314 5
315 315 6
316 316 7
317 317 8
318 318
319 319 $ log 'sort(!merge() & (modifies(b) | user(bob) | keyword(bug) | keyword(issue) & 1::9), "-date")'
320 320 4
321 321 2
322 322 5
323 323
324 324 $ log 'not 0 and 0:2'
325 325 1
326 326 2
327 327 $ log 'not 1 and 0:2'
328 328 0
329 329 2
330 330 $ log 'not 2 and 0:2'
331 331 0
332 332 1
333 333 $ log '(1 and 2)::'
334 334 $ log '(1 and 2):'
335 335 $ log '(1 and 2):3'
336 336 $ log 'sort(head(), -rev)'
337 337 9
338 338 7
339 339 6
340 340 5
341 341 4
342 342 3
343 343 2
344 344 1
345 345 0
346 346 $ log '4::8 - 8'
347 347 4
348 348
349 349 issue2437
350 350
351 351 $ log '3 and p1(5)'
352 352 3
353 353 $ log '4 and p2(6)'
354 354 4
355 355 $ log '1 and parents(:2)'
356 356 1
357 357 $ log '2 and children(1:)'
358 358 2
359 359 $ log 'roots(all()) or roots(all())'
360 360 0
361 361 $ log 'heads(branch(é)) or heads(branch(é))'
362 362 9
363 363 $ log 'ancestors(8) and (heads(branch("-a-b-c-")) or heads(branch(é)))'
364 364 4
365 365
366 366 issue2654: report a parse error if the revset was not completely parsed
367 367
368 368 $ log '1 OR 2'
369 369 hg: parse error at 2: invalid token
370 370 [255]
371 371
372 or operator should preserve ordering:
373 $ log 'reverse(2::4) or tip'
374 4
375 2
376 9
General Comments 0
You need to be logged in to leave comments. Login now