xref: /titanic_50/usr/src/tools/onbld/Scm/WorkSpace.py (revision c8937b0da86ffcdeb5a328afe14cb7bb0a5a4d2b)
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 parentfile.cmp(localfile.data()):
401            return True
402
403    def context(self, message, user):
404        '''Return a Mercurial context object representing the entire
405        ActiveList as one change.'''
406        return activectx(self, message, user)
407
408    def as_text(self, paths):
409        '''Return the ActiveList as a block of text in a format
410        intended to aid debugging and simplify the test suite.
411
412        paths should be a list of paths for which file-level data
413        should be included.  If it is empty, the whole active list is
414        included.'''
415
416        cstr = cStringIO.StringIO()
417
418        cstr.write('parent tip: %s:%s\n' % (self.parenttip.rev(),
419                                            self.parenttip))
420        if self.localtip:
421            rev = self.localtip.rev()
422            cstr.write('local tip:  %s:%s\n' %
423                       (rev is None and "working" or rev, self.localtip))
424        else:
425            cstr.write('local tip:  None\n')
426
427        cstr.write('entries:\n')
428        for entry in self:
429            if paths and self.ws.filepath(entry.name) not in paths:
430                continue
431
432            cstr.write('  - %s\n' % entry.name)
433            cstr.write('    parentname: %s\n' % entry.parentname)
434            cstr.write('    change: %s\n' % entry.change)
435            cstr.write('    renamed: %s\n' % entry.renamed)
436            cstr.write('    comments:\n')
437            cstr.write('      ' + '\n      '.join(entry.comments) + '\n')
438            cstr.write('\n')
439
440        return cstr.getvalue()
441
442
443class WorkList(object):
444    '''A (user-maintained) list of files changed in this workspace as
445    compared to any parent workspace.
446
447    Internally, the WorkList is stored in .hg/cdm/worklist as a list
448    of file pathnames, one per-line.
449
450    This may only safely be used as a hint regarding possible
451    modifications to the working copy, it should not be relied upon to
452    suggest anything about committed changes.'''
453
454    def __init__(self, ws):
455        '''Load the WorkList for the specified WorkSpace from disk.'''
456
457        self._ws = ws
458        self._repo = ws.repo
459        self._file = os.path.join('cdm', 'worklist')
460        self._files = set()
461        self._valid = False
462
463        if os.path.exists(self._repo.join(self._file)):
464            self.load()
465
466    def __nonzero__(self):
467        '''A WorkList object is true if it was loaded from disk,
468        rather than freshly created.
469        '''
470
471        return self._valid
472
473    def list(self):
474        '''List of pathnames contained in the WorkList
475        '''
476
477        return list(self._files)
478
479    def status(self):
480        '''Return the status (in tuple form) of files from the
481        WorkList as they are in the working copy
482        '''
483
484        match = self._ws.matcher(files=self.list())
485        return self._repo.status(match=match)
486
487    def add(self, fname):
488        '''Add FNAME to the WorkList.
489        '''
490
491        self._files.add(fname)
492
493    def write(self):
494        '''Write the WorkList out to disk.
495        '''
496
497        dirn = os.path.split(self._file)[0]
498
499        if dirn and not os.path.exists(self._repo.join(dirn)):
500            try:
501                os.makedirs(self._repo.join(dirn))
502            except EnvironmentError, e:
503                raise util.Abort("Couldn't create directory %s: %s" %
504                                 (self._repo.join(dirn), e))
505
506        fh = self._repo.opener(self._file, 'w', atomictemp=True)
507
508        for name in self._files:
509            fh.write("%s\n" % name)
510
511        fh.rename()
512        fh.close()
513
514    def load(self):
515        '''Read in the WorkList from disk.
516        '''
517
518        fh = self._repo.opener(self._file, 'r')
519        self._files = set(l.rstrip('\n') for l in fh)
520        self._valid = True
521        fh.close()
522
523    def delete(self):
524        '''Empty the WorkList
525
526        Remove the on-disk WorkList and clear the file-list of the
527        in-memory copy
528        '''
529
530        if os.path.exists(self._repo.join(self._file)):
531            os.unlink(self._repo.join(self._file))
532
533        self._files = set()
534        self._valid = False
535
536
537class activectx(context.memctx):
538    '''Represent an ActiveList as a Mercurial context object.
539
540    Part of the  WorkSpace.squishdeltas implementation.'''
541
542    def __init__(self, active, message, user):
543        '''Build an activectx object.
544
545          active  - The ActiveList object used as the source for all data.
546          message - Changeset description
547          user    - Committing user'''
548
549        def filectxfn(repository, ctx, fname):
550            fctx = active.localtip.filectx(fname)
551            data = fctx.data()
552
553            #
554            # .hgtags is a special case, tags referring to active list
555            # component changesets should be elided.
556            #
557            if fname == '.hgtags':
558                data = '\n'.join(active.prune_tags(data.splitlines()))
559
560            return context.memfilectx(fname, data, 'l' in fctx.flags(),
561                                      'x' in fctx.flags(),
562                                      active[fname].parentname)
563
564        self.__active = active
565        parents = (active.parenttip.node(), node.nullid)
566        extra = {'branch': active.localtip.branch()}
567        context.memctx.__init__(self, active.ws.repo, parents, message,
568                                active.files(), filectxfn, user=user,
569                                extra=extra)
570
571    def modified(self):
572        return [entry.name for entry in self.__active if entry.is_modified()]
573
574    def added(self):
575        return [entry.name for entry in self.__active if entry.is_added()]
576
577    def removed(self):
578        ret = set(entry.name for entry in self.__active if entry.is_removed())
579        ret.update(set(x.parentname for x in self.__active if x.is_renamed()))
580        return list(ret)
581
582    def files(self):
583        return self.__active.files()
584
585
586class WorkSpace(object):
587
588    def __init__(self, repository):
589        self.repo = repository
590        self.ui = self.repo.ui
591        self.name = self.repo.root
592
593        self.activecache = {}
594
595    def parent(self, spec=None):
596        '''Return the canonical workspace parent, either SPEC (which
597        will be expanded) if provided or the default parent
598        otherwise.'''
599
600        if spec:
601            return self.ui.expandpath(spec)
602
603        p = self.ui.expandpath('default')
604        if p == 'default':
605            return None
606        else:
607            return p
608
609    def _localtip(self, outgoing, wctx):
610        '''Return the most representative changeset to act as the
611        localtip.
612
613        If the working directory is modified (has file changes, is a
614        merge, or has switched branches), this will be a workingctx.
615
616        If the working directory is unmodified, this will be the most
617        recent (highest revision number) local (outgoing) head on the
618        current branch, if no heads are determined to be outgoing, it
619        will be the most recent head on the current branch.
620        '''
621
622        if (wctx.files() or len(wctx.parents()) > 1 or
623            wctx.branch() != wctx.parents()[0].branch()):
624            return wctx
625
626        heads = self.repo.heads(start=wctx.parents()[0].node())
627        headctxs = [self.repo.changectx(n) for n in heads]
628        localctxs = [c for c in headctxs if c.node() in outgoing]
629
630        ltip = sorted(localctxs or headctxs, key=lambda x: x.rev())[-1]
631
632        if len(heads) > 1:
633            self.ui.warn('The current branch has more than one head, '
634                         'using %s\n' % ltip.rev())
635
636        return ltip
637
638    def parenttip(self, heads, outgoing):
639        '''Return the highest-numbered, non-outgoing changeset that is
640        an ancestor of a changeset in heads.
641
642        This returns the most recent changeset on a given branch that
643        is shared between a parent and child workspace, in effect the
644        common ancestor of the chosen local tip and the parent
645        workspace.
646        '''
647
648        def tipmost_shared(head, outnodes):
649            '''Return the changeset on the same branch as head that is
650            not in outnodes and is closest to the tip.
651
652            Walk outgoing changesets from head to the bottom of the
653            workspace (revision 0) and return the the first changeset
654            we see that is not in outnodes.
655
656            If none is found (all revisions >= 0 are outgoing), the
657            only possible parenttip is the null node (node.nullid)
658            which is returned explicitly.
659            '''
660            for ctx in self._walkctxs(head, self.repo.changectx(0),
661                                      follow=True,
662                                      pick=lambda c: c.node() not in outnodes):
663                return ctx
664
665            return self.repo.changectx(node.nullid)
666
667        nodes = set(outgoing)
668        ptips = map(lambda x: tipmost_shared(x, nodes), heads)
669        return sorted(ptips, key=lambda x: x.rev(), reverse=True)[0]
670
671    def status(self, base='.', head=None, files=None):
672        '''Translate from the hg 6-tuple status format to a hash keyed
673        on change-type'''
674
675        states = ['modified', 'added', 'removed', 'deleted', 'unknown',
676                  'ignored']
677
678        match = self.matcher(files=files)
679        chngs = self.repo.status(base, head, match=match)
680
681        ret = {}
682        for paths, change in zip(chngs, states):
683            ret.update((f, change) for f in paths)
684        return ret
685
686    def findoutgoing(self, parent):
687        '''Return the base set of outgoing nodes.
688
689        A caching wrapper around mercurial.localrepo.findoutgoing().
690        Complains (to the user), if the parent workspace is
691        non-existent or inaccessible'''
692
693        self.ui.pushbuffer()
694        try:
695            try:
696                ui = self.ui
697                if hasattr(cmdutil, 'remoteui'):
698                    ui = cmdutil.remoteui(ui, {})
699                pws = hg.repository(ui, parent)
700                if Version.at_least("1.6"):
701                    return discovery.findoutgoing(self.repo, pws)
702                else:
703                    return self.repo.findoutgoing(pws)
704            except error.RepoError:
705                self.ui.warn("Warning: Parent workspace '%s' is not "
706                             "accessible\n"
707                             "active list will be incomplete\n\n" % parent)
708                return []
709        finally:
710            self.ui.popbuffer()
711    findoutgoing = util.cachefunc(findoutgoing)
712
713    def modified(self):
714        '''Return a list of files modified in the workspace'''
715
716        wctx = self.workingctx()
717        return sorted(wctx.files() + wctx.deleted()) or None
718
719    def merged(self):
720        '''Return boolean indicating whether the workspace has an uncommitted
721        merge'''
722
723        wctx = self.workingctx()
724        return len(wctx.parents()) > 1
725
726    def branched(self):
727        '''Return boolean indicating whether the workspace has an
728        uncommitted named branch'''
729
730        wctx = self.workingctx()
731        return wctx.branch() != wctx.parents()[0].branch()
732
733    def active(self, parent=None, thorough=False):
734        '''Return an ActiveList describing changes between workspace
735        and parent workspace (including uncommitted changes).
736        If the workspace has no parent, ActiveList will still describe any
737        uncommitted changes.
738
739        If thorough is True use neither the WorkList nor any cached
740        results (though the result of this call will be cached for
741        future, non-thorough, calls).'''
742
743        parent = self.parent(parent)
744
745        #
746        # Use the cached copy if we can (we have one, and weren't
747        # asked to be thorough)
748        #
749        if not thorough and parent in self.activecache:
750            return self.activecache[parent]
751
752        #
753        # outbases: The set of outgoing nodes with no outgoing ancestors
754        # outnodes: The full set of outgoing nodes
755        #
756        if parent:
757            outbases = self.findoutgoing(parent)
758            outnodes = self.repo.changelog.nodesbetween(outbases)[0]
759        else:               # No parent, no outgoing nodes
760            outbases = []
761            outnodes = []
762
763        wctx = self.workingctx(worklist=not thorough)
764        localtip = self._localtip(outnodes, wctx)
765
766        if localtip.rev() is None:
767            heads = localtip.parents()
768        else:
769            heads = [localtip]
770
771        parenttip = self.parenttip(heads, outnodes)
772
773        #
774        # If we couldn't find a parenttip, the two repositories must
775        # be unrelated (Hg catches most of this, but this case is
776        # valid for it but invalid for us)
777        #
778        if parenttip == None:
779            raise util.Abort('repository is unrelated')
780
781        headnodes = [h.node() for h in heads]
782        ctxs = [self.repo.changectx(n) for n in
783                self.repo.changelog.nodesbetween(outbases, headnodes)[0]]
784
785        if localtip.rev() is None:
786            ctxs.append(localtip)
787
788        act = ActiveList(self, parenttip, ctxs)
789        self.activecache[parent] = act
790
791        return act
792
793    def squishdeltas(self, active, message, user=None):
794        '''Create a single conglomerate changeset based on a given
795        active list.  Removes the original changesets comprising the
796        given active list, and any tags pointing to them.
797
798        Operation:
799
800          - Commit an activectx object representing the specified
801            active list,
802
803          - Remove any local tags pointing to changesets in the
804            specified active list.
805
806          - Remove the changesets comprising the specified active
807            list.
808
809          - Remove any metadata that may refer to changesets that were
810            removed.
811
812        Calling code is expected to hold both the working copy lock
813        and repository lock of the destination workspace
814        '''
815
816        def strip_local_tags(active):
817            '''Remove any local tags referring to the specified nodes.'''
818
819            if os.path.exists(self.repo.join('localtags')):
820                fh = None
821                try:
822                    fh = self.repo.opener('localtags')
823                    tags = active.prune_tags(fh)
824                    fh.close()
825
826                    fh = self.repo.opener('localtags', 'w', atomictemp=True)
827                    fh.writelines(tags)
828                    fh.rename()
829                finally:
830                    if fh and not fh.closed:
831                        fh.close()
832
833        if active.files():
834            for entry in active:
835                #
836                # Work around Mercurial issue #1666, if the source
837                # file of a rename exists in the working copy
838                # Mercurial will complain, and remove the file.
839                #
840                # We preemptively remove the file to avoid the
841                # complaint (the user was asked about this in
842                # cdm_recommit)
843                #
844                if entry.is_renamed():
845                    path = self.repo.wjoin(entry.parentname)
846                    if os.path.exists(path):
847                        os.unlink(path)
848
849            self.repo.commitctx(active.context(message, user))
850            wsstate = "recommitted"
851            destination = self.repo.changelog.tip()
852        else:
853            #
854            # If all we're doing is stripping the old nodes, we want to
855            # update the working copy such that we're not at a revision
856            # that's about to go away.
857            #
858            wsstate = "tip"
859            destination = active.parenttip.node()
860
861        self.clean(destination)
862
863        #
864        # Tags were elided by the activectx object.  Local tags,
865        # however, must be removed manually.
866        #
867        try:
868            strip_local_tags(active)
869        except EnvironmentError, e:
870            raise util.Abort('Could not recommit tags: %s\n' % e)
871
872        # Silence all the strip and update fun
873        self.ui.pushbuffer()
874
875        #
876        # Remove the previous child-local changes by stripping the
877        # nodes that form the base of the ActiveList (removing their
878        # children in the process).
879        #
880        try:
881            try:
882                for base in active.bases():
883                    #
884                    # Any cached information about the repository is
885                    # likely to be invalid during the strip.  The
886                    # caching of branch tags is especially
887                    # problematic.
888                    #
889                    self.repo.invalidate()
890                    repair.strip(self.ui, self.repo, base.node(), backup=False)
891            except:
892                #
893                # If this fails, it may leave us in a surprising place in
894                # the history.
895                #
896                # We want to warn the user that something went wrong,
897                # and what will happen next, re-raise the exception, and
898                # bring the working copy back into a consistent state
899                # (which the finally block will do)
900                #
901                self.ui.warn("stripping failed, your workspace will have "
902                             "superfluous heads.\n"
903                             "your workspace has been updated to the "
904                             "%s changeset.\n" % wsstate)
905                raise               # Re-raise the exception
906        finally:
907            self.clean()
908            self.repo.dirstate.write() # Flush the dirstate
909            self.repo.invalidate()     # Invalidate caches
910
911            #
912            # We need to remove Hg's undo information (used for rollback),
913            # since it refers to data that will probably not exist after
914            # the strip.
915            #
916            if os.path.exists(self.repo.sjoin('undo')):
917                try:
918                    os.unlink(self.repo.sjoin('undo'))
919                except EnvironmentError, e:
920                    raise util.Abort('failed to remove undo data: %s\n' % e)
921
922            self.ui.popbuffer()
923
924    def filepath(self, path):
925        'Return the full path to a workspace file.'
926
927        return self.repo.pathto(path)
928
929    def clean(self, rev=None):
930        '''Bring workspace up to REV (or tip) forcefully (discarding in
931        progress changes)'''
932
933        if rev != None:
934            rev = self.repo.lookup(rev)
935        else:
936            rev = self.repo.changelog.tip()
937
938        hg.clean(self.repo, rev, show_stats=False)
939
940    def mq_applied(self):
941        '''True if the workspace has Mq patches applied'''
942
943        q = mq.queue(self.ui, self.repo.join(''))
944        return q.applied
945
946    def workingctx(self, worklist=False):
947        '''Return a workingctx object representing the working copy.
948
949        If worklist is true, return a workingctx object created based
950        on the status of files in the workspace's worklist.'''
951
952        wl = WorkList(self)
953
954        if worklist and wl:
955            return context.workingctx(self.repo, changes=wl.status())
956        else:
957            return self.repo.changectx(None)
958
959    def matcher(self, pats=None, opts=None, files=None):
960        '''Return a match object suitable for Mercurial based on
961        specified criteria.
962
963        If files is specified it is a list of pathnames relative to
964        the repository root to be matched precisely.
965
966        If pats and/or opts are specified, these are as to
967        cmdutil.match'''
968
969        of_patterns = pats is not None or opts is not None
970        of_files = files is not None
971        opts = opts or {}       # must be a dict
972
973        assert not (of_patterns and of_files)
974
975        if of_patterns:
976            return cmdutil.match(self.repo, pats, opts)
977        elif of_files:
978            return cmdutil.matchfiles(self.repo, files)
979        else:
980            return cmdutil.matchall(self.repo)
981
982    def diff(self, node1=None, node2=None, match=None, opts=None):
983        '''Return the diff of changes between two changesets as a string'''
984
985        #
986        # Retain compatibility by only calling diffopts() if it
987        # obviously has not already been done.
988        #
989        if isinstance(opts, dict):
990            opts = patch.diffopts(self.ui, opts)
991
992        ret = cStringIO.StringIO()
993        for chunk in patch.diff(self.repo, node1, node2, match=match,
994                                opts=opts):
995            ret.write(chunk)
996
997        return ret.getvalue()
998
999    if Version.at_least("1.6"):
1000        def copy(self, src, dest):
1001            '''Copy a file from src to dest
1002            '''
1003
1004            self.workingctx().copy(src, dest)
1005    else:
1006        def copy(self, src, dest):
1007            '''Copy a file from src to dest
1008            '''
1009
1010            self.repo.copy(src, dest)
1011
1012
1013    if Version.at_least("1.4"):
1014
1015        def _walkctxs(self, base, head, follow=False, pick=None):
1016            '''Generate changectxs between BASE and HEAD.
1017
1018            Walk changesets between BASE and HEAD (in the order implied by
1019            their relation), following a given branch if FOLLOW is a true
1020            value, yielding changectxs where PICK (if specified) returns a
1021            true value.
1022
1023            PICK is a function of one argument, a changectx.'''
1024
1025            chosen = {}
1026
1027            def prep(ctx, fns):
1028                chosen[ctx.rev()] = not pick or pick(ctx)
1029
1030            opts = {'rev': ['%s:%s' % (base.rev(), head.rev())],
1031                    'follow': follow}
1032            matcher = cmdutil.matchall(self.repo)
1033
1034            for ctx in cmdutil.walkchangerevs(self.repo, matcher, opts, prep):
1035                if chosen[ctx.rev()]:
1036                    yield ctx
1037    else:
1038
1039        def _walkctxs(self, base, head, follow=False, pick=None):
1040            '''Generate changectxs between BASE and HEAD.
1041
1042            Walk changesets between BASE and HEAD (in the order implied by
1043            their relation), following a given branch if FOLLOW is a true
1044            value, yielding changectxs where PICK (if specified) returns a
1045            true value.
1046
1047            PICK is a function of one argument, a changectx.'''
1048
1049            opts = {'rev': ['%s:%s' % (base.rev(), head.rev())],
1050                    'follow': follow}
1051
1052            changectx = self.repo.changectx
1053            getcset = util.cachefunc(lambda r: changectx(r).changeset())
1054
1055            #
1056            # See the docstring of mercurial.cmdutil.walkchangerevs() for
1057            # the phased approach to the iterator returned.  The important
1058            # part to note is that the 'add' phase gathers nodes, which
1059            # the 'iter' phase then iterates through.
1060            #
1061            changeiter = cmdutil.walkchangerevs(self.ui, self.repo,
1062                                                [], getcset, opts)[0]
1063
1064            matched = {}
1065            for st, rev, fns in changeiter:
1066                if st == 'add':
1067                    ctx = changectx(rev)
1068                    if not pick or pick(ctx):
1069                        matched[rev] = ctx
1070                elif st == 'iter':
1071                    if rev in matched:
1072                        yield matched[rev]
1073