##// END OF EJS Templates
revset: add follow(filename) to follow a filename's history across copies
Matt Mackall -
r14343:9ed227f7 default
parent child Browse files
Show More
@@ -1,1009 +1,1027
1 1 # revset.py - revision set queries for mercurial
2 2 #
3 3 # Copyright 2010 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 import re
9 9 import parser, util, error, discovery, hbisect
10 10 import 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 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 pattern.
337 337 """
338 338 # i18n: "contains" is a keyword
339 339 pat = getstring(x, _("contains requires a pattern"))
340 340 m = matchmod.match(repo.root, repo.getcwd(), [pat])
341 341 s = []
342 342 if m.files() == [pat]:
343 343 for r in subset:
344 344 if pat in repo[r]:
345 345 s.append(r)
346 346 else:
347 347 for r in subset:
348 348 for f in repo[r].manifest():
349 349 if m(f):
350 350 s.append(r)
351 351 break
352 352 return s
353 353
354 354 def date(repo, subset, x):
355 355 """``date(interval)``
356 356 Changesets within the interval, see :hg:`help dates`.
357 357 """
358 358 # i18n: "date" is a keyword
359 359 ds = getstring(x, _("date requires a string"))
360 360 dm = util.matchdate(ds)
361 361 return [r for r in subset if dm(repo[r].date()[0])]
362 362
363 363 def descendants(repo, subset, x):
364 364 """``descendants(set)``
365 365 Changesets which are descendants of changesets in set.
366 366 """
367 367 args = getset(repo, range(len(repo)), x)
368 368 if not args:
369 369 return []
370 370 s = set(repo.changelog.descendants(*args)) | set(args)
371 371 return [r for r in subset if r in s]
372 372
373 373 def filelog(repo, subset, x):
374 374 """``filelog(pattern)``
375 375 Changesets connected to the specified filelog.
376 376 """
377 377
378 378 pat = getstring(x, _("filelog requires a pattern"))
379 379 m = matchmod.match(repo.root, repo.getcwd(), [pat], default='relpath')
380 380 s = set()
381 381
382 382 if not m.anypats():
383 383 for f in m.files():
384 384 fl = repo.file(f)
385 385 for fr in fl:
386 386 s.add(fl.linkrev(fr))
387 387 else:
388 388 for f in repo[None]:
389 389 if m(f):
390 390 fl = repo.file(f)
391 391 for fr in fl:
392 392 s.add(fl.linkrev(fr))
393 393
394 394 return [r for r in subset if r in s]
395 395
396 396 def follow(repo, subset, x):
397 """``follow([file])``
398 An alias for ``::.`` (ancestors of the working copy's first parent).
399 If a filename is specified, the history of the given file is followed,
400 including copies.
401 """
402 # i18n: "follow" is a keyword
403 l = getargs(x, 0, 1, _("follow takes no arguments or a filename"))
404 p = repo['.'].rev()
405 if l:
406 x = getstring(l[0], "follow expected a filename")
407 s = set(ctx.rev() for ctx in repo['.'][x].ancestors())
408 else:
409 s = set(repo.changelog.ancestors(p))
410
411 s |= set([p])
412 return [r for r in subset if r in s]
413
414 def followfile(repo, subset, f):
397 415 """``follow()``
398 416 An alias for ``::.`` (ancestors of the working copy's first parent).
399 417 """
400 418 # i18n: "follow" is a keyword
401 419 getargs(x, 0, 0, _("follow takes no arguments"))
402 420 p = repo['.'].rev()
403 421 s = set(repo.changelog.ancestors(p)) | set([p])
404 422 return [r for r in subset if r in s]
405 423
406 424 def getall(repo, subset, x):
407 425 """``all()``
408 426 All changesets, the same as ``0:tip``.
409 427 """
410 428 # i18n: "all" is a keyword
411 429 getargs(x, 0, 0, _("all takes no arguments"))
412 430 return subset
413 431
414 432 def grep(repo, subset, x):
415 433 """``grep(regex)``
416 434 Like ``keyword(string)`` but accepts a regex. Use ``grep(r'...')``
417 435 to ensure special escape characters are handled correctly.
418 436 """
419 437 try:
420 438 # i18n: "grep" is a keyword
421 439 gr = re.compile(getstring(x, _("grep requires a string")))
422 440 except re.error, e:
423 441 raise error.ParseError(_('invalid match pattern: %s') % e)
424 442 l = []
425 443 for r in subset:
426 444 c = repo[r]
427 445 for e in c.files() + [c.user(), c.description()]:
428 446 if gr.search(e):
429 447 l.append(r)
430 448 break
431 449 return l
432 450
433 451 def hasfile(repo, subset, x):
434 452 """``file(pattern)``
435 453 Changesets affecting files matched by pattern.
436 454 """
437 455 # i18n: "file" is a keyword
438 456 pat = getstring(x, _("file requires a pattern"))
439 457 m = matchmod.match(repo.root, repo.getcwd(), [pat])
440 458 s = []
441 459 for r in subset:
442 460 for f in repo[r].files():
443 461 if m(f):
444 462 s.append(r)
445 463 break
446 464 return s
447 465
448 466 def head(repo, subset, x):
449 467 """``head()``
450 468 Changeset is a named branch head.
451 469 """
452 470 # i18n: "head" is a keyword
453 471 getargs(x, 0, 0, _("head takes no arguments"))
454 472 hs = set()
455 473 for b, ls in repo.branchmap().iteritems():
456 474 hs.update(repo[h].rev() for h in ls)
457 475 return [r for r in subset if r in hs]
458 476
459 477 def heads(repo, subset, x):
460 478 """``heads(set)``
461 479 Members of set with no children in set.
462 480 """
463 481 s = getset(repo, subset, x)
464 482 ps = set(parents(repo, subset, x))
465 483 return [r for r in s if r not in ps]
466 484
467 485 def keyword(repo, subset, x):
468 486 """``keyword(string)``
469 487 Search commit message, user name, and names of changed files for
470 488 string.
471 489 """
472 490 # i18n: "keyword" is a keyword
473 491 kw = getstring(x, _("keyword requires a string")).lower()
474 492 l = []
475 493 for r in subset:
476 494 c = repo[r]
477 495 t = " ".join(c.files() + [c.user(), c.description()])
478 496 if kw in t.lower():
479 497 l.append(r)
480 498 return l
481 499
482 500 def limit(repo, subset, x):
483 501 """``limit(set, n)``
484 502 First n members of set.
485 503 """
486 504 # i18n: "limit" is a keyword
487 505 l = getargs(x, 2, 2, _("limit requires two arguments"))
488 506 try:
489 507 # i18n: "limit" is a keyword
490 508 lim = int(getstring(l[1], _("limit requires a number")))
491 509 except ValueError:
492 510 # i18n: "limit" is a keyword
493 511 raise error.ParseError(_("limit expects a number"))
494 512 ss = set(subset)
495 513 os = getset(repo, range(len(repo)), l[0])[:lim]
496 514 return [r for r in os if r in ss]
497 515
498 516 def last(repo, subset, x):
499 517 """``last(set, n)``
500 518 Last n members of set.
501 519 """
502 520 # i18n: "last" is a keyword
503 521 l = getargs(x, 2, 2, _("last requires two arguments"))
504 522 try:
505 523 # i18n: "last" is a keyword
506 524 lim = int(getstring(l[1], _("last requires a number")))
507 525 except ValueError:
508 526 # i18n: "last" is a keyword
509 527 raise error.ParseError(_("last expects a number"))
510 528 ss = set(subset)
511 529 os = getset(repo, range(len(repo)), l[0])[-lim:]
512 530 return [r for r in os if r in ss]
513 531
514 532 def maxrev(repo, subset, x):
515 533 """``max(set)``
516 534 Changeset with highest revision number in set.
517 535 """
518 536 os = getset(repo, range(len(repo)), x)
519 537 if os:
520 538 m = max(os)
521 539 if m in subset:
522 540 return [m]
523 541 return []
524 542
525 543 def merge(repo, subset, x):
526 544 """``merge()``
527 545 Changeset is a merge changeset.
528 546 """
529 547 # i18n: "merge" is a keyword
530 548 getargs(x, 0, 0, _("merge takes no arguments"))
531 549 cl = repo.changelog
532 550 return [r for r in subset if cl.parentrevs(r)[1] != -1]
533 551
534 552 def minrev(repo, subset, x):
535 553 """``min(set)``
536 554 Changeset with lowest revision number in set.
537 555 """
538 556 os = getset(repo, range(len(repo)), x)
539 557 if os:
540 558 m = min(os)
541 559 if m in subset:
542 560 return [m]
543 561 return []
544 562
545 563 def modifies(repo, subset, x):
546 564 """``modifies(pattern)``
547 565 Changesets modifying files matched by pattern.
548 566 """
549 567 # i18n: "modifies" is a keyword
550 568 pat = getstring(x, _("modifies requires a pattern"))
551 569 return checkstatus(repo, subset, pat, 0)
552 570
553 571 def node(repo, subset, x):
554 572 """``id(string)``
555 573 Revision non-ambiguously specified by the given hex string prefix.
556 574 """
557 575 # i18n: "id" is a keyword
558 576 l = getargs(x, 1, 1, _("id requires one argument"))
559 577 # i18n: "id" is a keyword
560 578 n = getstring(l[0], _("id requires a string"))
561 579 if len(n) == 40:
562 580 rn = repo[n].rev()
563 581 else:
564 582 rn = repo.changelog.rev(repo.changelog._partialmatch(n))
565 583 return [r for r in subset if r == rn]
566 584
567 585 def outgoing(repo, subset, x):
568 586 """``outgoing([path])``
569 587 Changesets not found in the specified destination repository, or the
570 588 default push location.
571 589 """
572 590 import hg # avoid start-up nasties
573 591 # i18n: "outgoing" is a keyword
574 592 l = getargs(x, 0, 1, _("outgoing requires a repository path"))
575 593 # i18n: "outgoing" is a keyword
576 594 dest = l and getstring(l[0], _("outgoing requires a repository path")) or ''
577 595 dest = repo.ui.expandpath(dest or 'default-push', dest or 'default')
578 596 dest, branches = hg.parseurl(dest)
579 597 revs, checkout = hg.addbranchrevs(repo, repo, branches, [])
580 598 if revs:
581 599 revs = [repo.lookup(rev) for rev in revs]
582 600 other = hg.repository(hg.remoteui(repo, {}), dest)
583 601 repo.ui.pushbuffer()
584 602 common, outheads = discovery.findcommonoutgoing(repo, other, onlyheads=revs)
585 603 repo.ui.popbuffer()
586 604 cl = repo.changelog
587 605 o = set([cl.rev(r) for r in repo.changelog.findmissing(common, outheads)])
588 606 return [r for r in subset if r in o]
589 607
590 608 def p1(repo, subset, x):
591 609 """``p1([set])``
592 610 First parent of changesets in set, or the working directory.
593 611 """
594 612 if x is None:
595 613 p = repo[x].p1().rev()
596 614 return [r for r in subset if r == p]
597 615
598 616 ps = set()
599 617 cl = repo.changelog
600 618 for r in getset(repo, range(len(repo)), x):
601 619 ps.add(cl.parentrevs(r)[0])
602 620 return [r for r in subset if r in ps]
603 621
604 622 def p2(repo, subset, x):
605 623 """``p2([set])``
606 624 Second parent of changesets in set, or the working directory.
607 625 """
608 626 if x is None:
609 627 ps = repo[x].parents()
610 628 try:
611 629 p = ps[1].rev()
612 630 return [r for r in subset if r == p]
613 631 except IndexError:
614 632 return []
615 633
616 634 ps = set()
617 635 cl = repo.changelog
618 636 for r in getset(repo, range(len(repo)), x):
619 637 ps.add(cl.parentrevs(r)[1])
620 638 return [r for r in subset if r in ps]
621 639
622 640 def parents(repo, subset, x):
623 641 """``parents([set])``
624 642 The set of all parents for all changesets in set, or the working directory.
625 643 """
626 644 if x is None:
627 645 ps = tuple(p.rev() for p in repo[x].parents())
628 646 return [r for r in subset if r in ps]
629 647
630 648 ps = set()
631 649 cl = repo.changelog
632 650 for r in getset(repo, range(len(repo)), x):
633 651 ps.update(cl.parentrevs(r))
634 652 return [r for r in subset if r in ps]
635 653
636 654 def parentspec(repo, subset, x, n):
637 655 """``set^0``
638 656 The set.
639 657 ``set^1`` (or ``set^``), ``set^2``
640 658 First or second parent, respectively, of all changesets in set.
641 659 """
642 660 try:
643 661 n = int(n[1])
644 662 if n not in (0, 1, 2):
645 663 raise ValueError
646 664 except ValueError:
647 665 raise error.ParseError(_("^ expects a number 0, 1, or 2"))
648 666 ps = set()
649 667 cl = repo.changelog
650 668 for r in getset(repo, subset, x):
651 669 if n == 0:
652 670 ps.add(r)
653 671 elif n == 1:
654 672 ps.add(cl.parentrevs(r)[0])
655 673 elif n == 2:
656 674 parents = cl.parentrevs(r)
657 675 if len(parents) > 1:
658 676 ps.add(parents[1])
659 677 return [r for r in subset if r in ps]
660 678
661 679 def present(repo, subset, x):
662 680 """``present(set)``
663 681 An empty set, if any revision in set isn't found; otherwise,
664 682 all revisions in set.
665 683 """
666 684 try:
667 685 return getset(repo, subset, x)
668 686 except error.RepoLookupError:
669 687 return []
670 688
671 689 def removes(repo, subset, x):
672 690 """``removes(pattern)``
673 691 Changesets which remove files matching pattern.
674 692 """
675 693 # i18n: "removes" is a keyword
676 694 pat = getstring(x, _("removes requires a pattern"))
677 695 return checkstatus(repo, subset, pat, 2)
678 696
679 697 def rev(repo, subset, x):
680 698 """``rev(number)``
681 699 Revision with the given numeric identifier.
682 700 """
683 701 # i18n: "rev" is a keyword
684 702 l = getargs(x, 1, 1, _("rev requires one argument"))
685 703 try:
686 704 # i18n: "rev" is a keyword
687 705 l = int(getstring(l[0], _("rev requires a number")))
688 706 except ValueError:
689 707 # i18n: "rev" is a keyword
690 708 raise error.ParseError(_("rev expects a number"))
691 709 return [r for r in subset if r == l]
692 710
693 711 def reverse(repo, subset, x):
694 712 """``reverse(set)``
695 713 Reverse order of set.
696 714 """
697 715 l = getset(repo, subset, x)
698 716 l.reverse()
699 717 return l
700 718
701 719 def roots(repo, subset, x):
702 720 """``roots(set)``
703 721 Changesets with no parent changeset in set.
704 722 """
705 723 s = getset(repo, subset, x)
706 724 cs = set(children(repo, subset, x))
707 725 return [r for r in s if r not in cs]
708 726
709 727 def sort(repo, subset, x):
710 728 """``sort(set[, [-]key...])``
711 729 Sort set by keys. The default sort order is ascending, specify a key
712 730 as ``-key`` to sort in descending order.
713 731
714 732 The keys can be:
715 733
716 734 - ``rev`` for the revision number,
717 735 - ``branch`` for the branch name,
718 736 - ``desc`` for the commit message (description),
719 737 - ``user`` for user name (``author`` can be used as an alias),
720 738 - ``date`` for the commit date
721 739 """
722 740 # i18n: "sort" is a keyword
723 741 l = getargs(x, 1, 2, _("sort requires one or two arguments"))
724 742 keys = "rev"
725 743 if len(l) == 2:
726 744 keys = getstring(l[1], _("sort spec must be a string"))
727 745
728 746 s = l[0]
729 747 keys = keys.split()
730 748 l = []
731 749 def invert(s):
732 750 return "".join(chr(255 - ord(c)) for c in s)
733 751 for r in getset(repo, subset, s):
734 752 c = repo[r]
735 753 e = []
736 754 for k in keys:
737 755 if k == 'rev':
738 756 e.append(r)
739 757 elif k == '-rev':
740 758 e.append(-r)
741 759 elif k == 'branch':
742 760 e.append(c.branch())
743 761 elif k == '-branch':
744 762 e.append(invert(c.branch()))
745 763 elif k == 'desc':
746 764 e.append(c.description())
747 765 elif k == '-desc':
748 766 e.append(invert(c.description()))
749 767 elif k in 'user author':
750 768 e.append(c.user())
751 769 elif k in '-user -author':
752 770 e.append(invert(c.user()))
753 771 elif k == 'date':
754 772 e.append(c.date()[0])
755 773 elif k == '-date':
756 774 e.append(-c.date()[0])
757 775 else:
758 776 raise error.ParseError(_("unknown sort key %r") % k)
759 777 e.append(r)
760 778 l.append(e)
761 779 l.sort()
762 780 return [e[-1] for e in l]
763 781
764 782 def tag(repo, subset, x):
765 783 """``tag(name)``
766 784 The specified tag by name, or all tagged revisions if no name is given.
767 785 """
768 786 # i18n: "tag" is a keyword
769 787 args = getargs(x, 0, 1, _("tag takes one or no arguments"))
770 788 cl = repo.changelog
771 789 if args:
772 790 tn = getstring(args[0],
773 791 # i18n: "tag" is a keyword
774 792 _('the argument to tag must be a string'))
775 793 if not repo.tags().get(tn, None):
776 794 raise util.Abort(_("tag '%s' does not exist") % tn)
777 795 s = set([cl.rev(n) for t, n in repo.tagslist() if t == tn])
778 796 else:
779 797 s = set([cl.rev(n) for t, n in repo.tagslist() if t != 'tip'])
780 798 return [r for r in subset if r in s]
781 799
782 800 def tagged(repo, subset, x):
783 801 return tag(repo, subset, x)
784 802
785 803 def user(repo, subset, x):
786 804 """``user(string)``
787 805 User name is string.
788 806 """
789 807 return author(repo, subset, x)
790 808
791 809 symbols = {
792 810 "adds": adds,
793 811 "all": getall,
794 812 "ancestor": ancestor,
795 813 "ancestors": ancestors,
796 814 "author": author,
797 815 "bisected": bisected,
798 816 "bookmark": bookmark,
799 817 "branch": branch,
800 818 "children": children,
801 819 "closed": closed,
802 820 "contains": contains,
803 821 "date": date,
804 822 "descendants": descendants,
805 823 "file": hasfile,
806 824 "filelog": filelog,
807 825 "follow": follow,
808 826 "grep": grep,
809 827 "head": head,
810 828 "heads": heads,
811 829 "keyword": keyword,
812 830 "last": last,
813 831 "limit": limit,
814 832 "max": maxrev,
815 833 "min": minrev,
816 834 "merge": merge,
817 835 "modifies": modifies,
818 836 "id": node,
819 837 "outgoing": outgoing,
820 838 "p1": p1,
821 839 "p2": p2,
822 840 "parents": parents,
823 841 "present": present,
824 842 "removes": removes,
825 843 "reverse": reverse,
826 844 "rev": rev,
827 845 "roots": roots,
828 846 "sort": sort,
829 847 "tag": tag,
830 848 "tagged": tagged,
831 849 "user": user,
832 850 }
833 851
834 852 methods = {
835 853 "range": rangeset,
836 854 "string": stringset,
837 855 "symbol": symbolset,
838 856 "and": andset,
839 857 "or": orset,
840 858 "not": notset,
841 859 "list": listset,
842 860 "func": func,
843 861 "ancestor": ancestorspec,
844 862 "parent": parentspec,
845 863 "parentpost": p1,
846 864 }
847 865
848 866 def optimize(x, small):
849 867 if x is None:
850 868 return 0, x
851 869
852 870 smallbonus = 1
853 871 if small:
854 872 smallbonus = .5
855 873
856 874 op = x[0]
857 875 if op == 'minus':
858 876 return optimize(('and', x[1], ('not', x[2])), small)
859 877 elif op == 'dagrange':
860 878 return optimize(('and', ('func', ('symbol', 'descendants'), x[1]),
861 879 ('func', ('symbol', 'ancestors'), x[2])), small)
862 880 elif op == 'dagrangepre':
863 881 return optimize(('func', ('symbol', 'ancestors'), x[1]), small)
864 882 elif op == 'dagrangepost':
865 883 return optimize(('func', ('symbol', 'descendants'), x[1]), small)
866 884 elif op == 'rangepre':
867 885 return optimize(('range', ('string', '0'), x[1]), small)
868 886 elif op == 'rangepost':
869 887 return optimize(('range', x[1], ('string', 'tip')), small)
870 888 elif op == 'negate':
871 889 return optimize(('string',
872 890 '-' + getstring(x[1], _("can't negate that"))), small)
873 891 elif op in 'string symbol negate':
874 892 return smallbonus, x # single revisions are small
875 893 elif op == 'and' or op == 'dagrange':
876 894 wa, ta = optimize(x[1], True)
877 895 wb, tb = optimize(x[2], True)
878 896 w = min(wa, wb)
879 897 if wa > wb:
880 898 return w, (op, tb, ta)
881 899 return w, (op, ta, tb)
882 900 elif op == 'or':
883 901 wa, ta = optimize(x[1], False)
884 902 wb, tb = optimize(x[2], False)
885 903 if wb < wa:
886 904 wb, wa = wa, wb
887 905 return max(wa, wb), (op, ta, tb)
888 906 elif op == 'not':
889 907 o = optimize(x[1], not small)
890 908 return o[0], (op, o[1])
891 909 elif op == 'parentpost':
892 910 o = optimize(x[1], small)
893 911 return o[0], (op, o[1])
894 912 elif op == 'group':
895 913 return optimize(x[1], small)
896 914 elif op in 'range list parent ancestorspec':
897 915 wa, ta = optimize(x[1], small)
898 916 wb, tb = optimize(x[2], small)
899 917 return wa + wb, (op, ta, tb)
900 918 elif op == 'func':
901 919 f = getstring(x[1], _("not a symbol"))
902 920 wa, ta = optimize(x[2], small)
903 921 if f in "grep date user author keyword branch file outgoing closed":
904 922 w = 10 # slow
905 923 elif f in "modifies adds removes":
906 924 w = 30 # slower
907 925 elif f == "contains":
908 926 w = 100 # very slow
909 927 elif f == "ancestor":
910 928 w = 1 * smallbonus
911 929 elif f in "reverse limit":
912 930 w = 0
913 931 elif f in "sort":
914 932 w = 10 # assume most sorts look at changelog
915 933 else:
916 934 w = 1
917 935 return w + wa, (op, x[1], ta)
918 936 return 1, x
919 937
920 938 class revsetalias(object):
921 939 funcre = re.compile('^([^(]+)\(([^)]+)\)$')
922 940 args = ()
923 941
924 942 def __init__(self, token, value):
925 943 '''Aliases like:
926 944
927 945 h = heads(default)
928 946 b($1) = ancestors($1) - ancestors(default)
929 947 '''
930 948 if isinstance(token, tuple):
931 949 self.type, self.name = token
932 950 else:
933 951 m = self.funcre.search(token)
934 952 if m:
935 953 self.type = 'func'
936 954 self.name = m.group(1)
937 955 self.args = [x.strip() for x in m.group(2).split(',')]
938 956 else:
939 957 self.type = 'symbol'
940 958 self.name = token
941 959
942 960 if isinstance(value, str):
943 961 for arg in self.args:
944 962 value = value.replace(arg, repr(arg))
945 963 self.replacement, pos = parse(value)
946 964 if pos != len(value):
947 965 raise error.ParseError('invalid token', pos)
948 966 else:
949 967 self.replacement = value
950 968
951 969 def match(self, tree):
952 970 if not tree:
953 971 return False
954 972 if tree == (self.type, self.name):
955 973 return True
956 974 if tree[0] != self.type:
957 975 return False
958 976 if len(tree) > 1 and tree[1] != ('symbol', self.name):
959 977 return False
960 978 # 'func' + funcname + args
961 979 if ((self.args and len(tree) != 3) or
962 980 (len(self.args) == 1 and tree[2][0] == 'list') or
963 981 (len(self.args) > 1 and (tree[2][0] != 'list' or
964 982 len(tree[2]) - 1 != len(self.args)))):
965 983 raise error.ParseError('invalid amount of arguments', len(tree) - 2)
966 984 return True
967 985
968 986 def replace(self, tree):
969 987 if tree == (self.type, self.name):
970 988 return self.replacement
971 989 result = self.replacement
972 990 def getsubtree(i):
973 991 if tree[2][0] == 'list':
974 992 return tree[2][i + 1]
975 993 return tree[i + 2]
976 994 for i, v in enumerate(self.args):
977 995 valalias = revsetalias(('string', v), getsubtree(i))
978 996 result = valalias.process(result)
979 997 return result
980 998
981 999 def process(self, tree):
982 1000 if self.match(tree):
983 1001 return self.replace(tree)
984 1002 if isinstance(tree, tuple):
985 1003 return tuple(map(self.process, tree))
986 1004 return tree
987 1005
988 1006 def findaliases(ui, tree):
989 1007 for k, v in ui.configitems('revsetalias'):
990 1008 alias = revsetalias(k, v)
991 1009 tree = alias.process(tree)
992 1010 return tree
993 1011
994 1012 parse = parser.parser(tokenize, elements).parse
995 1013
996 1014 def match(ui, spec):
997 1015 if not spec:
998 1016 raise error.ParseError(_("empty query"))
999 1017 tree, pos = parse(spec)
1000 1018 if (pos != len(spec)):
1001 1019 raise error.ParseError("invalid token", pos)
1002 1020 tree = findaliases(ui, tree)
1003 1021 weight, tree = optimize(tree, True)
1004 1022 def mfunc(repo, subset):
1005 1023 return getset(repo, subset, tree)
1006 1024 return mfunc
1007 1025
1008 1026 # tell hggettext to extract docstrings from these functions:
1009 1027 i18nfunctions = symbols.values()
General Comments 0
You need to be logged in to leave comments. Login now