xref: /titanic_50/usr/src/tools/onbld/Scm/WorkSpace.py (revision 036abaca93ddab92ba33036159c30112ab844810)
1cdf0c1d5Smjnelson#
2cdf0c1d5Smjnelson#  This program is free software; you can redistribute it and/or modify
3cdf0c1d5Smjnelson#  it under the terms of the GNU General Public License version 2
4cdf0c1d5Smjnelson#  as published by the Free Software Foundation.
5cdf0c1d5Smjnelson#
6cdf0c1d5Smjnelson#  This program is distributed in the hope that it will be useful,
7cdf0c1d5Smjnelson#  but WITHOUT ANY WARRANTY; without even the implied warranty of
8cdf0c1d5Smjnelson#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
9cdf0c1d5Smjnelson#  GNU General Public License for more details.
10cdf0c1d5Smjnelson#
11cdf0c1d5Smjnelson#  You should have received a copy of the GNU General Public License
12cdf0c1d5Smjnelson#  along with this program; if not, write to the Free Software
13cdf0c1d5Smjnelson#  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
14cdf0c1d5Smjnelson#
15cdf0c1d5Smjnelson
16cdf0c1d5Smjnelson#
1778add226Sjmcp# Copyright (c) 2008, 2010, Oracle and/or its affiliates. All rights reserved.
18605a716eSRichard Lowe# Copyright 2008, 2011, Richard Lowe
19cdf0c1d5Smjnelson#
20cdf0c1d5Smjnelson
21cdf0c1d5Smjnelson#
22cdf0c1d5Smjnelson# Theory:
23cdf0c1d5Smjnelson#
24cdf0c1d5Smjnelson# Workspaces have a non-binding parent/child relationship.
25cdf0c1d5Smjnelson# All important operations apply to the changes between the two.
26cdf0c1d5Smjnelson#
27cdf0c1d5Smjnelson# However, for the sake of remote operation, the 'parent' of a
28cdf0c1d5Smjnelson# workspace is not seen as a literal entity, instead the figurative
29cdf0c1d5Smjnelson# parent contains the last changeset common to both parent and child,
30cdf0c1d5Smjnelson# as such the 'parent tip' is actually nothing of the sort, but instead a
31cdf0c1d5Smjnelson# convenient imitation.
32cdf0c1d5Smjnelson#
33cdf0c1d5Smjnelson# Any change made to a workspace is a change to a file therein, such
34cdf0c1d5Smjnelson# changes can be represented briefly as whether the file was
35cdf0c1d5Smjnelson# modified/added/removed as compared to the parent workspace, whether
36cdf0c1d5Smjnelson# the file has a different name in the parent and if so, whether it
37cdf0c1d5Smjnelson# was renamed or merely copied.  Each changed file has an
38cdf0c1d5Smjnelson# associated ActiveEntry.
39cdf0c1d5Smjnelson#
40605a716eSRichard Lowe# The ActiveList, being a list of ActiveEntry objects, can thus
41605a716eSRichard Lowe# present the entire change in workspace state between a parent and
42605a716eSRichard Lowe# its child and is the important bit here (in that if it is incorrect,
43605a716eSRichard Lowe# everything else will be as incorrect, or more)
44cdf0c1d5Smjnelson#
45cdf0c1d5Smjnelson
46cdf0c1d5Smjnelsonimport cStringIO
47cdf0c1d5Smjnelsonimport os
4887039217SRichard Lowefrom mercurial import cmdutil, context, error, hg, node, patch, repair, util
49cdf0c1d5Smjnelsonfrom hgext import mq
50cdf0c1d5Smjnelson
512b5878deSRich Lowefrom onbld.Scm import Version
522b5878deSRich Lowe
53605a716eSRichard Lowe
54c959a081SRichard Lowe#
5587039217SRichard Lowe# Mercurial 1.6 moves findoutgoing into a discover module
56c959a081SRichard Lowe#
5787039217SRichard Loweif Version.at_least("1.6"):
5887039217SRichard Lowe    from mercurial import discovery
59c959a081SRichard Lowe
60cdf0c1d5Smjnelson
61cdf0c1d5Smjnelsonclass ActiveEntry(object):
62cdf0c1d5Smjnelson    '''Representation of the changes made to a single file.
63cdf0c1d5Smjnelson
64cdf0c1d5Smjnelson    MODIFIED   - Contents changed, but no other changes were made
65cdf0c1d5Smjnelson    ADDED      - File is newly created
66cdf0c1d5Smjnelson    REMOVED    - File is being removed
67cdf0c1d5Smjnelson
68cdf0c1d5Smjnelson    Copies are represented by an Entry whose .parentname is non-nil
69cdf0c1d5Smjnelson
70cdf0c1d5Smjnelson    Truly copied files have non-nil .parentname and .renamed = False
71cdf0c1d5Smjnelson    Renames have non-nil .parentname and .renamed = True
72cdf0c1d5Smjnelson
73cdf0c1d5Smjnelson    Do not access any of this information directly, do so via the
74cdf0c1d5Smjnelson
75cdf0c1d5Smjnelson    .is_<change>() methods.'''
76cdf0c1d5Smjnelson
77605a716eSRichard Lowe    MODIFIED = intern('modified')
78605a716eSRichard Lowe    ADDED = intern('added')
79605a716eSRichard Lowe    REMOVED = intern('removed')
80cdf0c1d5Smjnelson
81605a716eSRichard Lowe    def __init__(self, name, change):
82cdf0c1d5Smjnelson        self.name = name
83605a716eSRichard Lowe        self.change = intern(change)
84605a716eSRichard Lowe
85605a716eSRichard Lowe        assert change in (self.MODIFIED, self.ADDED, self.REMOVED)
86605a716eSRichard Lowe
87cdf0c1d5Smjnelson        self.parentname = None
88cdf0c1d5Smjnelson        # As opposed to copied (or neither)
89cdf0c1d5Smjnelson        self.renamed = False
90cdf0c1d5Smjnelson        self.comments = []
91cdf0c1d5Smjnelson
92cdf0c1d5Smjnelson    def __cmp__(self, other):
93cdf0c1d5Smjnelson        return cmp(self.name, other.name)
94cdf0c1d5Smjnelson
95cdf0c1d5Smjnelson    def is_added(self):
96605a716eSRichard Lowe        '''Return True if this ActiveEntry represents an added file'''
97605a716eSRichard Lowe        return self.change is self.ADDED
98cdf0c1d5Smjnelson
99cdf0c1d5Smjnelson    def is_modified(self):
100605a716eSRichard Lowe        '''Return True if this ActiveEntry represents a modified file'''
101605a716eSRichard Lowe        return self.change is self.MODIFIED
102cdf0c1d5Smjnelson
103cdf0c1d5Smjnelson    def is_removed(self):
104605a716eSRichard Lowe        '''Return True if this ActiveEntry represents a removed file'''
105605a716eSRichard Lowe        return self.change is self.REMOVED
106cdf0c1d5Smjnelson
107cdf0c1d5Smjnelson    def is_renamed(self):
108605a716eSRichard Lowe        '''Return True if this ActiveEntry represents a renamed file'''
109cdf0c1d5Smjnelson        return self.parentname and self.renamed
110cdf0c1d5Smjnelson
111cdf0c1d5Smjnelson    def is_copied(self):
112605a716eSRichard Lowe        '''Return True if this ActiveEntry represents a copied file'''
113cdf0c1d5Smjnelson        return self.parentname and not self.renamed
114cdf0c1d5Smjnelson
115cdf0c1d5Smjnelson
116cdf0c1d5Smjnelsonclass ActiveList(object):
117605a716eSRichard Lowe    '''Complete representation of change between two changesets.
118cdf0c1d5Smjnelson
119605a716eSRichard Lowe    In practice, a container for ActiveEntry objects, and methods to
120605a716eSRichard Lowe    create them, and deal with them as a group.'''
121cdf0c1d5Smjnelson
122cdf0c1d5Smjnelson    def __init__(self, ws, parenttip, revs=None):
123605a716eSRichard Lowe        '''Initialize the ActiveList
124605a716eSRichard Lowe
125605a716eSRichard Lowe        parenttip is the revision with which to compare (likely to be
126605a716eSRichard Lowe        from the parent), revs is a topologically sorted list of
127605a716eSRichard Lowe        revisions ending with the revision to compare with (likely to
128605a716eSRichard Lowe        be the child-local revisions).'''
129605a716eSRichard Lowe
130605a716eSRichard Lowe        assert parenttip is not None
131605a716eSRichard Lowe
132cdf0c1d5Smjnelson        self.ws = ws
133cdf0c1d5Smjnelson        self.revs = revs
134cdf0c1d5Smjnelson        self.parenttip = parenttip
135cdf0c1d5Smjnelson        self.localtip = None
136cdf0c1d5Smjnelson
137605a716eSRichard Lowe        self._active = {}
138cdf0c1d5Smjnelson        self._comments = []
139cdf0c1d5Smjnelson
140605a716eSRichard Lowe        if revs:
141605a716eSRichard Lowe            self.localtip = revs[-1]
142605a716eSRichard Lowe            self._build()
143cdf0c1d5Smjnelson
144605a716eSRichard Lowe    def _status(self):
145605a716eSRichard Lowe        '''Return the status of any file mentioned in any of the
146605a716eSRichard Lowe        changesets making up this active list.'''
147cdf0c1d5Smjnelson
148605a716eSRichard Lowe        files = set()
149605a716eSRichard Lowe        for c in self.revs:
150605a716eSRichard Lowe            files.update(c.files())
151cdf0c1d5Smjnelson
152cdf0c1d5Smjnelson        #
153605a716eSRichard Lowe        # Any file not in the parenttip or the localtip is ephemeral
154605a716eSRichard Lowe        # and can be ignored. Mercurial will complain regarding these
155605a716eSRichard Lowe        # files if the localtip is a workingctx, so remove them in
156605a716eSRichard Lowe        # that case.
157cdf0c1d5Smjnelson        #
158605a716eSRichard Lowe        # Compare against the dirstate because a workingctx manifest
159605a716eSRichard Lowe        # is created on-demand and is particularly expensive.
160cdf0c1d5Smjnelson        #
161605a716eSRichard Lowe        if self.localtip.rev() is None:
162605a716eSRichard Lowe            for f in files.copy():
163605a716eSRichard Lowe                if f not in self.parenttip and f not in self.ws.repo.dirstate:
164605a716eSRichard Lowe                    files.remove(f)
165605a716eSRichard Lowe
166605a716eSRichard Lowe        return self.ws.status(self.parenttip, self.localtip, files=files)
167605a716eSRichard Lowe
168605a716eSRichard Lowe    def _build(self):
169605a716eSRichard Lowe        '''Construct ActiveEntry objects for each changed file.
170605a716eSRichard Lowe
171605a716eSRichard Lowe        This works in 3 stages:
172605a716eSRichard Lowe
173605a716eSRichard Lowe          - Create entries for every changed file with
174605a716eSRichard Lowe            semi-appropriate change type
175605a716eSRichard Lowe
176605a716eSRichard Lowe          - Track renames/copies, and set change comments (both
177605a716eSRichard Lowe            ActiveList-wide, and per-file).
178605a716eSRichard Lowe
179605a716eSRichard Lowe          - Cleanup
180605a716eSRichard Lowe            - Drop circular renames
181605a716eSRichard Lowe            - Drop the removal of the old name of any rename
182605a716eSRichard Lowe            - Drop entries for modified files that haven't actually changed'''
183cdf0c1d5Smjnelson
184cdf0c1d5Smjnelson        #
185605a716eSRichard Lowe        # Keep a cache of filectx objects (keyed on pathname) so that
186605a716eSRichard Lowe        # we can avoid opening filelogs numerous times.
187cdf0c1d5Smjnelson        #
188605a716eSRichard Lowe        fctxcache = {}
189cdf0c1d5Smjnelson
190605a716eSRichard Lowe        def oldname(ctx, fname):
191605a716eSRichard Lowe            '''Return the name 'fname' held prior to any possible
192605a716eSRichard Lowe            rename/copy in the given changeset.'''
193cdf0c1d5Smjnelson            try:
194605a716eSRichard Lowe                if fname in fctxcache:
195605a716eSRichard Lowe                    octx = fctxcache[fname]
196605a716eSRichard Lowe                    fctx = ctx.filectx(fname, filelog=octx.filelog())
197605a716eSRichard Lowe                else:
198cdf0c1d5Smjnelson                    fctx = ctx.filectx(fname)
199605a716eSRichard Lowe                    #
200605a716eSRichard Lowe                    # workingfilectx objects may not refer to the
201605a716eSRichard Lowe                    # right filelog (in case of rename).  Don't cache
202605a716eSRichard Lowe                    # them.
203605a716eSRichard Lowe                    #
204605a716eSRichard Lowe                    if not isinstance(fctx, context.workingfilectx):
205605a716eSRichard Lowe                        fctxcache[fname] = fctx
20687039217SRichard Lowe            except error.LookupError:
207605a716eSRichard Lowe                return None
208cdf0c1d5Smjnelson
209cdf0c1d5Smjnelson            rn = fctx.renamed()
210605a716eSRichard Lowe            return rn and rn[0] or fname
211605a716eSRichard Lowe
212605a716eSRichard Lowe        status = self._status()
213605a716eSRichard Lowe        self._active = dict((fname, ActiveEntry(fname, kind))
214605a716eSRichard Lowe                            for fname, kind in status.iteritems()
215605a716eSRichard Lowe                            if kind in ('modified', 'added', 'removed'))
216cdf0c1d5Smjnelson
217cdf0c1d5Smjnelson        #
218605a716eSRichard Lowe        # We do two things:
219605a716eSRichard Lowe        #    - Gather checkin comments (for the entire ActiveList, and
220605a716eSRichard Lowe        #      per-file)
221605a716eSRichard Lowe        #    - Set the .parentname of any copied/renamed file
222cdf0c1d5Smjnelson        #
223605a716eSRichard Lowe        # renames/copies:
224605a716eSRichard Lowe        #   We walk the list of revisions backward such that only files
225605a716eSRichard Lowe        #   that ultimately remain active need be considered.
226cdf0c1d5Smjnelson        #
227605a716eSRichard Lowe        #   At each iteration (revision) we update the .parentname of
228605a716eSRichard Lowe        #   any active file renamed or copied in that revision (the
229605a716eSRichard Lowe        #   current .parentname if set, or .name otherwise, reflects
230605a716eSRichard Lowe        #   the name of a given active file in the revision currently
231605a716eSRichard Lowe        #   being looked at)
232605a716eSRichard Lowe        #
233605a716eSRichard Lowe        for ctx in reversed(self.revs):
234605a716eSRichard Lowe            desc = ctx.description().splitlines()
235605a716eSRichard Lowe            self._comments = desc + self._comments
236605a716eSRichard Lowe            cfiles = set(ctx.files())
237605a716eSRichard Lowe
238605a716eSRichard Lowe            for entry in self:
239605a716eSRichard Lowe                fname = entry.parentname or entry.name
240605a716eSRichard Lowe                if fname not in cfiles:
241cdf0c1d5Smjnelson                    continue
242cdf0c1d5Smjnelson
243605a716eSRichard Lowe                entry.comments = desc + entry.comments
244cdf0c1d5Smjnelson
245cdf0c1d5Smjnelson                #
246605a716eSRichard Lowe                # We don't care about the name history of any file
247605a716eSRichard Lowe                # that ends up being removed, since that trumps any
248605a716eSRichard Lowe                # possible renames or copies along the way.
249cdf0c1d5Smjnelson                #
250605a716eSRichard Lowe                # Changes that we may care about involving an
251605a716eSRichard Lowe                # intermediate name of a removed file will appear
252605a716eSRichard Lowe                # separately (related to the eventual name along
253605a716eSRichard Lowe                # that line)
254cdf0c1d5Smjnelson                #
255605a716eSRichard Lowe                if not entry.is_removed():
256605a716eSRichard Lowe                    entry.parentname = oldname(ctx, fname)
257605a716eSRichard Lowe
258cdf0c1d5Smjnelson        for entry in self._active.values():
259605a716eSRichard Lowe            #
260605a716eSRichard Lowe            # For any file marked as copied or renamed, clear the
261605a716eSRichard Lowe            # .parentname if the copy or rename is cyclic (source ==
262605a716eSRichard Lowe            # destination) or if the .parentname did not exist in the
263605a716eSRichard Lowe            # parenttip.
264605a716eSRichard Lowe            #
265605a716eSRichard Lowe            # If the parentname is marked as removed, set the renamed
266605a716eSRichard Lowe            # flag and remove any ActiveEntry we may have for the
267605a716eSRichard Lowe            # .parentname.
268605a716eSRichard Lowe            #
269605a716eSRichard Lowe            if entry.parentname:
270605a716eSRichard Lowe                if (entry.parentname == entry.name or
271605a716eSRichard Lowe                    entry.parentname not in self.parenttip):
272605a716eSRichard Lowe                    entry.parentname = None
273605a716eSRichard Lowe                elif status.get(entry.parentname) == 'removed':
274605a716eSRichard Lowe                    entry.renamed = True
275cdf0c1d5Smjnelson
276605a716eSRichard Lowe                    if entry.parentname in self:
277605a716eSRichard Lowe                        del self[entry.parentname]
278cdf0c1d5Smjnelson
279cdf0c1d5Smjnelson            #
280605a716eSRichard Lowe            # There are cases during a merge where a file will be seen
281605a716eSRichard Lowe            # as modified by status but in reality be an addition (not
282605a716eSRichard Lowe            # in the parenttip), so we have to check whether the file
283605a716eSRichard Lowe            # is in the parenttip and set it as an addition, if not.
284cdf0c1d5Smjnelson            #
285605a716eSRichard Lowe            # If a file is modified (and not a copy or rename), we do
286605a716eSRichard Lowe            # a full comparison to the copy in the parenttip and
287605a716eSRichard Lowe            # ignore files that are parts of active revisions but
288605a716eSRichard Lowe            # unchanged.
289cdf0c1d5Smjnelson            #
290605a716eSRichard Lowe            if entry.name not in self.parenttip:
291cdf0c1d5Smjnelson                entry.change = ActiveEntry.ADDED
29278add226Sjmcp            elif entry.is_modified():
29378add226Sjmcp                if not self._changed_file(entry.name):
294cdf0c1d5Smjnelson                    del self[entry.name]
295cdf0c1d5Smjnelson
296cdf0c1d5Smjnelson    def __contains__(self, fname):
297cdf0c1d5Smjnelson        return fname in self._active
298cdf0c1d5Smjnelson
299cdf0c1d5Smjnelson    def __getitem__(self, key):
300cdf0c1d5Smjnelson        return self._active[key]
301cdf0c1d5Smjnelson
302cdf0c1d5Smjnelson    def __setitem__(self, key, value):
303cdf0c1d5Smjnelson        self._active[key] = value
304cdf0c1d5Smjnelson
305cdf0c1d5Smjnelson    def __delitem__(self, key):
306cdf0c1d5Smjnelson        del self._active[key]
307cdf0c1d5Smjnelson
308cdf0c1d5Smjnelson    def __iter__(self):
309605a716eSRichard Lowe        return self._active.itervalues()
310cdf0c1d5Smjnelson
311cdf0c1d5Smjnelson    def files(self):
312c959a081SRichard Lowe        '''Return the list of pathnames of all files touched by this
313c959a081SRichard Lowe        ActiveList
314cdf0c1d5Smjnelson
315c959a081SRichard Lowe        Where files have been renamed, this will include both their
316c959a081SRichard Lowe        current name and the name which they had in the parent tip.
317c959a081SRichard Lowe        '''
318c959a081SRichard Lowe
319c959a081SRichard Lowe        ret = self._active.keys()
320605a716eSRichard Lowe        ret.extend(x.parentname for x in self if x.is_renamed())
321605a716eSRichard Lowe        return set(ret)
322cdf0c1d5Smjnelson
323cdf0c1d5Smjnelson    def comments(self):
324605a716eSRichard Lowe        '''Return the full set of changeset comments associated with
325605a716eSRichard Lowe        this ActiveList'''
326605a716eSRichard Lowe
327cdf0c1d5Smjnelson        return self._comments
328cdf0c1d5Smjnelson
329cdf0c1d5Smjnelson    def bases(self):
330c7f512e4Sjmcp        '''Return the list of changesets that are roots of the ActiveList.
331cdf0c1d5Smjnelson
332c7f512e4Sjmcp        This is the set of active changesets where neither parent
333c7f512e4Sjmcp        changeset is itself active.'''
334cdf0c1d5Smjnelson
335c7f512e4Sjmcp        revset = set(self.revs)
336c7f512e4Sjmcp        return filter(lambda ctx: not [p for p in ctx.parents() if p in revset],
337c7f512e4Sjmcp                      self.revs)
338cdf0c1d5Smjnelson
339cdf0c1d5Smjnelson    def tags(self):
340cdf0c1d5Smjnelson        '''Find tags that refer to a changeset in the ActiveList,
341cdf0c1d5Smjnelson        returning a list of 3-tuples (tag, node, is_local) for each.
342cdf0c1d5Smjnelson
343cdf0c1d5Smjnelson        We return all instances of a tag that refer to such a node,
344cdf0c1d5Smjnelson        not just that which takes precedence.'''
345cdf0c1d5Smjnelson
346c959a081SRichard Lowe        def colliding_tags(iterable, nodes, local):
347c959a081SRichard Lowe            for nd, name in [line.rstrip().split(' ', 1) for line in iterable]:
348c959a081SRichard Lowe                if nd in nodes:
349c959a081SRichard Lowe                    yield (name, self.ws.repo.lookup(nd), local)
350c959a081SRichard Lowe
351c959a081SRichard Lowe        tags = []
352c959a081SRichard Lowe        nodes = set(node.hex(ctx.node()) for ctx in self.revs)
353c959a081SRichard Lowe
354cdf0c1d5Smjnelson        if os.path.exists(self.ws.repo.join('localtags')):
355c959a081SRichard Lowe            fh = self.ws.repo.opener('localtags')
356c959a081SRichard Lowe            tags.extend(colliding_tags(fh, nodes, True))
357c959a081SRichard Lowe            fh.close()
358cdf0c1d5Smjnelson
359cdf0c1d5Smjnelson        # We want to use the tags file from the localtip
360c959a081SRichard Lowe        if '.hgtags' in self.localtip:
361c959a081SRichard Lowe            data = self.localtip.filectx('.hgtags').data().splitlines()
362c959a081SRichard Lowe            tags.extend(colliding_tags(data, nodes, False))
363cdf0c1d5Smjnelson
364cdf0c1d5Smjnelson        return tags
365cdf0c1d5Smjnelson
366c959a081SRichard Lowe    def prune_tags(self, data):
367c959a081SRichard Lowe        '''Return a copy of data, which should correspond to the
368c959a081SRichard Lowe        contents of a Mercurial tags file, with any tags that refer to
369c959a081SRichard Lowe        changesets which are components of the ActiveList removed.'''
370c959a081SRichard Lowe
371c959a081SRichard Lowe        nodes = set(node.hex(ctx.node()) for ctx in self.revs)
372c959a081SRichard Lowe        return [t for t in data if t.split(' ', 1)[0] not in nodes]
373c959a081SRichard Lowe
37478add226Sjmcp    def _changed_file(self, path):
37578add226Sjmcp        '''Compare the parent and local versions of a given file.
376cdf0c1d5Smjnelson        Return True if file changed, False otherwise.
377cdf0c1d5Smjnelson
37878add226Sjmcp        Note that this compares the given path in both versions, not the given
37978add226Sjmcp        entry; renamed and copied files are compared by name, not history.
38078add226Sjmcp
381cdf0c1d5Smjnelson        The fast path compares file metadata, slow path is a
382cdf0c1d5Smjnelson        real comparison of file content.'''
383cdf0c1d5Smjnelson
38487039217SRichard Lowe        if ((path in self.parenttip) != (path in self.localtip)):
38578add226Sjmcp            return True
38678add226Sjmcp
38778add226Sjmcp        parentfile = self.parenttip.filectx(path)
38878add226Sjmcp        localfile = self.localtip.filectx(path)
389cdf0c1d5Smjnelson
390cdf0c1d5Smjnelson        #
391cdf0c1d5Smjnelson        # NB: Keep these ordered such as to make every attempt
392cdf0c1d5Smjnelson        #     to short-circuit the more time consuming checks.
393cdf0c1d5Smjnelson        #
394cdf0c1d5Smjnelson        if parentfile.size() != localfile.size():
395cdf0c1d5Smjnelson            return True
396cdf0c1d5Smjnelson
397c959a081SRichard Lowe        if parentfile.flags() != localfile.flags():
398cdf0c1d5Smjnelson            return True
399cdf0c1d5Smjnelson
400*036abacaSRichard Lowe        if Version.at_least("1.7"):
401*036abacaSRichard Lowe            if parentfile.cmp(localfile):
402*036abacaSRichard Lowe                return True
403*036abacaSRichard Lowe        else:
404cdf0c1d5Smjnelson            if parentfile.cmp(localfile.data()):
405cdf0c1d5Smjnelson                return True
406cdf0c1d5Smjnelson
407c959a081SRichard Lowe    def context(self, message, user):
408c959a081SRichard Lowe        '''Return a Mercurial context object representing the entire
409c959a081SRichard Lowe        ActiveList as one change.'''
410c959a081SRichard Lowe        return activectx(self, message, user)
411c959a081SRichard Lowe
412605a716eSRichard Lowe    def as_text(self, paths):
413605a716eSRichard Lowe        '''Return the ActiveList as a block of text in a format
414605a716eSRichard Lowe        intended to aid debugging and simplify the test suite.
415605a716eSRichard Lowe
416605a716eSRichard Lowe        paths should be a list of paths for which file-level data
417605a716eSRichard Lowe        should be included.  If it is empty, the whole active list is
418605a716eSRichard Lowe        included.'''
419605a716eSRichard Lowe
420605a716eSRichard Lowe        cstr = cStringIO.StringIO()
421605a716eSRichard Lowe
422605a716eSRichard Lowe        cstr.write('parent tip: %s:%s\n' % (self.parenttip.rev(),
423605a716eSRichard Lowe                                            self.parenttip))
424605a716eSRichard Lowe        if self.localtip:
425605a716eSRichard Lowe            rev = self.localtip.rev()
426605a716eSRichard Lowe            cstr.write('local tip:  %s:%s\n' %
427605a716eSRichard Lowe                       (rev is None and "working" or rev, self.localtip))
428605a716eSRichard Lowe        else:
429605a716eSRichard Lowe            cstr.write('local tip:  None\n')
430605a716eSRichard Lowe
431605a716eSRichard Lowe        cstr.write('entries:\n')
432605a716eSRichard Lowe        for entry in self:
433605a716eSRichard Lowe            if paths and self.ws.filepath(entry.name) not in paths:
434605a716eSRichard Lowe                continue
435605a716eSRichard Lowe
436605a716eSRichard Lowe            cstr.write('  - %s\n' % entry.name)
437605a716eSRichard Lowe            cstr.write('    parentname: %s\n' % entry.parentname)
438605a716eSRichard Lowe            cstr.write('    change: %s\n' % entry.change)
439605a716eSRichard Lowe            cstr.write('    renamed: %s\n' % entry.renamed)
440605a716eSRichard Lowe            cstr.write('    comments:\n')
441605a716eSRichard Lowe            cstr.write('      ' + '\n      '.join(entry.comments) + '\n')
442605a716eSRichard Lowe            cstr.write('\n')
443605a716eSRichard Lowe
444605a716eSRichard Lowe        return cstr.getvalue()
445605a716eSRichard Lowe
446605a716eSRichard Lowe
447605a716eSRichard Loweclass WorkList(object):
448605a716eSRichard Lowe    '''A (user-maintained) list of files changed in this workspace as
449605a716eSRichard Lowe    compared to any parent workspace.
450605a716eSRichard Lowe
451605a716eSRichard Lowe    Internally, the WorkList is stored in .hg/cdm/worklist as a list
452605a716eSRichard Lowe    of file pathnames, one per-line.
453605a716eSRichard Lowe
454605a716eSRichard Lowe    This may only safely be used as a hint regarding possible
455605a716eSRichard Lowe    modifications to the working copy, it should not be relied upon to
456605a716eSRichard Lowe    suggest anything about committed changes.'''
457605a716eSRichard Lowe
458605a716eSRichard Lowe    def __init__(self, ws):
459605a716eSRichard Lowe        '''Load the WorkList for the specified WorkSpace from disk.'''
460605a716eSRichard Lowe
461605a716eSRichard Lowe        self._ws = ws
462605a716eSRichard Lowe        self._repo = ws.repo
463605a716eSRichard Lowe        self._file = os.path.join('cdm', 'worklist')
464605a716eSRichard Lowe        self._files = set()
465605a716eSRichard Lowe        self._valid = False
466605a716eSRichard Lowe
467605a716eSRichard Lowe        if os.path.exists(self._repo.join(self._file)):
468605a716eSRichard Lowe            self.load()
469605a716eSRichard Lowe
470605a716eSRichard Lowe    def __nonzero__(self):
471605a716eSRichard Lowe        '''A WorkList object is true if it was loaded from disk,
472605a716eSRichard Lowe        rather than freshly created.
473605a716eSRichard Lowe        '''
474605a716eSRichard Lowe
475605a716eSRichard Lowe        return self._valid
476605a716eSRichard Lowe
477605a716eSRichard Lowe    def list(self):
478605a716eSRichard Lowe        '''List of pathnames contained in the WorkList
479605a716eSRichard Lowe        '''
480605a716eSRichard Lowe
481605a716eSRichard Lowe        return list(self._files)
482605a716eSRichard Lowe
483605a716eSRichard Lowe    def status(self):
484605a716eSRichard Lowe        '''Return the status (in tuple form) of files from the
485605a716eSRichard Lowe        WorkList as they are in the working copy
486605a716eSRichard Lowe        '''
487605a716eSRichard Lowe
488605a716eSRichard Lowe        match = self._ws.matcher(files=self.list())
489605a716eSRichard Lowe        return self._repo.status(match=match)
490605a716eSRichard Lowe
491605a716eSRichard Lowe    def add(self, fname):
492605a716eSRichard Lowe        '''Add FNAME to the WorkList.
493605a716eSRichard Lowe        '''
494605a716eSRichard Lowe
495605a716eSRichard Lowe        self._files.add(fname)
496605a716eSRichard Lowe
497605a716eSRichard Lowe    def write(self):
498605a716eSRichard Lowe        '''Write the WorkList out to disk.
499605a716eSRichard Lowe        '''
500605a716eSRichard Lowe
501605a716eSRichard Lowe        dirn = os.path.split(self._file)[0]
502605a716eSRichard Lowe
503605a716eSRichard Lowe        if dirn and not os.path.exists(self._repo.join(dirn)):
504605a716eSRichard Lowe            try:
505605a716eSRichard Lowe                os.makedirs(self._repo.join(dirn))
506605a716eSRichard Lowe            except EnvironmentError, e:
507605a716eSRichard Lowe                raise util.Abort("Couldn't create directory %s: %s" %
508605a716eSRichard Lowe                                 (self._repo.join(dirn), e))
509605a716eSRichard Lowe
510605a716eSRichard Lowe        fh = self._repo.opener(self._file, 'w', atomictemp=True)
511605a716eSRichard Lowe
512605a716eSRichard Lowe        for name in self._files:
513605a716eSRichard Lowe            fh.write("%s\n" % name)
514605a716eSRichard Lowe
515605a716eSRichard Lowe        fh.rename()
516605a716eSRichard Lowe        fh.close()
517605a716eSRichard Lowe
518605a716eSRichard Lowe    def load(self):
519605a716eSRichard Lowe        '''Read in the WorkList from disk.
520605a716eSRichard Lowe        '''
521605a716eSRichard Lowe
522605a716eSRichard Lowe        fh = self._repo.opener(self._file, 'r')
523605a716eSRichard Lowe        self._files = set(l.rstrip('\n') for l in fh)
524605a716eSRichard Lowe        self._valid = True
525605a716eSRichard Lowe        fh.close()
526605a716eSRichard Lowe
527605a716eSRichard Lowe    def delete(self):
528605a716eSRichard Lowe        '''Empty the WorkList
529605a716eSRichard Lowe
530605a716eSRichard Lowe        Remove the on-disk WorkList and clear the file-list of the
531605a716eSRichard Lowe        in-memory copy
532605a716eSRichard Lowe        '''
533605a716eSRichard Lowe
534605a716eSRichard Lowe        if os.path.exists(self._repo.join(self._file)):
535605a716eSRichard Lowe            os.unlink(self._repo.join(self._file))
536605a716eSRichard Lowe
537605a716eSRichard Lowe        self._files = set()
538605a716eSRichard Lowe        self._valid = False
539605a716eSRichard Lowe
540c959a081SRichard Lowe
541c959a081SRichard Loweclass activectx(context.memctx):
542c959a081SRichard Lowe    '''Represent an ActiveList as a Mercurial context object.
543c959a081SRichard Lowe
544c959a081SRichard Lowe    Part of the  WorkSpace.squishdeltas implementation.'''
545c959a081SRichard Lowe
546c959a081SRichard Lowe    def __init__(self, active, message, user):
547c959a081SRichard Lowe        '''Build an activectx object.
548c959a081SRichard Lowe
549c959a081SRichard Lowe          active  - The ActiveList object used as the source for all data.
550c959a081SRichard Lowe          message - Changeset description
551c959a081SRichard Lowe          user    - Committing user'''
552c959a081SRichard Lowe
553c959a081SRichard Lowe        def filectxfn(repository, ctx, fname):
554c959a081SRichard Lowe            fctx = active.localtip.filectx(fname)
555c959a081SRichard Lowe            data = fctx.data()
556c959a081SRichard Lowe
557c959a081SRichard Lowe            #
558c959a081SRichard Lowe            # .hgtags is a special case, tags referring to active list
559c959a081SRichard Lowe            # component changesets should be elided.
560c959a081SRichard Lowe            #
561c959a081SRichard Lowe            if fname == '.hgtags':
562c959a081SRichard Lowe                data = '\n'.join(active.prune_tags(data.splitlines()))
563c959a081SRichard Lowe
564c959a081SRichard Lowe            return context.memfilectx(fname, data, 'l' in fctx.flags(),
565c959a081SRichard Lowe                                      'x' in fctx.flags(),
566c959a081SRichard Lowe                                      active[fname].parentname)
567c959a081SRichard Lowe
568c959a081SRichard Lowe        self.__active = active
569c959a081SRichard Lowe        parents = (active.parenttip.node(), node.nullid)
570c959a081SRichard Lowe        extra = {'branch': active.localtip.branch()}
571c959a081SRichard Lowe        context.memctx.__init__(self, active.ws.repo, parents, message,
572c959a081SRichard Lowe                                active.files(), filectxfn, user=user,
573c959a081SRichard Lowe                                extra=extra)
574c959a081SRichard Lowe
575c959a081SRichard Lowe    def modified(self):
576c959a081SRichard Lowe        return [entry.name for entry in self.__active if entry.is_modified()]
577c959a081SRichard Lowe
578c959a081SRichard Lowe    def added(self):
579c959a081SRichard Lowe        return [entry.name for entry in self.__active if entry.is_added()]
580c959a081SRichard Lowe
581c959a081SRichard Lowe    def removed(self):
582bb664d7bSRichard Lowe        ret = set(entry.name for entry in self.__active if entry.is_removed())
583bb664d7bSRichard Lowe        ret.update(set(x.parentname for x in self.__active if x.is_renamed()))
584bb664d7bSRichard Lowe        return list(ret)
585c959a081SRichard Lowe
586c959a081SRichard Lowe    def files(self):
587c959a081SRichard Lowe        return self.__active.files()
588c959a081SRichard Lowe
589cdf0c1d5Smjnelson
590cdf0c1d5Smjnelsonclass WorkSpace(object):
591cdf0c1d5Smjnelson
592cdf0c1d5Smjnelson    def __init__(self, repository):
593cdf0c1d5Smjnelson        self.repo = repository
594cdf0c1d5Smjnelson        self.ui = self.repo.ui
595cdf0c1d5Smjnelson        self.name = self.repo.root
596cdf0c1d5Smjnelson
597cdf0c1d5Smjnelson        self.activecache = {}
598cdf0c1d5Smjnelson
599cdf0c1d5Smjnelson    def parent(self, spec=None):
6000df7087fSRichard Lowe        '''Return the canonical workspace parent, either SPEC (which
6010df7087fSRichard Lowe        will be expanded) if provided or the default parent
6020df7087fSRichard Lowe        otherwise.'''
603cdf0c1d5Smjnelson
6040df7087fSRichard Lowe        if spec:
6050df7087fSRichard Lowe            return self.ui.expandpath(spec)
606cdf0c1d5Smjnelson
6070df7087fSRichard Lowe        p = self.ui.expandpath('default')
6080df7087fSRichard Lowe        if p == 'default':
6090df7087fSRichard Lowe            return None
6100df7087fSRichard Lowe        else:
6110df7087fSRichard Lowe            return p
612cdf0c1d5Smjnelson
6130df7087fSRichard Lowe    def _localtip(self, outgoing, wctx):
6140df7087fSRichard Lowe        '''Return the most representative changeset to act as the
6150df7087fSRichard Lowe        localtip.
616cdf0c1d5Smjnelson
6170df7087fSRichard Lowe        If the working directory is modified (has file changes, is a
6180df7087fSRichard Lowe        merge, or has switched branches), this will be a workingctx.
619cdf0c1d5Smjnelson
6200df7087fSRichard Lowe        If the working directory is unmodified, this will be the most
6210df7087fSRichard Lowe        recent (highest revision number) local (outgoing) head on the
6220df7087fSRichard Lowe        current branch, if no heads are determined to be outgoing, it
6230df7087fSRichard Lowe        will be the most recent head on the current branch.
6240df7087fSRichard Lowe        '''
625cdf0c1d5Smjnelson
626cdf0c1d5Smjnelson        if (wctx.files() or len(wctx.parents()) > 1 or
627cdf0c1d5Smjnelson            wctx.branch() != wctx.parents()[0].branch()):
6280df7087fSRichard Lowe            return wctx
629cdf0c1d5Smjnelson
6300df7087fSRichard Lowe        heads = self.repo.heads(start=wctx.parents()[0].node())
6310df7087fSRichard Lowe        headctxs = [self.repo.changectx(n) for n in heads]
6320df7087fSRichard Lowe        localctxs = [c for c in headctxs if c.node() in outgoing]
633cdf0c1d5Smjnelson
6340df7087fSRichard Lowe        ltip = sorted(localctxs or headctxs, key=lambda x: x.rev())[-1]
635cdf0c1d5Smjnelson
6360df7087fSRichard Lowe        if len(heads) > 1:
6370df7087fSRichard Lowe            self.ui.warn('The current branch has more than one head, '
6380df7087fSRichard Lowe                         'using %s\n' % ltip.rev())
6390df7087fSRichard Lowe
6400df7087fSRichard Lowe        return ltip
6410df7087fSRichard Lowe
642605a716eSRichard Lowe    def parenttip(self, heads, outgoing):
6430df7087fSRichard Lowe        '''Return the highest-numbered, non-outgoing changeset that is
6440df7087fSRichard Lowe        an ancestor of a changeset in heads.
6450df7087fSRichard Lowe
646605a716eSRichard Lowe        This returns the most recent changeset on a given branch that
647605a716eSRichard Lowe        is shared between a parent and child workspace, in effect the
648605a716eSRichard Lowe        common ancestor of the chosen local tip and the parent
649605a716eSRichard Lowe        workspace.
6500df7087fSRichard Lowe        '''
651cdf0c1d5Smjnelson
652cdf0c1d5Smjnelson        def tipmost_shared(head, outnodes):
65387039217SRichard Lowe            '''Return the changeset on the same branch as head that is
65487039217SRichard Lowe            not in outnodes and is closest to the tip.
655cdf0c1d5Smjnelson
65687039217SRichard Lowe            Walk outgoing changesets from head to the bottom of the
65787039217SRichard Lowe            workspace (revision 0) and return the the first changeset
65887039217SRichard Lowe            we see that is not in outnodes.
659cdf0c1d5Smjnelson
66087039217SRichard Lowe            If none is found (all revisions >= 0 are outgoing), the
6610df7087fSRichard Lowe            only possible parenttip is the null node (node.nullid)
6620df7087fSRichard Lowe            which is returned explicitly.
66387039217SRichard Lowe            '''
66487039217SRichard Lowe            for ctx in self._walkctxs(head, self.repo.changectx(0),
66587039217SRichard Lowe                                      follow=True,
66687039217SRichard Lowe                                      pick=lambda c: c.node() not in outnodes):
66787039217SRichard Lowe                return ctx
6680df7087fSRichard Lowe
66987039217SRichard Lowe            return self.repo.changectx(node.nullid)
670cdf0c1d5Smjnelson
6710df7087fSRichard Lowe        nodes = set(outgoing)
6720df7087fSRichard Lowe        ptips = map(lambda x: tipmost_shared(x, nodes), heads)
67387039217SRichard Lowe        return sorted(ptips, key=lambda x: x.rev(), reverse=True)[0]
674cdf0c1d5Smjnelson
675605a716eSRichard Lowe    def status(self, base='.', head=None, files=None):
676cdf0c1d5Smjnelson        '''Translate from the hg 6-tuple status format to a hash keyed
677cdf0c1d5Smjnelson        on change-type'''
678c959a081SRichard Lowe
679cdf0c1d5Smjnelson        states = ['modified', 'added', 'removed', 'deleted', 'unknown',
680cdf0c1d5Smjnelson                  'ignored']
6812b5878deSRich Lowe
682605a716eSRichard Lowe        match = self.matcher(files=files)
683605a716eSRichard Lowe        chngs = self.repo.status(base, head, match=match)
684605a716eSRichard Lowe
685605a716eSRichard Lowe        ret = {}
686605a716eSRichard Lowe        for paths, change in zip(chngs, states):
687605a716eSRichard Lowe            ret.update((f, change) for f in paths)
688605a716eSRichard Lowe        return ret
689cdf0c1d5Smjnelson
690cdf0c1d5Smjnelson    def findoutgoing(self, parent):
691c959a081SRichard Lowe        '''Return the base set of outgoing nodes.
692c959a081SRichard Lowe
693c959a081SRichard Lowe        A caching wrapper around mercurial.localrepo.findoutgoing().
694c959a081SRichard Lowe        Complains (to the user), if the parent workspace is
695c959a081SRichard Lowe        non-existent or inaccessible'''
696c959a081SRichard Lowe
697cdf0c1d5Smjnelson        self.ui.pushbuffer()
698cdf0c1d5Smjnelson        try:
699c959a081SRichard Lowe            try:
700c959a081SRichard Lowe                ui = self.ui
701c959a081SRichard Lowe                if hasattr(cmdutil, 'remoteui'):
702c959a081SRichard Lowe                    ui = cmdutil.remoteui(ui, {})
703c959a081SRichard Lowe                pws = hg.repository(ui, parent)
70487039217SRichard Lowe                if Version.at_least("1.6"):
70587039217SRichard Lowe                    return discovery.findoutgoing(self.repo, pws)
70687039217SRichard Lowe                else:
707c959a081SRichard Lowe                    return self.repo.findoutgoing(pws)
70887039217SRichard Lowe            except error.RepoError:
709c959a081SRichard Lowe                self.ui.warn("Warning: Parent workspace '%s' is not "
710c959a081SRichard Lowe                             "accessible\n"
711c959a081SRichard Lowe                             "active list will be incomplete\n\n" % parent)
712c959a081SRichard Lowe                return []
713c959a081SRichard Lowe        finally:
714cdf0c1d5Smjnelson            self.ui.popbuffer()
715c959a081SRichard Lowe    findoutgoing = util.cachefunc(findoutgoing)
716cdf0c1d5Smjnelson
717cdf0c1d5Smjnelson    def modified(self):
718cdf0c1d5Smjnelson        '''Return a list of files modified in the workspace'''
719605a716eSRichard Lowe
7202b5878deSRich Lowe        wctx = self.workingctx()
721cdf0c1d5Smjnelson        return sorted(wctx.files() + wctx.deleted()) or None
722cdf0c1d5Smjnelson
723cdf0c1d5Smjnelson    def merged(self):
724cdf0c1d5Smjnelson        '''Return boolean indicating whether the workspace has an uncommitted
725cdf0c1d5Smjnelson        merge'''
726605a716eSRichard Lowe
7272b5878deSRich Lowe        wctx = self.workingctx()
728cdf0c1d5Smjnelson        return len(wctx.parents()) > 1
729cdf0c1d5Smjnelson
730cdf0c1d5Smjnelson    def branched(self):
731cdf0c1d5Smjnelson        '''Return boolean indicating whether the workspace has an
732cdf0c1d5Smjnelson        uncommitted named branch'''
733cdf0c1d5Smjnelson
7342b5878deSRich Lowe        wctx = self.workingctx()
735cdf0c1d5Smjnelson        return wctx.branch() != wctx.parents()[0].branch()
736cdf0c1d5Smjnelson
737605a716eSRichard Lowe    def active(self, parent=None, thorough=False):
738cdf0c1d5Smjnelson        '''Return an ActiveList describing changes between workspace
739cdf0c1d5Smjnelson        and parent workspace (including uncommitted changes).
740605a716eSRichard Lowe        If the workspace has no parent, ActiveList will still describe any
741605a716eSRichard Lowe        uncommitted changes.
742605a716eSRichard Lowe
743605a716eSRichard Lowe        If thorough is True use neither the WorkList nor any cached
744605a716eSRichard Lowe        results (though the result of this call will be cached for
745605a716eSRichard Lowe        future, non-thorough, calls).'''
746cdf0c1d5Smjnelson
747cdf0c1d5Smjnelson        parent = self.parent(parent)
748605a716eSRichard Lowe
749605a716eSRichard Lowe        #
750605a716eSRichard Lowe        # Use the cached copy if we can (we have one, and weren't
751605a716eSRichard Lowe        # asked to be thorough)
752605a716eSRichard Lowe        #
753605a716eSRichard Lowe        if not thorough and parent in self.activecache:
754cdf0c1d5Smjnelson            return self.activecache[parent]
755cdf0c1d5Smjnelson
756605a716eSRichard Lowe        #
757605a716eSRichard Lowe        # outbases: The set of outgoing nodes with no outgoing ancestors
758605a716eSRichard Lowe        # outnodes: The full set of outgoing nodes
759605a716eSRichard Lowe        #
760cdf0c1d5Smjnelson        if parent:
761605a716eSRichard Lowe            outbases = self.findoutgoing(parent)
762605a716eSRichard Lowe            outnodes = self.repo.changelog.nodesbetween(outbases)[0]
763605a716eSRichard Lowe        else:               # No parent, no outgoing nodes
764605a716eSRichard Lowe            outbases = []
7650df7087fSRichard Lowe            outnodes = []
766cdf0c1d5Smjnelson
767605a716eSRichard Lowe        wctx = self.workingctx(worklist=not thorough)
768605a716eSRichard Lowe        localtip = self._localtip(outnodes, wctx)
769cdf0c1d5Smjnelson
7700df7087fSRichard Lowe        if localtip.rev() is None:
7710df7087fSRichard Lowe            heads = localtip.parents()
772cdf0c1d5Smjnelson        else:
7730df7087fSRichard Lowe            heads = [localtip]
7740df7087fSRichard Lowe
775605a716eSRichard Lowe        parenttip = self.parenttip(heads, outnodes)
776605a716eSRichard Lowe
777605a716eSRichard Lowe        #
778605a716eSRichard Lowe        # If we couldn't find a parenttip, the two repositories must
779605a716eSRichard Lowe        # be unrelated (Hg catches most of this, but this case is
780605a716eSRichard Lowe        # valid for it but invalid for us)
781605a716eSRichard Lowe        #
782605a716eSRichard Lowe        if parenttip == None:
783605a716eSRichard Lowe            raise util.Abort('repository is unrelated')
784605a716eSRichard Lowe
785605a716eSRichard Lowe        headnodes = [h.node() for h in heads]
786cdf0c1d5Smjnelson        ctxs = [self.repo.changectx(n) for n in
787605a716eSRichard Lowe                self.repo.changelog.nodesbetween(outbases, headnodes)[0]]
788cdf0c1d5Smjnelson
7890df7087fSRichard Lowe        if localtip.rev() is None:
7900df7087fSRichard Lowe            ctxs.append(localtip)
7910df7087fSRichard Lowe
792605a716eSRichard Lowe        act = ActiveList(self, parenttip, ctxs)
793cdf0c1d5Smjnelson        self.activecache[parent] = act
794605a716eSRichard Lowe
795cdf0c1d5Smjnelson        return act
796cdf0c1d5Smjnelson
797cdf0c1d5Smjnelson    def squishdeltas(self, active, message, user=None):
798c959a081SRichard Lowe        '''Create a single conglomerate changeset based on a given
799c959a081SRichard Lowe        active list.  Removes the original changesets comprising the
800c959a081SRichard Lowe        given active list, and any tags pointing to them.
801cdf0c1d5Smjnelson
802c959a081SRichard Lowe        Operation:
803cdf0c1d5Smjnelson
804c959a081SRichard Lowe          - Commit an activectx object representing the specified
805c959a081SRichard Lowe            active list,
806c959a081SRichard Lowe
807c959a081SRichard Lowe          - Remove any local tags pointing to changesets in the
808c959a081SRichard Lowe            specified active list.
809c959a081SRichard Lowe
810c959a081SRichard Lowe          - Remove the changesets comprising the specified active
811c959a081SRichard Lowe            list.
812c959a081SRichard Lowe
813c959a081SRichard Lowe          - Remove any metadata that may refer to changesets that were
814c959a081SRichard Lowe            removed.
815c959a081SRichard Lowe
816c959a081SRichard Lowe        Calling code is expected to hold both the working copy lock
817c959a081SRichard Lowe        and repository lock of the destination workspace
818c959a081SRichard Lowe        '''
819c959a081SRichard Lowe
820c959a081SRichard Lowe        def strip_local_tags(active):
821c959a081SRichard Lowe            '''Remove any local tags referring to the specified nodes.'''
822cdf0c1d5Smjnelson
823cdf0c1d5Smjnelson            if os.path.exists(self.repo.join('localtags')):
824c959a081SRichard Lowe                fh = None
825c959a081SRichard Lowe                try:
826c959a081SRichard Lowe                    fh = self.repo.opener('localtags')
827c959a081SRichard Lowe                    tags = active.prune_tags(fh)
828c959a081SRichard Lowe                    fh.close()
829c959a081SRichard Lowe
830cdf0c1d5Smjnelson                    fh = self.repo.opener('localtags', 'w', atomictemp=True)
831cdf0c1d5Smjnelson                    fh.writelines(tags)
832cdf0c1d5Smjnelson                    fh.rename()
833c959a081SRichard Lowe                finally:
834c959a081SRichard Lowe                    if fh and not fh.closed:
835c959a081SRichard Lowe                        fh.close()
836cdf0c1d5Smjnelson
837cdf0c1d5Smjnelson        if active.files():
838c959a081SRichard Lowe            for entry in active:
839c959a081SRichard Lowe                #
840c959a081SRichard Lowe                # Work around Mercurial issue #1666, if the source
841c959a081SRichard Lowe                # file of a rename exists in the working copy
842c959a081SRichard Lowe                # Mercurial will complain, and remove the file.
843c959a081SRichard Lowe                #
844c959a081SRichard Lowe                # We preemptively remove the file to avoid the
845c959a081SRichard Lowe                # complaint (the user was asked about this in
846c959a081SRichard Lowe                # cdm_recommit)
847c959a081SRichard Lowe                #
848c959a081SRichard Lowe                if entry.is_renamed():
849c959a081SRichard Lowe                    path = self.repo.wjoin(entry.parentname)
850c959a081SRichard Lowe                    if os.path.exists(path):
851c959a081SRichard Lowe                        os.unlink(path)
852c959a081SRichard Lowe
853c959a081SRichard Lowe            self.repo.commitctx(active.context(message, user))
854c959a081SRichard Lowe            wsstate = "recommitted"
855c959a081SRichard Lowe            destination = self.repo.changelog.tip()
856cdf0c1d5Smjnelson        else:
857cdf0c1d5Smjnelson            #
858cdf0c1d5Smjnelson            # If all we're doing is stripping the old nodes, we want to
859cdf0c1d5Smjnelson            # update the working copy such that we're not at a revision
860cdf0c1d5Smjnelson            # that's about to go away.
861cdf0c1d5Smjnelson            #
862c959a081SRichard Lowe            wsstate = "tip"
863c959a081SRichard Lowe            destination = active.parenttip.node()
864c959a081SRichard Lowe
865c959a081SRichard Lowe        self.clean(destination)
866c959a081SRichard Lowe
867c959a081SRichard Lowe        #
868c959a081SRichard Lowe        # Tags were elided by the activectx object.  Local tags,
869c959a081SRichard Lowe        # however, must be removed manually.
870c959a081SRichard Lowe        #
871c959a081SRichard Lowe        try:
872c959a081SRichard Lowe            strip_local_tags(active)
873c959a081SRichard Lowe        except EnvironmentError, e:
874c959a081SRichard Lowe            raise util.Abort('Could not recommit tags: %s\n' % e)
875cdf0c1d5Smjnelson
876cdf0c1d5Smjnelson        # Silence all the strip and update fun
877cdf0c1d5Smjnelson        self.ui.pushbuffer()
878cdf0c1d5Smjnelson
879cdf0c1d5Smjnelson        #
880605a716eSRichard Lowe        # Remove the previous child-local changes by stripping the
881605a716eSRichard Lowe        # nodes that form the base of the ActiveList (removing their
882605a716eSRichard Lowe        # children in the process).
883cdf0c1d5Smjnelson        #
884cdf0c1d5Smjnelson        try:
885cdf0c1d5Smjnelson            try:
886c7f512e4Sjmcp                for base in active.bases():
887c959a081SRichard Lowe                    #
888c959a081SRichard Lowe                    # Any cached information about the repository is
889c959a081SRichard Lowe                    # likely to be invalid during the strip.  The
890c959a081SRichard Lowe                    # caching of branch tags is especially
891c959a081SRichard Lowe                    # problematic.
892c959a081SRichard Lowe                    #
893c959a081SRichard Lowe                    self.repo.invalidate()
894c7f512e4Sjmcp                    repair.strip(self.ui, self.repo, base.node(), backup=False)
895cdf0c1d5Smjnelson            except:
896cdf0c1d5Smjnelson                #
897cdf0c1d5Smjnelson                # If this fails, it may leave us in a surprising place in
898cdf0c1d5Smjnelson                # the history.
899cdf0c1d5Smjnelson                #
900cdf0c1d5Smjnelson                # We want to warn the user that something went wrong,
901cdf0c1d5Smjnelson                # and what will happen next, re-raise the exception, and
902cdf0c1d5Smjnelson                # bring the working copy back into a consistent state
903cdf0c1d5Smjnelson                # (which the finally block will do)
904cdf0c1d5Smjnelson                #
905cdf0c1d5Smjnelson                self.ui.warn("stripping failed, your workspace will have "
906cdf0c1d5Smjnelson                             "superfluous heads.\n"
907cdf0c1d5Smjnelson                             "your workspace has been updated to the "
908c959a081SRichard Lowe                             "%s changeset.\n" % wsstate)
909cdf0c1d5Smjnelson                raise               # Re-raise the exception
910cdf0c1d5Smjnelson        finally:
911c959a081SRichard Lowe            self.clean()
912c959a081SRichard Lowe            self.repo.dirstate.write() # Flush the dirstate
913c959a081SRichard Lowe            self.repo.invalidate()     # Invalidate caches
914c959a081SRichard Lowe
915cdf0c1d5Smjnelson            #
916cdf0c1d5Smjnelson            # We need to remove Hg's undo information (used for rollback),
917cdf0c1d5Smjnelson            # since it refers to data that will probably not exist after
918cdf0c1d5Smjnelson            # the strip.
919cdf0c1d5Smjnelson            #
920cdf0c1d5Smjnelson            if os.path.exists(self.repo.sjoin('undo')):
921cdf0c1d5Smjnelson                try:
922cdf0c1d5Smjnelson                    os.unlink(self.repo.sjoin('undo'))
923cdf0c1d5Smjnelson                except EnvironmentError, e:
924cdf0c1d5Smjnelson                    raise util.Abort('failed to remove undo data: %s\n' % e)
925cdf0c1d5Smjnelson
926cdf0c1d5Smjnelson            self.ui.popbuffer()
927cdf0c1d5Smjnelson
928cdf0c1d5Smjnelson    def filepath(self, path):
929cdf0c1d5Smjnelson        'Return the full path to a workspace file.'
930605a716eSRichard Lowe
931cdf0c1d5Smjnelson        return self.repo.pathto(path)
932cdf0c1d5Smjnelson
933cdf0c1d5Smjnelson    def clean(self, rev=None):
934cdf0c1d5Smjnelson        '''Bring workspace up to REV (or tip) forcefully (discarding in
935cdf0c1d5Smjnelson        progress changes)'''
9362b5878deSRich Lowe
937cdf0c1d5Smjnelson        if rev != None:
938cdf0c1d5Smjnelson            rev = self.repo.lookup(rev)
939cdf0c1d5Smjnelson        else:
940cdf0c1d5Smjnelson            rev = self.repo.changelog.tip()
941cdf0c1d5Smjnelson
942cdf0c1d5Smjnelson        hg.clean(self.repo, rev, show_stats=False)
943cdf0c1d5Smjnelson
944cdf0c1d5Smjnelson    def mq_applied(self):
945cdf0c1d5Smjnelson        '''True if the workspace has Mq patches applied'''
946605a716eSRichard Lowe
947cdf0c1d5Smjnelson        q = mq.queue(self.ui, self.repo.join(''))
948cdf0c1d5Smjnelson        return q.applied
9492b5878deSRich Lowe
950605a716eSRichard Lowe    def workingctx(self, worklist=False):
951605a716eSRichard Lowe        '''Return a workingctx object representing the working copy.
952605a716eSRichard Lowe
953605a716eSRichard Lowe        If worklist is true, return a workingctx object created based
954605a716eSRichard Lowe        on the status of files in the workspace's worklist.'''
955605a716eSRichard Lowe
956605a716eSRichard Lowe        wl = WorkList(self)
957605a716eSRichard Lowe
958605a716eSRichard Lowe        if worklist and wl:
959605a716eSRichard Lowe            return context.workingctx(self.repo, changes=wl.status())
960605a716eSRichard Lowe        else:
9612b5878deSRich Lowe            return self.repo.changectx(None)
9622b5878deSRich Lowe
963605a716eSRichard Lowe    def matcher(self, pats=None, opts=None, files=None):
964605a716eSRichard Lowe        '''Return a match object suitable for Mercurial based on
965605a716eSRichard Lowe        specified criteria.
966605a716eSRichard Lowe
967605a716eSRichard Lowe        If files is specified it is a list of pathnames relative to
968605a716eSRichard Lowe        the repository root to be matched precisely.
969605a716eSRichard Lowe
970605a716eSRichard Lowe        If pats and/or opts are specified, these are as to
971605a716eSRichard Lowe        cmdutil.match'''
972605a716eSRichard Lowe
973605a716eSRichard Lowe        of_patterns = pats is not None or opts is not None
974605a716eSRichard Lowe        of_files = files is not None
975605a716eSRichard Lowe        opts = opts or {}       # must be a dict
976605a716eSRichard Lowe
977605a716eSRichard Lowe        assert not (of_patterns and of_files)
978605a716eSRichard Lowe
979605a716eSRichard Lowe        if of_patterns:
980605a716eSRichard Lowe            return cmdutil.match(self.repo, pats, opts)
981605a716eSRichard Lowe        elif of_files:
982605a716eSRichard Lowe            return cmdutil.matchfiles(self.repo, files)
983605a716eSRichard Lowe        else:
984605a716eSRichard Lowe            return cmdutil.matchall(self.repo)
985605a716eSRichard Lowe
9862b5878deSRich Lowe    def diff(self, node1=None, node2=None, match=None, opts=None):
987605a716eSRichard Lowe        '''Return the diff of changes between two changesets as a string'''
988605a716eSRichard Lowe
989605a716eSRichard Lowe        #
990605a716eSRichard Lowe        # Retain compatibility by only calling diffopts() if it
991605a716eSRichard Lowe        # obviously has not already been done.
992605a716eSRichard Lowe        #
993605a716eSRichard Lowe        if isinstance(opts, dict):
994605a716eSRichard Lowe            opts = patch.diffopts(self.ui, opts)
995605a716eSRichard Lowe
9962b5878deSRich Lowe        ret = cStringIO.StringIO()
9972b5878deSRich Lowe        for chunk in patch.diff(self.repo, node1, node2, match=match,
9982b5878deSRich Lowe                                opts=opts):
9992b5878deSRich Lowe            ret.write(chunk)
10002b5878deSRich Lowe
10012b5878deSRich Lowe        return ret.getvalue()
100287039217SRichard Lowe
100387039217SRichard Lowe    if Version.at_least("1.6"):
100487039217SRichard Lowe        def copy(self, src, dest):
100587039217SRichard Lowe            '''Copy a file from src to dest
100687039217SRichard Lowe            '''
100787039217SRichard Lowe
100887039217SRichard Lowe            self.workingctx().copy(src, dest)
100987039217SRichard Lowe    else:
101087039217SRichard Lowe        def copy(self, src, dest):
101187039217SRichard Lowe            '''Copy a file from src to dest
101287039217SRichard Lowe            '''
101387039217SRichard Lowe
101487039217SRichard Lowe            self.repo.copy(src, dest)
101587039217SRichard Lowe
101687039217SRichard Lowe
101787039217SRichard Lowe    if Version.at_least("1.4"):
101887039217SRichard Lowe
101987039217SRichard Lowe        def _walkctxs(self, base, head, follow=False, pick=None):
102087039217SRichard Lowe            '''Generate changectxs between BASE and HEAD.
102187039217SRichard Lowe
102287039217SRichard Lowe            Walk changesets between BASE and HEAD (in the order implied by
102387039217SRichard Lowe            their relation), following a given branch if FOLLOW is a true
102487039217SRichard Lowe            value, yielding changectxs where PICK (if specified) returns a
102587039217SRichard Lowe            true value.
102687039217SRichard Lowe
102787039217SRichard Lowe            PICK is a function of one argument, a changectx.'''
102887039217SRichard Lowe
102987039217SRichard Lowe            chosen = {}
103087039217SRichard Lowe
103187039217SRichard Lowe            def prep(ctx, fns):
103287039217SRichard Lowe                chosen[ctx.rev()] = not pick or pick(ctx)
103387039217SRichard Lowe
103487039217SRichard Lowe            opts = {'rev': ['%s:%s' % (base.rev(), head.rev())],
103587039217SRichard Lowe                    'follow': follow}
103687039217SRichard Lowe            matcher = cmdutil.matchall(self.repo)
103787039217SRichard Lowe
103887039217SRichard Lowe            for ctx in cmdutil.walkchangerevs(self.repo, matcher, opts, prep):
103987039217SRichard Lowe                if chosen[ctx.rev()]:
104087039217SRichard Lowe                    yield ctx
104187039217SRichard Lowe    else:
104287039217SRichard Lowe
104387039217SRichard Lowe        def _walkctxs(self, base, head, follow=False, pick=None):
104487039217SRichard Lowe            '''Generate changectxs between BASE and HEAD.
104587039217SRichard Lowe
104687039217SRichard Lowe            Walk changesets between BASE and HEAD (in the order implied by
104787039217SRichard Lowe            their relation), following a given branch if FOLLOW is a true
104887039217SRichard Lowe            value, yielding changectxs where PICK (if specified) returns a
104987039217SRichard Lowe            true value.
105087039217SRichard Lowe
105187039217SRichard Lowe            PICK is a function of one argument, a changectx.'''
105287039217SRichard Lowe
105387039217SRichard Lowe            opts = {'rev': ['%s:%s' % (base.rev(), head.rev())],
105487039217SRichard Lowe                    'follow': follow}
105587039217SRichard Lowe
105687039217SRichard Lowe            changectx = self.repo.changectx
105787039217SRichard Lowe            getcset = util.cachefunc(lambda r: changectx(r).changeset())
105887039217SRichard Lowe
105987039217SRichard Lowe            #
106087039217SRichard Lowe            # See the docstring of mercurial.cmdutil.walkchangerevs() for
106187039217SRichard Lowe            # the phased approach to the iterator returned.  The important
106287039217SRichard Lowe            # part to note is that the 'add' phase gathers nodes, which
106387039217SRichard Lowe            # the 'iter' phase then iterates through.
106487039217SRichard Lowe            #
106587039217SRichard Lowe            changeiter = cmdutil.walkchangerevs(self.ui, self.repo,
106687039217SRichard Lowe                                                [], getcset, opts)[0]
106787039217SRichard Lowe
106887039217SRichard Lowe            matched = {}
106987039217SRichard Lowe            for st, rev, fns in changeiter:
107087039217SRichard Lowe                if st == 'add':
107187039217SRichard Lowe                    ctx = changectx(rev)
107287039217SRichard Lowe                    if not pick or pick(ctx):
107387039217SRichard Lowe                        matched[rev] = ctx
107487039217SRichard Lowe                elif st == 'iter':
107587039217SRichard Lowe                    if rev in matched:
107687039217SRichard Lowe                        yield matched[rev]
1077