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