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