##// END OF EJS Templates
revset: balance %l or-expressions (issue3129)
Matt Mackall -
r15595:a585d78e stable
parent child Browse files
Show More
@@ -1,1135 +1,1141
1 1 # revset.py - revision set queries for mercurial
2 2 #
3 3 # Copyright 2010 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 import re
9 9 import parser, util, error, discovery, hbisect
10 10 import node as nodemod
11 11 import bookmarks as bookmarksmod
12 12 import match as matchmod
13 13 from i18n import _
14 14
15 15 elements = {
16 16 "(": (20, ("group", 1, ")"), ("func", 1, ")")),
17 17 "~": (18, None, ("ancestor", 18)),
18 18 "^": (18, None, ("parent", 18), ("parentpost", 18)),
19 19 "-": (5, ("negate", 19), ("minus", 5)),
20 20 "::": (17, ("dagrangepre", 17), ("dagrange", 17),
21 21 ("dagrangepost", 17)),
22 22 "..": (17, ("dagrangepre", 17), ("dagrange", 17),
23 23 ("dagrangepost", 17)),
24 24 ":": (15, ("rangepre", 15), ("range", 15), ("rangepost", 15)),
25 25 "not": (10, ("not", 10)),
26 26 "!": (10, ("not", 10)),
27 27 "and": (5, None, ("and", 5)),
28 28 "&": (5, None, ("and", 5)),
29 29 "or": (4, None, ("or", 4)),
30 30 "|": (4, None, ("or", 4)),
31 31 "+": (4, None, ("or", 4)),
32 32 ",": (2, None, ("list", 2)),
33 33 ")": (0, None, None),
34 34 "symbol": (0, ("symbol",), None),
35 35 "string": (0, ("string",), None),
36 36 "end": (0, None, None),
37 37 }
38 38
39 39 keywords = set(['and', 'or', 'not'])
40 40
41 41 def tokenize(program):
42 42 pos, l = 0, len(program)
43 43 while pos < l:
44 44 c = program[pos]
45 45 if c.isspace(): # skip inter-token whitespace
46 46 pass
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 == '.' and program[pos:pos + 2] == '..': # look ahead carefully
51 51 yield ('..', None, pos)
52 52 pos += 1 # skip ahead
53 53 elif c in "():,-|&+!~^": # handle simple operators
54 54 yield (c, None, pos)
55 55 elif (c in '"\'' or c == 'r' and
56 56 program[pos:pos + 2] in ("r'", 'r"')): # handle quoted strings
57 57 if c == 'r':
58 58 pos += 1
59 59 c = program[pos]
60 60 decode = lambda x: x
61 61 else:
62 62 decode = lambda x: x.decode('string-escape')
63 63 pos += 1
64 64 s = pos
65 65 while pos < l: # find closing quote
66 66 d = program[pos]
67 67 if d == '\\': # skip over escaped characters
68 68 pos += 2
69 69 continue
70 70 if d == c:
71 71 yield ('string', decode(program[s:pos]), s)
72 72 break
73 73 pos += 1
74 74 else:
75 75 raise error.ParseError(_("unterminated string"), s)
76 76 elif c.isalnum() or c in '._' or ord(c) > 127: # gather up a symbol/keyword
77 77 s = pos
78 78 pos += 1
79 79 while pos < l: # find end of symbol
80 80 d = program[pos]
81 81 if not (d.isalnum() or d in "._" or ord(d) > 127):
82 82 break
83 83 if d == '.' and program[pos - 1] == '.': # special case for ..
84 84 pos -= 1
85 85 break
86 86 pos += 1
87 87 sym = program[s:pos]
88 88 if sym in keywords: # operator keywords
89 89 yield (sym, None, s)
90 90 else:
91 91 yield ('symbol', sym, s)
92 92 pos -= 1
93 93 else:
94 94 raise error.ParseError(_("syntax error"), pos)
95 95 pos += 1
96 96 yield ('end', None, pos)
97 97
98 98 # helpers
99 99
100 100 def getstring(x, err):
101 101 if x and (x[0] == 'string' or x[0] == 'symbol'):
102 102 return x[1]
103 103 raise error.ParseError(err)
104 104
105 105 def getlist(x):
106 106 if not x:
107 107 return []
108 108 if x[0] == 'list':
109 109 return getlist(x[1]) + [x[2]]
110 110 return [x]
111 111
112 112 def getargs(x, min, max, err):
113 113 l = getlist(x)
114 114 if len(l) < min or len(l) > max:
115 115 raise error.ParseError(err)
116 116 return l
117 117
118 118 def getset(repo, subset, x):
119 119 if not x:
120 120 raise error.ParseError(_("missing argument"))
121 121 return methods[x[0]](repo, subset, *x[1:])
122 122
123 123 # operator methods
124 124
125 125 def stringset(repo, subset, x):
126 126 x = repo[x].rev()
127 127 if x == -1 and len(subset) == len(repo):
128 128 return [-1]
129 129 if len(subset) == len(repo) or x in subset:
130 130 return [x]
131 131 return []
132 132
133 133 def symbolset(repo, subset, x):
134 134 if x in symbols:
135 135 raise error.ParseError(_("can't use %s here") % x)
136 136 return stringset(repo, subset, x)
137 137
138 138 def rangeset(repo, subset, x, y):
139 139 m = getset(repo, subset, x)
140 140 if not m:
141 141 m = getset(repo, range(len(repo)), x)
142 142
143 143 n = getset(repo, subset, y)
144 144 if not n:
145 145 n = getset(repo, range(len(repo)), y)
146 146
147 147 if not m or not n:
148 148 return []
149 149 m, n = m[0], n[-1]
150 150
151 151 if m < n:
152 152 r = range(m, n + 1)
153 153 else:
154 154 r = range(m, n - 1, -1)
155 155 s = set(subset)
156 156 return [x for x in r if x in s]
157 157
158 158 def andset(repo, subset, x, y):
159 159 return getset(repo, getset(repo, subset, x), y)
160 160
161 161 def orset(repo, subset, x, y):
162 162 xl = getset(repo, subset, x)
163 163 s = set(xl)
164 164 yl = getset(repo, [r for r in subset if r not in s], y)
165 165 return xl + yl
166 166
167 167 def notset(repo, subset, x):
168 168 s = set(getset(repo, subset, x))
169 169 return [r for r in subset if r not in s]
170 170
171 171 def listset(repo, subset, a, b):
172 172 raise error.ParseError(_("can't use a list in this context"))
173 173
174 174 def func(repo, subset, a, b):
175 175 if a[0] == 'symbol' and a[1] in symbols:
176 176 return symbols[a[1]](repo, subset, b)
177 177 raise error.ParseError(_("not a function: %s") % a[1])
178 178
179 179 # functions
180 180
181 181 def adds(repo, subset, x):
182 182 """``adds(pattern)``
183 183 Changesets that add a file matching pattern.
184 184 """
185 185 # i18n: "adds" is a keyword
186 186 pat = getstring(x, _("adds requires a pattern"))
187 187 return checkstatus(repo, subset, pat, 1)
188 188
189 189 def ancestor(repo, subset, x):
190 190 """``ancestor(single, single)``
191 191 Greatest common ancestor of the two changesets.
192 192 """
193 193 # i18n: "ancestor" is a keyword
194 194 l = getargs(x, 2, 2, _("ancestor requires two arguments"))
195 195 r = range(len(repo))
196 196 a = getset(repo, r, l[0])
197 197 b = getset(repo, r, l[1])
198 198 if len(a) != 1 or len(b) != 1:
199 199 # i18n: "ancestor" is a keyword
200 200 raise error.ParseError(_("ancestor arguments must be single revisions"))
201 201 an = [repo[a[0]].ancestor(repo[b[0]]).rev()]
202 202
203 203 return [r for r in an if r in subset]
204 204
205 205 def ancestors(repo, subset, x):
206 206 """``ancestors(set)``
207 207 Changesets that are ancestors of a changeset in set.
208 208 """
209 209 args = getset(repo, range(len(repo)), x)
210 210 if not args:
211 211 return []
212 212 s = set(repo.changelog.ancestors(*args)) | set(args)
213 213 return [r for r in subset if r in s]
214 214
215 215 def ancestorspec(repo, subset, x, n):
216 216 """``set~n``
217 217 Changesets that are the Nth ancestor (first parents only) of a changeset in set.
218 218 """
219 219 try:
220 220 n = int(n[1])
221 221 except (TypeError, ValueError):
222 222 raise error.ParseError(_("~ expects a number"))
223 223 ps = set()
224 224 cl = repo.changelog
225 225 for r in getset(repo, subset, x):
226 226 for i in range(n):
227 227 r = cl.parentrevs(r)[0]
228 228 ps.add(r)
229 229 return [r for r in subset if r in ps]
230 230
231 231 def author(repo, subset, x):
232 232 """``author(string)``
233 233 Alias for ``user(string)``.
234 234 """
235 235 # i18n: "author" is a keyword
236 236 n = getstring(x, _("author requires a string")).lower()
237 237 return [r for r in subset if n in repo[r].user().lower()]
238 238
239 239 def bisect(repo, subset, x):
240 240 """``bisect(string)``
241 241 Changesets marked in the specified bisect status:
242 242
243 243 - ``good``, ``bad``, ``skip``: csets explicitly marked as good/bad/skip
244 244 - ``goods``, ``bads`` : csets topologicaly good/bad
245 245 - ``range`` : csets taking part in the bisection
246 246 - ``pruned`` : csets that are goods, bads or skipped
247 247 - ``untested`` : csets whose fate is yet unknown
248 248 - ``ignored`` : csets ignored due to DAG topology
249 249 """
250 250 status = getstring(x, _("bisect requires a string")).lower()
251 251 return [r for r in subset if r in hbisect.get(repo, status)]
252 252
253 253 # Backward-compatibility
254 254 # - no help entry so that we do not advertise it any more
255 255 def bisected(repo, subset, x):
256 256 return bisect(repo, subset, x)
257 257
258 258 def bookmark(repo, subset, x):
259 259 """``bookmark([name])``
260 260 The named bookmark or all bookmarks.
261 261 """
262 262 # i18n: "bookmark" is a keyword
263 263 args = getargs(x, 0, 1, _('bookmark takes one or no arguments'))
264 264 if args:
265 265 bm = getstring(args[0],
266 266 # i18n: "bookmark" is a keyword
267 267 _('the argument to bookmark must be a string'))
268 268 bmrev = bookmarksmod.listbookmarks(repo).get(bm, None)
269 269 if not bmrev:
270 270 raise util.Abort(_("bookmark '%s' does not exist") % bm)
271 271 bmrev = repo[bmrev].rev()
272 272 return [r for r in subset if r == bmrev]
273 273 bms = set([repo[r].rev()
274 274 for r in bookmarksmod.listbookmarks(repo).values()])
275 275 return [r for r in subset if r in bms]
276 276
277 277 def branch(repo, subset, x):
278 278 """``branch(string or set)``
279 279 All changesets belonging to the given branch or the branches of the given
280 280 changesets.
281 281 """
282 282 try:
283 283 b = getstring(x, '')
284 284 if b in repo.branchmap():
285 285 return [r for r in subset if repo[r].branch() == b]
286 286 except error.ParseError:
287 287 # not a string, but another revspec, e.g. tip()
288 288 pass
289 289
290 290 s = getset(repo, range(len(repo)), x)
291 291 b = set()
292 292 for r in s:
293 293 b.add(repo[r].branch())
294 294 s = set(s)
295 295 return [r for r in subset if r in s or repo[r].branch() in b]
296 296
297 297 def checkstatus(repo, subset, pat, field):
298 298 m = matchmod.match(repo.root, repo.getcwd(), [pat])
299 299 s = []
300 300 fast = (m.files() == [pat])
301 301 for r in subset:
302 302 c = repo[r]
303 303 if fast:
304 304 if pat not in c.files():
305 305 continue
306 306 else:
307 307 for f in c.files():
308 308 if m(f):
309 309 break
310 310 else:
311 311 continue
312 312 files = repo.status(c.p1().node(), c.node())[field]
313 313 if fast:
314 314 if pat in files:
315 315 s.append(r)
316 316 else:
317 317 for f in files:
318 318 if m(f):
319 319 s.append(r)
320 320 break
321 321 return s
322 322
323 323 def children(repo, subset, x):
324 324 """``children(set)``
325 325 Child changesets of changesets in set.
326 326 """
327 327 cs = set()
328 328 cl = repo.changelog
329 329 s = set(getset(repo, range(len(repo)), x))
330 330 for r in xrange(0, len(repo)):
331 331 for p in cl.parentrevs(r):
332 332 if p in s:
333 333 cs.add(r)
334 334 return [r for r in subset if r in cs]
335 335
336 336 def closed(repo, subset, x):
337 337 """``closed()``
338 338 Changeset is closed.
339 339 """
340 340 # i18n: "closed" is a keyword
341 341 getargs(x, 0, 0, _("closed takes no arguments"))
342 342 return [r for r in subset if repo[r].extra().get('close')]
343 343
344 344 def contains(repo, subset, x):
345 345 """``contains(pattern)``
346 346 Revision contains a file matching pattern. See :hg:`help patterns`
347 347 for information about file patterns.
348 348 """
349 349 # i18n: "contains" is a keyword
350 350 pat = getstring(x, _("contains requires a pattern"))
351 351 m = matchmod.match(repo.root, repo.getcwd(), [pat])
352 352 s = []
353 353 if m.files() == [pat]:
354 354 for r in subset:
355 355 if pat in repo[r]:
356 356 s.append(r)
357 357 else:
358 358 for r in subset:
359 359 for f in repo[r].manifest():
360 360 if m(f):
361 361 s.append(r)
362 362 break
363 363 return s
364 364
365 365 def date(repo, subset, x):
366 366 """``date(interval)``
367 367 Changesets within the interval, see :hg:`help dates`.
368 368 """
369 369 # i18n: "date" is a keyword
370 370 ds = getstring(x, _("date requires a string"))
371 371 dm = util.matchdate(ds)
372 372 return [r for r in subset if dm(repo[r].date()[0])]
373 373
374 374 def desc(repo, subset, x):
375 375 """``desc(string)``
376 376 Search commit message for string. The match is case-insensitive.
377 377 """
378 378 # i18n: "desc" is a keyword
379 379 ds = getstring(x, _("desc requires a string")).lower()
380 380 l = []
381 381 for r in subset:
382 382 c = repo[r]
383 383 if ds in c.description().lower():
384 384 l.append(r)
385 385 return l
386 386
387 387 def descendants(repo, subset, x):
388 388 """``descendants(set)``
389 389 Changesets which are descendants of changesets in set.
390 390 """
391 391 args = getset(repo, range(len(repo)), x)
392 392 if not args:
393 393 return []
394 394 s = set(repo.changelog.descendants(*args)) | set(args)
395 395 return [r for r in subset if r in s]
396 396
397 397 def filelog(repo, subset, x):
398 398 """``filelog(pattern)``
399 399 Changesets connected to the specified filelog.
400 400 """
401 401
402 402 pat = getstring(x, _("filelog requires a pattern"))
403 403 m = matchmod.match(repo.root, repo.getcwd(), [pat], default='relpath')
404 404 s = set()
405 405
406 406 if not m.anypats():
407 407 for f in m.files():
408 408 fl = repo.file(f)
409 409 for fr in fl:
410 410 s.add(fl.linkrev(fr))
411 411 else:
412 412 for f in repo[None]:
413 413 if m(f):
414 414 fl = repo.file(f)
415 415 for fr in fl:
416 416 s.add(fl.linkrev(fr))
417 417
418 418 return [r for r in subset if r in s]
419 419
420 420 def first(repo, subset, x):
421 421 """``first(set, [n])``
422 422 An alias for limit().
423 423 """
424 424 return limit(repo, subset, x)
425 425
426 426 def follow(repo, subset, x):
427 427 """``follow([file])``
428 428 An alias for ``::.`` (ancestors of the working copy's first parent).
429 429 If a filename is specified, the history of the given file is followed,
430 430 including copies.
431 431 """
432 432 # i18n: "follow" is a keyword
433 433 l = getargs(x, 0, 1, _("follow takes no arguments or a filename"))
434 434 p = repo['.'].rev()
435 435 if l:
436 436 x = getstring(l[0], _("follow expected a filename"))
437 437 if x in repo['.']:
438 438 s = set(ctx.rev() for ctx in repo['.'][x].ancestors())
439 439 else:
440 440 return []
441 441 else:
442 442 s = set(repo.changelog.ancestors(p))
443 443
444 444 s |= set([p])
445 445 return [r for r in subset if r in s]
446 446
447 447 def followfile(repo, subset, x):
448 448 """``follow()``
449 449 An alias for ``::.`` (ancestors of the working copy's first parent).
450 450 """
451 451 # i18n: "follow" is a keyword
452 452 getargs(x, 0, 0, _("follow takes no arguments"))
453 453 p = repo['.'].rev()
454 454 s = set(repo.changelog.ancestors(p)) | set([p])
455 455 return [r for r in subset if r in s]
456 456
457 457 def getall(repo, subset, x):
458 458 """``all()``
459 459 All changesets, the same as ``0:tip``.
460 460 """
461 461 # i18n: "all" is a keyword
462 462 getargs(x, 0, 0, _("all takes no arguments"))
463 463 return subset
464 464
465 465 def grep(repo, subset, x):
466 466 """``grep(regex)``
467 467 Like ``keyword(string)`` but accepts a regex. Use ``grep(r'...')``
468 468 to ensure special escape characters are handled correctly. Unlike
469 469 ``keyword(string)``, the match is case-sensitive.
470 470 """
471 471 try:
472 472 # i18n: "grep" is a keyword
473 473 gr = re.compile(getstring(x, _("grep requires a string")))
474 474 except re.error, e:
475 475 raise error.ParseError(_('invalid match pattern: %s') % e)
476 476 l = []
477 477 for r in subset:
478 478 c = repo[r]
479 479 for e in c.files() + [c.user(), c.description()]:
480 480 if gr.search(e):
481 481 l.append(r)
482 482 break
483 483 return l
484 484
485 485 def hasfile(repo, subset, x):
486 486 """``file(pattern)``
487 487 Changesets affecting files matched by pattern.
488 488 """
489 489 # i18n: "file" is a keyword
490 490 pat = getstring(x, _("file requires a pattern"))
491 491 m = matchmod.match(repo.root, repo.getcwd(), [pat])
492 492 s = []
493 493 for r in subset:
494 494 for f in repo[r].files():
495 495 if m(f):
496 496 s.append(r)
497 497 break
498 498 return s
499 499
500 500 def head(repo, subset, x):
501 501 """``head()``
502 502 Changeset is a named branch head.
503 503 """
504 504 # i18n: "head" is a keyword
505 505 getargs(x, 0, 0, _("head takes no arguments"))
506 506 hs = set()
507 507 for b, ls in repo.branchmap().iteritems():
508 508 hs.update(repo[h].rev() for h in ls)
509 509 return [r for r in subset if r in hs]
510 510
511 511 def heads(repo, subset, x):
512 512 """``heads(set)``
513 513 Members of set with no children in set.
514 514 """
515 515 s = getset(repo, subset, x)
516 516 ps = set(parents(repo, subset, x))
517 517 return [r for r in s if r not in ps]
518 518
519 519 def keyword(repo, subset, x):
520 520 """``keyword(string)``
521 521 Search commit message, user name, and names of changed files for
522 522 string. The match is case-insensitive.
523 523 """
524 524 # i18n: "keyword" is a keyword
525 525 kw = getstring(x, _("keyword requires a string")).lower()
526 526 l = []
527 527 for r in subset:
528 528 c = repo[r]
529 529 t = " ".join(c.files() + [c.user(), c.description()])
530 530 if kw in t.lower():
531 531 l.append(r)
532 532 return l
533 533
534 534 def limit(repo, subset, x):
535 535 """``limit(set, [n])``
536 536 First n members of set, defaulting to 1.
537 537 """
538 538 # i18n: "limit" is a keyword
539 539 l = getargs(x, 1, 2, _("limit requires one or two arguments"))
540 540 try:
541 541 lim = 1
542 542 if len(l) == 2:
543 543 # i18n: "limit" is a keyword
544 544 lim = int(getstring(l[1], _("limit requires a number")))
545 545 except (TypeError, ValueError):
546 546 # i18n: "limit" is a keyword
547 547 raise error.ParseError(_("limit expects a number"))
548 548 ss = set(subset)
549 549 os = getset(repo, range(len(repo)), l[0])[:lim]
550 550 return [r for r in os if r in ss]
551 551
552 552 def last(repo, subset, x):
553 553 """``last(set, [n])``
554 554 Last n members of set, defaulting to 1.
555 555 """
556 556 # i18n: "last" is a keyword
557 557 l = getargs(x, 1, 2, _("last requires one or two arguments"))
558 558 try:
559 559 lim = 1
560 560 if len(l) == 2:
561 561 # i18n: "last" is a keyword
562 562 lim = int(getstring(l[1], _("last requires a number")))
563 563 except (TypeError, ValueError):
564 564 # i18n: "last" is a keyword
565 565 raise error.ParseError(_("last expects a number"))
566 566 ss = set(subset)
567 567 os = getset(repo, range(len(repo)), l[0])[-lim:]
568 568 return [r for r in os if r in ss]
569 569
570 570 def maxrev(repo, subset, x):
571 571 """``max(set)``
572 572 Changeset with highest revision number in set.
573 573 """
574 574 os = getset(repo, range(len(repo)), x)
575 575 if os:
576 576 m = max(os)
577 577 if m in subset:
578 578 return [m]
579 579 return []
580 580
581 581 def merge(repo, subset, x):
582 582 """``merge()``
583 583 Changeset is a merge changeset.
584 584 """
585 585 # i18n: "merge" is a keyword
586 586 getargs(x, 0, 0, _("merge takes no arguments"))
587 587 cl = repo.changelog
588 588 return [r for r in subset if cl.parentrevs(r)[1] != -1]
589 589
590 590 def minrev(repo, subset, x):
591 591 """``min(set)``
592 592 Changeset with lowest revision number in set.
593 593 """
594 594 os = getset(repo, range(len(repo)), x)
595 595 if os:
596 596 m = min(os)
597 597 if m in subset:
598 598 return [m]
599 599 return []
600 600
601 601 def modifies(repo, subset, x):
602 602 """``modifies(pattern)``
603 603 Changesets modifying files matched by pattern.
604 604 """
605 605 # i18n: "modifies" is a keyword
606 606 pat = getstring(x, _("modifies requires a pattern"))
607 607 return checkstatus(repo, subset, pat, 0)
608 608
609 609 def node(repo, subset, x):
610 610 """``id(string)``
611 611 Revision non-ambiguously specified by the given hex string prefix.
612 612 """
613 613 # i18n: "id" is a keyword
614 614 l = getargs(x, 1, 1, _("id requires one argument"))
615 615 # i18n: "id" is a keyword
616 616 n = getstring(l[0], _("id requires a string"))
617 617 if len(n) == 40:
618 618 rn = repo[n].rev()
619 619 else:
620 620 rn = repo.changelog.rev(repo.changelog._partialmatch(n))
621 621 return [r for r in subset if r == rn]
622 622
623 623 def outgoing(repo, subset, x):
624 624 """``outgoing([path])``
625 625 Changesets not found in the specified destination repository, or the
626 626 default push location.
627 627 """
628 628 import hg # avoid start-up nasties
629 629 # i18n: "outgoing" is a keyword
630 630 l = getargs(x, 0, 1, _("outgoing takes one or no arguments"))
631 631 # i18n: "outgoing" is a keyword
632 632 dest = l and getstring(l[0], _("outgoing requires a repository path")) or ''
633 633 dest = repo.ui.expandpath(dest or 'default-push', dest or 'default')
634 634 dest, branches = hg.parseurl(dest)
635 635 revs, checkout = hg.addbranchrevs(repo, repo, branches, [])
636 636 if revs:
637 637 revs = [repo.lookup(rev) for rev in revs]
638 638 other = hg.peer(repo, {}, dest)
639 639 repo.ui.pushbuffer()
640 640 common, outheads = discovery.findcommonoutgoing(repo, other, onlyheads=revs)
641 641 repo.ui.popbuffer()
642 642 cl = repo.changelog
643 643 o = set([cl.rev(r) for r in repo.changelog.findmissing(common, outheads)])
644 644 return [r for r in subset if r in o]
645 645
646 646 def p1(repo, subset, x):
647 647 """``p1([set])``
648 648 First parent of changesets in set, or the working directory.
649 649 """
650 650 if x is None:
651 651 p = repo[x].p1().rev()
652 652 return [r for r in subset if r == p]
653 653
654 654 ps = set()
655 655 cl = repo.changelog
656 656 for r in getset(repo, range(len(repo)), x):
657 657 ps.add(cl.parentrevs(r)[0])
658 658 return [r for r in subset if r in ps]
659 659
660 660 def p2(repo, subset, x):
661 661 """``p2([set])``
662 662 Second parent of changesets in set, or the working directory.
663 663 """
664 664 if x is None:
665 665 ps = repo[x].parents()
666 666 try:
667 667 p = ps[1].rev()
668 668 return [r for r in subset if r == p]
669 669 except IndexError:
670 670 return []
671 671
672 672 ps = set()
673 673 cl = repo.changelog
674 674 for r in getset(repo, range(len(repo)), x):
675 675 ps.add(cl.parentrevs(r)[1])
676 676 return [r for r in subset if r in ps]
677 677
678 678 def parents(repo, subset, x):
679 679 """``parents([set])``
680 680 The set of all parents for all changesets in set, or the working directory.
681 681 """
682 682 if x is None:
683 683 ps = tuple(p.rev() for p in repo[x].parents())
684 684 return [r for r in subset if r in ps]
685 685
686 686 ps = set()
687 687 cl = repo.changelog
688 688 for r in getset(repo, range(len(repo)), x):
689 689 ps.update(cl.parentrevs(r))
690 690 return [r for r in subset if r in ps]
691 691
692 692 def parentspec(repo, subset, x, n):
693 693 """``set^0``
694 694 The set.
695 695 ``set^1`` (or ``set^``), ``set^2``
696 696 First or second parent, respectively, of all changesets in set.
697 697 """
698 698 try:
699 699 n = int(n[1])
700 700 if n not in (0, 1, 2):
701 701 raise ValueError
702 702 except (TypeError, ValueError):
703 703 raise error.ParseError(_("^ expects a number 0, 1, or 2"))
704 704 ps = set()
705 705 cl = repo.changelog
706 706 for r in getset(repo, subset, x):
707 707 if n == 0:
708 708 ps.add(r)
709 709 elif n == 1:
710 710 ps.add(cl.parentrevs(r)[0])
711 711 elif n == 2:
712 712 parents = cl.parentrevs(r)
713 713 if len(parents) > 1:
714 714 ps.add(parents[1])
715 715 return [r for r in subset if r in ps]
716 716
717 717 def present(repo, subset, x):
718 718 """``present(set)``
719 719 An empty set, if any revision in set isn't found; otherwise,
720 720 all revisions in set.
721 721 """
722 722 try:
723 723 return getset(repo, subset, x)
724 724 except error.RepoLookupError:
725 725 return []
726 726
727 727 def removes(repo, subset, x):
728 728 """``removes(pattern)``
729 729 Changesets which remove files matching pattern.
730 730 """
731 731 # i18n: "removes" is a keyword
732 732 pat = getstring(x, _("removes requires a pattern"))
733 733 return checkstatus(repo, subset, pat, 2)
734 734
735 735 def rev(repo, subset, x):
736 736 """``rev(number)``
737 737 Revision with the given numeric identifier.
738 738 """
739 739 # i18n: "rev" is a keyword
740 740 l = getargs(x, 1, 1, _("rev requires one argument"))
741 741 try:
742 742 # i18n: "rev" is a keyword
743 743 l = int(getstring(l[0], _("rev requires a number")))
744 744 except (TypeError, ValueError):
745 745 # i18n: "rev" is a keyword
746 746 raise error.ParseError(_("rev expects a number"))
747 747 return [r for r in subset if r == l]
748 748
749 749 def reverse(repo, subset, x):
750 750 """``reverse(set)``
751 751 Reverse order of set.
752 752 """
753 753 l = getset(repo, subset, x)
754 754 l.reverse()
755 755 return l
756 756
757 757 def roots(repo, subset, x):
758 758 """``roots(set)``
759 759 Changesets with no parent changeset in set.
760 760 """
761 761 s = getset(repo, subset, x)
762 762 cs = set(children(repo, subset, x))
763 763 return [r for r in s if r not in cs]
764 764
765 765 def sort(repo, subset, x):
766 766 """``sort(set[, [-]key...])``
767 767 Sort set by keys. The default sort order is ascending, specify a key
768 768 as ``-key`` to sort in descending order.
769 769
770 770 The keys can be:
771 771
772 772 - ``rev`` for the revision number,
773 773 - ``branch`` for the branch name,
774 774 - ``desc`` for the commit message (description),
775 775 - ``user`` for user name (``author`` can be used as an alias),
776 776 - ``date`` for the commit date
777 777 """
778 778 # i18n: "sort" is a keyword
779 779 l = getargs(x, 1, 2, _("sort requires one or two arguments"))
780 780 keys = "rev"
781 781 if len(l) == 2:
782 782 keys = getstring(l[1], _("sort spec must be a string"))
783 783
784 784 s = l[0]
785 785 keys = keys.split()
786 786 l = []
787 787 def invert(s):
788 788 return "".join(chr(255 - ord(c)) for c in s)
789 789 for r in getset(repo, subset, s):
790 790 c = repo[r]
791 791 e = []
792 792 for k in keys:
793 793 if k == 'rev':
794 794 e.append(r)
795 795 elif k == '-rev':
796 796 e.append(-r)
797 797 elif k == 'branch':
798 798 e.append(c.branch())
799 799 elif k == '-branch':
800 800 e.append(invert(c.branch()))
801 801 elif k == 'desc':
802 802 e.append(c.description())
803 803 elif k == '-desc':
804 804 e.append(invert(c.description()))
805 805 elif k in 'user author':
806 806 e.append(c.user())
807 807 elif k in '-user -author':
808 808 e.append(invert(c.user()))
809 809 elif k == 'date':
810 810 e.append(c.date()[0])
811 811 elif k == '-date':
812 812 e.append(-c.date()[0])
813 813 else:
814 814 raise error.ParseError(_("unknown sort key %r") % k)
815 815 e.append(r)
816 816 l.append(e)
817 817 l.sort()
818 818 return [e[-1] for e in l]
819 819
820 820 def tag(repo, subset, x):
821 821 """``tag([name])``
822 822 The specified tag by name, or all tagged revisions if no name is given.
823 823 """
824 824 # i18n: "tag" is a keyword
825 825 args = getargs(x, 0, 1, _("tag takes one or no arguments"))
826 826 cl = repo.changelog
827 827 if args:
828 828 tn = getstring(args[0],
829 829 # i18n: "tag" is a keyword
830 830 _('the argument to tag must be a string'))
831 831 if not repo.tags().get(tn, None):
832 832 raise util.Abort(_("tag '%s' does not exist") % tn)
833 833 s = set([cl.rev(n) for t, n in repo.tagslist() if t == tn])
834 834 else:
835 835 s = set([cl.rev(n) for t, n in repo.tagslist() if t != 'tip'])
836 836 return [r for r in subset if r in s]
837 837
838 838 def tagged(repo, subset, x):
839 839 return tag(repo, subset, x)
840 840
841 841 def user(repo, subset, x):
842 842 """``user(string)``
843 843 User name contains string. The match is case-insensitive.
844 844 """
845 845 return author(repo, subset, x)
846 846
847 847 symbols = {
848 848 "adds": adds,
849 849 "all": getall,
850 850 "ancestor": ancestor,
851 851 "ancestors": ancestors,
852 852 "author": author,
853 853 "bisect": bisect,
854 854 "bisected": bisected,
855 855 "bookmark": bookmark,
856 856 "branch": branch,
857 857 "children": children,
858 858 "closed": closed,
859 859 "contains": contains,
860 860 "date": date,
861 861 "desc": desc,
862 862 "descendants": descendants,
863 863 "file": hasfile,
864 864 "filelog": filelog,
865 865 "first": first,
866 866 "follow": follow,
867 867 "grep": grep,
868 868 "head": head,
869 869 "heads": heads,
870 870 "id": node,
871 871 "keyword": keyword,
872 872 "last": last,
873 873 "limit": limit,
874 874 "max": maxrev,
875 875 "merge": merge,
876 876 "min": minrev,
877 877 "modifies": modifies,
878 878 "outgoing": outgoing,
879 879 "p1": p1,
880 880 "p2": p2,
881 881 "parents": parents,
882 882 "present": present,
883 883 "removes": removes,
884 884 "rev": rev,
885 885 "reverse": reverse,
886 886 "roots": roots,
887 887 "sort": sort,
888 888 "tag": tag,
889 889 "tagged": tagged,
890 890 "user": user,
891 891 }
892 892
893 893 methods = {
894 894 "range": rangeset,
895 895 "string": stringset,
896 896 "symbol": symbolset,
897 897 "and": andset,
898 898 "or": orset,
899 899 "not": notset,
900 900 "list": listset,
901 901 "func": func,
902 902 "ancestor": ancestorspec,
903 903 "parent": parentspec,
904 904 "parentpost": p1,
905 905 }
906 906
907 907 def optimize(x, small):
908 908 if x is None:
909 909 return 0, x
910 910
911 911 smallbonus = 1
912 912 if small:
913 913 smallbonus = .5
914 914
915 915 op = x[0]
916 916 if op == 'minus':
917 917 return optimize(('and', x[1], ('not', x[2])), small)
918 918 elif op == 'dagrange':
919 919 return optimize(('and', ('func', ('symbol', 'descendants'), x[1]),
920 920 ('func', ('symbol', 'ancestors'), x[2])), small)
921 921 elif op == 'dagrangepre':
922 922 return optimize(('func', ('symbol', 'ancestors'), x[1]), small)
923 923 elif op == 'dagrangepost':
924 924 return optimize(('func', ('symbol', 'descendants'), x[1]), small)
925 925 elif op == 'rangepre':
926 926 return optimize(('range', ('string', '0'), x[1]), small)
927 927 elif op == 'rangepost':
928 928 return optimize(('range', x[1], ('string', 'tip')), small)
929 929 elif op == 'negate':
930 930 return optimize(('string',
931 931 '-' + getstring(x[1], _("can't negate that"))), small)
932 932 elif op in 'string symbol negate':
933 933 return smallbonus, x # single revisions are small
934 934 elif op == 'and' or op == 'dagrange':
935 935 wa, ta = optimize(x[1], True)
936 936 wb, tb = optimize(x[2], True)
937 937 w = min(wa, wb)
938 938 if wa > wb:
939 939 return w, (op, tb, ta)
940 940 return w, (op, ta, tb)
941 941 elif op == 'or':
942 942 wa, ta = optimize(x[1], False)
943 943 wb, tb = optimize(x[2], False)
944 944 if wb < wa:
945 945 wb, wa = wa, wb
946 946 return max(wa, wb), (op, ta, tb)
947 947 elif op == 'not':
948 948 o = optimize(x[1], not small)
949 949 return o[0], (op, o[1])
950 950 elif op == 'parentpost':
951 951 o = optimize(x[1], small)
952 952 return o[0], (op, o[1])
953 953 elif op == 'group':
954 954 return optimize(x[1], small)
955 955 elif op in 'range list parent ancestorspec':
956 956 if op == 'parent':
957 957 # x^:y means (x^) : y, not x ^ (:y)
958 958 post = ('parentpost', x[1])
959 959 if x[2][0] == 'dagrangepre':
960 960 return optimize(('dagrange', post, x[2][1]), small)
961 961 elif x[2][0] == 'rangepre':
962 962 return optimize(('range', post, x[2][1]), small)
963 963
964 964 wa, ta = optimize(x[1], small)
965 965 wb, tb = optimize(x[2], small)
966 966 return wa + wb, (op, ta, tb)
967 967 elif op == 'func':
968 968 f = getstring(x[1], _("not a symbol"))
969 969 wa, ta = optimize(x[2], small)
970 970 if f in ("author branch closed date desc file grep keyword "
971 971 "outgoing user"):
972 972 w = 10 # slow
973 973 elif f in "modifies adds removes":
974 974 w = 30 # slower
975 975 elif f == "contains":
976 976 w = 100 # very slow
977 977 elif f == "ancestor":
978 978 w = 1 * smallbonus
979 979 elif f in "reverse limit first":
980 980 w = 0
981 981 elif f in "sort":
982 982 w = 10 # assume most sorts look at changelog
983 983 else:
984 984 w = 1
985 985 return w + wa, (op, x[1], ta)
986 986 return 1, x
987 987
988 988 class revsetalias(object):
989 989 funcre = re.compile('^([^(]+)\(([^)]+)\)$')
990 990 args = None
991 991
992 992 def __init__(self, name, value):
993 993 '''Aliases like:
994 994
995 995 h = heads(default)
996 996 b($1) = ancestors($1) - ancestors(default)
997 997 '''
998 998 if isinstance(name, tuple): # parameter substitution
999 999 self.tree = name
1000 1000 self.replacement = value
1001 1001 else: # alias definition
1002 1002 m = self.funcre.search(name)
1003 1003 if m:
1004 1004 self.tree = ('func', ('symbol', m.group(1)))
1005 1005 self.args = [x.strip() for x in m.group(2).split(',')]
1006 1006 for arg in self.args:
1007 1007 value = value.replace(arg, repr(arg))
1008 1008 else:
1009 1009 self.tree = ('symbol', name)
1010 1010
1011 1011 self.replacement, pos = parse(value)
1012 1012 if pos != len(value):
1013 1013 raise error.ParseError(_('invalid token'), pos)
1014 1014
1015 1015 def process(self, tree):
1016 1016 if isinstance(tree, tuple):
1017 1017 if self.args is None:
1018 1018 if tree == self.tree:
1019 1019 return self.replacement
1020 1020 elif tree[:2] == self.tree:
1021 1021 l = getlist(tree[2])
1022 1022 if len(l) != len(self.args):
1023 1023 raise error.ParseError(
1024 1024 _('invalid number of arguments: %s') % len(l))
1025 1025 result = self.replacement
1026 1026 for a, v in zip(self.args, l):
1027 1027 valalias = revsetalias(('string', a), v)
1028 1028 result = valalias.process(result)
1029 1029 return result
1030 1030 return tuple(map(self.process, tree))
1031 1031 return tree
1032 1032
1033 1033 def findaliases(ui, tree):
1034 1034 for k, v in ui.configitems('revsetalias'):
1035 1035 alias = revsetalias(k, v)
1036 1036 tree = alias.process(tree)
1037 1037 return tree
1038 1038
1039 1039 parse = parser.parser(tokenize, elements).parse
1040 1040
1041 1041 def match(ui, spec):
1042 1042 if not spec:
1043 1043 raise error.ParseError(_("empty query"))
1044 1044 tree, pos = parse(spec)
1045 1045 if (pos != len(spec)):
1046 1046 raise error.ParseError(_("invalid token"), pos)
1047 1047 if ui:
1048 1048 tree = findaliases(ui, tree)
1049 1049 weight, tree = optimize(tree, True)
1050 1050 def mfunc(repo, subset):
1051 1051 return getset(repo, subset, tree)
1052 1052 return mfunc
1053 1053
1054 1054 def formatspec(expr, *args):
1055 1055 '''
1056 1056 This is a convenience function for using revsets internally, and
1057 1057 escapes arguments appropriately. Aliases are intentionally ignored
1058 1058 so that intended expression behavior isn't accidentally subverted.
1059 1059
1060 1060 Supported arguments:
1061 1061
1062 1062 %r = revset expression, parenthesized
1063 1063 %d = int(arg), no quoting
1064 1064 %s = string(arg), escaped and single-quoted
1065 1065 %b = arg.branch(), escaped and single-quoted
1066 1066 %n = hex(arg), single-quoted
1067 1067 %% = a literal '%'
1068 1068
1069 1069 Prefixing the type with 'l' specifies a parenthesized list of that type.
1070 1070
1071 1071 >>> formatspec('%r:: and %lr', '10 or 11', ("this()", "that()"))
1072 1072 '(10 or 11):: and ((this()) or (that()))'
1073 1073 >>> formatspec('%d:: and not %d::', 10, 20)
1074 1074 '10:: and not 20::'
1075 1075 >>> formatspec('%ld or %ld', [], [1])
1076 '(0-0) or (1)'
1076 '(0-0) or 1'
1077 1077 >>> formatspec('keyword(%s)', 'foo\\xe9')
1078 1078 "keyword('foo\\\\xe9')"
1079 1079 >>> b = lambda: 'default'
1080 1080 >>> b.branch = b
1081 1081 >>> formatspec('branch(%b)', b)
1082 1082 "branch('default')"
1083 1083 >>> formatspec('root(%ls)', ['a', 'b', 'c', 'd'])
1084 "root(('a' or 'b' or 'c' or 'd'))"
1084 "root((('a' or 'b') or ('c' or 'd')))"
1085 1085 '''
1086 1086
1087 1087 def quote(s):
1088 1088 return repr(str(s))
1089 1089
1090 1090 def argtype(c, arg):
1091 1091 if c == 'd':
1092 1092 return str(int(arg))
1093 1093 elif c == 's':
1094 1094 return quote(arg)
1095 1095 elif c == 'r':
1096 1096 parse(arg) # make sure syntax errors are confined
1097 1097 return '(%s)' % arg
1098 1098 elif c == 'n':
1099 1099 return quote(nodemod.hex(arg))
1100 1100 elif c == 'b':
1101 1101 return quote(arg.branch())
1102 1102
1103 def listexp(s, t):
1104 "balance a list s of type t to limit parse tree depth"
1105 l = len(s)
1106 if l == 0:
1107 return '(0-0)' # a minimal way to represent an empty set
1108 if l == 1:
1109 return argtype(t, s[0])
1110 m = l / 2
1111 return '(%s or %s)' % (listexp(s[:m], t), listexp(s[m:], t))
1112
1103 1113 ret = ''
1104 1114 pos = 0
1105 1115 arg = 0
1106 1116 while pos < len(expr):
1107 1117 c = expr[pos]
1108 1118 if c == '%':
1109 1119 pos += 1
1110 1120 d = expr[pos]
1111 1121 if d == '%':
1112 1122 ret += d
1113 1123 elif d in 'dsnbr':
1114 1124 ret += argtype(d, args[arg])
1115 1125 arg += 1
1116 1126 elif d == 'l':
1117 1127 # a list of some type
1118 1128 pos += 1
1119 1129 d = expr[pos]
1120 if args[arg]:
1121 lv = ' or '.join(argtype(d, e) for e in args[arg])
1122 else:
1123 lv = '0-0' # a minimal way to represent an empty set
1124 ret += '(%s)' % lv
1130 ret += listexp(args[arg], d)
1125 1131 arg += 1
1126 1132 else:
1127 1133 raise util.Abort('unexpected revspec format character %s' % d)
1128 1134 else:
1129 1135 ret += c
1130 1136 pos += 1
1131 1137
1132 1138 return ret
1133 1139
1134 1140 # tell hggettext to extract docstrings from these functions:
1135 1141 i18nfunctions = symbols.values()
General Comments 0
You need to be logged in to leave comments. Login now