xref: /titanic_50/usr/src/tools/onbld/Scm/WorkSpace.py (revision 1b1b6fbd07b95cdbe86f7b731a2fc22050bb8ae8)
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