xref: /titanic_44/usr/src/tools/onbld/Scm/WorkSpace.py (revision 036abaca93ddab92ba33036159c30112ab844810)
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, 2011, 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 of ActiveEntry objects, can thus
41# present the entire change in workspace state between a parent and
42# its child and is the important bit here (in that if it is incorrect,
43# everything else will 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#
55# Mercurial 1.6 moves findoutgoing into a discover module
56#
57if Version.at_least("1.6"):
58    from mercurial import discovery
59
60
61class ActiveEntry(object):
62    '''Representation of the changes made to a single file.
63
64    MODIFIED   - Contents changed, but no other changes were made
65    ADDED      - File is newly created
66    REMOVED    - File is being removed
67
68    Copies are represented by an Entry whose .parentname is non-nil
69
70    Truly copied files have non-nil .parentname and .renamed = False
71    Renames have non-nil .parentname and .renamed = True
72
73    Do not access any of this information directly, do so via the
74
75    .is_<change>() methods.'''
76
77    MODIFIED = intern('modified')
78    ADDED = intern('added')
79    REMOVED = intern('removed')
80
81    def __init__(self, name, change):
82        self.name = name
83        self.change = intern(change)
84
85        assert change in (self.MODIFIED, self.ADDED, self.REMOVED)
86
87        self.parentname = None
88        # As opposed to copied (or neither)
89        self.renamed = False
90        self.comments = []
91
92    def __cmp__(self, other):
93        return cmp(self.name, other.name)
94
95    def is_added(self):
96        '''Return True if this ActiveEntry represents an added file'''
97        return self.change is self.ADDED
98
99    def is_modified(self):
100        '''Return True if this ActiveEntry represents a modified file'''
101        return self.change is self.MODIFIED
102
103    def is_removed(self):
104        '''Return True if this ActiveEntry represents a removed file'''
105        return self.change is self.REMOVED
106
107    def is_renamed(self):
108        '''Return True if this ActiveEntry represents a renamed file'''
109        return self.parentname and self.renamed
110
111    def is_copied(self):
112        '''Return True if this ActiveEntry represents a copied file'''
113        return self.parentname and not self.renamed
114
115
116class ActiveList(object):
117    '''Complete representation of change between two changesets.
118
119    In practice, a container for ActiveEntry objects, and methods to
120    create them, and deal with them as a group.'''
121
122    def __init__(self, ws, parenttip, revs=None):
123        '''Initialize the ActiveList
124
125        parenttip is the revision with which to compare (likely to be
126        from the parent), revs is a topologically sorted list of
127        revisions ending with the revision to compare with (likely to
128        be the child-local revisions).'''
129
130        assert parenttip is not None
131
132        self.ws = ws
133        self.revs = revs
134        self.parenttip = parenttip
135        self.localtip = None
136
137        self._active = {}
138        self._comments = []
139
140        if revs:
141            self.localtip = revs[-1]
142            self._build()
143
144    def _status(self):
145        '''Return the status of any file mentioned in any of the
146        changesets making up this active list.'''
147
148        files = set()
149        for c in self.revs:
150            files.update(c.files())
151
152        #
153        # Any file not in the parenttip or the localtip is ephemeral
154        # and can be ignored. Mercurial will complain regarding these
155        # files if the localtip is a workingctx, so remove them in
156        # that case.
157        #
158        # Compare against the dirstate because a workingctx manifest
159        # is created on-demand and is particularly expensive.
160        #
161        if self.localtip.rev() is None:
162            for f in files.copy():
163                if f not in self.parenttip and f not in self.ws.repo.dirstate:
164                    files.remove(f)
165
166        return self.ws.status(self.parenttip, self.localtip, files=files)
167
168    def _build(self):
169        '''Construct ActiveEntry objects for each changed file.
170
171        This works in 3 stages:
172
173          - Create entries for every changed file with
174            semi-appropriate change type
175
176          - Track renames/copies, and set change comments (both
177            ActiveList-wide, and per-file).
178
179          - Cleanup
180            - Drop circular renames
181            - Drop the removal of the old name of any rename
182            - Drop entries for modified files that haven't actually changed'''
183
184        #
185        # Keep a cache of filectx objects (keyed on pathname) so that
186        # we can avoid opening filelogs numerous times.
187        #
188        fctxcache = {}
189
190        def oldname(ctx, fname):
191            '''Return the name 'fname' held prior to any possible
192            rename/copy in the given changeset.'''
193            try:
194                if fname in fctxcache:
195                    octx = fctxcache[fname]
196                    fctx = ctx.filectx(fname, filelog=octx.filelog())
197                else:
198                    fctx = ctx.filectx(fname)
199                    #
200                    # workingfilectx objects may not refer to the
201                    # right filelog (in case of rename).  Don't cache
202                    # them.
203                    #
204                    if not isinstance(fctx, context.workingfilectx):
205                        fctxcache[fname] = fctx
206            except error.LookupError:
207                return None
208
209            rn = fctx.renamed()
210            return rn and rn[0] or fname
211
212        status = self._status()
213        self._active = dict((fname, ActiveEntry(fname, kind))
214                            for fname, kind in status.iteritems()
215                            if kind in ('modified', 'added', 'removed'))
216
217        #
218        # We do two things:
219        #    - Gather checkin comments (for the entire ActiveList, and
220        #      per-file)
221        #    - Set the .parentname of any copied/renamed file
222        #
223        # renames/copies:
224        #   We walk the list of revisions backward such that only files
225        #   that ultimately remain active need be considered.
226        #
227        #   At each iteration (revision) we update the .parentname of
228        #   any active file renamed or copied in that revision (the
229        #   current .parentname if set, or .name otherwise, reflects
230        #   the name of a given active file in the revision currently
231        #   being looked at)
232        #
233        for ctx in reversed(self.revs):
234            desc = ctx.description().splitlines()
235            self._comments = desc + self._comments
236            cfiles = set(ctx.files())
237
238            for entry in self:
239                fname = entry.parentname or entry.name
240                if fname not in cfiles:
241                    continue
242
243                entry.comments = desc + entry.comments
244
245                #
246                # We don't care about the name history of any file
247                # that ends up being removed, since that trumps any
248                # possible renames or copies along the way.
249                #
250                # Changes that we may care about involving an
251                # intermediate name of a removed file will appear
252                # separately (related to the eventual name along
253                # that line)
254                #
255                if not entry.is_removed():
256                    entry.parentname = oldname(ctx, fname)
257
258        for entry in self._active.values():
259            #
260            # For any file marked as copied or renamed, clear the
261            # .parentname if the copy or rename is cyclic (source ==
262            # destination) or if the .parentname did not exist in the
263            # parenttip.
264            #
265            # If the parentname is marked as removed, set the renamed
266            # flag and remove any ActiveEntry we may have for the
267            # .parentname.
268            #
269            if entry.parentname:
270                if (entry.parentname == entry.name or
271                    entry.parentname not in self.parenttip):
272                    entry.parentname = None
273                elif status.get(entry.parentname) == 'removed':
274                    entry.renamed = True
275
276                    if entry.parentname in self:
277                        del self[entry.parentname]
278
279            #
280            # There are cases during a merge where a file will be seen
281            # as modified by status but in reality be an addition (not
282            # in the parenttip), so we have to check whether the file
283            # is in the parenttip and set it as an addition, if not.
284            #
285            # If a file is modified (and not a copy or rename), we do
286            # a full comparison to the copy in the parenttip and
287            # ignore files that are parts of active revisions but
288            # unchanged.
289            #
290            if entry.name not in self.parenttip:
291                entry.change = ActiveEntry.ADDED
292            elif entry.is_modified():
293                if not self._changed_file(entry.name):
294                    del self[entry.name]
295
296    def __contains__(self, fname):
297        return fname in self._active
298
299    def __getitem__(self, key):
300        return self._active[key]
301
302    def __setitem__(self, key, value):
303        self._active[key] = value
304
305    def __delitem__(self, key):
306        del self._active[key]
307
308    def __iter__(self):
309        return self._active.itervalues()
310
311    def files(self):
312        '''Return the list of pathnames of all files touched by this
313        ActiveList
314
315        Where files have been renamed, this will include both their
316        current name and the name which they had in the parent tip.
317        '''
318
319        ret = self._active.keys()
320        ret.extend(x.parentname for x in self if x.is_renamed())
321        return set(ret)
322
323    def comments(self):
324        '''Return the full set of changeset comments associated with
325        this ActiveList'''
326
327        return self._comments
328
329    def bases(self):
330        '''Return the list of changesets that are roots of the ActiveList.
331
332        This is the set of active changesets where neither parent
333        changeset is itself active.'''
334
335        revset = set(self.revs)
336        return filter(lambda ctx: not [p for p in ctx.parents() if p in revset],
337                      self.revs)
338
339    def tags(self):
340        '''Find tags that refer to a changeset in the ActiveList,
341        returning a list of 3-tuples (tag, node, is_local) for each.
342
343        We return all instances of a tag that refer to such a node,
344        not just that which takes precedence.'''
345
346        def colliding_tags(iterable, nodes, local):
347            for nd, name in [line.rstrip().split(' ', 1) for line in iterable]:
348                if nd in nodes:
349                    yield (name, self.ws.repo.lookup(nd), local)
350
351        tags = []
352        nodes = set(node.hex(ctx.node()) for ctx in self.revs)
353
354        if os.path.exists(self.ws.repo.join('localtags')):
355            fh = self.ws.repo.opener('localtags')
356            tags.extend(colliding_tags(fh, nodes, True))
357            fh.close()
358
359        # We want to use the tags file from the localtip
360        if '.hgtags' in self.localtip:
361            data = self.localtip.filectx('.hgtags').data().splitlines()
362            tags.extend(colliding_tags(data, nodes, False))
363
364        return tags
365
366    def prune_tags(self, data):
367        '''Return a copy of data, which should correspond to the
368        contents of a Mercurial tags file, with any tags that refer to
369        changesets which are components of the ActiveList removed.'''
370
371        nodes = set(node.hex(ctx.node()) for ctx in self.revs)
372        return [t for t in data if t.split(' ', 1)[0] not in nodes]
373
374    def _changed_file(self, path):
375        '''Compare the parent and local versions of a given file.
376        Return True if file changed, False otherwise.
377
378        Note that this compares the given path in both versions, not the given
379        entry; renamed and copied files are compared by name, not history.
380
381        The fast path compares file metadata, slow path is a
382        real comparison of file content.'''
383
384        if ((path in self.parenttip) != (path in self.localtip)):
385            return True
386
387        parentfile = self.parenttip.filectx(path)
388        localfile = self.localtip.filectx(path)
389
390        #
391        # NB: Keep these ordered such as to make every attempt
392        #     to short-circuit the more time consuming checks.
393        #
394        if parentfile.size() != localfile.size():
395            return True
396
397        if parentfile.flags() != localfile.flags():
398            return True
399
400        if Version.at_least("1.7"):
401            if parentfile.cmp(localfile):
402                return True
403        else:
404            if parentfile.cmp(localfile.data()):
405                return True
406
407    def context(self, message, user):
408        '''Return a Mercurial context object representing the entire
409        ActiveList as one change.'''
410        return activectx(self, message, user)
411
412    def as_text(self, paths):
413        '''Return the ActiveList as a block of text in a format
414        intended to aid debugging and simplify the test suite.
415
416        paths should be a list of paths for which file-level data
417        should be included.  If it is empty, the whole active list is
418        included.'''
419
420        cstr = cStringIO.StringIO()
421
422        cstr.write('parent tip: %s:%s\n' % (self.parenttip.rev(),
423                                            self.parenttip))
424        if self.localtip:
425            rev = self.localtip.rev()
426            cstr.write('local tip:  %s:%s\n' %
427                       (rev is None and "working" or rev, self.localtip))
428        else:
429            cstr.write('local tip:  None\n')
430
431        cstr.write('entries:\n')
432        for entry in self:
433            if paths and self.ws.filepath(entry.name) not in paths:
434                continue
435
436            cstr.write('  - %s\n' % entry.name)
437            cstr.write('    parentname: %s\n' % entry.parentname)
438            cstr.write('    change: %s\n' % entry.change)
439            cstr.write('    renamed: %s\n' % entry.renamed)
440            cstr.write('    comments:\n')
441            cstr.write('      ' + '\n      '.join(entry.comments) + '\n')
442            cstr.write('\n')
443
444        return cstr.getvalue()
445
446
447class WorkList(object):
448    '''A (user-maintained) list of files changed in this workspace as
449    compared to any parent workspace.
450
451    Internally, the WorkList is stored in .hg/cdm/worklist as a list
452    of file pathnames, one per-line.
453
454    This may only safely be used as a hint regarding possible
455    modifications to the working copy, it should not be relied upon to
456    suggest anything about committed changes.'''
457
458    def __init__(self, ws):
459        '''Load the WorkList for the specified WorkSpace from disk.'''
460
461        self._ws = ws
462        self._repo = ws.repo
463        self._file = os.path.join('cdm', 'worklist')
464        self._files = set()
465        self._valid = False
466
467        if os.path.exists(self._repo.join(self._file)):
468            self.load()
469
470    def __nonzero__(self):
471        '''A WorkList object is true if it was loaded from disk,
472        rather than freshly created.
473        '''
474
475        return self._valid
476
477    def list(self):
478        '''List of pathnames contained in the WorkList
479        '''
480
481        return list(self._files)
482
483    def status(self):
484        '''Return the status (in tuple form) of files from the
485        WorkList as they are in the working copy
486        '''
487
488        match = self._ws.matcher(files=self.list())
489        return self._repo.status(match=match)
490
491    def add(self, fname):
492        '''Add FNAME to the WorkList.
493        '''
494
495        self._files.add(fname)
496
497    def write(self):
498        '''Write the WorkList out to disk.
499        '''
500
501        dirn = os.path.split(self._file)[0]
502
503        if dirn and not os.path.exists(self._repo.join(dirn)):
504            try:
505                os.makedirs(self._repo.join(dirn))
506            except EnvironmentError, e:
507                raise util.Abort("Couldn't create directory %s: %s" %
508                                 (self._repo.join(dirn), e))
509
510        fh = self._repo.opener(self._file, 'w', atomictemp=True)
511
512        for name in self._files:
513            fh.write("%s\n" % name)
514
515        fh.rename()
516        fh.close()
517
518    def load(self):
519        '''Read in the WorkList from disk.
520        '''
521
522        fh = self._repo.opener(self._file, 'r')
523        self._files = set(l.rstrip('\n') for l in fh)
524        self._valid = True
525        fh.close()
526
527    def delete(self):
528        '''Empty the WorkList
529
530        Remove the on-disk WorkList and clear the file-list of the
531        in-memory copy
532        '''
533
534        if os.path.exists(self._repo.join(self._file)):
535            os.unlink(self._repo.join(self._file))
536
537        self._files = set()
538        self._valid = False
539
540
541class activectx(context.memctx):
542    '''Represent an ActiveList as a Mercurial context object.
543
544    Part of the  WorkSpace.squishdeltas implementation.'''
545
546    def __init__(self, active, message, user):
547        '''Build an activectx object.
548
549          active  - The ActiveList object used as the source for all data.
550          message - Changeset description
551          user    - Committing user'''
552
553        def filectxfn(repository, ctx, fname):
554            fctx = active.localtip.filectx(fname)
555            data = fctx.data()
556
557            #
558            # .hgtags is a special case, tags referring to active list
559            # component changesets should be elided.
560            #
561            if fname == '.hgtags':
562                data = '\n'.join(active.prune_tags(data.splitlines()))
563
564            return context.memfilectx(fname, data, 'l' in fctx.flags(),
565                                      'x' in fctx.flags(),
566                                      active[fname].parentname)
567
568        self.__active = active
569        parents = (active.parenttip.node(), node.nullid)
570        extra = {'branch': active.localtip.branch()}
571        context.memctx.__init__(self, active.ws.repo, parents, message,
572                                active.files(), filectxfn, user=user,
573                                extra=extra)
574
575    def modified(self):
576        return [entry.name for entry in self.__active if entry.is_modified()]
577
578    def added(self):
579        return [entry.name for entry in self.__active if entry.is_added()]
580
581    def removed(self):
582        ret = set(entry.name for entry in self.__active if entry.is_removed())
583        ret.update(set(x.parentname for x in self.__active if x.is_renamed()))
584        return list(ret)
585
586    def files(self):
587        return self.__active.files()
588
589
590class WorkSpace(object):
591
592    def __init__(self, repository):
593        self.repo = repository
594        self.ui = self.repo.ui
595        self.name = self.repo.root
596
597        self.activecache = {}
598
599    def parent(self, spec=None):
600        '''Return the canonical workspace parent, either SPEC (which
601        will be expanded) if provided or the default parent
602        otherwise.'''
603
604        if spec:
605            return self.ui.expandpath(spec)
606
607        p = self.ui.expandpath('default')
608        if p == 'default':
609            return None
610        else:
611            return p
612
613    def _localtip(self, outgoing, wctx):
614        '''Return the most representative changeset to act as the
615        localtip.
616
617        If the working directory is modified (has file changes, is a
618        merge, or has switched branches), this will be a workingctx.
619
620        If the working directory is unmodified, this will be the most
621        recent (highest revision number) local (outgoing) head on the
622        current branch, if no heads are determined to be outgoing, it
623        will be the most recent head on the current branch.
624        '''
625
626        if (wctx.files() or len(wctx.parents()) > 1 or
627            wctx.branch() != wctx.parents()[0].branch()):
628            return wctx
629
630        heads = self.repo.heads(start=wctx.parents()[0].node())
631        headctxs = [self.repo.changectx(n) for n in heads]
632        localctxs = [c for c in headctxs if c.node() in outgoing]
633
634        ltip = sorted(localctxs or headctxs, key=lambda x: x.rev())[-1]
635
636        if len(heads) > 1:
637            self.ui.warn('The current branch has more than one head, '
638                         'using %s\n' % ltip.rev())
639
640        return ltip
641
642    def parenttip(self, heads, outgoing):
643        '''Return the highest-numbered, non-outgoing changeset that is
644        an ancestor of a changeset in heads.
645
646        This returns the most recent changeset on a given branch that
647        is shared between a parent and child workspace, in effect the
648        common ancestor of the chosen local tip and the parent
649        workspace.
650        '''
651
652        def tipmost_shared(head, outnodes):
653            '''Return the changeset on the same branch as head that is
654            not in outnodes and is closest to the tip.
655
656            Walk outgoing changesets from head to the bottom of the
657            workspace (revision 0) and return the the first changeset
658            we see that is not in outnodes.
659
660            If none is found (all revisions >= 0 are outgoing), the
661            only possible parenttip is the null node (node.nullid)
662            which is returned explicitly.
663            '''
664            for ctx in self._walkctxs(head, self.repo.changectx(0),
665                                      follow=True,
666                                      pick=lambda c: c.node() not in outnodes):
667                return ctx
668
669            return self.repo.changectx(node.nullid)
670
671        nodes = set(outgoing)
672        ptips = map(lambda x: tipmost_shared(x, nodes), heads)
673        return sorted(ptips, key=lambda x: x.rev(), reverse=True)[0]
674
675    def status(self, base='.', head=None, files=None):
676        '''Translate from the hg 6-tuple status format to a hash keyed
677        on change-type'''
678
679        states = ['modified', 'added', 'removed', 'deleted', 'unknown',
680                  'ignored']
681
682        match = self.matcher(files=files)
683        chngs = self.repo.status(base, head, match=match)
684
685        ret = {}
686        for paths, change in zip(chngs, states):
687            ret.update((f, change) for f in paths)
688        return ret
689
690    def findoutgoing(self, parent):
691        '''Return the base set of outgoing nodes.
692
693        A caching wrapper around mercurial.localrepo.findoutgoing().
694        Complains (to the user), if the parent workspace is
695        non-existent or inaccessible'''
696
697        self.ui.pushbuffer()
698        try:
699            try:
700                ui = self.ui
701                if hasattr(cmdutil, 'remoteui'):
702                    ui = cmdutil.remoteui(ui, {})
703                pws = hg.repository(ui, parent)
704                if Version.at_least("1.6"):
705                    return discovery.findoutgoing(self.repo, pws)
706                else:
707                    return self.repo.findoutgoing(pws)
708            except error.RepoError:
709                self.ui.warn("Warning: Parent workspace '%s' is not "
710                             "accessible\n"
711                             "active list will be incomplete\n\n" % parent)
712                return []
713        finally:
714            self.ui.popbuffer()
715    findoutgoing = util.cachefunc(findoutgoing)
716
717    def modified(self):
718        '''Return a list of files modified in the workspace'''
719
720        wctx = self.workingctx()
721        return sorted(wctx.files() + wctx.deleted()) or None
722
723    def merged(self):
724        '''Return boolean indicating whether the workspace has an uncommitted
725        merge'''
726
727        wctx = self.workingctx()
728        return len(wctx.parents()) > 1
729
730    def branched(self):
731        '''Return boolean indicating whether the workspace has an
732        uncommitted named branch'''
733
734        wctx = self.workingctx()
735        return wctx.branch() != wctx.parents()[0].branch()
736
737    def active(self, parent=None, thorough=False):
738        '''Return an ActiveList describing changes between workspace
739        and parent workspace (including uncommitted changes).
740        If the workspace has no parent, ActiveList will still describe any
741        uncommitted changes.
742
743        If thorough is True use neither the WorkList nor any cached
744        results (though the result of this call will be cached for
745        future, non-thorough, calls).'''
746
747        parent = self.parent(parent)
748
749        #
750        # Use the cached copy if we can (we have one, and weren't
751        # asked to be thorough)
752        #
753        if not thorough and parent in self.activecache:
754            return self.activecache[parent]
755
756        #
757        # outbases: The set of outgoing nodes with no outgoing ancestors
758        # outnodes: The full set of outgoing nodes
759        #
760        if parent:
761            outbases = self.findoutgoing(parent)
762            outnodes = self.repo.changelog.nodesbetween(outbases)[0]
763        else:               # No parent, no outgoing nodes
764            outbases = []
765            outnodes = []
766
767        wctx = self.workingctx(worklist=not thorough)
768        localtip = self._localtip(outnodes, wctx)
769
770        if localtip.rev() is None:
771            heads = localtip.parents()
772        else:
773            heads = [localtip]
774
775        parenttip = self.parenttip(heads, outnodes)
776
777        #
778        # If we couldn't find a parenttip, the two repositories must
779        # be unrelated (Hg catches most of this, but this case is
780        # valid for it but invalid for us)
781        #
782        if parenttip == None:
783            raise util.Abort('repository is unrelated')
784
785        headnodes = [h.node() for h in heads]
786        ctxs = [self.repo.changectx(n) for n in
787                self.repo.changelog.nodesbetween(outbases, headnodes)[0]]
788
789        if localtip.rev() is None:
790            ctxs.append(localtip)
791
792        act = ActiveList(self, parenttip, ctxs)
793        self.activecache[parent] = act
794
795        return act
796
797    def squishdeltas(self, active, message, user=None):
798        '''Create a single conglomerate changeset based on a given
799        active list.  Removes the original changesets comprising the
800        given active list, and any tags pointing to them.
801
802        Operation:
803
804          - Commit an activectx object representing the specified
805            active list,
806
807          - Remove any local tags pointing to changesets in the
808            specified active list.
809
810          - Remove the changesets comprising the specified active
811            list.
812
813          - Remove any metadata that may refer to changesets that were
814            removed.
815
816        Calling code is expected to hold both the working copy lock
817        and repository lock of the destination workspace
818        '''
819
820        def strip_local_tags(active):
821            '''Remove any local tags referring to the specified nodes.'''
822
823            if os.path.exists(self.repo.join('localtags')):
824                fh = None
825                try:
826                    fh = self.repo.opener('localtags')
827                    tags = active.prune_tags(fh)
828                    fh.close()
829
830                    fh = self.repo.opener('localtags', 'w', atomictemp=True)
831                    fh.writelines(tags)
832                    fh.rename()
833                finally:
834                    if fh and not fh.closed:
835                        fh.close()
836
837        if active.files():
838            for entry in active:
839                #
840                # Work around Mercurial issue #1666, if the source
841                # file of a rename exists in the working copy
842                # Mercurial will complain, and remove the file.
843                #
844                # We preemptively remove the file to avoid the
845                # complaint (the user was asked about this in
846                # cdm_recommit)
847                #
848                if entry.is_renamed():
849                    path = self.repo.wjoin(entry.parentname)
850                    if os.path.exists(path):
851                        os.unlink(path)
852
853            self.repo.commitctx(active.context(message, user))
854            wsstate = "recommitted"
855            destination = self.repo.changelog.tip()
856        else:
857            #
858            # If all we're doing is stripping the old nodes, we want to
859            # update the working copy such that we're not at a revision
860            # that's about to go away.
861            #
862            wsstate = "tip"
863            destination = active.parenttip.node()
864
865        self.clean(destination)
866
867        #
868        # Tags were elided by the activectx object.  Local tags,
869        # however, must be removed manually.
870        #
871        try:
872            strip_local_tags(active)
873        except EnvironmentError, e:
874            raise util.Abort('Could not recommit tags: %s\n' % e)
875
876        # Silence all the strip and update fun
877        self.ui.pushbuffer()
878
879        #
880        # Remove the previous child-local changes by stripping the
881        # nodes that form the base of the ActiveList (removing their
882        # children in the process).
883        #
884        try:
885            try:
886                for base in active.bases():
887                    #
888                    # Any cached information about the repository is
889                    # likely to be invalid during the strip.  The
890                    # caching of branch tags is especially
891                    # problematic.
892                    #
893                    self.repo.invalidate()
894                    repair.strip(self.ui, self.repo, base.node(), backup=False)
895            except:
896                #
897                # If this fails, it may leave us in a surprising place in
898                # the history.
899                #
900                # We want to warn the user that something went wrong,
901                # and what will happen next, re-raise the exception, and
902                # bring the working copy back into a consistent state
903                # (which the finally block will do)
904                #
905                self.ui.warn("stripping failed, your workspace will have "
906                             "superfluous heads.\n"
907                             "your workspace has been updated to the "
908                             "%s changeset.\n" % wsstate)
909                raise               # Re-raise the exception
910        finally:
911            self.clean()
912            self.repo.dirstate.write() # Flush the dirstate
913            self.repo.invalidate()     # Invalidate caches
914
915            #
916            # We need to remove Hg's undo information (used for rollback),
917            # since it refers to data that will probably not exist after
918            # the strip.
919            #
920            if os.path.exists(self.repo.sjoin('undo')):
921                try:
922                    os.unlink(self.repo.sjoin('undo'))
923                except EnvironmentError, e:
924                    raise util.Abort('failed to remove undo data: %s\n' % e)
925
926            self.ui.popbuffer()
927
928    def filepath(self, path):
929        'Return the full path to a workspace file.'
930
931        return self.repo.pathto(path)
932
933    def clean(self, rev=None):
934        '''Bring workspace up to REV (or tip) forcefully (discarding in
935        progress changes)'''
936
937        if rev != None:
938            rev = self.repo.lookup(rev)
939        else:
940            rev = self.repo.changelog.tip()
941
942        hg.clean(self.repo, rev, show_stats=False)
943
944    def mq_applied(self):
945        '''True if the workspace has Mq patches applied'''
946
947        q = mq.queue(self.ui, self.repo.join(''))
948        return q.applied
949
950    def workingctx(self, worklist=False):
951        '''Return a workingctx object representing the working copy.
952
953        If worklist is true, return a workingctx object created based
954        on the status of files in the workspace's worklist.'''
955
956        wl = WorkList(self)
957
958        if worklist and wl:
959            return context.workingctx(self.repo, changes=wl.status())
960        else:
961            return self.repo.changectx(None)
962
963    def matcher(self, pats=None, opts=None, files=None):
964        '''Return a match object suitable for Mercurial based on
965        specified criteria.
966
967        If files is specified it is a list of pathnames relative to
968        the repository root to be matched precisely.
969
970        If pats and/or opts are specified, these are as to
971        cmdutil.match'''
972
973        of_patterns = pats is not None or opts is not None
974        of_files = files is not None
975        opts = opts or {}       # must be a dict
976
977        assert not (of_patterns and of_files)
978
979        if of_patterns:
980            return cmdutil.match(self.repo, pats, opts)
981        elif of_files:
982            return cmdutil.matchfiles(self.repo, files)
983        else:
984            return cmdutil.matchall(self.repo)
985
986    def diff(self, node1=None, node2=None, match=None, opts=None):
987        '''Return the diff of changes between two changesets as a string'''
988
989        #
990        # Retain compatibility by only calling diffopts() if it
991        # obviously has not already been done.
992        #
993        if isinstance(opts, dict):
994            opts = patch.diffopts(self.ui, opts)
995
996        ret = cStringIO.StringIO()
997        for chunk in patch.diff(self.repo, node1, node2, match=match,
998                                opts=opts):
999            ret.write(chunk)
1000
1001        return ret.getvalue()
1002
1003    if Version.at_least("1.6"):
1004        def copy(self, src, dest):
1005            '''Copy a file from src to dest
1006            '''
1007
1008            self.workingctx().copy(src, dest)
1009    else:
1010        def copy(self, src, dest):
1011            '''Copy a file from src to dest
1012            '''
1013
1014            self.repo.copy(src, dest)
1015
1016
1017    if Version.at_least("1.4"):
1018
1019        def _walkctxs(self, base, head, follow=False, pick=None):
1020            '''Generate changectxs between BASE and HEAD.
1021
1022            Walk changesets between BASE and HEAD (in the order implied by
1023            their relation), following a given branch if FOLLOW is a true
1024            value, yielding changectxs where PICK (if specified) returns a
1025            true value.
1026
1027            PICK is a function of one argument, a changectx.'''
1028
1029            chosen = {}
1030
1031            def prep(ctx, fns):
1032                chosen[ctx.rev()] = not pick or pick(ctx)
1033
1034            opts = {'rev': ['%s:%s' % (base.rev(), head.rev())],
1035                    'follow': follow}
1036            matcher = cmdutil.matchall(self.repo)
1037
1038            for ctx in cmdutil.walkchangerevs(self.repo, matcher, opts, prep):
1039                if chosen[ctx.rev()]:
1040                    yield ctx
1041    else:
1042
1043        def _walkctxs(self, base, head, follow=False, pick=None):
1044            '''Generate changectxs between BASE and HEAD.
1045
1046            Walk changesets between BASE and HEAD (in the order implied by
1047            their relation), following a given branch if FOLLOW is a true
1048            value, yielding changectxs where PICK (if specified) returns a
1049            true value.
1050
1051            PICK is a function of one argument, a changectx.'''
1052
1053            opts = {'rev': ['%s:%s' % (base.rev(), head.rev())],
1054                    'follow': follow}
1055
1056            changectx = self.repo.changectx
1057            getcset = util.cachefunc(lambda r: changectx(r).changeset())
1058
1059            #
1060            # See the docstring of mercurial.cmdutil.walkchangerevs() for
1061            # the phased approach to the iterator returned.  The important
1062            # part to note is that the 'add' phase gathers nodes, which
1063            # the 'iter' phase then iterates through.
1064            #
1065            changeiter = cmdutil.walkchangerevs(self.ui, self.repo,
1066                                                [], getcset, opts)[0]
1067
1068            matched = {}
1069            for st, rev, fns in changeiter:
1070                if st == 'add':
1071                    ctx = changectx(rev)
1072                    if not pick or pick(ctx):
1073                        matched[rev] = ctx
1074                elif st == 'iter':
1075                    if rev in matched:
1076                        yield matched[rev]
1077