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