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