1# 2# This program is free software; you can redistribute it and/or modify 3# it under the terms of the GNU General Public License version 2 4# as published by the Free Software Foundation. 5# 6# This program is distributed in the hope that it will be useful, 7# but WITHOUT ANY WARRANTY; without even the implied warranty of 8# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9# GNU General Public License for more details. 10# 11# You should have received a copy of the GNU General Public License 12# along with this program; if not, write to the Free Software 13# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 14# 15 16# 17# Copyright (c) 2008, 2010, Oracle and/or its affiliates. All rights reserved. 18# Copyright 2008, 2010, Richard Lowe 19# 20 21# 22# Theory: 23# 24# Workspaces have a non-binding parent/child relationship. 25# All important operations apply to the changes between the two. 26# 27# However, for the sake of remote operation, the 'parent' of a 28# workspace is not seen as a literal entity, instead the figurative 29# parent contains the last changeset common to both parent and child, 30# as such the 'parent tip' is actually nothing of the sort, but instead a 31# convenient imitation. 32# 33# Any change made to a workspace is a change to a file therein, such 34# changes can be represented briefly as whether the file was 35# modified/added/removed as compared to the parent workspace, whether 36# the file has a different name in the parent and if so, whether it 37# was renamed or merely copied. Each changed file has an 38# associated ActiveEntry. 39# 40# The ActiveList being a list ActiveEntrys can thus present the entire 41# change in workspace state between a parent and its child, and is the 42# important bit here (in that if it is incorrect, everything else will 43# be as incorrect, or more) 44# 45 46import cStringIO 47import os 48from mercurial import cmdutil, context, error, hg, node, patch, repair, util 49from hgext import mq 50 51from onbld.Scm import Version 52 53# 54# Mercurial 1.6 moves findoutgoing into a discover module 55# 56if Version.at_least("1.6"): 57 from mercurial import discovery 58 59 60class ActiveEntry(object): 61 '''Representation of the changes made to a single file. 62 63 MODIFIED - Contents changed, but no other changes were made 64 ADDED - File is newly created 65 REMOVED - File is being removed 66 67 Copies are represented by an Entry whose .parentname is non-nil 68 69 Truly copied files have non-nil .parentname and .renamed = False 70 Renames have non-nil .parentname and .renamed = True 71 72 Do not access any of this information directly, do so via the 73 74 .is_<change>() methods.''' 75 76 MODIFIED = 1 77 ADDED = 2 78 REMOVED = 3 79 80 def __init__(self, name): 81 self.name = name 82 self.change = None 83 self.parentname = None 84 # As opposed to copied (or neither) 85 self.renamed = False 86 self.comments = [] 87 88 # 89 # ActiveEntrys sort by the name of the file they represent. 90 # 91 def __cmp__(self, other): 92 return cmp(self.name, other.name) 93 94 def is_added(self): 95 return self.change == self.ADDED 96 97 def is_modified(self): 98 return self.change == self.MODIFIED 99 100 def is_removed(self): 101 return self.change == self.REMOVED 102 103 def is_renamed(self): 104 return self.parentname and self.renamed 105 106 def is_copied(self): 107 return self.parentname and not self.renamed 108 109 110class ActiveList(object): 111 '''Complete representation of workspace change. 112 113 In practice, a container for ActiveEntrys, and methods to build them, 114 update them, and deal with them en masse.''' 115 116 def __init__(self, ws, parenttip, revs=None): 117 self._active = {} 118 self.ws = ws 119 120 self.revs = revs 121 122 self.base = None 123 self.parenttip = parenttip 124 125 # 126 # If we couldn't find a parenttip, the two repositories must 127 # be unrelated (Hg catches most of this, but this case is valid for it 128 # but invalid for us) 129 # 130 if self.parenttip == None: 131 raise util.Abort('repository is unrelated') 132 self.localtip = None 133 134 if revs: 135 self.base = revs[0] 136 self.localtip = revs[-1] 137 138 self._comments = [] 139 140 self._build(revs) 141 142 def _build(self, revs): 143 if not revs: 144 return 145 146 status = self.ws.status(self.parenttip.node(), self.localtip.node()) 147 148 files = [] 149 for ctype in status.values(): 150 files.extend(ctype) 151 152 # 153 # When a file is renamed, two operations actually occur. 154 # A file copy from source to dest and a removal of source. 155 # 156 # These are represented as two distinct entries in the 157 # changectx and status (one on the dest file for the 158 # copy, one on the source file for the remove). 159 # 160 # Since these are unconnected in both the context and 161 # status we can only make the association by explicitly 162 # looking for it. 163 # 164 # We deal with this thusly: 165 # 166 # We maintain a dict dest -> source of all copies 167 # (updating dest as appropriate, but leaving source alone). 168 # 169 # After all other processing, we mark as renamed any pair 170 # where source is on the removed list. 171 # 172 copies = {} 173 174 # 175 # Walk revs looking for renames and adding files that 176 # are in both change context and status to the active 177 # list. 178 # 179 for ctx in revs: 180 desc = ctx.description().splitlines() 181 182 self._comments.extend(desc) 183 184 for fname in ctx.files(): 185 # 186 # We store comments per-entry as well, for the sake of 187 # webrev and similar. We store twice to avoid the problems 188 # of uniquifying comments for the general list (and possibly 189 # destroying multi-line entities in the process). 190 # 191 if fname not in self: 192 self._addentry(fname) 193 self[fname].comments.extend(desc) 194 195 try: 196 fctx = ctx.filectx(fname) 197 except error.LookupError: 198 continue 199 200 # 201 # NB: .renamed() is a misnomer, this actually checks 202 # for copies. 203 # 204 rn = fctx.renamed() 205 if rn: 206 # 207 # If the source file is a known copy we know its 208 # ancestry leads us to the parent. 209 # Otherwise make sure the source file is known to 210 # be in the parent, we need not care otherwise. 211 # 212 # We detect cycles at a later point. There is no 213 # reason to continuously handle them. 214 # 215 if rn[0] in copies: 216 copies[fname] = copies[rn[0]] 217 elif rn[0] in self.parenttip.manifest(): 218 copies[fname] = rn[0] 219 220 # 221 # Walk the copy list marking as copied any non-cyclic pair 222 # where the destination file is still present in the local 223 # tip (to avoid ephemeral changes) 224 # 225 # Where source is removed, mark as renamed, and remove the 226 # AL entry for the source file 227 # 228 for fname, oldname in copies.iteritems(): 229 if fname == oldname or fname not in self.localtip.manifest(): 230 continue 231 232 self[fname].parentname = oldname 233 234 if oldname in status['removed']: 235 self[fname].renamed = True 236 if oldname in self: 237 del self[oldname] 238 239 # 240 # Walk the active list setting the change type for each active 241 # file. 242 # 243 # In the case of modified files that are not renames or 244 # copies, we do a content comparison, and drop entries that 245 # are not actually modified. 246 # 247 # We walk a copy of the AL such that we can drop entries 248 # within the loop. 249 # 250 for entry in self._active.values(): 251 if entry.name not in files: 252 del self[entry.name] 253 continue 254 255 if entry.name in status['added']: 256 entry.change = ActiveEntry.ADDED 257 elif entry.name in status['removed']: 258 entry.change = ActiveEntry.REMOVED 259 elif entry.name in status['modified']: 260 entry.change = ActiveEntry.MODIFIED 261 262 # 263 # There are cases during a merge where a file will be in 264 # the status return as modified, but in reality be an 265 # addition (ie, not in the parenttip). 266 # 267 # We need to check whether the file is actually present 268 # in the parenttip, and set it as an add, if not. 269 # 270 if entry.name not in self.parenttip.manifest(): 271 entry.change = ActiveEntry.ADDED 272 elif entry.is_modified(): 273 if not self._changed_file(entry.name): 274 del self[entry.name] 275 continue 276 277 assert entry.change 278 279 def __contains__(self, fname): 280 return fname in self._active 281 282 def __getitem__(self, key): 283 return self._active[key] 284 285 def __setitem__(self, key, value): 286 self._active[key] = value 287 288 def __delitem__(self, key): 289 del self._active[key] 290 291 def __iter__(self): 292 for entry in self._active.values(): 293 yield entry 294 295 def _addentry(self, fname): 296 if fname not in self: 297 self[fname] = ActiveEntry(fname) 298 299 def files(self): 300 '''Return the list of pathnames of all files touched by this 301 ActiveList 302 303 Where files have been renamed, this will include both their 304 current name and the name which they had in the parent tip. 305 ''' 306 307 ret = self._active.keys() 308 ret.extend([x.parentname for x in self 309 if x.is_renamed() and x.parentname not in ret]) 310 return ret 311 312 def comments(self): 313 return self._comments 314 315 def bases(self): 316 '''Return the list of changesets that are roots of the ActiveList. 317 318 This is the set of active changesets where neither parent 319 changeset is itself active.''' 320 321 revset = set(self.revs) 322 return filter(lambda ctx: not [p for p in ctx.parents() if p in revset], 323 self.revs) 324 325 def tags(self): 326 '''Find tags that refer to a changeset in the ActiveList, 327 returning a list of 3-tuples (tag, node, is_local) for each. 328 329 We return all instances of a tag that refer to such a node, 330 not just that which takes precedence.''' 331 332 def colliding_tags(iterable, nodes, local): 333 for nd, name in [line.rstrip().split(' ', 1) for line in iterable]: 334 if nd in nodes: 335 yield (name, self.ws.repo.lookup(nd), local) 336 337 tags = [] 338 nodes = set(node.hex(ctx.node()) for ctx in self.revs) 339 340 if os.path.exists(self.ws.repo.join('localtags')): 341 fh = self.ws.repo.opener('localtags') 342 tags.extend(colliding_tags(fh, nodes, True)) 343 fh.close() 344 345 # We want to use the tags file from the localtip 346 if '.hgtags' in self.localtip: 347 data = self.localtip.filectx('.hgtags').data().splitlines() 348 tags.extend(colliding_tags(data, nodes, False)) 349 350 return tags 351 352 def prune_tags(self, data): 353 '''Return a copy of data, which should correspond to the 354 contents of a Mercurial tags file, with any tags that refer to 355 changesets which are components of the ActiveList removed.''' 356 357 nodes = set(node.hex(ctx.node()) for ctx in self.revs) 358 return [t for t in data if t.split(' ', 1)[0] not in nodes] 359 360 def _changed_file(self, path): 361 '''Compare the parent and local versions of a given file. 362 Return True if file changed, False otherwise. 363 364 Note that this compares the given path in both versions, not the given 365 entry; renamed and copied files are compared by name, not history. 366 367 The fast path compares file metadata, slow path is a 368 real comparison of file content.''' 369 370 if ((path in self.parenttip) != (path in self.localtip)): 371 return True 372 373 parentfile = self.parenttip.filectx(path) 374 localfile = self.localtip.filectx(path) 375 376 # 377 # NB: Keep these ordered such as to make every attempt 378 # to short-circuit the more time consuming checks. 379 # 380 if parentfile.size() != localfile.size(): 381 return True 382 383 if parentfile.flags() != localfile.flags(): 384 return True 385 386 if parentfile.cmp(localfile.data()): 387 return True 388 389 def context(self, message, user): 390 '''Return a Mercurial context object representing the entire 391 ActiveList as one change.''' 392 return activectx(self, message, user) 393 394 395class activectx(context.memctx): 396 '''Represent an ActiveList as a Mercurial context object. 397 398 Part of the WorkSpace.squishdeltas implementation.''' 399 400 def __init__(self, active, message, user): 401 '''Build an activectx object. 402 403 active - The ActiveList object used as the source for all data. 404 message - Changeset description 405 user - Committing user''' 406 407 def filectxfn(repository, ctx, fname): 408 fctx = active.localtip.filectx(fname) 409 data = fctx.data() 410 411 # 412 # .hgtags is a special case, tags referring to active list 413 # component changesets should be elided. 414 # 415 if fname == '.hgtags': 416 data = '\n'.join(active.prune_tags(data.splitlines())) 417 418 return context.memfilectx(fname, data, 'l' in fctx.flags(), 419 'x' in fctx.flags(), 420 active[fname].parentname) 421 422 self.__active = active 423 parents = (active.parenttip.node(), node.nullid) 424 extra = {'branch': active.localtip.branch()} 425 context.memctx.__init__(self, active.ws.repo, parents, message, 426 active.files(), filectxfn, user=user, 427 extra=extra) 428 429 def modified(self): 430 return [entry.name for entry in self.__active if entry.is_modified()] 431 432 def added(self): 433 return [entry.name for entry in self.__active if entry.is_added()] 434 435 def removed(self): 436 ret = set(entry.name for entry in self.__active if entry.is_removed()) 437 ret.update(set(x.parentname for x in self.__active if x.is_renamed())) 438 return list(ret) 439 440 def files(self): 441 return self.__active.files() 442 443 444class WorkSpace(object): 445 446 def __init__(self, repository): 447 self.repo = repository 448 self.ui = self.repo.ui 449 self.name = self.repo.root 450 451 self.activecache = {} 452 453 def parent(self, spec=None): 454 '''Return the canonical workspace parent, either SPEC (which 455 will be expanded) if provided or the default parent 456 otherwise.''' 457 458 if spec: 459 return self.ui.expandpath(spec) 460 461 p = self.ui.expandpath('default') 462 if p == 'default': 463 return None 464 else: 465 return p 466 467 def _localtip(self, outgoing, wctx): 468 '''Return the most representative changeset to act as the 469 localtip. 470 471 If the working directory is modified (has file changes, is a 472 merge, or has switched branches), this will be a workingctx. 473 474 If the working directory is unmodified, this will be the most 475 recent (highest revision number) local (outgoing) head on the 476 current branch, if no heads are determined to be outgoing, it 477 will be the most recent head on the current branch. 478 ''' 479 480 # 481 # A modified working copy is seen as a proto-branch, and thus 482 # our only option as the local tip. 483 # 484 if (wctx.files() or len(wctx.parents()) > 1 or 485 wctx.branch() != wctx.parents()[0].branch()): 486 return wctx 487 488 heads = self.repo.heads(start=wctx.parents()[0].node()) 489 headctxs = [self.repo.changectx(n) for n in heads] 490 localctxs = [c for c in headctxs if c.node() in outgoing] 491 492 ltip = sorted(localctxs or headctxs, key=lambda x: x.rev())[-1] 493 494 if len(heads) > 1: 495 self.ui.warn('The current branch has more than one head, ' 496 'using %s\n' % ltip.rev()) 497 498 return ltip 499 500 def _parenttip(self, heads, outgoing): 501 '''Return the highest-numbered, non-outgoing changeset that is 502 an ancestor of a changeset in heads. 503 504 This is intended to find the most recent changeset on a given 505 branch that is shared between a parent and child workspace, 506 such that it can act as a stand-in for the parent workspace. 507 ''' 508 509 def tipmost_shared(head, outnodes): 510 '''Return the changeset on the same branch as head that is 511 not in outnodes and is closest to the tip. 512 513 Walk outgoing changesets from head to the bottom of the 514 workspace (revision 0) and return the the first changeset 515 we see that is not in outnodes. 516 517 If none is found (all revisions >= 0 are outgoing), the 518 only possible parenttip is the null node (node.nullid) 519 which is returned explicitly. 520 ''' 521 for ctx in self._walkctxs(head, self.repo.changectx(0), 522 follow=True, 523 pick=lambda c: c.node() not in outnodes): 524 return ctx 525 526 return self.repo.changectx(node.nullid) 527 528 nodes = set(outgoing) 529 ptips = map(lambda x: tipmost_shared(x, nodes), heads) 530 return sorted(ptips, key=lambda x: x.rev(), reverse=True)[0] 531 532 def status(self, base='.', head=None): 533 '''Translate from the hg 6-tuple status format to a hash keyed 534 on change-type''' 535 536 states = ['modified', 'added', 'removed', 'deleted', 'unknown', 537 'ignored'] 538 539 chngs = self.repo.status(base, head) 540 return dict(zip(states, chngs)) 541 542 def findoutgoing(self, parent): 543 '''Return the base set of outgoing nodes. 544 545 A caching wrapper around mercurial.localrepo.findoutgoing(). 546 Complains (to the user), if the parent workspace is 547 non-existent or inaccessible''' 548 549 self.ui.pushbuffer() 550 try: 551 try: 552 ui = self.ui 553 if hasattr(cmdutil, 'remoteui'): 554 ui = cmdutil.remoteui(ui, {}) 555 pws = hg.repository(ui, parent) 556 if Version.at_least("1.6"): 557 return discovery.findoutgoing(self.repo, pws) 558 else: 559 return self.repo.findoutgoing(pws) 560 except error.RepoError: 561 self.ui.warn("Warning: Parent workspace '%s' is not " 562 "accessible\n" 563 "active list will be incomplete\n\n" % parent) 564 return [] 565 finally: 566 self.ui.popbuffer() 567 findoutgoing = util.cachefunc(findoutgoing) 568 569 def modified(self): 570 '''Return a list of files modified in the workspace''' 571 wctx = self.workingctx() 572 return sorted(wctx.files() + wctx.deleted()) or None 573 574 def merged(self): 575 '''Return boolean indicating whether the workspace has an uncommitted 576 merge''' 577 wctx = self.workingctx() 578 return len(wctx.parents()) > 1 579 580 def branched(self): 581 '''Return boolean indicating whether the workspace has an 582 uncommitted named branch''' 583 584 wctx = self.workingctx() 585 return wctx.branch() != wctx.parents()[0].branch() 586 587 def active(self, parent=None): 588 '''Return an ActiveList describing changes between workspace 589 and parent workspace (including uncommitted changes). 590 If workspace has no parent, ActiveList will still describe any 591 uncommitted changes.''' 592 593 parent = self.parent(parent) 594 if parent in self.activecache: 595 return self.activecache[parent] 596 597 if parent: 598 outgoing = self.findoutgoing(parent) 599 outnodes = self.repo.changelog.nodesbetween(outgoing)[0] 600 else: 601 outgoing = [] # No parent, no outgoing nodes 602 outnodes = [] 603 604 localtip = self._localtip(outnodes, self.workingctx()) 605 606 if localtip.rev() is None: 607 heads = localtip.parents() 608 else: 609 heads = [localtip] 610 611 ctxs = [self.repo.changectx(n) for n in 612 self.repo.changelog.nodesbetween(outgoing, 613 [h.node() for h in heads])[0]] 614 615 if localtip.rev() is None: 616 ctxs.append(localtip) 617 618 act = ActiveList(self, self._parenttip(heads, outnodes), ctxs) 619 620 self.activecache[parent] = act 621 return act 622 623 def pdiff(self, pats, opts, parent=None): 624 'Return diffs relative to PARENT, as best as we can make out' 625 626 parent = self.parent(parent) 627 act = self.active(parent) 628 629 # 630 # act.localtip maybe nil, in the case of uncommitted local 631 # changes. 632 # 633 if not act.revs: 634 return 635 636 matchfunc = cmdutil.match(self.repo, pats, opts) 637 opts = patch.diffopts(self.ui, opts) 638 639 return self.diff(act.parenttip.node(), act.localtip.node(), 640 match=matchfunc, opts=opts) 641 642 def squishdeltas(self, active, message, user=None): 643 '''Create a single conglomerate changeset based on a given 644 active list. Removes the original changesets comprising the 645 given active list, and any tags pointing to them. 646 647 Operation: 648 649 - Commit an activectx object representing the specified 650 active list, 651 652 - Remove any local tags pointing to changesets in the 653 specified active list. 654 655 - Remove the changesets comprising the specified active 656 list. 657 658 - Remove any metadata that may refer to changesets that were 659 removed. 660 661 Calling code is expected to hold both the working copy lock 662 and repository lock of the destination workspace 663 ''' 664 665 def strip_local_tags(active): 666 '''Remove any local tags referring to the specified nodes.''' 667 668 if os.path.exists(self.repo.join('localtags')): 669 fh = None 670 try: 671 fh = self.repo.opener('localtags') 672 tags = active.prune_tags(fh) 673 fh.close() 674 675 fh = self.repo.opener('localtags', 'w', atomictemp=True) 676 fh.writelines(tags) 677 fh.rename() 678 finally: 679 if fh and not fh.closed: 680 fh.close() 681 682 if active.files(): 683 for entry in active: 684 # 685 # Work around Mercurial issue #1666, if the source 686 # file of a rename exists in the working copy 687 # Mercurial will complain, and remove the file. 688 # 689 # We preemptively remove the file to avoid the 690 # complaint (the user was asked about this in 691 # cdm_recommit) 692 # 693 if entry.is_renamed(): 694 path = self.repo.wjoin(entry.parentname) 695 if os.path.exists(path): 696 os.unlink(path) 697 698 self.repo.commitctx(active.context(message, user)) 699 wsstate = "recommitted" 700 destination = self.repo.changelog.tip() 701 else: 702 # 703 # If all we're doing is stripping the old nodes, we want to 704 # update the working copy such that we're not at a revision 705 # that's about to go away. 706 # 707 wsstate = "tip" 708 destination = active.parenttip.node() 709 710 self.clean(destination) 711 712 # 713 # Tags were elided by the activectx object. Local tags, 714 # however, must be removed manually. 715 # 716 try: 717 strip_local_tags(active) 718 except EnvironmentError, e: 719 raise util.Abort('Could not recommit tags: %s\n' % e) 720 721 # Silence all the strip and update fun 722 self.ui.pushbuffer() 723 724 # 725 # Remove the active lists component changesets by stripping 726 # the base of any active branch (of which there may be 727 # several) 728 # 729 try: 730 try: 731 for base in active.bases(): 732 # 733 # Any cached information about the repository is 734 # likely to be invalid during the strip. The 735 # caching of branch tags is especially 736 # problematic. 737 # 738 self.repo.invalidate() 739 repair.strip(self.ui, self.repo, base.node(), backup=False) 740 except: 741 # 742 # If this fails, it may leave us in a surprising place in 743 # the history. 744 # 745 # We want to warn the user that something went wrong, 746 # and what will happen next, re-raise the exception, and 747 # bring the working copy back into a consistent state 748 # (which the finally block will do) 749 # 750 self.ui.warn("stripping failed, your workspace will have " 751 "superfluous heads.\n" 752 "your workspace has been updated to the " 753 "%s changeset.\n" % wsstate) 754 raise # Re-raise the exception 755 finally: 756 self.clean() 757 self.repo.dirstate.write() # Flush the dirstate 758 self.repo.invalidate() # Invalidate caches 759 760 # 761 # We need to remove Hg's undo information (used for rollback), 762 # since it refers to data that will probably not exist after 763 # the strip. 764 # 765 if os.path.exists(self.repo.sjoin('undo')): 766 try: 767 os.unlink(self.repo.sjoin('undo')) 768 except EnvironmentError, e: 769 raise util.Abort('failed to remove undo data: %s\n' % e) 770 771 self.ui.popbuffer() 772 773 def filepath(self, path): 774 'Return the full path to a workspace file.' 775 return self.repo.pathto(path) 776 777 def clean(self, rev=None): 778 '''Bring workspace up to REV (or tip) forcefully (discarding in 779 progress changes)''' 780 781 if rev != None: 782 rev = self.repo.lookup(rev) 783 else: 784 rev = self.repo.changelog.tip() 785 786 hg.clean(self.repo, rev, show_stats=False) 787 788 def mq_applied(self): 789 '''True if the workspace has Mq patches applied''' 790 q = mq.queue(self.ui, self.repo.join('')) 791 return q.applied 792 793 def workingctx(self): 794 return self.repo.changectx(None) 795 796 def diff(self, node1=None, node2=None, match=None, opts=None): 797 ret = cStringIO.StringIO() 798 try: 799 for chunk in patch.diff(self.repo, node1, node2, match=match, 800 opts=opts): 801 ret.write(chunk) 802 finally: 803 # Workaround Hg bug 1651 804 if not Version.at_least("1.3"): 805 self.repo.dirstate.invalidate() 806 807 return ret.getvalue() 808 809 if Version.at_least("1.6"): 810 def copy(self, src, dest): 811 '''Copy a file from src to dest 812 ''' 813 814 self.workingctx().copy(src, dest) 815 else: 816 def copy(self, src, dest): 817 '''Copy a file from src to dest 818 ''' 819 820 self.repo.copy(src, dest) 821 822 823 if Version.at_least("1.4"): 824 825 def _walkctxs(self, base, head, follow=False, pick=None): 826 '''Generate changectxs between BASE and HEAD. 827 828 Walk changesets between BASE and HEAD (in the order implied by 829 their relation), following a given branch if FOLLOW is a true 830 value, yielding changectxs where PICK (if specified) returns a 831 true value. 832 833 PICK is a function of one argument, a changectx.''' 834 835 chosen = {} 836 837 def prep(ctx, fns): 838 chosen[ctx.rev()] = not pick or pick(ctx) 839 840 opts = {'rev': ['%s:%s' % (base.rev(), head.rev())], 841 'follow': follow} 842 matcher = cmdutil.matchall(self.repo) 843 844 for ctx in cmdutil.walkchangerevs(self.repo, matcher, opts, prep): 845 if chosen[ctx.rev()]: 846 yield ctx 847 else: 848 849 def _walkctxs(self, base, head, follow=False, pick=None): 850 '''Generate changectxs between BASE and HEAD. 851 852 Walk changesets between BASE and HEAD (in the order implied by 853 their relation), following a given branch if FOLLOW is a true 854 value, yielding changectxs where PICK (if specified) returns a 855 true value. 856 857 PICK is a function of one argument, a changectx.''' 858 859 opts = {'rev': ['%s:%s' % (base.rev(), head.rev())], 860 'follow': follow} 861 862 changectx = self.repo.changectx 863 getcset = util.cachefunc(lambda r: changectx(r).changeset()) 864 865 # 866 # See the docstring of mercurial.cmdutil.walkchangerevs() for 867 # the phased approach to the iterator returned. The important 868 # part to note is that the 'add' phase gathers nodes, which 869 # the 'iter' phase then iterates through. 870 # 871 changeiter = cmdutil.walkchangerevs(self.ui, self.repo, 872 [], getcset, opts)[0] 873 874 matched = {} 875 for st, rev, fns in changeiter: 876 if st == 'add': 877 ctx = changectx(rev) 878 if not pick or pick(ctx): 879 matched[rev] = ctx 880 elif st == 'iter': 881 if rev in matched: 882 yield matched[rev] 883