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