##// END OF EJS Templates
revset: add formatspec convenience query builder
Matt Mackall -
r14901:a773119f default
parent child Browse files
Show More
@@ -1,1030 +1,1087 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 import parser, util, error, discovery, hbisect
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 410 def follow(repo, subset, x):
411 411 """``follow([file])``
412 412 An alias for ``::.`` (ancestors of the working copy's first parent).
413 413 If a filename is specified, the history of the given file is followed,
414 414 including copies.
415 415 """
416 416 # i18n: "follow" is a keyword
417 417 l = getargs(x, 0, 1, _("follow takes no arguments or a filename"))
418 418 p = repo['.'].rev()
419 419 if l:
420 420 x = getstring(l[0], _("follow expected a filename"))
421 421 s = set(ctx.rev() for ctx in repo['.'][x].ancestors())
422 422 else:
423 423 s = set(repo.changelog.ancestors(p))
424 424
425 425 s |= set([p])
426 426 return [r for r in subset if r in s]
427 427
428 428 def followfile(repo, subset, x):
429 429 """``follow()``
430 430 An alias for ``::.`` (ancestors of the working copy's first parent).
431 431 """
432 432 # i18n: "follow" is a keyword
433 433 getargs(x, 0, 0, _("follow takes no arguments"))
434 434 p = repo['.'].rev()
435 435 s = set(repo.changelog.ancestors(p)) | set([p])
436 436 return [r for r in subset if r in s]
437 437
438 438 def getall(repo, subset, x):
439 439 """``all()``
440 440 All changesets, the same as ``0:tip``.
441 441 """
442 442 # i18n: "all" is a keyword
443 443 getargs(x, 0, 0, _("all takes no arguments"))
444 444 return subset
445 445
446 446 def grep(repo, subset, x):
447 447 """``grep(regex)``
448 448 Like ``keyword(string)`` but accepts a regex. Use ``grep(r'...')``
449 449 to ensure special escape characters are handled correctly. Unlike
450 450 ``keyword(string)``, the match is case-sensitive.
451 451 """
452 452 try:
453 453 # i18n: "grep" is a keyword
454 454 gr = re.compile(getstring(x, _("grep requires a string")))
455 455 except re.error, e:
456 456 raise error.ParseError(_('invalid match pattern: %s') % e)
457 457 l = []
458 458 for r in subset:
459 459 c = repo[r]
460 460 for e in c.files() + [c.user(), c.description()]:
461 461 if gr.search(e):
462 462 l.append(r)
463 463 break
464 464 return l
465 465
466 466 def hasfile(repo, subset, x):
467 467 """``file(pattern)``
468 468 Changesets affecting files matched by pattern.
469 469 """
470 470 # i18n: "file" is a keyword
471 471 pat = getstring(x, _("file requires a pattern"))
472 472 m = matchmod.match(repo.root, repo.getcwd(), [pat])
473 473 s = []
474 474 for r in subset:
475 475 for f in repo[r].files():
476 476 if m(f):
477 477 s.append(r)
478 478 break
479 479 return s
480 480
481 481 def head(repo, subset, x):
482 482 """``head()``
483 483 Changeset is a named branch head.
484 484 """
485 485 # i18n: "head" is a keyword
486 486 getargs(x, 0, 0, _("head takes no arguments"))
487 487 hs = set()
488 488 for b, ls in repo.branchmap().iteritems():
489 489 hs.update(repo[h].rev() for h in ls)
490 490 return [r for r in subset if r in hs]
491 491
492 492 def heads(repo, subset, x):
493 493 """``heads(set)``
494 494 Members of set with no children in set.
495 495 """
496 496 s = getset(repo, subset, x)
497 497 ps = set(parents(repo, subset, x))
498 498 return [r for r in s if r not in ps]
499 499
500 500 def keyword(repo, subset, x):
501 501 """``keyword(string)``
502 502 Search commit message, user name, and names of changed files for
503 503 string. The match is case-insensitive.
504 504 """
505 505 # i18n: "keyword" is a keyword
506 506 kw = getstring(x, _("keyword requires a string")).lower()
507 507 l = []
508 508 for r in subset:
509 509 c = repo[r]
510 510 t = " ".join(c.files() + [c.user(), c.description()])
511 511 if kw in t.lower():
512 512 l.append(r)
513 513 return l
514 514
515 515 def limit(repo, subset, x):
516 516 """``limit(set, n)``
517 517 First n members of set.
518 518 """
519 519 # i18n: "limit" is a keyword
520 520 l = getargs(x, 2, 2, _("limit requires two arguments"))
521 521 try:
522 522 # i18n: "limit" is a keyword
523 523 lim = int(getstring(l[1], _("limit requires a number")))
524 524 except (TypeError, ValueError):
525 525 # i18n: "limit" is a keyword
526 526 raise error.ParseError(_("limit expects a number"))
527 527 ss = set(subset)
528 528 os = getset(repo, range(len(repo)), l[0])[:lim]
529 529 return [r for r in os if r in ss]
530 530
531 531 def last(repo, subset, x):
532 532 """``last(set, n)``
533 533 Last n members of set.
534 534 """
535 535 # i18n: "last" is a keyword
536 536 l = getargs(x, 2, 2, _("last requires two arguments"))
537 537 try:
538 538 # i18n: "last" is a keyword
539 539 lim = int(getstring(l[1], _("last requires a number")))
540 540 except (TypeError, ValueError):
541 541 # i18n: "last" is a keyword
542 542 raise error.ParseError(_("last expects a number"))
543 543 ss = set(subset)
544 544 os = getset(repo, range(len(repo)), l[0])[-lim:]
545 545 return [r for r in os if r in ss]
546 546
547 547 def maxrev(repo, subset, x):
548 548 """``max(set)``
549 549 Changeset with highest revision number in set.
550 550 """
551 551 os = getset(repo, range(len(repo)), x)
552 552 if os:
553 553 m = max(os)
554 554 if m in subset:
555 555 return [m]
556 556 return []
557 557
558 558 def merge(repo, subset, x):
559 559 """``merge()``
560 560 Changeset is a merge changeset.
561 561 """
562 562 # i18n: "merge" is a keyword
563 563 getargs(x, 0, 0, _("merge takes no arguments"))
564 564 cl = repo.changelog
565 565 return [r for r in subset if cl.parentrevs(r)[1] != -1]
566 566
567 567 def minrev(repo, subset, x):
568 568 """``min(set)``
569 569 Changeset with lowest revision number in set.
570 570 """
571 571 os = getset(repo, range(len(repo)), x)
572 572 if os:
573 573 m = min(os)
574 574 if m in subset:
575 575 return [m]
576 576 return []
577 577
578 578 def modifies(repo, subset, x):
579 579 """``modifies(pattern)``
580 580 Changesets modifying files matched by pattern.
581 581 """
582 582 # i18n: "modifies" is a keyword
583 583 pat = getstring(x, _("modifies requires a pattern"))
584 584 return checkstatus(repo, subset, pat, 0)
585 585
586 586 def node(repo, subset, x):
587 587 """``id(string)``
588 588 Revision non-ambiguously specified by the given hex string prefix.
589 589 """
590 590 # i18n: "id" is a keyword
591 591 l = getargs(x, 1, 1, _("id requires one argument"))
592 592 # i18n: "id" is a keyword
593 593 n = getstring(l[0], _("id requires a string"))
594 594 if len(n) == 40:
595 595 rn = repo[n].rev()
596 596 else:
597 597 rn = repo.changelog.rev(repo.changelog._partialmatch(n))
598 598 return [r for r in subset if r == rn]
599 599
600 600 def outgoing(repo, subset, x):
601 601 """``outgoing([path])``
602 602 Changesets not found in the specified destination repository, or the
603 603 default push location.
604 604 """
605 605 import hg # avoid start-up nasties
606 606 # i18n: "outgoing" is a keyword
607 607 l = getargs(x, 0, 1, _("outgoing takes one or no arguments"))
608 608 # i18n: "outgoing" is a keyword
609 609 dest = l and getstring(l[0], _("outgoing requires a repository path")) or ''
610 610 dest = repo.ui.expandpath(dest or 'default-push', dest or 'default')
611 611 dest, branches = hg.parseurl(dest)
612 612 revs, checkout = hg.addbranchrevs(repo, repo, branches, [])
613 613 if revs:
614 614 revs = [repo.lookup(rev) for rev in revs]
615 615 other = hg.peer(repo, {}, dest)
616 616 repo.ui.pushbuffer()
617 617 common, outheads = discovery.findcommonoutgoing(repo, other, onlyheads=revs)
618 618 repo.ui.popbuffer()
619 619 cl = repo.changelog
620 620 o = set([cl.rev(r) for r in repo.changelog.findmissing(common, outheads)])
621 621 return [r for r in subset if r in o]
622 622
623 623 def p1(repo, subset, x):
624 624 """``p1([set])``
625 625 First parent of changesets in set, or the working directory.
626 626 """
627 627 if x is None:
628 628 p = repo[x].p1().rev()
629 629 return [r for r in subset if r == p]
630 630
631 631 ps = set()
632 632 cl = repo.changelog
633 633 for r in getset(repo, range(len(repo)), x):
634 634 ps.add(cl.parentrevs(r)[0])
635 635 return [r for r in subset if r in ps]
636 636
637 637 def p2(repo, subset, x):
638 638 """``p2([set])``
639 639 Second parent of changesets in set, or the working directory.
640 640 """
641 641 if x is None:
642 642 ps = repo[x].parents()
643 643 try:
644 644 p = ps[1].rev()
645 645 return [r for r in subset if r == p]
646 646 except IndexError:
647 647 return []
648 648
649 649 ps = set()
650 650 cl = repo.changelog
651 651 for r in getset(repo, range(len(repo)), x):
652 652 ps.add(cl.parentrevs(r)[1])
653 653 return [r for r in subset if r in ps]
654 654
655 655 def parents(repo, subset, x):
656 656 """``parents([set])``
657 657 The set of all parents for all changesets in set, or the working directory.
658 658 """
659 659 if x is None:
660 660 ps = tuple(p.rev() for p in repo[x].parents())
661 661 return [r for r in subset if r in ps]
662 662
663 663 ps = set()
664 664 cl = repo.changelog
665 665 for r in getset(repo, range(len(repo)), x):
666 666 ps.update(cl.parentrevs(r))
667 667 return [r for r in subset if r in ps]
668 668
669 669 def parentspec(repo, subset, x, n):
670 670 """``set^0``
671 671 The set.
672 672 ``set^1`` (or ``set^``), ``set^2``
673 673 First or second parent, respectively, of all changesets in set.
674 674 """
675 675 try:
676 676 n = int(n[1])
677 677 if n not in (0, 1, 2):
678 678 raise ValueError
679 679 except (TypeError, ValueError):
680 680 raise error.ParseError(_("^ expects a number 0, 1, or 2"))
681 681 ps = set()
682 682 cl = repo.changelog
683 683 for r in getset(repo, subset, x):
684 684 if n == 0:
685 685 ps.add(r)
686 686 elif n == 1:
687 687 ps.add(cl.parentrevs(r)[0])
688 688 elif n == 2:
689 689 parents = cl.parentrevs(r)
690 690 if len(parents) > 1:
691 691 ps.add(parents[1])
692 692 return [r for r in subset if r in ps]
693 693
694 694 def present(repo, subset, x):
695 695 """``present(set)``
696 696 An empty set, if any revision in set isn't found; otherwise,
697 697 all revisions in set.
698 698 """
699 699 try:
700 700 return getset(repo, subset, x)
701 701 except error.RepoLookupError:
702 702 return []
703 703
704 704 def removes(repo, subset, x):
705 705 """``removes(pattern)``
706 706 Changesets which remove files matching pattern.
707 707 """
708 708 # i18n: "removes" is a keyword
709 709 pat = getstring(x, _("removes requires a pattern"))
710 710 return checkstatus(repo, subset, pat, 2)
711 711
712 712 def rev(repo, subset, x):
713 713 """``rev(number)``
714 714 Revision with the given numeric identifier.
715 715 """
716 716 # i18n: "rev" is a keyword
717 717 l = getargs(x, 1, 1, _("rev requires one argument"))
718 718 try:
719 719 # i18n: "rev" is a keyword
720 720 l = int(getstring(l[0], _("rev requires a number")))
721 721 except (TypeError, ValueError):
722 722 # i18n: "rev" is a keyword
723 723 raise error.ParseError(_("rev expects a number"))
724 724 return [r for r in subset if r == l]
725 725
726 726 def reverse(repo, subset, x):
727 727 """``reverse(set)``
728 728 Reverse order of set.
729 729 """
730 730 l = getset(repo, subset, x)
731 731 l.reverse()
732 732 return l
733 733
734 734 def roots(repo, subset, x):
735 735 """``roots(set)``
736 736 Changesets with no parent changeset in set.
737 737 """
738 738 s = getset(repo, subset, x)
739 739 cs = set(children(repo, subset, x))
740 740 return [r for r in s if r not in cs]
741 741
742 742 def sort(repo, subset, x):
743 743 """``sort(set[, [-]key...])``
744 744 Sort set by keys. The default sort order is ascending, specify a key
745 745 as ``-key`` to sort in descending order.
746 746
747 747 The keys can be:
748 748
749 749 - ``rev`` for the revision number,
750 750 - ``branch`` for the branch name,
751 751 - ``desc`` for the commit message (description),
752 752 - ``user`` for user name (``author`` can be used as an alias),
753 753 - ``date`` for the commit date
754 754 """
755 755 # i18n: "sort" is a keyword
756 756 l = getargs(x, 1, 2, _("sort requires one or two arguments"))
757 757 keys = "rev"
758 758 if len(l) == 2:
759 759 keys = getstring(l[1], _("sort spec must be a string"))
760 760
761 761 s = l[0]
762 762 keys = keys.split()
763 763 l = []
764 764 def invert(s):
765 765 return "".join(chr(255 - ord(c)) for c in s)
766 766 for r in getset(repo, subset, s):
767 767 c = repo[r]
768 768 e = []
769 769 for k in keys:
770 770 if k == 'rev':
771 771 e.append(r)
772 772 elif k == '-rev':
773 773 e.append(-r)
774 774 elif k == 'branch':
775 775 e.append(c.branch())
776 776 elif k == '-branch':
777 777 e.append(invert(c.branch()))
778 778 elif k == 'desc':
779 779 e.append(c.description())
780 780 elif k == '-desc':
781 781 e.append(invert(c.description()))
782 782 elif k in 'user author':
783 783 e.append(c.user())
784 784 elif k in '-user -author':
785 785 e.append(invert(c.user()))
786 786 elif k == 'date':
787 787 e.append(c.date()[0])
788 788 elif k == '-date':
789 789 e.append(-c.date()[0])
790 790 else:
791 791 raise error.ParseError(_("unknown sort key %r") % k)
792 792 e.append(r)
793 793 l.append(e)
794 794 l.sort()
795 795 return [e[-1] for e in l]
796 796
797 797 def tag(repo, subset, x):
798 798 """``tag([name])``
799 799 The specified tag by name, or all tagged revisions if no name is given.
800 800 """
801 801 # i18n: "tag" is a keyword
802 802 args = getargs(x, 0, 1, _("tag takes one or no arguments"))
803 803 cl = repo.changelog
804 804 if args:
805 805 tn = getstring(args[0],
806 806 # i18n: "tag" is a keyword
807 807 _('the argument to tag must be a string'))
808 808 if not repo.tags().get(tn, None):
809 809 raise util.Abort(_("tag '%s' does not exist") % tn)
810 810 s = set([cl.rev(n) for t, n in repo.tagslist() if t == tn])
811 811 else:
812 812 s = set([cl.rev(n) for t, n in repo.tagslist() if t != 'tip'])
813 813 return [r for r in subset if r in s]
814 814
815 815 def tagged(repo, subset, x):
816 816 return tag(repo, subset, x)
817 817
818 818 def user(repo, subset, x):
819 819 """``user(string)``
820 820 User name contains string. The match is case-insensitive.
821 821 """
822 822 return author(repo, subset, x)
823 823
824 824 symbols = {
825 825 "adds": adds,
826 826 "all": getall,
827 827 "ancestor": ancestor,
828 828 "ancestors": ancestors,
829 829 "author": author,
830 830 "bisected": bisected,
831 831 "bookmark": bookmark,
832 832 "branch": branch,
833 833 "children": children,
834 834 "closed": closed,
835 835 "contains": contains,
836 836 "date": date,
837 837 "desc": desc,
838 838 "descendants": descendants,
839 839 "file": hasfile,
840 840 "filelog": filelog,
841 841 "follow": follow,
842 842 "grep": grep,
843 843 "head": head,
844 844 "heads": heads,
845 845 "id": node,
846 846 "keyword": keyword,
847 847 "last": last,
848 848 "limit": limit,
849 849 "max": maxrev,
850 850 "merge": merge,
851 851 "min": minrev,
852 852 "modifies": modifies,
853 853 "outgoing": outgoing,
854 854 "p1": p1,
855 855 "p2": p2,
856 856 "parents": parents,
857 857 "present": present,
858 858 "removes": removes,
859 859 "rev": rev,
860 860 "reverse": reverse,
861 861 "roots": roots,
862 862 "sort": sort,
863 863 "tag": tag,
864 864 "tagged": tagged,
865 865 "user": user,
866 866 }
867 867
868 868 methods = {
869 869 "range": rangeset,
870 870 "string": stringset,
871 871 "symbol": symbolset,
872 872 "and": andset,
873 873 "or": orset,
874 874 "not": notset,
875 875 "list": listset,
876 876 "func": func,
877 877 "ancestor": ancestorspec,
878 878 "parent": parentspec,
879 879 "parentpost": p1,
880 880 }
881 881
882 882 def optimize(x, small):
883 883 if x is None:
884 884 return 0, x
885 885
886 886 smallbonus = 1
887 887 if small:
888 888 smallbonus = .5
889 889
890 890 op = x[0]
891 891 if op == 'minus':
892 892 return optimize(('and', x[1], ('not', x[2])), small)
893 893 elif op == 'dagrange':
894 894 return optimize(('and', ('func', ('symbol', 'descendants'), x[1]),
895 895 ('func', ('symbol', 'ancestors'), x[2])), small)
896 896 elif op == 'dagrangepre':
897 897 return optimize(('func', ('symbol', 'ancestors'), x[1]), small)
898 898 elif op == 'dagrangepost':
899 899 return optimize(('func', ('symbol', 'descendants'), x[1]), small)
900 900 elif op == 'rangepre':
901 901 return optimize(('range', ('string', '0'), x[1]), small)
902 902 elif op == 'rangepost':
903 903 return optimize(('range', x[1], ('string', 'tip')), small)
904 904 elif op == 'negate':
905 905 return optimize(('string',
906 906 '-' + getstring(x[1], _("can't negate that"))), small)
907 907 elif op in 'string symbol negate':
908 908 return smallbonus, x # single revisions are small
909 909 elif op == 'and' or op == 'dagrange':
910 910 wa, ta = optimize(x[1], True)
911 911 wb, tb = optimize(x[2], True)
912 912 w = min(wa, wb)
913 913 if wa > wb:
914 914 return w, (op, tb, ta)
915 915 return w, (op, ta, tb)
916 916 elif op == 'or':
917 917 wa, ta = optimize(x[1], False)
918 918 wb, tb = optimize(x[2], False)
919 919 if wb < wa:
920 920 wb, wa = wa, wb
921 921 return max(wa, wb), (op, ta, tb)
922 922 elif op == 'not':
923 923 o = optimize(x[1], not small)
924 924 return o[0], (op, o[1])
925 925 elif op == 'parentpost':
926 926 o = optimize(x[1], small)
927 927 return o[0], (op, o[1])
928 928 elif op == 'group':
929 929 return optimize(x[1], small)
930 930 elif op in 'range list parent ancestorspec':
931 931 if op == 'parent':
932 932 # x^:y means (x^) : y, not x ^ (:y)
933 933 post = ('parentpost', x[1])
934 934 if x[2][0] == 'dagrangepre':
935 935 return optimize(('dagrange', post, x[2][1]), small)
936 936 elif x[2][0] == 'rangepre':
937 937 return optimize(('range', post, x[2][1]), small)
938 938
939 939 wa, ta = optimize(x[1], small)
940 940 wb, tb = optimize(x[2], small)
941 941 return wa + wb, (op, ta, tb)
942 942 elif op == 'func':
943 943 f = getstring(x[1], _("not a symbol"))
944 944 wa, ta = optimize(x[2], small)
945 945 if f in ("author branch closed date desc file grep keyword "
946 946 "outgoing user"):
947 947 w = 10 # slow
948 948 elif f in "modifies adds removes":
949 949 w = 30 # slower
950 950 elif f == "contains":
951 951 w = 100 # very slow
952 952 elif f == "ancestor":
953 953 w = 1 * smallbonus
954 954 elif f in "reverse limit":
955 955 w = 0
956 956 elif f in "sort":
957 957 w = 10 # assume most sorts look at changelog
958 958 else:
959 959 w = 1
960 960 return w + wa, (op, x[1], ta)
961 961 return 1, x
962 962
963 963 class revsetalias(object):
964 964 funcre = re.compile('^([^(]+)\(([^)]+)\)$')
965 965 args = None
966 966
967 967 def __init__(self, name, value):
968 968 '''Aliases like:
969 969
970 970 h = heads(default)
971 971 b($1) = ancestors($1) - ancestors(default)
972 972 '''
973 973 if isinstance(name, tuple): # parameter substitution
974 974 self.tree = name
975 975 self.replacement = value
976 976 else: # alias definition
977 977 m = self.funcre.search(name)
978 978 if m:
979 979 self.tree = ('func', ('symbol', m.group(1)))
980 980 self.args = [x.strip() for x in m.group(2).split(',')]
981 981 for arg in self.args:
982 982 value = value.replace(arg, repr(arg))
983 983 else:
984 984 self.tree = ('symbol', name)
985 985
986 986 self.replacement, pos = parse(value)
987 987 if pos != len(value):
988 988 raise error.ParseError(_('invalid token'), pos)
989 989
990 990 def process(self, tree):
991 991 if isinstance(tree, tuple):
992 992 if self.args is None:
993 993 if tree == self.tree:
994 994 return self.replacement
995 995 elif tree[:2] == self.tree:
996 996 l = getlist(tree[2])
997 997 if len(l) != len(self.args):
998 998 raise error.ParseError(
999 999 _('invalid number of arguments: %s') % len(l))
1000 1000 result = self.replacement
1001 1001 for a, v in zip(self.args, l):
1002 1002 valalias = revsetalias(('string', a), v)
1003 1003 result = valalias.process(result)
1004 1004 return result
1005 1005 return tuple(map(self.process, tree))
1006 1006 return tree
1007 1007
1008 1008 def findaliases(ui, tree):
1009 1009 for k, v in ui.configitems('revsetalias'):
1010 1010 alias = revsetalias(k, v)
1011 1011 tree = alias.process(tree)
1012 1012 return tree
1013 1013
1014 1014 parse = parser.parser(tokenize, elements).parse
1015 1015
1016 1016 def match(ui, spec):
1017 1017 if not spec:
1018 1018 raise error.ParseError(_("empty query"))
1019 1019 tree, pos = parse(spec)
1020 1020 if (pos != len(spec)):
1021 1021 raise error.ParseError(_("invalid token"), pos)
1022 1022 if ui:
1023 1023 tree = findaliases(ui, tree)
1024 1024 weight, tree = optimize(tree, True)
1025 1025 def mfunc(repo, subset):
1026 1026 return getset(repo, subset, tree)
1027 1027 return mfunc
1028 1028
1029 def formatspec(expr, *args):
1030 '''
1031 This is a convenience function for using revsets internally, and
1032 escapes arguments appropriately. Aliases are intentionally ignored
1033 so that intended expression behavior isn't accidentally subverted.
1034
1035 Supported arguments:
1036
1037 %d = int(arg), no quoting
1038 %s = string(arg), escaped and single-quoted
1039 %b = arg.branch(), escaped and single-quoted
1040 %n = hex(arg), single-quoted
1041 %% = a literal '%'
1042
1043 >>> formatspec('%d:: and not %d::', 10, 20)
1044 '10:: and not 20::'
1045 >>> formatspec('keyword(%s)', 'foo\\xe9')
1046 "keyword('foo\\\\xe9')"
1047 >>> b = lambda: 'default'
1048 >>> b.branch = b
1049 >>> formatspec('branch(%b)', b)
1050 "branch('default')"
1051 '''
1052
1053 def quote(s):
1054 return repr(str(s))
1055
1056 ret = ''
1057 pos = 0
1058 arg = 0
1059 while pos < len(expr):
1060 c = expr[pos]
1061 if c == '%':
1062 pos += 1
1063 d = expr[pos]
1064 if d == '%':
1065 ret += d
1066 elif d == 'd':
1067 ret += str(int(args[arg]))
1068 arg += 1
1069 elif d == 's':
1070 ret += quote(args[arg])
1071 arg += 1
1072 elif d == 'n':
1073 ret += quote(node.hex(args[arg]))
1074 arg += 1
1075 elif d == 'b':
1076 ret += quote(args[arg].branch())
1077 arg += 1
1078 else:
1079 raise util.Abort('unexpected revspec format character %s' % d)
1080 else:
1081 ret += c
1082 pos += 1
1083
1084 return ret
1085
1029 1086 # tell hggettext to extract docstrings from these functions:
1030 1087 i18nfunctions = symbols.values()
@@ -1,35 +1,38 b''
1 1 # this is hack to make sure no escape characters are inserted into the output
2 2 import os
3 3 if 'TERM' in os.environ:
4 4 del os.environ['TERM']
5 5 import doctest
6 6
7 7 import mercurial.changelog
8 8 doctest.testmod(mercurial.changelog)
9 9
10 10 import mercurial.dagparser
11 11 doctest.testmod(mercurial.dagparser, optionflags=doctest.NORMALIZE_WHITESPACE)
12 12
13 13 import mercurial.match
14 14 doctest.testmod(mercurial.match)
15 15
16 16 import mercurial.store
17 17 doctest.testmod(mercurial.store)
18 18
19 19 import mercurial.ui
20 20 doctest.testmod(mercurial.ui)
21 21
22 22 import mercurial.url
23 23 doctest.testmod(mercurial.url)
24 24
25 25 import mercurial.util
26 26 doctest.testmod(mercurial.util)
27 27
28 28 import mercurial.encoding
29 29 doctest.testmod(mercurial.encoding)
30 30
31 31 import mercurial.hgweb.hgwebdir_mod
32 32 doctest.testmod(mercurial.hgweb.hgwebdir_mod)
33 33
34 34 import hgext.convert.cvsps
35 35 doctest.testmod(hgext.convert.cvsps)
36
37 import mercurial.revset
38 doctest.testmod(mercurial.revset)
General Comments 0
You need to be logged in to leave comments. Login now