##// END OF EJS Templates
revset: add hidden() revset
Patrick Mezard -
r17390:74b44f25 default
parent child Browse files
Show More
@@ -1,1856 +1,1865 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, phases
10 10 import node
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 def _revancestors(repo, revs, followfirst):
17 17 """Like revlog.ancestors(), but supports followfirst."""
18 18 cut = followfirst and 1 or None
19 19 cl = repo.changelog
20 20 visit = util.deque(revs)
21 21 seen = set([node.nullrev])
22 22 while visit:
23 23 for parent in cl.parentrevs(visit.popleft())[:cut]:
24 24 if parent not in seen:
25 25 visit.append(parent)
26 26 seen.add(parent)
27 27 yield parent
28 28
29 29 def _revdescendants(repo, revs, followfirst):
30 30 """Like revlog.descendants() but supports followfirst."""
31 31 cut = followfirst and 1 or None
32 32 cl = repo.changelog
33 33 first = min(revs)
34 34 nullrev = node.nullrev
35 35 if first == nullrev:
36 36 # Are there nodes with a null first parent and a non-null
37 37 # second one? Maybe. Do we care? Probably not.
38 38 for i in cl:
39 39 yield i
40 40 return
41 41
42 42 seen = set(revs)
43 43 for i in xrange(first + 1, len(cl)):
44 44 for x in cl.parentrevs(i)[:cut]:
45 45 if x != nullrev and x in seen:
46 46 seen.add(i)
47 47 yield i
48 48 break
49 49
50 50 def _revsbetween(repo, roots, heads):
51 51 """Return all paths between roots and heads, inclusive of both endpoint
52 52 sets."""
53 53 if not roots:
54 54 return []
55 55 parentrevs = repo.changelog.parentrevs
56 56 visit = heads[:]
57 57 reachable = set()
58 58 seen = {}
59 59 minroot = min(roots)
60 60 roots = set(roots)
61 61 # open-code the post-order traversal due to the tiny size of
62 62 # sys.getrecursionlimit()
63 63 while visit:
64 64 rev = visit.pop()
65 65 if rev in roots:
66 66 reachable.add(rev)
67 67 parents = parentrevs(rev)
68 68 seen[rev] = parents
69 69 for parent in parents:
70 70 if parent >= minroot and parent not in seen:
71 71 visit.append(parent)
72 72 if not reachable:
73 73 return []
74 74 for rev in sorted(seen):
75 75 for parent in seen[rev]:
76 76 if parent in reachable:
77 77 reachable.add(rev)
78 78 return sorted(reachable)
79 79
80 80 elements = {
81 81 "(": (20, ("group", 1, ")"), ("func", 1, ")")),
82 82 "~": (18, None, ("ancestor", 18)),
83 83 "^": (18, None, ("parent", 18), ("parentpost", 18)),
84 84 "-": (5, ("negate", 19), ("minus", 5)),
85 85 "::": (17, ("dagrangepre", 17), ("dagrange", 17),
86 86 ("dagrangepost", 17)),
87 87 "..": (17, ("dagrangepre", 17), ("dagrange", 17),
88 88 ("dagrangepost", 17)),
89 89 ":": (15, ("rangepre", 15), ("range", 15), ("rangepost", 15)),
90 90 "not": (10, ("not", 10)),
91 91 "!": (10, ("not", 10)),
92 92 "and": (5, None, ("and", 5)),
93 93 "&": (5, None, ("and", 5)),
94 94 "or": (4, None, ("or", 4)),
95 95 "|": (4, None, ("or", 4)),
96 96 "+": (4, None, ("or", 4)),
97 97 ",": (2, None, ("list", 2)),
98 98 ")": (0, None, None),
99 99 "symbol": (0, ("symbol",), None),
100 100 "string": (0, ("string",), None),
101 101 "end": (0, None, None),
102 102 }
103 103
104 104 keywords = set(['and', 'or', 'not'])
105 105
106 106 def tokenize(program):
107 107 pos, l = 0, len(program)
108 108 while pos < l:
109 109 c = program[pos]
110 110 if c.isspace(): # skip inter-token whitespace
111 111 pass
112 112 elif c == ':' and program[pos:pos + 2] == '::': # look ahead carefully
113 113 yield ('::', None, pos)
114 114 pos += 1 # skip ahead
115 115 elif c == '.' and program[pos:pos + 2] == '..': # look ahead carefully
116 116 yield ('..', None, pos)
117 117 pos += 1 # skip ahead
118 118 elif c in "():,-|&+!~^": # handle simple operators
119 119 yield (c, None, pos)
120 120 elif (c in '"\'' or c == 'r' and
121 121 program[pos:pos + 2] in ("r'", 'r"')): # handle quoted strings
122 122 if c == 'r':
123 123 pos += 1
124 124 c = program[pos]
125 125 decode = lambda x: x
126 126 else:
127 127 decode = lambda x: x.decode('string-escape')
128 128 pos += 1
129 129 s = pos
130 130 while pos < l: # find closing quote
131 131 d = program[pos]
132 132 if d == '\\': # skip over escaped characters
133 133 pos += 2
134 134 continue
135 135 if d == c:
136 136 yield ('string', decode(program[s:pos]), s)
137 137 break
138 138 pos += 1
139 139 else:
140 140 raise error.ParseError(_("unterminated string"), s)
141 141 # gather up a symbol/keyword
142 142 elif c.isalnum() or c in '._' or ord(c) > 127:
143 143 s = pos
144 144 pos += 1
145 145 while pos < l: # find end of symbol
146 146 d = program[pos]
147 147 if not (d.isalnum() or d in "._/" or ord(d) > 127):
148 148 break
149 149 if d == '.' and program[pos - 1] == '.': # special case for ..
150 150 pos -= 1
151 151 break
152 152 pos += 1
153 153 sym = program[s:pos]
154 154 if sym in keywords: # operator keywords
155 155 yield (sym, None, s)
156 156 else:
157 157 yield ('symbol', sym, s)
158 158 pos -= 1
159 159 else:
160 160 raise error.ParseError(_("syntax error"), pos)
161 161 pos += 1
162 162 yield ('end', None, pos)
163 163
164 164 # helpers
165 165
166 166 def getstring(x, err):
167 167 if x and (x[0] == 'string' or x[0] == 'symbol'):
168 168 return x[1]
169 169 raise error.ParseError(err)
170 170
171 171 def getlist(x):
172 172 if not x:
173 173 return []
174 174 if x[0] == 'list':
175 175 return getlist(x[1]) + [x[2]]
176 176 return [x]
177 177
178 178 def getargs(x, min, max, err):
179 179 l = getlist(x)
180 180 if len(l) < min or (max >= 0 and len(l) > max):
181 181 raise error.ParseError(err)
182 182 return l
183 183
184 184 def getset(repo, subset, x):
185 185 if not x:
186 186 raise error.ParseError(_("missing argument"))
187 187 return methods[x[0]](repo, subset, *x[1:])
188 188
189 189 def _getrevsource(repo, r):
190 190 extra = repo[r].extra()
191 191 for label in ('source', 'transplant_source', 'rebase_source'):
192 192 if label in extra:
193 193 try:
194 194 return repo[extra[label]].rev()
195 195 except error.RepoLookupError:
196 196 pass
197 197 return None
198 198
199 199 # operator methods
200 200
201 201 def stringset(repo, subset, x):
202 202 x = repo[x].rev()
203 203 if x == -1 and len(subset) == len(repo):
204 204 return [-1]
205 205 if len(subset) == len(repo) or x in subset:
206 206 return [x]
207 207 return []
208 208
209 209 def symbolset(repo, subset, x):
210 210 if x in symbols:
211 211 raise error.ParseError(_("can't use %s here") % x)
212 212 return stringset(repo, subset, x)
213 213
214 214 def rangeset(repo, subset, x, y):
215 215 m = getset(repo, subset, x)
216 216 if not m:
217 217 m = getset(repo, range(len(repo)), x)
218 218
219 219 n = getset(repo, subset, y)
220 220 if not n:
221 221 n = getset(repo, range(len(repo)), y)
222 222
223 223 if not m or not n:
224 224 return []
225 225 m, n = m[0], n[-1]
226 226
227 227 if m < n:
228 228 r = range(m, n + 1)
229 229 else:
230 230 r = range(m, n - 1, -1)
231 231 s = set(subset)
232 232 return [x for x in r if x in s]
233 233
234 234 def dagrange(repo, subset, x, y):
235 235 if subset:
236 236 r = range(len(repo))
237 237 xs = _revsbetween(repo, getset(repo, r, x), getset(repo, r, y))
238 238 s = set(subset)
239 239 return [r for r in xs if r in s]
240 240 return []
241 241
242 242 def andset(repo, subset, x, y):
243 243 return getset(repo, getset(repo, subset, x), y)
244 244
245 245 def orset(repo, subset, x, y):
246 246 xl = getset(repo, subset, x)
247 247 s = set(xl)
248 248 yl = getset(repo, [r for r in subset if r not in s], y)
249 249 return xl + yl
250 250
251 251 def notset(repo, subset, x):
252 252 s = set(getset(repo, subset, x))
253 253 return [r for r in subset if r not in s]
254 254
255 255 def listset(repo, subset, a, b):
256 256 raise error.ParseError(_("can't use a list in this context"))
257 257
258 258 def func(repo, subset, a, b):
259 259 if a[0] == 'symbol' and a[1] in symbols:
260 260 return symbols[a[1]](repo, subset, b)
261 261 raise error.ParseError(_("not a function: %s") % a[1])
262 262
263 263 # functions
264 264
265 265 def adds(repo, subset, x):
266 266 """``adds(pattern)``
267 267 Changesets that add a file matching pattern.
268 268 """
269 269 # i18n: "adds" is a keyword
270 270 pat = getstring(x, _("adds requires a pattern"))
271 271 return checkstatus(repo, subset, pat, 1)
272 272
273 273 def ancestor(repo, subset, x):
274 274 """``ancestor(single, single)``
275 275 Greatest common ancestor of the two changesets.
276 276 """
277 277 # i18n: "ancestor" is a keyword
278 278 l = getargs(x, 2, 2, _("ancestor requires two arguments"))
279 279 r = range(len(repo))
280 280 a = getset(repo, r, l[0])
281 281 b = getset(repo, r, l[1])
282 282 if len(a) != 1 or len(b) != 1:
283 283 # i18n: "ancestor" is a keyword
284 284 raise error.ParseError(_("ancestor arguments must be single revisions"))
285 285 an = [repo[a[0]].ancestor(repo[b[0]]).rev()]
286 286
287 287 return [r for r in an if r in subset]
288 288
289 289 def _ancestors(repo, subset, x, followfirst=False):
290 290 args = getset(repo, range(len(repo)), x)
291 291 if not args:
292 292 return []
293 293 s = set(_revancestors(repo, args, followfirst)) | set(args)
294 294 return [r for r in subset if r in s]
295 295
296 296 def ancestors(repo, subset, x):
297 297 """``ancestors(set)``
298 298 Changesets that are ancestors of a changeset in set.
299 299 """
300 300 return _ancestors(repo, subset, x)
301 301
302 302 def _firstancestors(repo, subset, x):
303 303 # ``_firstancestors(set)``
304 304 # Like ``ancestors(set)`` but follows only the first parents.
305 305 return _ancestors(repo, subset, x, followfirst=True)
306 306
307 307 def ancestorspec(repo, subset, x, n):
308 308 """``set~n``
309 309 Changesets that are the Nth ancestor (first parents only) of a changeset
310 310 in set.
311 311 """
312 312 try:
313 313 n = int(n[1])
314 314 except (TypeError, ValueError):
315 315 raise error.ParseError(_("~ expects a number"))
316 316 ps = set()
317 317 cl = repo.changelog
318 318 for r in getset(repo, subset, x):
319 319 for i in range(n):
320 320 r = cl.parentrevs(r)[0]
321 321 ps.add(r)
322 322 return [r for r in subset if r in ps]
323 323
324 324 def author(repo, subset, x):
325 325 """``author(string)``
326 326 Alias for ``user(string)``.
327 327 """
328 328 # i18n: "author" is a keyword
329 329 n = encoding.lower(getstring(x, _("author requires a string")))
330 330 kind, pattern, matcher = _substringmatcher(n)
331 331 return [r for r in subset if matcher(encoding.lower(repo[r].user()))]
332 332
333 333 def bisect(repo, subset, x):
334 334 """``bisect(string)``
335 335 Changesets marked in the specified bisect status:
336 336
337 337 - ``good``, ``bad``, ``skip``: csets explicitly marked as good/bad/skip
338 338 - ``goods``, ``bads`` : csets topologicaly good/bad
339 339 - ``range`` : csets taking part in the bisection
340 340 - ``pruned`` : csets that are goods, bads or skipped
341 341 - ``untested`` : csets whose fate is yet unknown
342 342 - ``ignored`` : csets ignored due to DAG topology
343 343 - ``current`` : the cset currently being bisected
344 344 """
345 345 # i18n: "bisect" is a keyword
346 346 status = getstring(x, _("bisect requires a string")).lower()
347 347 state = set(hbisect.get(repo, status))
348 348 return [r for r in subset if r in state]
349 349
350 350 # Backward-compatibility
351 351 # - no help entry so that we do not advertise it any more
352 352 def bisected(repo, subset, x):
353 353 return bisect(repo, subset, x)
354 354
355 355 def bookmark(repo, subset, x):
356 356 """``bookmark([name])``
357 357 The named bookmark or all bookmarks.
358 358
359 359 If `name` starts with `re:`, the remainder of the name is treated as
360 360 a regular expression. To match a bookmark that actually starts with `re:`,
361 361 use the prefix `literal:`.
362 362 """
363 363 # i18n: "bookmark" is a keyword
364 364 args = getargs(x, 0, 1, _('bookmark takes one or no arguments'))
365 365 if args:
366 366 bm = getstring(args[0],
367 367 # i18n: "bookmark" is a keyword
368 368 _('the argument to bookmark must be a string'))
369 369 kind, pattern, matcher = _stringmatcher(bm)
370 370 if kind == 'literal':
371 371 bmrev = bookmarksmod.listbookmarks(repo).get(bm, None)
372 372 if not bmrev:
373 373 raise util.Abort(_("bookmark '%s' does not exist") % bm)
374 374 bmrev = repo[bmrev].rev()
375 375 return [r for r in subset if r == bmrev]
376 376 else:
377 377 matchrevs = set()
378 378 for name, bmrev in bookmarksmod.listbookmarks(repo).iteritems():
379 379 if matcher(name):
380 380 matchrevs.add(bmrev)
381 381 if not matchrevs:
382 382 raise util.Abort(_("no bookmarks exist that match '%s'")
383 383 % pattern)
384 384 bmrevs = set()
385 385 for bmrev in matchrevs:
386 386 bmrevs.add(repo[bmrev].rev())
387 387 return [r for r in subset if r in bmrevs]
388 388
389 389 bms = set([repo[r].rev()
390 390 for r in bookmarksmod.listbookmarks(repo).values()])
391 391 return [r for r in subset if r in bms]
392 392
393 393 def branch(repo, subset, x):
394 394 """``branch(string or set)``
395 395 All changesets belonging to the given branch or the branches of the given
396 396 changesets.
397 397
398 398 If `string` starts with `re:`, the remainder of the name is treated as
399 399 a regular expression. To match a branch that actually starts with `re:`,
400 400 use the prefix `literal:`.
401 401 """
402 402 try:
403 403 b = getstring(x, '')
404 404 except error.ParseError:
405 405 # not a string, but another revspec, e.g. tip()
406 406 pass
407 407 else:
408 408 kind, pattern, matcher = _stringmatcher(b)
409 409 if kind == 'literal':
410 410 # note: falls through to the revspec case if no branch with
411 411 # this name exists
412 412 if pattern in repo.branchmap():
413 413 return [r for r in subset if matcher(repo[r].branch())]
414 414 else:
415 415 return [r for r in subset if matcher(repo[r].branch())]
416 416
417 417 s = getset(repo, range(len(repo)), x)
418 418 b = set()
419 419 for r in s:
420 420 b.add(repo[r].branch())
421 421 s = set(s)
422 422 return [r for r in subset if r in s or repo[r].branch() in b]
423 423
424 424 def checkstatus(repo, subset, pat, field):
425 425 m = None
426 426 s = []
427 427 hasset = matchmod.patkind(pat) == 'set'
428 428 fname = None
429 429 for r in subset:
430 430 c = repo[r]
431 431 if not m or hasset:
432 432 m = matchmod.match(repo.root, repo.getcwd(), [pat], ctx=c)
433 433 if not m.anypats() and len(m.files()) == 1:
434 434 fname = m.files()[0]
435 435 if fname is not None:
436 436 if fname not in c.files():
437 437 continue
438 438 else:
439 439 for f in c.files():
440 440 if m(f):
441 441 break
442 442 else:
443 443 continue
444 444 files = repo.status(c.p1().node(), c.node())[field]
445 445 if fname is not None:
446 446 if fname in files:
447 447 s.append(r)
448 448 else:
449 449 for f in files:
450 450 if m(f):
451 451 s.append(r)
452 452 break
453 453 return s
454 454
455 455 def _children(repo, narrow, parentset):
456 456 cs = set()
457 457 pr = repo.changelog.parentrevs
458 458 for r in narrow:
459 459 for p in pr(r):
460 460 if p in parentset:
461 461 cs.add(r)
462 462 return cs
463 463
464 464 def children(repo, subset, x):
465 465 """``children(set)``
466 466 Child changesets of changesets in set.
467 467 """
468 468 s = set(getset(repo, range(len(repo)), x))
469 469 cs = _children(repo, subset, s)
470 470 return [r for r in subset if r in cs]
471 471
472 472 def closed(repo, subset, x):
473 473 """``closed()``
474 474 Changeset is closed.
475 475 """
476 476 # i18n: "closed" is a keyword
477 477 getargs(x, 0, 0, _("closed takes no arguments"))
478 478 return [r for r in subset if repo[r].closesbranch()]
479 479
480 480 def contains(repo, subset, x):
481 481 """``contains(pattern)``
482 482 Revision contains a file matching pattern. See :hg:`help patterns`
483 483 for information about file patterns.
484 484 """
485 485 # i18n: "contains" is a keyword
486 486 pat = getstring(x, _("contains requires a pattern"))
487 487 m = None
488 488 s = []
489 489 if not matchmod.patkind(pat):
490 490 for r in subset:
491 491 if pat in repo[r]:
492 492 s.append(r)
493 493 else:
494 494 for r in subset:
495 495 c = repo[r]
496 496 if not m or matchmod.patkind(pat) == 'set':
497 497 m = matchmod.match(repo.root, repo.getcwd(), [pat], ctx=c)
498 498 for f in c.manifest():
499 499 if m(f):
500 500 s.append(r)
501 501 break
502 502 return s
503 503
504 504 def converted(repo, subset, x):
505 505 """``converted([id])``
506 506 Changesets converted from the given identifier in the old repository if
507 507 present, or all converted changesets if no identifier is specified.
508 508 """
509 509
510 510 # There is exactly no chance of resolving the revision, so do a simple
511 511 # string compare and hope for the best
512 512
513 513 rev = None
514 514 # i18n: "converted" is a keyword
515 515 l = getargs(x, 0, 1, _('converted takes one or no arguments'))
516 516 if l:
517 517 # i18n: "converted" is a keyword
518 518 rev = getstring(l[0], _('converted requires a revision'))
519 519
520 520 def _matchvalue(r):
521 521 source = repo[r].extra().get('convert_revision', None)
522 522 return source is not None and (rev is None or source.startswith(rev))
523 523
524 524 return [r for r in subset if _matchvalue(r)]
525 525
526 526 def date(repo, subset, x):
527 527 """``date(interval)``
528 528 Changesets within the interval, see :hg:`help dates`.
529 529 """
530 530 # i18n: "date" is a keyword
531 531 ds = getstring(x, _("date requires a string"))
532 532 dm = util.matchdate(ds)
533 533 return [r for r in subset if dm(repo[r].date()[0])]
534 534
535 535 def desc(repo, subset, x):
536 536 """``desc(string)``
537 537 Search commit message for string. The match is case-insensitive.
538 538 """
539 539 # i18n: "desc" is a keyword
540 540 ds = encoding.lower(getstring(x, _("desc requires a string")))
541 541 l = []
542 542 for r in subset:
543 543 c = repo[r]
544 544 if ds in encoding.lower(c.description()):
545 545 l.append(r)
546 546 return l
547 547
548 548 def _descendants(repo, subset, x, followfirst=False):
549 549 args = getset(repo, range(len(repo)), x)
550 550 if not args:
551 551 return []
552 552 s = set(_revdescendants(repo, args, followfirst)) | set(args)
553 553 return [r for r in subset if r in s]
554 554
555 555 def descendants(repo, subset, x):
556 556 """``descendants(set)``
557 557 Changesets which are descendants of changesets in set.
558 558 """
559 559 return _descendants(repo, subset, x)
560 560
561 561 def _firstdescendants(repo, subset, x):
562 562 # ``_firstdescendants(set)``
563 563 # Like ``descendants(set)`` but follows only the first parents.
564 564 return _descendants(repo, subset, x, followfirst=True)
565 565
566 566 def destination(repo, subset, x):
567 567 """``destination([set])``
568 568 Changesets that were created by a graft, transplant or rebase operation,
569 569 with the given revisions specified as the source. Omitting the optional set
570 570 is the same as passing all().
571 571 """
572 572 if x is not None:
573 573 args = set(getset(repo, range(len(repo)), x))
574 574 else:
575 575 args = set(getall(repo, range(len(repo)), x))
576 576
577 577 dests = set()
578 578
579 579 # subset contains all of the possible destinations that can be returned, so
580 580 # iterate over them and see if their source(s) were provided in the args.
581 581 # Even if the immediate src of r is not in the args, src's source (or
582 582 # further back) may be. Scanning back further than the immediate src allows
583 583 # transitive transplants and rebases to yield the same results as transitive
584 584 # grafts.
585 585 for r in subset:
586 586 src = _getrevsource(repo, r)
587 587 lineage = None
588 588
589 589 while src is not None:
590 590 if lineage is None:
591 591 lineage = list()
592 592
593 593 lineage.append(r)
594 594
595 595 # The visited lineage is a match if the current source is in the arg
596 596 # set. Since every candidate dest is visited by way of iterating
597 597 # subset, any dests futher back in the lineage will be tested by a
598 598 # different iteration over subset. Likewise, if the src was already
599 599 # selected, the current lineage can be selected without going back
600 600 # further.
601 601 if src in args or src in dests:
602 602 dests.update(lineage)
603 603 break
604 604
605 605 r = src
606 606 src = _getrevsource(repo, r)
607 607
608 608 return [r for r in subset if r in dests]
609 609
610 610 def draft(repo, subset, x):
611 611 """``draft()``
612 612 Changeset in draft phase."""
613 613 # i18n: "draft" is a keyword
614 614 getargs(x, 0, 0, _("draft takes no arguments"))
615 615 pc = repo._phasecache
616 616 return [r for r in subset if pc.phase(repo, r) == phases.draft]
617 617
618 618 def extinct(repo, subset, x):
619 619 """``extinct()``
620 620 Obsolete changesets with obsolete descendants only.
621 621 """
622 622 # i18n: "extinct" is a keyword
623 623 getargs(x, 0, 0, _("extinct takes no arguments"))
624 624 extinctset = set(repo.revs('(obsolete()::) - (::(not obsolete()))'))
625 625 return [r for r in subset if r in extinctset]
626 626
627 627 def extra(repo, subset, x):
628 628 """``extra(label, [value])``
629 629 Changesets with the given label in the extra metadata, with the given
630 630 optional value.
631 631
632 632 If `value` starts with `re:`, the remainder of the value is treated as
633 633 a regular expression. To match a value that actually starts with `re:`,
634 634 use the prefix `literal:`.
635 635 """
636 636
637 637 # i18n: "extra" is a keyword
638 638 l = getargs(x, 1, 2, _('extra takes at least 1 and at most 2 arguments'))
639 639 # i18n: "extra" is a keyword
640 640 label = getstring(l[0], _('first argument to extra must be a string'))
641 641 value = None
642 642
643 643 if len(l) > 1:
644 644 # i18n: "extra" is a keyword
645 645 value = getstring(l[1], _('second argument to extra must be a string'))
646 646 kind, value, matcher = _stringmatcher(value)
647 647
648 648 def _matchvalue(r):
649 649 extra = repo[r].extra()
650 650 return label in extra and (value is None or matcher(extra[label]))
651 651
652 652 return [r for r in subset if _matchvalue(r)]
653 653
654 654 def filelog(repo, subset, x):
655 655 """``filelog(pattern)``
656 656 Changesets connected to the specified filelog.
657 657
658 658 For performance reasons, ``filelog()`` does not show every changeset
659 659 that affects the requested file(s). See :hg:`help log` for details. For
660 660 a slower, more accurate result, use ``file()``.
661 661 """
662 662
663 663 # i18n: "filelog" is a keyword
664 664 pat = getstring(x, _("filelog requires a pattern"))
665 665 m = matchmod.match(repo.root, repo.getcwd(), [pat], default='relpath',
666 666 ctx=repo[None])
667 667 s = set()
668 668
669 669 if not matchmod.patkind(pat):
670 670 for f in m.files():
671 671 fl = repo.file(f)
672 672 for fr in fl:
673 673 s.add(fl.linkrev(fr))
674 674 else:
675 675 for f in repo[None]:
676 676 if m(f):
677 677 fl = repo.file(f)
678 678 for fr in fl:
679 679 s.add(fl.linkrev(fr))
680 680
681 681 return [r for r in subset if r in s]
682 682
683 683 def first(repo, subset, x):
684 684 """``first(set, [n])``
685 685 An alias for limit().
686 686 """
687 687 return limit(repo, subset, x)
688 688
689 689 def _follow(repo, subset, x, name, followfirst=False):
690 690 l = getargs(x, 0, 1, _("%s takes no arguments or a filename") % name)
691 691 c = repo['.']
692 692 if l:
693 693 x = getstring(l[0], _("%s expected a filename") % name)
694 694 if x in c:
695 695 cx = c[x]
696 696 s = set(ctx.rev() for ctx in cx.ancestors(followfirst=followfirst))
697 697 # include the revision responsible for the most recent version
698 698 s.add(cx.linkrev())
699 699 else:
700 700 return []
701 701 else:
702 702 s = set(_revancestors(repo, [c.rev()], followfirst)) | set([c.rev()])
703 703
704 704 return [r for r in subset if r in s]
705 705
706 706 def follow(repo, subset, x):
707 707 """``follow([file])``
708 708 An alias for ``::.`` (ancestors of the working copy's first parent).
709 709 If a filename is specified, the history of the given file is followed,
710 710 including copies.
711 711 """
712 712 return _follow(repo, subset, x, 'follow')
713 713
714 714 def _followfirst(repo, subset, x):
715 715 # ``followfirst([file])``
716 716 # Like ``follow([file])`` but follows only the first parent of
717 717 # every revision or file revision.
718 718 return _follow(repo, subset, x, '_followfirst', followfirst=True)
719 719
720 720 def getall(repo, subset, x):
721 721 """``all()``
722 722 All changesets, the same as ``0:tip``.
723 723 """
724 724 # i18n: "all" is a keyword
725 725 getargs(x, 0, 0, _("all takes no arguments"))
726 726 return subset
727 727
728 728 def grep(repo, subset, x):
729 729 """``grep(regex)``
730 730 Like ``keyword(string)`` but accepts a regex. Use ``grep(r'...')``
731 731 to ensure special escape characters are handled correctly. Unlike
732 732 ``keyword(string)``, the match is case-sensitive.
733 733 """
734 734 try:
735 735 # i18n: "grep" is a keyword
736 736 gr = re.compile(getstring(x, _("grep requires a string")))
737 737 except re.error, e:
738 738 raise error.ParseError(_('invalid match pattern: %s') % e)
739 739 l = []
740 740 for r in subset:
741 741 c = repo[r]
742 742 for e in c.files() + [c.user(), c.description()]:
743 743 if gr.search(e):
744 744 l.append(r)
745 745 break
746 746 return l
747 747
748 748 def _matchfiles(repo, subset, x):
749 749 # _matchfiles takes a revset list of prefixed arguments:
750 750 #
751 751 # [p:foo, i:bar, x:baz]
752 752 #
753 753 # builds a match object from them and filters subset. Allowed
754 754 # prefixes are 'p:' for regular patterns, 'i:' for include
755 755 # patterns and 'x:' for exclude patterns. Use 'r:' prefix to pass
756 756 # a revision identifier, or the empty string to reference the
757 757 # working directory, from which the match object is
758 758 # initialized. Use 'd:' to set the default matching mode, default
759 759 # to 'glob'. At most one 'r:' and 'd:' argument can be passed.
760 760
761 761 # i18n: "_matchfiles" is a keyword
762 762 l = getargs(x, 1, -1, _("_matchfiles requires at least one argument"))
763 763 pats, inc, exc = [], [], []
764 764 hasset = False
765 765 rev, default = None, None
766 766 for arg in l:
767 767 # i18n: "_matchfiles" is a keyword
768 768 s = getstring(arg, _("_matchfiles requires string arguments"))
769 769 prefix, value = s[:2], s[2:]
770 770 if prefix == 'p:':
771 771 pats.append(value)
772 772 elif prefix == 'i:':
773 773 inc.append(value)
774 774 elif prefix == 'x:':
775 775 exc.append(value)
776 776 elif prefix == 'r:':
777 777 if rev is not None:
778 778 # i18n: "_matchfiles" is a keyword
779 779 raise error.ParseError(_('_matchfiles expected at most one '
780 780 'revision'))
781 781 rev = value
782 782 elif prefix == 'd:':
783 783 if default is not None:
784 784 # i18n: "_matchfiles" is a keyword
785 785 raise error.ParseError(_('_matchfiles expected at most one '
786 786 'default mode'))
787 787 default = value
788 788 else:
789 789 # i18n: "_matchfiles" is a keyword
790 790 raise error.ParseError(_('invalid _matchfiles prefix: %s') % prefix)
791 791 if not hasset and matchmod.patkind(value) == 'set':
792 792 hasset = True
793 793 if not default:
794 794 default = 'glob'
795 795 m = None
796 796 s = []
797 797 for r in subset:
798 798 c = repo[r]
799 799 if not m or (hasset and rev is None):
800 800 ctx = c
801 801 if rev is not None:
802 802 ctx = repo[rev or None]
803 803 m = matchmod.match(repo.root, repo.getcwd(), pats, include=inc,
804 804 exclude=exc, ctx=ctx, default=default)
805 805 for f in c.files():
806 806 if m(f):
807 807 s.append(r)
808 808 break
809 809 return s
810 810
811 811 def hasfile(repo, subset, x):
812 812 """``file(pattern)``
813 813 Changesets affecting files matched by pattern.
814 814
815 815 For a faster but less accurate result, consider using ``filelog()``
816 816 instead.
817 817 """
818 818 # i18n: "file" is a keyword
819 819 pat = getstring(x, _("file requires a pattern"))
820 820 return _matchfiles(repo, subset, ('string', 'p:' + pat))
821 821
822 822 def head(repo, subset, x):
823 823 """``head()``
824 824 Changeset is a named branch head.
825 825 """
826 826 # i18n: "head" is a keyword
827 827 getargs(x, 0, 0, _("head takes no arguments"))
828 828 hs = set()
829 829 for b, ls in repo.branchmap().iteritems():
830 830 hs.update(repo[h].rev() for h in ls)
831 831 return [r for r in subset if r in hs]
832 832
833 833 def heads(repo, subset, x):
834 834 """``heads(set)``
835 835 Members of set with no children in set.
836 836 """
837 837 s = getset(repo, subset, x)
838 838 ps = set(parents(repo, subset, x))
839 839 return [r for r in s if r not in ps]
840 840
841 def hidden(repo, subset, x):
842 """``hidden()``
843 Hidden changesets.
844 """
845 # i18n: "hidden" is a keyword
846 getargs(x, 0, 0, _("hidden takes no arguments"))
847 return [r for r in subset if r in repo.hiddenrevs]
848
841 849 def keyword(repo, subset, x):
842 850 """``keyword(string)``
843 851 Search commit message, user name, and names of changed files for
844 852 string. The match is case-insensitive.
845 853 """
846 854 # i18n: "keyword" is a keyword
847 855 kw = encoding.lower(getstring(x, _("keyword requires a string")))
848 856 l = []
849 857 for r in subset:
850 858 c = repo[r]
851 859 t = " ".join(c.files() + [c.user(), c.description()])
852 860 if kw in encoding.lower(t):
853 861 l.append(r)
854 862 return l
855 863
856 864 def limit(repo, subset, x):
857 865 """``limit(set, [n])``
858 866 First n members of set, defaulting to 1.
859 867 """
860 868 # i18n: "limit" is a keyword
861 869 l = getargs(x, 1, 2, _("limit requires one or two arguments"))
862 870 try:
863 871 lim = 1
864 872 if len(l) == 2:
865 873 # i18n: "limit" is a keyword
866 874 lim = int(getstring(l[1], _("limit requires a number")))
867 875 except (TypeError, ValueError):
868 876 # i18n: "limit" is a keyword
869 877 raise error.ParseError(_("limit expects a number"))
870 878 ss = set(subset)
871 879 os = getset(repo, range(len(repo)), l[0])[:lim]
872 880 return [r for r in os if r in ss]
873 881
874 882 def last(repo, subset, x):
875 883 """``last(set, [n])``
876 884 Last n members of set, defaulting to 1.
877 885 """
878 886 # i18n: "last" is a keyword
879 887 l = getargs(x, 1, 2, _("last requires one or two arguments"))
880 888 try:
881 889 lim = 1
882 890 if len(l) == 2:
883 891 # i18n: "last" is a keyword
884 892 lim = int(getstring(l[1], _("last requires a number")))
885 893 except (TypeError, ValueError):
886 894 # i18n: "last" is a keyword
887 895 raise error.ParseError(_("last expects a number"))
888 896 ss = set(subset)
889 897 os = getset(repo, range(len(repo)), l[0])[-lim:]
890 898 return [r for r in os if r in ss]
891 899
892 900 def maxrev(repo, subset, x):
893 901 """``max(set)``
894 902 Changeset with highest revision number in set.
895 903 """
896 904 os = getset(repo, range(len(repo)), x)
897 905 if os:
898 906 m = max(os)
899 907 if m in subset:
900 908 return [m]
901 909 return []
902 910
903 911 def merge(repo, subset, x):
904 912 """``merge()``
905 913 Changeset is a merge changeset.
906 914 """
907 915 # i18n: "merge" is a keyword
908 916 getargs(x, 0, 0, _("merge takes no arguments"))
909 917 cl = repo.changelog
910 918 return [r for r in subset if cl.parentrevs(r)[1] != -1]
911 919
912 920 def minrev(repo, subset, x):
913 921 """``min(set)``
914 922 Changeset with lowest revision number in set.
915 923 """
916 924 os = getset(repo, range(len(repo)), x)
917 925 if os:
918 926 m = min(os)
919 927 if m in subset:
920 928 return [m]
921 929 return []
922 930
923 931 def modifies(repo, subset, x):
924 932 """``modifies(pattern)``
925 933 Changesets modifying files matched by pattern.
926 934 """
927 935 # i18n: "modifies" is a keyword
928 936 pat = getstring(x, _("modifies requires a pattern"))
929 937 return checkstatus(repo, subset, pat, 0)
930 938
931 939 def node_(repo, subset, x):
932 940 """``id(string)``
933 941 Revision non-ambiguously specified by the given hex string prefix.
934 942 """
935 943 # i18n: "id" is a keyword
936 944 l = getargs(x, 1, 1, _("id requires one argument"))
937 945 # i18n: "id" is a keyword
938 946 n = getstring(l[0], _("id requires a string"))
939 947 if len(n) == 40:
940 948 rn = repo[n].rev()
941 949 else:
942 950 rn = None
943 951 pm = repo.changelog._partialmatch(n)
944 952 if pm is not None:
945 953 rn = repo.changelog.rev(pm)
946 954
947 955 return [r for r in subset if r == rn]
948 956
949 957 def obsolete(repo, subset, x):
950 958 """``obsolete()``
951 959 Mutable changeset with a newer version."""
952 960 # i18n: "obsolete" is a keyword
953 961 getargs(x, 0, 0, _("obsolete takes no arguments"))
954 962 return [r for r in subset if repo[r].obsolete()]
955 963
956 964 def origin(repo, subset, x):
957 965 """``origin([set])``
958 966 Changesets that were specified as a source for the grafts, transplants or
959 967 rebases that created the given revisions. Omitting the optional set is the
960 968 same as passing all(). If a changeset created by these operations is itself
961 969 specified as a source for one of these operations, only the source changeset
962 970 for the first operation is selected.
963 971 """
964 972 if x is not None:
965 973 args = set(getset(repo, range(len(repo)), x))
966 974 else:
967 975 args = set(getall(repo, range(len(repo)), x))
968 976
969 977 def _firstsrc(rev):
970 978 src = _getrevsource(repo, rev)
971 979 if src is None:
972 980 return None
973 981
974 982 while True:
975 983 prev = _getrevsource(repo, src)
976 984
977 985 if prev is None:
978 986 return src
979 987 src = prev
980 988
981 989 o = set([_firstsrc(r) for r in args])
982 990 return [r for r in subset if r in o]
983 991
984 992 def outgoing(repo, subset, x):
985 993 """``outgoing([path])``
986 994 Changesets not found in the specified destination repository, or the
987 995 default push location.
988 996 """
989 997 import hg # avoid start-up nasties
990 998 # i18n: "outgoing" is a keyword
991 999 l = getargs(x, 0, 1, _("outgoing takes one or no arguments"))
992 1000 # i18n: "outgoing" is a keyword
993 1001 dest = l and getstring(l[0], _("outgoing requires a repository path")) or ''
994 1002 dest = repo.ui.expandpath(dest or 'default-push', dest or 'default')
995 1003 dest, branches = hg.parseurl(dest)
996 1004 revs, checkout = hg.addbranchrevs(repo, repo, branches, [])
997 1005 if revs:
998 1006 revs = [repo.lookup(rev) for rev in revs]
999 1007 other = hg.peer(repo, {}, dest)
1000 1008 repo.ui.pushbuffer()
1001 1009 outgoing = discovery.findcommonoutgoing(repo, other, onlyheads=revs)
1002 1010 repo.ui.popbuffer()
1003 1011 cl = repo.changelog
1004 1012 o = set([cl.rev(r) for r in outgoing.missing])
1005 1013 return [r for r in subset if r in o]
1006 1014
1007 1015 def p1(repo, subset, x):
1008 1016 """``p1([set])``
1009 1017 First parent of changesets in set, or the working directory.
1010 1018 """
1011 1019 if x is None:
1012 1020 p = repo[x].p1().rev()
1013 1021 return [r for r in subset if r == p]
1014 1022
1015 1023 ps = set()
1016 1024 cl = repo.changelog
1017 1025 for r in getset(repo, range(len(repo)), x):
1018 1026 ps.add(cl.parentrevs(r)[0])
1019 1027 return [r for r in subset if r in ps]
1020 1028
1021 1029 def p2(repo, subset, x):
1022 1030 """``p2([set])``
1023 1031 Second parent of changesets in set, or the working directory.
1024 1032 """
1025 1033 if x is None:
1026 1034 ps = repo[x].parents()
1027 1035 try:
1028 1036 p = ps[1].rev()
1029 1037 return [r for r in subset if r == p]
1030 1038 except IndexError:
1031 1039 return []
1032 1040
1033 1041 ps = set()
1034 1042 cl = repo.changelog
1035 1043 for r in getset(repo, range(len(repo)), x):
1036 1044 ps.add(cl.parentrevs(r)[1])
1037 1045 return [r for r in subset if r in ps]
1038 1046
1039 1047 def parents(repo, subset, x):
1040 1048 """``parents([set])``
1041 1049 The set of all parents for all changesets in set, or the working directory.
1042 1050 """
1043 1051 if x is None:
1044 1052 ps = tuple(p.rev() for p in repo[x].parents())
1045 1053 return [r for r in subset if r in ps]
1046 1054
1047 1055 ps = set()
1048 1056 cl = repo.changelog
1049 1057 for r in getset(repo, range(len(repo)), x):
1050 1058 ps.update(cl.parentrevs(r))
1051 1059 return [r for r in subset if r in ps]
1052 1060
1053 1061 def parentspec(repo, subset, x, n):
1054 1062 """``set^0``
1055 1063 The set.
1056 1064 ``set^1`` (or ``set^``), ``set^2``
1057 1065 First or second parent, respectively, of all changesets in set.
1058 1066 """
1059 1067 try:
1060 1068 n = int(n[1])
1061 1069 if n not in (0, 1, 2):
1062 1070 raise ValueError
1063 1071 except (TypeError, ValueError):
1064 1072 raise error.ParseError(_("^ expects a number 0, 1, or 2"))
1065 1073 ps = set()
1066 1074 cl = repo.changelog
1067 1075 for r in getset(repo, subset, x):
1068 1076 if n == 0:
1069 1077 ps.add(r)
1070 1078 elif n == 1:
1071 1079 ps.add(cl.parentrevs(r)[0])
1072 1080 elif n == 2:
1073 1081 parents = cl.parentrevs(r)
1074 1082 if len(parents) > 1:
1075 1083 ps.add(parents[1])
1076 1084 return [r for r in subset if r in ps]
1077 1085
1078 1086 def present(repo, subset, x):
1079 1087 """``present(set)``
1080 1088 An empty set, if any revision in set isn't found; otherwise,
1081 1089 all revisions in set.
1082 1090
1083 1091 If any of specified revisions is not present in the local repository,
1084 1092 the query is normally aborted. But this predicate allows the query
1085 1093 to continue even in such cases.
1086 1094 """
1087 1095 try:
1088 1096 return getset(repo, subset, x)
1089 1097 except error.RepoLookupError:
1090 1098 return []
1091 1099
1092 1100 def public(repo, subset, x):
1093 1101 """``public()``
1094 1102 Changeset in public phase."""
1095 1103 # i18n: "public" is a keyword
1096 1104 getargs(x, 0, 0, _("public takes no arguments"))
1097 1105 pc = repo._phasecache
1098 1106 return [r for r in subset if pc.phase(repo, r) == phases.public]
1099 1107
1100 1108 def remote(repo, subset, x):
1101 1109 """``remote([id [,path]])``
1102 1110 Local revision that corresponds to the given identifier in a
1103 1111 remote repository, if present. Here, the '.' identifier is a
1104 1112 synonym for the current local branch.
1105 1113 """
1106 1114
1107 1115 import hg # avoid start-up nasties
1108 1116 # i18n: "remote" is a keyword
1109 1117 l = getargs(x, 0, 2, _("remote takes one, two or no arguments"))
1110 1118
1111 1119 q = '.'
1112 1120 if len(l) > 0:
1113 1121 # i18n: "remote" is a keyword
1114 1122 q = getstring(l[0], _("remote requires a string id"))
1115 1123 if q == '.':
1116 1124 q = repo['.'].branch()
1117 1125
1118 1126 dest = ''
1119 1127 if len(l) > 1:
1120 1128 # i18n: "remote" is a keyword
1121 1129 dest = getstring(l[1], _("remote requires a repository path"))
1122 1130 dest = repo.ui.expandpath(dest or 'default')
1123 1131 dest, branches = hg.parseurl(dest)
1124 1132 revs, checkout = hg.addbranchrevs(repo, repo, branches, [])
1125 1133 if revs:
1126 1134 revs = [repo.lookup(rev) for rev in revs]
1127 1135 other = hg.peer(repo, {}, dest)
1128 1136 n = other.lookup(q)
1129 1137 if n in repo:
1130 1138 r = repo[n].rev()
1131 1139 if r in subset:
1132 1140 return [r]
1133 1141 return []
1134 1142
1135 1143 def removes(repo, subset, x):
1136 1144 """``removes(pattern)``
1137 1145 Changesets which remove files matching pattern.
1138 1146 """
1139 1147 # i18n: "removes" is a keyword
1140 1148 pat = getstring(x, _("removes requires a pattern"))
1141 1149 return checkstatus(repo, subset, pat, 2)
1142 1150
1143 1151 def rev(repo, subset, x):
1144 1152 """``rev(number)``
1145 1153 Revision with the given numeric identifier.
1146 1154 """
1147 1155 # i18n: "rev" is a keyword
1148 1156 l = getargs(x, 1, 1, _("rev requires one argument"))
1149 1157 try:
1150 1158 # i18n: "rev" is a keyword
1151 1159 l = int(getstring(l[0], _("rev requires a number")))
1152 1160 except (TypeError, ValueError):
1153 1161 # i18n: "rev" is a keyword
1154 1162 raise error.ParseError(_("rev expects a number"))
1155 1163 return [r for r in subset if r == l]
1156 1164
1157 1165 def matching(repo, subset, x):
1158 1166 """``matching(revision [, field])``
1159 1167 Changesets in which a given set of fields match the set of fields in the
1160 1168 selected revision or set.
1161 1169
1162 1170 To match more than one field pass the list of fields to match separated
1163 1171 by spaces (e.g. ``author description``).
1164 1172
1165 1173 Valid fields are most regular revision fields and some special fields.
1166 1174
1167 1175 Regular revision fields are ``description``, ``author``, ``branch``,
1168 1176 ``date``, ``files``, ``phase``, ``parents``, ``substate``, ``user``
1169 1177 and ``diff``.
1170 1178 Note that ``author`` and ``user`` are synonyms. ``diff`` refers to the
1171 1179 contents of the revision. Two revisions matching their ``diff`` will
1172 1180 also match their ``files``.
1173 1181
1174 1182 Special fields are ``summary`` and ``metadata``:
1175 1183 ``summary`` matches the first line of the description.
1176 1184 ``metadata`` is equivalent to matching ``description user date``
1177 1185 (i.e. it matches the main metadata fields).
1178 1186
1179 1187 ``metadata`` is the default field which is used when no fields are
1180 1188 specified. You can match more than one field at a time.
1181 1189 """
1182 1190 # i18n: "matching" is a keyword
1183 1191 l = getargs(x, 1, 2, _("matching takes 1 or 2 arguments"))
1184 1192
1185 1193 revs = getset(repo, xrange(len(repo)), l[0])
1186 1194
1187 1195 fieldlist = ['metadata']
1188 1196 if len(l) > 1:
1189 1197 fieldlist = getstring(l[1],
1190 1198 # i18n: "matching" is a keyword
1191 1199 _("matching requires a string "
1192 1200 "as its second argument")).split()
1193 1201
1194 1202 # Make sure that there are no repeated fields,
1195 1203 # expand the 'special' 'metadata' field type
1196 1204 # and check the 'files' whenever we check the 'diff'
1197 1205 fields = []
1198 1206 for field in fieldlist:
1199 1207 if field == 'metadata':
1200 1208 fields += ['user', 'description', 'date']
1201 1209 elif field == 'diff':
1202 1210 # a revision matching the diff must also match the files
1203 1211 # since matching the diff is very costly, make sure to
1204 1212 # also match the files first
1205 1213 fields += ['files', 'diff']
1206 1214 else:
1207 1215 if field == 'author':
1208 1216 field = 'user'
1209 1217 fields.append(field)
1210 1218 fields = set(fields)
1211 1219 if 'summary' in fields and 'description' in fields:
1212 1220 # If a revision matches its description it also matches its summary
1213 1221 fields.discard('summary')
1214 1222
1215 1223 # We may want to match more than one field
1216 1224 # Not all fields take the same amount of time to be matched
1217 1225 # Sort the selected fields in order of increasing matching cost
1218 1226 fieldorder = ['phase', 'parents', 'user', 'date', 'branch', 'summary',
1219 1227 'files', 'description', 'substate', 'diff']
1220 1228 def fieldkeyfunc(f):
1221 1229 try:
1222 1230 return fieldorder.index(f)
1223 1231 except ValueError:
1224 1232 # assume an unknown field is very costly
1225 1233 return len(fieldorder)
1226 1234 fields = list(fields)
1227 1235 fields.sort(key=fieldkeyfunc)
1228 1236
1229 1237 # Each field will be matched with its own "getfield" function
1230 1238 # which will be added to the getfieldfuncs array of functions
1231 1239 getfieldfuncs = []
1232 1240 _funcs = {
1233 1241 'user': lambda r: repo[r].user(),
1234 1242 'branch': lambda r: repo[r].branch(),
1235 1243 'date': lambda r: repo[r].date(),
1236 1244 'description': lambda r: repo[r].description(),
1237 1245 'files': lambda r: repo[r].files(),
1238 1246 'parents': lambda r: repo[r].parents(),
1239 1247 'phase': lambda r: repo[r].phase(),
1240 1248 'substate': lambda r: repo[r].substate,
1241 1249 'summary': lambda r: repo[r].description().splitlines()[0],
1242 1250 'diff': lambda r: list(repo[r].diff(git=True),)
1243 1251 }
1244 1252 for info in fields:
1245 1253 getfield = _funcs.get(info, None)
1246 1254 if getfield is None:
1247 1255 raise error.ParseError(
1248 1256 # i18n: "matching" is a keyword
1249 1257 _("unexpected field name passed to matching: %s") % info)
1250 1258 getfieldfuncs.append(getfield)
1251 1259 # convert the getfield array of functions into a "getinfo" function
1252 1260 # which returns an array of field values (or a single value if there
1253 1261 # is only one field to match)
1254 1262 getinfo = lambda r: [f(r) for f in getfieldfuncs]
1255 1263
1256 1264 matches = set()
1257 1265 for rev in revs:
1258 1266 target = getinfo(rev)
1259 1267 for r in subset:
1260 1268 match = True
1261 1269 for n, f in enumerate(getfieldfuncs):
1262 1270 if target[n] != f(r):
1263 1271 match = False
1264 1272 break
1265 1273 if match:
1266 1274 matches.add(r)
1267 1275 return [r for r in subset if r in matches]
1268 1276
1269 1277 def reverse(repo, subset, x):
1270 1278 """``reverse(set)``
1271 1279 Reverse order of set.
1272 1280 """
1273 1281 l = getset(repo, subset, x)
1274 1282 if not isinstance(l, list):
1275 1283 l = list(l)
1276 1284 l.reverse()
1277 1285 return l
1278 1286
1279 1287 def roots(repo, subset, x):
1280 1288 """``roots(set)``
1281 1289 Changesets in set with no parent changeset in set.
1282 1290 """
1283 1291 s = set(getset(repo, xrange(len(repo)), x))
1284 1292 subset = [r for r in subset if r in s]
1285 1293 cs = _children(repo, subset, s)
1286 1294 return [r for r in subset if r not in cs]
1287 1295
1288 1296 def secret(repo, subset, x):
1289 1297 """``secret()``
1290 1298 Changeset in secret phase."""
1291 1299 # i18n: "secret" is a keyword
1292 1300 getargs(x, 0, 0, _("secret takes no arguments"))
1293 1301 pc = repo._phasecache
1294 1302 return [r for r in subset if pc.phase(repo, r) == phases.secret]
1295 1303
1296 1304 def sort(repo, subset, x):
1297 1305 """``sort(set[, [-]key...])``
1298 1306 Sort set by keys. The default sort order is ascending, specify a key
1299 1307 as ``-key`` to sort in descending order.
1300 1308
1301 1309 The keys can be:
1302 1310
1303 1311 - ``rev`` for the revision number,
1304 1312 - ``branch`` for the branch name,
1305 1313 - ``desc`` for the commit message (description),
1306 1314 - ``user`` for user name (``author`` can be used as an alias),
1307 1315 - ``date`` for the commit date
1308 1316 """
1309 1317 # i18n: "sort" is a keyword
1310 1318 l = getargs(x, 1, 2, _("sort requires one or two arguments"))
1311 1319 keys = "rev"
1312 1320 if len(l) == 2:
1313 1321 # i18n: "sort" is a keyword
1314 1322 keys = getstring(l[1], _("sort spec must be a string"))
1315 1323
1316 1324 s = l[0]
1317 1325 keys = keys.split()
1318 1326 l = []
1319 1327 def invert(s):
1320 1328 return "".join(chr(255 - ord(c)) for c in s)
1321 1329 for r in getset(repo, subset, s):
1322 1330 c = repo[r]
1323 1331 e = []
1324 1332 for k in keys:
1325 1333 if k == 'rev':
1326 1334 e.append(r)
1327 1335 elif k == '-rev':
1328 1336 e.append(-r)
1329 1337 elif k == 'branch':
1330 1338 e.append(c.branch())
1331 1339 elif k == '-branch':
1332 1340 e.append(invert(c.branch()))
1333 1341 elif k == 'desc':
1334 1342 e.append(c.description())
1335 1343 elif k == '-desc':
1336 1344 e.append(invert(c.description()))
1337 1345 elif k in 'user author':
1338 1346 e.append(c.user())
1339 1347 elif k in '-user -author':
1340 1348 e.append(invert(c.user()))
1341 1349 elif k == 'date':
1342 1350 e.append(c.date()[0])
1343 1351 elif k == '-date':
1344 1352 e.append(-c.date()[0])
1345 1353 else:
1346 1354 raise error.ParseError(_("unknown sort key %r") % k)
1347 1355 e.append(r)
1348 1356 l.append(e)
1349 1357 l.sort()
1350 1358 return [e[-1] for e in l]
1351 1359
1352 1360 def _stringmatcher(pattern):
1353 1361 """
1354 1362 accepts a string, possibly starting with 're:' or 'literal:' prefix.
1355 1363 returns the matcher name, pattern, and matcher function.
1356 1364 missing or unknown prefixes are treated as literal matches.
1357 1365
1358 1366 helper for tests:
1359 1367 >>> def test(pattern, *tests):
1360 1368 ... kind, pattern, matcher = _stringmatcher(pattern)
1361 1369 ... return (kind, pattern, [bool(matcher(t)) for t in tests])
1362 1370
1363 1371 exact matching (no prefix):
1364 1372 >>> test('abcdefg', 'abc', 'def', 'abcdefg')
1365 1373 ('literal', 'abcdefg', [False, False, True])
1366 1374
1367 1375 regex matching ('re:' prefix)
1368 1376 >>> test('re:a.+b', 'nomatch', 'fooadef', 'fooadefbar')
1369 1377 ('re', 'a.+b', [False, False, True])
1370 1378
1371 1379 force exact matches ('literal:' prefix)
1372 1380 >>> test('literal:re:foobar', 'foobar', 're:foobar')
1373 1381 ('literal', 're:foobar', [False, True])
1374 1382
1375 1383 unknown prefixes are ignored and treated as literals
1376 1384 >>> test('foo:bar', 'foo', 'bar', 'foo:bar')
1377 1385 ('literal', 'foo:bar', [False, False, True])
1378 1386 """
1379 1387 if pattern.startswith('re:'):
1380 1388 pattern = pattern[3:]
1381 1389 try:
1382 1390 regex = re.compile(pattern)
1383 1391 except re.error, e:
1384 1392 raise error.ParseError(_('invalid regular expression: %s')
1385 1393 % e)
1386 1394 return 're', pattern, regex.search
1387 1395 elif pattern.startswith('literal:'):
1388 1396 pattern = pattern[8:]
1389 1397 return 'literal', pattern, pattern.__eq__
1390 1398
1391 1399 def _substringmatcher(pattern):
1392 1400 kind, pattern, matcher = _stringmatcher(pattern)
1393 1401 if kind == 'literal':
1394 1402 matcher = lambda s: pattern in s
1395 1403 return kind, pattern, matcher
1396 1404
1397 1405 def tag(repo, subset, x):
1398 1406 """``tag([name])``
1399 1407 The specified tag by name, or all tagged revisions if no name is given.
1400 1408 """
1401 1409 # i18n: "tag" is a keyword
1402 1410 args = getargs(x, 0, 1, _("tag takes one or no arguments"))
1403 1411 cl = repo.changelog
1404 1412 if args:
1405 1413 pattern = getstring(args[0],
1406 1414 # i18n: "tag" is a keyword
1407 1415 _('the argument to tag must be a string'))
1408 1416 kind, pattern, matcher = _stringmatcher(pattern)
1409 1417 if kind == 'literal':
1410 1418 # avoid resolving all tags
1411 1419 tn = repo._tagscache.tags.get(pattern, None)
1412 1420 if tn is None:
1413 1421 raise util.Abort(_("tag '%s' does not exist") % pattern)
1414 1422 s = set([repo[tn].rev()])
1415 1423 else:
1416 1424 s = set([cl.rev(n) for t, n in repo.tagslist() if matcher(t)])
1417 1425 if not s:
1418 1426 raise util.Abort(_("no tags exist that match '%s'") % pattern)
1419 1427 else:
1420 1428 s = set([cl.rev(n) for t, n in repo.tagslist() if t != 'tip'])
1421 1429 return [r for r in subset if r in s]
1422 1430
1423 1431 def tagged(repo, subset, x):
1424 1432 return tag(repo, subset, x)
1425 1433
1426 1434 def unstable(repo, subset, x):
1427 1435 """``unstable()``
1428 1436 Non-obsolete changesets with obsolete ancestors.
1429 1437 """
1430 1438 # i18n: "unstable" is a keyword
1431 1439 getargs(x, 0, 0, _("unstable takes no arguments"))
1432 1440 unstableset = set(repo.revs('(obsolete()::) - obsolete()'))
1433 1441 return [r for r in subset if r in unstableset]
1434 1442
1435 1443
1436 1444 def user(repo, subset, x):
1437 1445 """``user(string)``
1438 1446 User name contains string. The match is case-insensitive.
1439 1447
1440 1448 If `string` starts with `re:`, the remainder of the string is treated as
1441 1449 a regular expression. To match a user that actually contains `re:`, use
1442 1450 the prefix `literal:`.
1443 1451 """
1444 1452 return author(repo, subset, x)
1445 1453
1446 1454 # for internal use
1447 1455 def _list(repo, subset, x):
1448 1456 s = getstring(x, "internal error")
1449 1457 if not s:
1450 1458 return []
1451 1459 if not isinstance(subset, set):
1452 1460 subset = set(subset)
1453 1461 ls = [repo[r].rev() for r in s.split('\0')]
1454 1462 return [r for r in ls if r in subset]
1455 1463
1456 1464 symbols = {
1457 1465 "adds": adds,
1458 1466 "all": getall,
1459 1467 "ancestor": ancestor,
1460 1468 "ancestors": ancestors,
1461 1469 "_firstancestors": _firstancestors,
1462 1470 "author": author,
1463 1471 "bisect": bisect,
1464 1472 "bisected": bisected,
1465 1473 "bookmark": bookmark,
1466 1474 "branch": branch,
1467 1475 "children": children,
1468 1476 "closed": closed,
1469 1477 "contains": contains,
1470 1478 "converted": converted,
1471 1479 "date": date,
1472 1480 "desc": desc,
1473 1481 "descendants": descendants,
1474 1482 "_firstdescendants": _firstdescendants,
1475 1483 "destination": destination,
1476 1484 "draft": draft,
1477 1485 "extinct": extinct,
1478 1486 "extra": extra,
1479 1487 "file": hasfile,
1480 1488 "filelog": filelog,
1481 1489 "first": first,
1482 1490 "follow": follow,
1483 1491 "_followfirst": _followfirst,
1484 1492 "grep": grep,
1485 1493 "head": head,
1486 1494 "heads": heads,
1495 "hidden": hidden,
1487 1496 "id": node_,
1488 1497 "keyword": keyword,
1489 1498 "last": last,
1490 1499 "limit": limit,
1491 1500 "_matchfiles": _matchfiles,
1492 1501 "max": maxrev,
1493 1502 "merge": merge,
1494 1503 "min": minrev,
1495 1504 "modifies": modifies,
1496 1505 "obsolete": obsolete,
1497 1506 "origin": origin,
1498 1507 "outgoing": outgoing,
1499 1508 "p1": p1,
1500 1509 "p2": p2,
1501 1510 "parents": parents,
1502 1511 "present": present,
1503 1512 "public": public,
1504 1513 "remote": remote,
1505 1514 "removes": removes,
1506 1515 "rev": rev,
1507 1516 "reverse": reverse,
1508 1517 "roots": roots,
1509 1518 "sort": sort,
1510 1519 "secret": secret,
1511 1520 "matching": matching,
1512 1521 "tag": tag,
1513 1522 "tagged": tagged,
1514 1523 "user": user,
1515 1524 "unstable": unstable,
1516 1525 "_list": _list,
1517 1526 }
1518 1527
1519 1528 methods = {
1520 1529 "range": rangeset,
1521 1530 "dagrange": dagrange,
1522 1531 "string": stringset,
1523 1532 "symbol": symbolset,
1524 1533 "and": andset,
1525 1534 "or": orset,
1526 1535 "not": notset,
1527 1536 "list": listset,
1528 1537 "func": func,
1529 1538 "ancestor": ancestorspec,
1530 1539 "parent": parentspec,
1531 1540 "parentpost": p1,
1532 1541 }
1533 1542
1534 1543 def optimize(x, small):
1535 1544 if x is None:
1536 1545 return 0, x
1537 1546
1538 1547 smallbonus = 1
1539 1548 if small:
1540 1549 smallbonus = .5
1541 1550
1542 1551 op = x[0]
1543 1552 if op == 'minus':
1544 1553 return optimize(('and', x[1], ('not', x[2])), small)
1545 1554 elif op == 'dagrangepre':
1546 1555 return optimize(('func', ('symbol', 'ancestors'), x[1]), small)
1547 1556 elif op == 'dagrangepost':
1548 1557 return optimize(('func', ('symbol', 'descendants'), x[1]), small)
1549 1558 elif op == 'rangepre':
1550 1559 return optimize(('range', ('string', '0'), x[1]), small)
1551 1560 elif op == 'rangepost':
1552 1561 return optimize(('range', x[1], ('string', 'tip')), small)
1553 1562 elif op == 'negate':
1554 1563 return optimize(('string',
1555 1564 '-' + getstring(x[1], _("can't negate that"))), small)
1556 1565 elif op in 'string symbol negate':
1557 1566 return smallbonus, x # single revisions are small
1558 1567 elif op == 'and':
1559 1568 wa, ta = optimize(x[1], True)
1560 1569 wb, tb = optimize(x[2], True)
1561 1570 w = min(wa, wb)
1562 1571 if wa > wb:
1563 1572 return w, (op, tb, ta)
1564 1573 return w, (op, ta, tb)
1565 1574 elif op == 'or':
1566 1575 wa, ta = optimize(x[1], False)
1567 1576 wb, tb = optimize(x[2], False)
1568 1577 if wb < wa:
1569 1578 wb, wa = wa, wb
1570 1579 return max(wa, wb), (op, ta, tb)
1571 1580 elif op == 'not':
1572 1581 o = optimize(x[1], not small)
1573 1582 return o[0], (op, o[1])
1574 1583 elif op == 'parentpost':
1575 1584 o = optimize(x[1], small)
1576 1585 return o[0], (op, o[1])
1577 1586 elif op == 'group':
1578 1587 return optimize(x[1], small)
1579 1588 elif op in 'dagrange range list parent ancestorspec':
1580 1589 if op == 'parent':
1581 1590 # x^:y means (x^) : y, not x ^ (:y)
1582 1591 post = ('parentpost', x[1])
1583 1592 if x[2][0] == 'dagrangepre':
1584 1593 return optimize(('dagrange', post, x[2][1]), small)
1585 1594 elif x[2][0] == 'rangepre':
1586 1595 return optimize(('range', post, x[2][1]), small)
1587 1596
1588 1597 wa, ta = optimize(x[1], small)
1589 1598 wb, tb = optimize(x[2], small)
1590 1599 return wa + wb, (op, ta, tb)
1591 1600 elif op == 'func':
1592 1601 f = getstring(x[1], _("not a symbol"))
1593 1602 wa, ta = optimize(x[2], small)
1594 1603 if f in ("author branch closed date desc file grep keyword "
1595 1604 "outgoing user"):
1596 1605 w = 10 # slow
1597 1606 elif f in "modifies adds removes":
1598 1607 w = 30 # slower
1599 1608 elif f == "contains":
1600 1609 w = 100 # very slow
1601 1610 elif f == "ancestor":
1602 1611 w = 1 * smallbonus
1603 1612 elif f in "reverse limit first":
1604 1613 w = 0
1605 1614 elif f in "sort":
1606 1615 w = 10 # assume most sorts look at changelog
1607 1616 else:
1608 1617 w = 1
1609 1618 return w + wa, (op, x[1], ta)
1610 1619 return 1, x
1611 1620
1612 1621 _aliasarg = ('func', ('symbol', '_aliasarg'))
1613 1622 def _getaliasarg(tree):
1614 1623 """If tree matches ('func', ('symbol', '_aliasarg'), ('string', X))
1615 1624 return X, None otherwise.
1616 1625 """
1617 1626 if (len(tree) == 3 and tree[:2] == _aliasarg
1618 1627 and tree[2][0] == 'string'):
1619 1628 return tree[2][1]
1620 1629 return None
1621 1630
1622 1631 def _checkaliasarg(tree, known=None):
1623 1632 """Check tree contains no _aliasarg construct or only ones which
1624 1633 value is in known. Used to avoid alias placeholders injection.
1625 1634 """
1626 1635 if isinstance(tree, tuple):
1627 1636 arg = _getaliasarg(tree)
1628 1637 if arg is not None and (not known or arg not in known):
1629 1638 raise error.ParseError(_("not a function: %s") % '_aliasarg')
1630 1639 for t in tree:
1631 1640 _checkaliasarg(t, known)
1632 1641
1633 1642 class revsetalias(object):
1634 1643 funcre = re.compile('^([^(]+)\(([^)]+)\)$')
1635 1644 args = None
1636 1645
1637 1646 def __init__(self, name, value):
1638 1647 '''Aliases like:
1639 1648
1640 1649 h = heads(default)
1641 1650 b($1) = ancestors($1) - ancestors(default)
1642 1651 '''
1643 1652 m = self.funcre.search(name)
1644 1653 if m:
1645 1654 self.name = m.group(1)
1646 1655 self.tree = ('func', ('symbol', m.group(1)))
1647 1656 self.args = [x.strip() for x in m.group(2).split(',')]
1648 1657 for arg in self.args:
1649 1658 # _aliasarg() is an unknown symbol only used separate
1650 1659 # alias argument placeholders from regular strings.
1651 1660 value = value.replace(arg, '_aliasarg(%r)' % (arg,))
1652 1661 else:
1653 1662 self.name = name
1654 1663 self.tree = ('symbol', name)
1655 1664
1656 1665 self.replacement, pos = parse(value)
1657 1666 if pos != len(value):
1658 1667 raise error.ParseError(_('invalid token'), pos)
1659 1668 # Check for placeholder injection
1660 1669 _checkaliasarg(self.replacement, self.args)
1661 1670
1662 1671 def _getalias(aliases, tree):
1663 1672 """If tree looks like an unexpanded alias, return it. Return None
1664 1673 otherwise.
1665 1674 """
1666 1675 if isinstance(tree, tuple) and tree:
1667 1676 if tree[0] == 'symbol' and len(tree) == 2:
1668 1677 name = tree[1]
1669 1678 alias = aliases.get(name)
1670 1679 if alias and alias.args is None and alias.tree == tree:
1671 1680 return alias
1672 1681 if tree[0] == 'func' and len(tree) > 1:
1673 1682 if tree[1][0] == 'symbol' and len(tree[1]) == 2:
1674 1683 name = tree[1][1]
1675 1684 alias = aliases.get(name)
1676 1685 if alias and alias.args is not None and alias.tree == tree[:2]:
1677 1686 return alias
1678 1687 return None
1679 1688
1680 1689 def _expandargs(tree, args):
1681 1690 """Replace _aliasarg instances with the substitution value of the
1682 1691 same name in args, recursively.
1683 1692 """
1684 1693 if not tree or not isinstance(tree, tuple):
1685 1694 return tree
1686 1695 arg = _getaliasarg(tree)
1687 1696 if arg is not None:
1688 1697 return args[arg]
1689 1698 return tuple(_expandargs(t, args) for t in tree)
1690 1699
1691 1700 def _expandaliases(aliases, tree, expanding, cache):
1692 1701 """Expand aliases in tree, recursively.
1693 1702
1694 1703 'aliases' is a dictionary mapping user defined aliases to
1695 1704 revsetalias objects.
1696 1705 """
1697 1706 if not isinstance(tree, tuple):
1698 1707 # Do not expand raw strings
1699 1708 return tree
1700 1709 alias = _getalias(aliases, tree)
1701 1710 if alias is not None:
1702 1711 if alias in expanding:
1703 1712 raise error.ParseError(_('infinite expansion of revset alias "%s" '
1704 1713 'detected') % alias.name)
1705 1714 expanding.append(alias)
1706 1715 if alias.name not in cache:
1707 1716 cache[alias.name] = _expandaliases(aliases, alias.replacement,
1708 1717 expanding, cache)
1709 1718 result = cache[alias.name]
1710 1719 expanding.pop()
1711 1720 if alias.args is not None:
1712 1721 l = getlist(tree[2])
1713 1722 if len(l) != len(alias.args):
1714 1723 raise error.ParseError(
1715 1724 _('invalid number of arguments: %s') % len(l))
1716 1725 l = [_expandaliases(aliases, a, [], cache) for a in l]
1717 1726 result = _expandargs(result, dict(zip(alias.args, l)))
1718 1727 else:
1719 1728 result = tuple(_expandaliases(aliases, t, expanding, cache)
1720 1729 for t in tree)
1721 1730 return result
1722 1731
1723 1732 def findaliases(ui, tree):
1724 1733 _checkaliasarg(tree)
1725 1734 aliases = {}
1726 1735 for k, v in ui.configitems('revsetalias'):
1727 1736 alias = revsetalias(k, v)
1728 1737 aliases[alias.name] = alias
1729 1738 return _expandaliases(aliases, tree, [], {})
1730 1739
1731 1740 parse = parser.parser(tokenize, elements).parse
1732 1741
1733 1742 def match(ui, spec):
1734 1743 if not spec:
1735 1744 raise error.ParseError(_("empty query"))
1736 1745 tree, pos = parse(spec)
1737 1746 if (pos != len(spec)):
1738 1747 raise error.ParseError(_("invalid token"), pos)
1739 1748 if ui:
1740 1749 tree = findaliases(ui, tree)
1741 1750 weight, tree = optimize(tree, True)
1742 1751 def mfunc(repo, subset):
1743 1752 return getset(repo, subset, tree)
1744 1753 return mfunc
1745 1754
1746 1755 def formatspec(expr, *args):
1747 1756 '''
1748 1757 This is a convenience function for using revsets internally, and
1749 1758 escapes arguments appropriately. Aliases are intentionally ignored
1750 1759 so that intended expression behavior isn't accidentally subverted.
1751 1760
1752 1761 Supported arguments:
1753 1762
1754 1763 %r = revset expression, parenthesized
1755 1764 %d = int(arg), no quoting
1756 1765 %s = string(arg), escaped and single-quoted
1757 1766 %b = arg.branch(), escaped and single-quoted
1758 1767 %n = hex(arg), single-quoted
1759 1768 %% = a literal '%'
1760 1769
1761 1770 Prefixing the type with 'l' specifies a parenthesized list of that type.
1762 1771
1763 1772 >>> formatspec('%r:: and %lr', '10 or 11', ("this()", "that()"))
1764 1773 '(10 or 11):: and ((this()) or (that()))'
1765 1774 >>> formatspec('%d:: and not %d::', 10, 20)
1766 1775 '10:: and not 20::'
1767 1776 >>> formatspec('%ld or %ld', [], [1])
1768 1777 "_list('') or 1"
1769 1778 >>> formatspec('keyword(%s)', 'foo\\xe9')
1770 1779 "keyword('foo\\\\xe9')"
1771 1780 >>> b = lambda: 'default'
1772 1781 >>> b.branch = b
1773 1782 >>> formatspec('branch(%b)', b)
1774 1783 "branch('default')"
1775 1784 >>> formatspec('root(%ls)', ['a', 'b', 'c', 'd'])
1776 1785 "root(_list('a\\x00b\\x00c\\x00d'))"
1777 1786 '''
1778 1787
1779 1788 def quote(s):
1780 1789 return repr(str(s))
1781 1790
1782 1791 def argtype(c, arg):
1783 1792 if c == 'd':
1784 1793 return str(int(arg))
1785 1794 elif c == 's':
1786 1795 return quote(arg)
1787 1796 elif c == 'r':
1788 1797 parse(arg) # make sure syntax errors are confined
1789 1798 return '(%s)' % arg
1790 1799 elif c == 'n':
1791 1800 return quote(node.hex(arg))
1792 1801 elif c == 'b':
1793 1802 return quote(arg.branch())
1794 1803
1795 1804 def listexp(s, t):
1796 1805 l = len(s)
1797 1806 if l == 0:
1798 1807 return "_list('')"
1799 1808 elif l == 1:
1800 1809 return argtype(t, s[0])
1801 1810 elif t == 'd':
1802 1811 return "_list('%s')" % "\0".join(str(int(a)) for a in s)
1803 1812 elif t == 's':
1804 1813 return "_list('%s')" % "\0".join(s)
1805 1814 elif t == 'n':
1806 1815 return "_list('%s')" % "\0".join(node.hex(a) for a in s)
1807 1816 elif t == 'b':
1808 1817 return "_list('%s')" % "\0".join(a.branch() for a in s)
1809 1818
1810 1819 m = l // 2
1811 1820 return '(%s or %s)' % (listexp(s[:m], t), listexp(s[m:], t))
1812 1821
1813 1822 ret = ''
1814 1823 pos = 0
1815 1824 arg = 0
1816 1825 while pos < len(expr):
1817 1826 c = expr[pos]
1818 1827 if c == '%':
1819 1828 pos += 1
1820 1829 d = expr[pos]
1821 1830 if d == '%':
1822 1831 ret += d
1823 1832 elif d in 'dsnbr':
1824 1833 ret += argtype(d, args[arg])
1825 1834 arg += 1
1826 1835 elif d == 'l':
1827 1836 # a list of some type
1828 1837 pos += 1
1829 1838 d = expr[pos]
1830 1839 ret += listexp(list(args[arg]), d)
1831 1840 arg += 1
1832 1841 else:
1833 1842 raise util.Abort('unexpected revspec format character %s' % d)
1834 1843 else:
1835 1844 ret += c
1836 1845 pos += 1
1837 1846
1838 1847 return ret
1839 1848
1840 1849 def prettyformat(tree):
1841 1850 def _prettyformat(tree, level, lines):
1842 1851 if not isinstance(tree, tuple) or tree[0] in ('string', 'symbol'):
1843 1852 lines.append((level, str(tree)))
1844 1853 else:
1845 1854 lines.append((level, '(%s' % tree[0]))
1846 1855 for s in tree[1:]:
1847 1856 _prettyformat(s, level + 1, lines)
1848 1857 lines[-1:] = [(lines[-1][0], lines[-1][1] + ')')]
1849 1858
1850 1859 lines = []
1851 1860 _prettyformat(tree, 0, lines)
1852 1861 output = '\n'.join((' '*l + s) for l, s in lines)
1853 1862 return output
1854 1863
1855 1864 # tell hggettext to extract docstrings from these functions:
1856 1865 i18nfunctions = symbols.values()
@@ -1,508 +1,511 b''
1 1 $ cat >> $HGRCPATH << EOF
2 2 > [extensions]
3 3 > graphlog=
4 4 > [phases]
5 5 > # public changeset are not obsolete
6 6 > publish=false
7 7 > EOF
8 8 $ mkcommit() {
9 9 > echo "$1" > "$1"
10 10 > hg add "$1"
11 11 > hg ci -m "add $1"
12 12 > }
13 13 $ getid() {
14 14 > hg id --debug -ir "desc('$1')"
15 15 > }
16 16
17 17 $ cat > debugkeys.py <<EOF
18 18 > def reposetup(ui, repo):
19 19 > class debugkeysrepo(repo.__class__):
20 20 > def listkeys(self, namespace):
21 21 > ui.write('listkeys %s\n' % (namespace,))
22 22 > return super(debugkeysrepo, self).listkeys(namespace)
23 23 >
24 24 > if repo.local():
25 25 > repo.__class__ = debugkeysrepo
26 26 > EOF
27 27
28 28 $ hg init tmpa
29 29 $ cd tmpa
30 30 $ mkcommit kill_me
31 31
32 32 Checking that the feature is properly disabled
33 33
34 34 $ hg debugobsolete -d '0 0' `getid kill_me` -u babar
35 35 abort: obsolete feature is not enabled on this repo
36 36 [255]
37 37
38 38 Enabling it
39 39
40 40 $ cat > ../obs.py << EOF
41 41 > import mercurial.obsolete
42 42 > mercurial.obsolete._enabled = True
43 43 > EOF
44 44 $ echo '[extensions]' >> $HGRCPATH
45 45 $ echo "obs=${TESTTMP}/obs.py" >> $HGRCPATH
46 46
47 47 Killing a single changeset without replacement
48 48
49 49 $ hg debugobsolete 0
50 50 abort: changeset references must be full hexadecimal node identifiers
51 51 [255]
52 52 $ hg debugobsolete '00'
53 53 abort: changeset references must be full hexadecimal node identifiers
54 54 [255]
55 55 $ hg debugobsolete -d '0 0' `getid kill_me` -u babar
56 56 $ hg debugobsolete
57 57 97b7c2d76b1845ed3eb988cd612611e72406cef0 0 {'date': '0 0', 'user': 'babar'}
58 58 $ cd ..
59 59
60 60 Killing a single changeset with replacement
61 61
62 62 $ hg init tmpb
63 63 $ cd tmpb
64 64 $ mkcommit a
65 65 $ mkcommit b
66 66 $ mkcommit original_c
67 67 $ hg up "desc('b')"
68 68 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
69 69 $ mkcommit new_c
70 70 created new head
71 $ hg log -r 'hidden()' --template '{rev}:{node|short} {desc}\n' --hidden
71 72 $ hg debugobsolete `getid original_c` `getid new_c` -d '56 12'
73 $ hg log -r 'hidden()' --template '{rev}:{node|short} {desc}\n' --hidden
74 2:245bde4270cd add original_c
72 75 $ hg debugobsolete
73 76 245bde4270cd1072a27757984f9cda8ba26f08ca cdbce2fbb16313928851e97e0d85413f3f7eb77f 0 {'date': '56 12', 'user': 'test'}
74 77
75 78 do it again (it read the obsstore before adding new changeset)
76 79
77 80 $ hg up '.^'
78 81 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
79 82 $ mkcommit new_2_c
80 83 created new head
81 84 $ hg debugobsolete -d '1337 0' `getid new_c` `getid new_2_c`
82 85 $ hg debugobsolete
83 86 245bde4270cd1072a27757984f9cda8ba26f08ca cdbce2fbb16313928851e97e0d85413f3f7eb77f 0 {'date': '56 12', 'user': 'test'}
84 87 cdbce2fbb16313928851e97e0d85413f3f7eb77f ca819180edb99ed25ceafb3e9584ac287e240b00 0 {'date': '1337 0', 'user': 'test'}
85 88
86 89 Register two markers with a missing node
87 90
88 91 $ hg up '.^'
89 92 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
90 93 $ mkcommit new_3_c
91 94 created new head
92 95 $ hg debugobsolete -d '1338 0' `getid new_2_c` 1337133713371337133713371337133713371337
93 96 $ hg debugobsolete -d '1339 0' 1337133713371337133713371337133713371337 `getid new_3_c`
94 97 $ hg debugobsolete
95 98 245bde4270cd1072a27757984f9cda8ba26f08ca cdbce2fbb16313928851e97e0d85413f3f7eb77f 0 {'date': '56 12', 'user': 'test'}
96 99 cdbce2fbb16313928851e97e0d85413f3f7eb77f ca819180edb99ed25ceafb3e9584ac287e240b00 0 {'date': '1337 0', 'user': 'test'}
97 100 ca819180edb99ed25ceafb3e9584ac287e240b00 1337133713371337133713371337133713371337 0 {'date': '1338 0', 'user': 'test'}
98 101 1337133713371337133713371337133713371337 5601fb93a350734d935195fee37f4054c529ff39 0 {'date': '1339 0', 'user': 'test'}
99 102
100 103 Check that graphlog detect that a changeset is obsolete:
101 104
102 105 $ hg glog
103 106 @ changeset: 5:5601fb93a350
104 107 | tag: tip
105 108 | parent: 1:7c3bad9141dc
106 109 | user: test
107 110 | date: Thu Jan 01 00:00:00 1970 +0000
108 111 | summary: add new_3_c
109 112 |
110 113 o changeset: 1:7c3bad9141dc
111 114 | user: test
112 115 | date: Thu Jan 01 00:00:00 1970 +0000
113 116 | summary: add b
114 117 |
115 118 o changeset: 0:1f0dee641bb7
116 119 user: test
117 120 date: Thu Jan 01 00:00:00 1970 +0000
118 121 summary: add a
119 122
120 123
121 124 Check that public changeset are not accounted as obsolete:
122 125
123 126 $ hg phase --public 2
124 127 $ hg --config 'extensions.graphlog=' glog
125 128 @ changeset: 5:5601fb93a350
126 129 | tag: tip
127 130 | parent: 1:7c3bad9141dc
128 131 | user: test
129 132 | date: Thu Jan 01 00:00:00 1970 +0000
130 133 | summary: add new_3_c
131 134 |
132 135 | o changeset: 2:245bde4270cd
133 136 |/ user: test
134 137 | date: Thu Jan 01 00:00:00 1970 +0000
135 138 | summary: add original_c
136 139 |
137 140 o changeset: 1:7c3bad9141dc
138 141 | user: test
139 142 | date: Thu Jan 01 00:00:00 1970 +0000
140 143 | summary: add b
141 144 |
142 145 o changeset: 0:1f0dee641bb7
143 146 user: test
144 147 date: Thu Jan 01 00:00:00 1970 +0000
145 148 summary: add a
146 149
147 150
148 151 $ cd ..
149 152
150 153 Exchange Test
151 154 ============================
152 155
153 156 Destination repo does not have any data
154 157 ---------------------------------------
155 158
156 159 Try to pull markers
157 160 (extinct changeset are excluded but marker are pushed)
158 161
159 162 $ hg init tmpc
160 163 $ cd tmpc
161 164 $ hg pull ../tmpb
162 165 pulling from ../tmpb
163 166 requesting all changes
164 167 adding changesets
165 168 adding manifests
166 169 adding file changes
167 170 added 4 changesets with 4 changes to 4 files (+1 heads)
168 171 (run 'hg heads' to see heads, 'hg merge' to merge)
169 172 $ hg debugobsolete
170 173 245bde4270cd1072a27757984f9cda8ba26f08ca cdbce2fbb16313928851e97e0d85413f3f7eb77f 0 {'date': '56 12', 'user': 'test'}
171 174 cdbce2fbb16313928851e97e0d85413f3f7eb77f ca819180edb99ed25ceafb3e9584ac287e240b00 0 {'date': '1337 0', 'user': 'test'}
172 175 ca819180edb99ed25ceafb3e9584ac287e240b00 1337133713371337133713371337133713371337 0 {'date': '1338 0', 'user': 'test'}
173 176 1337133713371337133713371337133713371337 5601fb93a350734d935195fee37f4054c529ff39 0 {'date': '1339 0', 'user': 'test'}
174 177
175 178 Rollback//Transaction support
176 179
177 180 $ hg debugobsolete -d '1340 0' aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
178 181 $ hg debugobsolete
179 182 245bde4270cd1072a27757984f9cda8ba26f08ca cdbce2fbb16313928851e97e0d85413f3f7eb77f 0 {'date': '56 12', 'user': 'test'}
180 183 cdbce2fbb16313928851e97e0d85413f3f7eb77f ca819180edb99ed25ceafb3e9584ac287e240b00 0 {'date': '1337 0', 'user': 'test'}
181 184 ca819180edb99ed25ceafb3e9584ac287e240b00 1337133713371337133713371337133713371337 0 {'date': '1338 0', 'user': 'test'}
182 185 1337133713371337133713371337133713371337 5601fb93a350734d935195fee37f4054c529ff39 0 {'date': '1339 0', 'user': 'test'}
183 186 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 0 {'date': '1340 0', 'user': 'test'}
184 187 $ hg rollback -n
185 188 repository tip rolled back to revision 3 (undo debugobsolete)
186 189 $ hg rollback
187 190 repository tip rolled back to revision 3 (undo debugobsolete)
188 191 $ hg debugobsolete
189 192 245bde4270cd1072a27757984f9cda8ba26f08ca cdbce2fbb16313928851e97e0d85413f3f7eb77f 0 {'date': '56 12', 'user': 'test'}
190 193 cdbce2fbb16313928851e97e0d85413f3f7eb77f ca819180edb99ed25ceafb3e9584ac287e240b00 0 {'date': '1337 0', 'user': 'test'}
191 194 ca819180edb99ed25ceafb3e9584ac287e240b00 1337133713371337133713371337133713371337 0 {'date': '1338 0', 'user': 'test'}
192 195 1337133713371337133713371337133713371337 5601fb93a350734d935195fee37f4054c529ff39 0 {'date': '1339 0', 'user': 'test'}
193 196
194 197 $ cd ..
195 198
196 199 Try to pull markers
197 200
198 201 $ hg init tmpd
199 202 $ hg -R tmpb push tmpd
200 203 pushing to tmpd
201 204 searching for changes
202 205 adding changesets
203 206 adding manifests
204 207 adding file changes
205 208 added 4 changesets with 4 changes to 4 files (+1 heads)
206 209 $ hg -R tmpd debugobsolete
207 210 245bde4270cd1072a27757984f9cda8ba26f08ca cdbce2fbb16313928851e97e0d85413f3f7eb77f 0 {'date': '56 12', 'user': 'test'}
208 211 cdbce2fbb16313928851e97e0d85413f3f7eb77f ca819180edb99ed25ceafb3e9584ac287e240b00 0 {'date': '1337 0', 'user': 'test'}
209 212 ca819180edb99ed25ceafb3e9584ac287e240b00 1337133713371337133713371337133713371337 0 {'date': '1338 0', 'user': 'test'}
210 213 1337133713371337133713371337133713371337 5601fb93a350734d935195fee37f4054c529ff39 0 {'date': '1339 0', 'user': 'test'}
211 214
212 215 Check obsolete keys are exchanged only if source has an obsolete store
213 216
214 217 $ hg init empty
215 218 $ hg --config extensions.debugkeys=debugkeys.py -R empty push tmpd
216 219 pushing to tmpd
217 220 no changes found
218 221 listkeys phases
219 222 listkeys bookmarks
220 223 [1]
221 224
222 225 clone support
223 226 (markers are copied and extinct changesets are included to allow hardlinks)
224 227
225 228 $ hg clone tmpb clone-dest
226 229 updating to branch default
227 230 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
228 231 $ hg -R clone-dest log -G --hidden
229 232 @ changeset: 5:5601fb93a350
230 233 | tag: tip
231 234 | parent: 1:7c3bad9141dc
232 235 | user: test
233 236 | date: Thu Jan 01 00:00:00 1970 +0000
234 237 | summary: add new_3_c
235 238 |
236 239 | x changeset: 4:ca819180edb9
237 240 |/ parent: 1:7c3bad9141dc
238 241 | user: test
239 242 | date: Thu Jan 01 00:00:00 1970 +0000
240 243 | summary: add new_2_c
241 244 |
242 245 | x changeset: 3:cdbce2fbb163
243 246 |/ parent: 1:7c3bad9141dc
244 247 | user: test
245 248 | date: Thu Jan 01 00:00:00 1970 +0000
246 249 | summary: add new_c
247 250 |
248 251 | o changeset: 2:245bde4270cd
249 252 |/ user: test
250 253 | date: Thu Jan 01 00:00:00 1970 +0000
251 254 | summary: add original_c
252 255 |
253 256 o changeset: 1:7c3bad9141dc
254 257 | user: test
255 258 | date: Thu Jan 01 00:00:00 1970 +0000
256 259 | summary: add b
257 260 |
258 261 o changeset: 0:1f0dee641bb7
259 262 user: test
260 263 date: Thu Jan 01 00:00:00 1970 +0000
261 264 summary: add a
262 265
263 266 $ hg -R clone-dest debugobsolete
264 267 245bde4270cd1072a27757984f9cda8ba26f08ca cdbce2fbb16313928851e97e0d85413f3f7eb77f 0 {'date': '56 12', 'user': 'test'}
265 268 cdbce2fbb16313928851e97e0d85413f3f7eb77f ca819180edb99ed25ceafb3e9584ac287e240b00 0 {'date': '1337 0', 'user': 'test'}
266 269 ca819180edb99ed25ceafb3e9584ac287e240b00 1337133713371337133713371337133713371337 0 {'date': '1338 0', 'user': 'test'}
267 270 1337133713371337133713371337133713371337 5601fb93a350734d935195fee37f4054c529ff39 0 {'date': '1339 0', 'user': 'test'}
268 271
269 272
270 273 Destination repo have existing data
271 274 ---------------------------------------
272 275
273 276 On pull
274 277
275 278 $ hg init tmpe
276 279 $ cd tmpe
277 280 $ hg debugobsolete -d '1339 0' 2448244824482448244824482448244824482448 1339133913391339133913391339133913391339
278 281 $ hg pull ../tmpb
279 282 pulling from ../tmpb
280 283 requesting all changes
281 284 adding changesets
282 285 adding manifests
283 286 adding file changes
284 287 added 4 changesets with 4 changes to 4 files (+1 heads)
285 288 (run 'hg heads' to see heads, 'hg merge' to merge)
286 289 $ hg debugobsolete
287 290 2448244824482448244824482448244824482448 1339133913391339133913391339133913391339 0 {'date': '1339 0', 'user': 'test'}
288 291 245bde4270cd1072a27757984f9cda8ba26f08ca cdbce2fbb16313928851e97e0d85413f3f7eb77f 0 {'date': '56 12', 'user': 'test'}
289 292 cdbce2fbb16313928851e97e0d85413f3f7eb77f ca819180edb99ed25ceafb3e9584ac287e240b00 0 {'date': '1337 0', 'user': 'test'}
290 293 ca819180edb99ed25ceafb3e9584ac287e240b00 1337133713371337133713371337133713371337 0 {'date': '1338 0', 'user': 'test'}
291 294 1337133713371337133713371337133713371337 5601fb93a350734d935195fee37f4054c529ff39 0 {'date': '1339 0', 'user': 'test'}
292 295
293 296
294 297 On push
295 298
296 299 $ hg push ../tmpc
297 300 pushing to ../tmpc
298 301 searching for changes
299 302 no changes found
300 303 [1]
301 304 $ hg -R ../tmpc debugobsolete
302 305 245bde4270cd1072a27757984f9cda8ba26f08ca cdbce2fbb16313928851e97e0d85413f3f7eb77f 0 {'date': '56 12', 'user': 'test'}
303 306 cdbce2fbb16313928851e97e0d85413f3f7eb77f ca819180edb99ed25ceafb3e9584ac287e240b00 0 {'date': '1337 0', 'user': 'test'}
304 307 ca819180edb99ed25ceafb3e9584ac287e240b00 1337133713371337133713371337133713371337 0 {'date': '1338 0', 'user': 'test'}
305 308 1337133713371337133713371337133713371337 5601fb93a350734d935195fee37f4054c529ff39 0 {'date': '1339 0', 'user': 'test'}
306 309 2448244824482448244824482448244824482448 1339133913391339133913391339133913391339 0 {'date': '1339 0', 'user': 'test'}
307 310
308 311 detect outgoing obsolete and unstable
309 312 ---------------------------------------
310 313
311 314
312 315 $ hg glog
313 316 o changeset: 3:5601fb93a350
314 317 | tag: tip
315 318 | parent: 1:7c3bad9141dc
316 319 | user: test
317 320 | date: Thu Jan 01 00:00:00 1970 +0000
318 321 | summary: add new_3_c
319 322 |
320 323 | o changeset: 2:245bde4270cd
321 324 |/ user: test
322 325 | date: Thu Jan 01 00:00:00 1970 +0000
323 326 | summary: add original_c
324 327 |
325 328 o changeset: 1:7c3bad9141dc
326 329 | user: test
327 330 | date: Thu Jan 01 00:00:00 1970 +0000
328 331 | summary: add b
329 332 |
330 333 o changeset: 0:1f0dee641bb7
331 334 user: test
332 335 date: Thu Jan 01 00:00:00 1970 +0000
333 336 summary: add a
334 337
335 338 $ hg up 'desc("new_3_c")'
336 339 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
337 340 $ mkcommit original_d
338 341 $ mkcommit original_e
339 342 $ hg debugobsolete `getid original_d` -d '0 0'
340 343 $ hg log -r 'obsolete()'
341 344 changeset: 4:7c694bff0650
342 345 user: test
343 346 date: Thu Jan 01 00:00:00 1970 +0000
344 347 summary: add original_d
345 348
346 349 $ hg glog -r '::unstable()'
347 350 @ changeset: 5:6e572121998e
348 351 | tag: tip
349 352 | user: test
350 353 | date: Thu Jan 01 00:00:00 1970 +0000
351 354 | summary: add original_e
352 355 |
353 356 x changeset: 4:7c694bff0650
354 357 | user: test
355 358 | date: Thu Jan 01 00:00:00 1970 +0000
356 359 | summary: add original_d
357 360 |
358 361 o changeset: 3:5601fb93a350
359 362 | parent: 1:7c3bad9141dc
360 363 | user: test
361 364 | date: Thu Jan 01 00:00:00 1970 +0000
362 365 | summary: add new_3_c
363 366 |
364 367 o changeset: 1:7c3bad9141dc
365 368 | user: test
366 369 | date: Thu Jan 01 00:00:00 1970 +0000
367 370 | summary: add b
368 371 |
369 372 o changeset: 0:1f0dee641bb7
370 373 user: test
371 374 date: Thu Jan 01 00:00:00 1970 +0000
372 375 summary: add a
373 376
374 377
375 378 refuse to push obsolete changeset
376 379
377 380 $ hg push ../tmpc/ -r 'desc("original_d")'
378 381 pushing to ../tmpc/
379 382 searching for changes
380 383 abort: push includes an obsolete changeset: 7c694bff0650!
381 384 [255]
382 385
383 386 refuse to push unstable changeset
384 387
385 388 $ hg push ../tmpc/
386 389 pushing to ../tmpc/
387 390 searching for changes
388 391 abort: push includes an unstable changeset: 6e572121998e!
389 392 [255]
390 393
391 394 Test that extinct changeset are properly detected
392 395
393 396 $ hg log -r 'extinct()'
394 397
395 398 Don't try to push extinct changeset
396 399
397 400 $ hg init ../tmpf
398 401 $ hg out ../tmpf
399 402 comparing with ../tmpf
400 403 searching for changes
401 404 changeset: 0:1f0dee641bb7
402 405 user: test
403 406 date: Thu Jan 01 00:00:00 1970 +0000
404 407 summary: add a
405 408
406 409 changeset: 1:7c3bad9141dc
407 410 user: test
408 411 date: Thu Jan 01 00:00:00 1970 +0000
409 412 summary: add b
410 413
411 414 changeset: 2:245bde4270cd
412 415 user: test
413 416 date: Thu Jan 01 00:00:00 1970 +0000
414 417 summary: add original_c
415 418
416 419 changeset: 3:5601fb93a350
417 420 parent: 1:7c3bad9141dc
418 421 user: test
419 422 date: Thu Jan 01 00:00:00 1970 +0000
420 423 summary: add new_3_c
421 424
422 425 changeset: 4:7c694bff0650
423 426 user: test
424 427 date: Thu Jan 01 00:00:00 1970 +0000
425 428 summary: add original_d
426 429
427 430 changeset: 5:6e572121998e
428 431 tag: tip
429 432 user: test
430 433 date: Thu Jan 01 00:00:00 1970 +0000
431 434 summary: add original_e
432 435
433 436 $ hg push ../tmpf -f # -f because be push unstable too
434 437 pushing to ../tmpf
435 438 searching for changes
436 439 adding changesets
437 440 adding manifests
438 441 adding file changes
439 442 added 6 changesets with 6 changes to 6 files (+1 heads)
440 443
441 444 no warning displayed
442 445
443 446 $ hg push ../tmpf
444 447 pushing to ../tmpf
445 448 searching for changes
446 449 no changes found
447 450 [1]
448 451
449 452 Do not warn about new head when the new head is a successors of a remote one
450 453
451 454 $ hg glog
452 455 @ changeset: 5:6e572121998e
453 456 | tag: tip
454 457 | user: test
455 458 | date: Thu Jan 01 00:00:00 1970 +0000
456 459 | summary: add original_e
457 460 |
458 461 x changeset: 4:7c694bff0650
459 462 | user: test
460 463 | date: Thu Jan 01 00:00:00 1970 +0000
461 464 | summary: add original_d
462 465 |
463 466 o changeset: 3:5601fb93a350
464 467 | parent: 1:7c3bad9141dc
465 468 | user: test
466 469 | date: Thu Jan 01 00:00:00 1970 +0000
467 470 | summary: add new_3_c
468 471 |
469 472 | o changeset: 2:245bde4270cd
470 473 |/ user: test
471 474 | date: Thu Jan 01 00:00:00 1970 +0000
472 475 | summary: add original_c
473 476 |
474 477 o changeset: 1:7c3bad9141dc
475 478 | user: test
476 479 | date: Thu Jan 01 00:00:00 1970 +0000
477 480 | summary: add b
478 481 |
479 482 o changeset: 0:1f0dee641bb7
480 483 user: test
481 484 date: Thu Jan 01 00:00:00 1970 +0000
482 485 summary: add a
483 486
484 487 $ hg up -q 'desc(new_3_c)'
485 488 $ mkcommit obsolete_e
486 489 created new head
487 490 $ hg debugobsolete `getid 'original_e'` `getid 'obsolete_e'`
488 491 $ hg push ../tmpf
489 492 pushing to ../tmpf
490 493 searching for changes
491 494 adding changesets
492 495 adding manifests
493 496 adding file changes
494 497 added 1 changesets with 1 changes to 1 files (+1 heads)
495 498
496 499 Checking _enable=False warning if obsolete marker exists
497 500
498 501 $ echo '[extensions]' >> $HGRCPATH
499 502 $ echo "obs=!" >> $HGRCPATH
500 503 $ hg log -r tip
501 504 obsolete feature not enabled but 7 markers found!
502 505 changeset: 6:d6a026544050
503 506 tag: tip
504 507 parent: 3:5601fb93a350
505 508 user: test
506 509 date: Thu Jan 01 00:00:00 1970 +0000
507 510 summary: add obsolete_e
508 511
General Comments 0
You need to be logged in to leave comments. Login now