xref: /onnv-gate/usr/src/tools/onbld/Scm/WorkSpace.py (revision 10399:ea7df0114f8c)
17078Smjnelson#
27078Smjnelson#  This program is free software; you can redistribute it and/or modify
37078Smjnelson#  it under the terms of the GNU General Public License version 2
47078Smjnelson#  as published by the Free Software Foundation.
57078Smjnelson#
67078Smjnelson#  This program is distributed in the hope that it will be useful,
77078Smjnelson#  but WITHOUT ANY WARRANTY; without even the implied warranty of
87078Smjnelson#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
97078Smjnelson#  GNU General Public License for more details.
107078Smjnelson#
117078Smjnelson#  You should have received a copy of the GNU General Public License
127078Smjnelson#  along with this program; if not, write to the Free Software
137078Smjnelson#  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
147078Smjnelson#
157078Smjnelson
167078Smjnelson#
179006Srichlowe@richlowe.net# Copyright 2009 Sun Microsystems, Inc.  All rights reserved.
187078Smjnelson# Use is subject to license terms.
197078Smjnelson#
207078Smjnelson
217078Smjnelson#
227078Smjnelson# Theory:
237078Smjnelson#
247078Smjnelson# Workspaces have a non-binding parent/child relationship.
257078Smjnelson# All important operations apply to the changes between the two.
267078Smjnelson#
277078Smjnelson# However, for the sake of remote operation, the 'parent' of a
287078Smjnelson# workspace is not seen as a literal entity, instead the figurative
297078Smjnelson# parent contains the last changeset common to both parent and child,
307078Smjnelson# as such the 'parent tip' is actually nothing of the sort, but instead a
317078Smjnelson# convenient imitation.
327078Smjnelson#
337078Smjnelson# Any change made to a workspace is a change to a file therein, such
347078Smjnelson# changes can be represented briefly as whether the file was
357078Smjnelson# modified/added/removed as compared to the parent workspace, whether
367078Smjnelson# the file has a different name in the parent and if so, whether it
377078Smjnelson# was renamed or merely copied.  Each changed file has an
387078Smjnelson# associated ActiveEntry.
397078Smjnelson#
407078Smjnelson# The ActiveList being a list ActiveEntrys can thus present the entire
417078Smjnelson# change in workspace state between a parent and its child, and is the
427078Smjnelson# important bit here (in that if it is incorrect, everything else will
437078Smjnelson# be as incorrect, or more)
447078Smjnelson#
457078Smjnelson
467078Smjnelsonimport cStringIO
477078Smjnelsonimport os
48*10399Srichlowe@richlowe.netfrom mercurial import cmdutil, context, hg, node, patch, repair, util
497078Smjnelsonfrom hgext import mq
507078Smjnelson
519006Srichlowe@richlowe.netfrom onbld.Scm import Version
529006Srichlowe@richlowe.net
53*10399Srichlowe@richlowe.net#
54*10399Srichlowe@richlowe.net# Mercurial >= 1.2 has its exception types in a mercurial.error
55*10399Srichlowe@richlowe.net# module, prior versions had them in their associated modules.
56*10399Srichlowe@richlowe.net#
57*10399Srichlowe@richlowe.netif Version.at_least("1.2"):
58*10399Srichlowe@richlowe.net    from mercurial import error
59*10399Srichlowe@richlowe.net    HgRepoError = error.RepoError
60*10399Srichlowe@richlowe.net    HgLookupError = error.LookupError
61*10399Srichlowe@richlowe.netelse:
62*10399Srichlowe@richlowe.net    from mercurial import repo, revlog
63*10399Srichlowe@richlowe.net    HgRepoError = repo.RepoError
64*10399Srichlowe@richlowe.net    HgLookupError = revlog.LookupError
65*10399Srichlowe@richlowe.net
667078Smjnelson
677078Smjnelsonclass ActiveEntry(object):
687078Smjnelson    '''Representation of the changes made to a single file.
697078Smjnelson
707078Smjnelson    MODIFIED   - Contents changed, but no other changes were made
717078Smjnelson    ADDED      - File is newly created
727078Smjnelson    REMOVED    - File is being removed
737078Smjnelson
747078Smjnelson    Copies are represented by an Entry whose .parentname is non-nil
757078Smjnelson
767078Smjnelson    Truly copied files have non-nil .parentname and .renamed = False
777078Smjnelson    Renames have non-nil .parentname and .renamed = True
787078Smjnelson
797078Smjnelson    Do not access any of this information directly, do so via the
807078Smjnelson
817078Smjnelson    .is_<change>() methods.'''
827078Smjnelson
837078Smjnelson    MODIFIED = 1
847078Smjnelson    ADDED = 2
857078Smjnelson    REMOVED = 3
867078Smjnelson
877078Smjnelson    def __init__(self, name):
887078Smjnelson        self.name = name
897078Smjnelson        self.change = None
907078Smjnelson        self.parentname = None
917078Smjnelson        # As opposed to copied (or neither)
927078Smjnelson        self.renamed = False
937078Smjnelson        self.comments = []
947078Smjnelson
957078Smjnelson    #
967078Smjnelson    # ActiveEntrys sort by the name of the file they represent.
977078Smjnelson    #
987078Smjnelson    def __cmp__(self, other):
997078Smjnelson        return cmp(self.name, other.name)
1007078Smjnelson
1017078Smjnelson    def is_added(self):
1027078Smjnelson        return self.change == self.ADDED
1037078Smjnelson
1047078Smjnelson    def is_modified(self):
1057078Smjnelson        return self.change == self.MODIFIED
1067078Smjnelson
1077078Smjnelson    def is_removed(self):
1087078Smjnelson        return self.change == self.REMOVED
1097078Smjnelson
1107078Smjnelson    def is_renamed(self):
1117078Smjnelson        return self.parentname and self.renamed
1127078Smjnelson
1137078Smjnelson    def is_copied(self):
1147078Smjnelson        return self.parentname and not self.renamed
1157078Smjnelson
1167078Smjnelson
1177078Smjnelsonclass ActiveList(object):
1187078Smjnelson    '''Complete representation of workspace change.
1197078Smjnelson
1207078Smjnelson    In practice, a container for ActiveEntrys, and methods to build them,
1217078Smjnelson    update them, and deal with them en masse.'''
1227078Smjnelson
1237078Smjnelson    def __init__(self, ws, parenttip, revs=None):
1247078Smjnelson        self._active = {}
1257078Smjnelson        self.ws = ws
1267078Smjnelson
1277078Smjnelson        self.revs = revs
1287078Smjnelson
1297078Smjnelson        self.base = None
1307078Smjnelson        self.parenttip = parenttip
1317078Smjnelson
1327078Smjnelson        #
1337078Smjnelson        # If we couldn't find a parenttip, the two repositories must
1347078Smjnelson        # be unrelated (Hg catches most of this, but this case is valid for it
1357078Smjnelson        # but invalid for us)
1367078Smjnelson        #
1377078Smjnelson        if self.parenttip == None:
1387078Smjnelson            raise util.Abort('repository is unrelated')
1397078Smjnelson        self.localtip = None
1407078Smjnelson
1417078Smjnelson        if revs:
1427078Smjnelson            self.base = revs[0]
1437078Smjnelson            self.localtip = revs[-1]
1447078Smjnelson
1457078Smjnelson        self._comments = []
1467078Smjnelson
1477078Smjnelson        self._build(revs)
1487078Smjnelson
1497078Smjnelson    def _build(self, revs):
1507078Smjnelson        if not revs:
1517078Smjnelson            return
1527078Smjnelson
1537078Smjnelson        status = self.ws.status(self.parenttip.node(), self.localtip.node())
1547078Smjnelson
1557078Smjnelson        files = []
1567078Smjnelson        for ctype in status.values():
1577078Smjnelson            files.extend(ctype)
1587078Smjnelson
1597078Smjnelson        #
1607078Smjnelson        # When a file is renamed, two operations actually occur.
1617078Smjnelson        # A file copy from source to dest and a removal of source.
1627078Smjnelson        #
1637078Smjnelson        # These are represented as two distinct entries in the
1647078Smjnelson        # changectx and status (one on the dest file for the
1657078Smjnelson        # copy, one on the source file for the remove).
1667078Smjnelson        #
1677078Smjnelson        # Since these are unconnected in both the context and
1687078Smjnelson        # status we can only make the association by explicitly
1697078Smjnelson        # looking for it.
1707078Smjnelson        #
1717078Smjnelson        # We deal with this thusly:
1727078Smjnelson        #
1737078Smjnelson        # We maintain a dict dest -> source of all copies
1747078Smjnelson        # (updating dest as appropriate, but leaving source alone).
1757078Smjnelson        #
1767078Smjnelson        # After all other processing, we mark as renamed any pair
1777078Smjnelson        # where source is on the removed list.
1787078Smjnelson        #
1797078Smjnelson        copies = {}
1807078Smjnelson
1817078Smjnelson        #
1827078Smjnelson        # Walk revs looking for renames and adding files that
1837078Smjnelson        # are in both change context and status to the active
1847078Smjnelson        # list.
1857078Smjnelson        #
1867078Smjnelson        for ctx in revs:
1877078Smjnelson            desc = ctx.description().splitlines()
1887078Smjnelson
1897078Smjnelson            self._comments.extend(desc)
1907078Smjnelson
1917078Smjnelson            for fname in ctx.files():
1927078Smjnelson                #
1937078Smjnelson                # We store comments per-entry as well, for the sake of
1947078Smjnelson                # webrev and similar.  We store twice to avoid the problems
1957078Smjnelson                # of uniquifying comments for the general list (and possibly
1967078Smjnelson                # destroying multi-line entities in the process).
1977078Smjnelson                #
1987078Smjnelson                if fname not in self:
1997078Smjnelson                    self._addentry(fname)
2007078Smjnelson                self[fname].comments.extend(desc)
2017078Smjnelson
2027078Smjnelson                try:
2037078Smjnelson                    fctx = ctx.filectx(fname)
204*10399Srichlowe@richlowe.net                except HgLookupError:
2057078Smjnelson                    continue
2067078Smjnelson
2077078Smjnelson                #
2087078Smjnelson                # NB: .renamed() is a misnomer, this actually checks
2097078Smjnelson                #     for copies.
2107078Smjnelson                #
2117078Smjnelson                rn = fctx.renamed()
2127078Smjnelson                if rn:
2137078Smjnelson                    #
2147078Smjnelson                    # If the source file is a known copy we know its
2157078Smjnelson                    # ancestry leads us to the parent.
2167078Smjnelson                    # Otherwise make sure the source file is known to
2177078Smjnelson                    # be in the parent, we need not care otherwise.
2187078Smjnelson                    #
2197078Smjnelson                    # We detect cycles at a later point.  There is no
2207078Smjnelson                    # reason to continuously handle them.
2217078Smjnelson                    #
2227078Smjnelson                    if rn[0] in copies:
2237078Smjnelson                        copies[fname] = copies[rn[0]]
2247078Smjnelson                    elif rn[0] in self.parenttip.manifest():
2257078Smjnelson                        copies[fname] = rn[0]
2267078Smjnelson
2277078Smjnelson        #
2287078Smjnelson        # Walk the copy list marking as copied any non-cyclic pair
2297078Smjnelson        # where the destination file is still present in the local
2307078Smjnelson        # tip (to avoid ephemeral changes)
2317078Smjnelson        #
2327078Smjnelson        # Where source is removed, mark as renamed, and remove the
2337078Smjnelson        # AL entry for the source file
2347078Smjnelson        #
2357078Smjnelson        for fname, oldname in copies.iteritems():
2367078Smjnelson            if fname == oldname or fname not in self.localtip.manifest():
2377078Smjnelson                continue
2387078Smjnelson
2397078Smjnelson            self[fname].parentname = oldname
2407078Smjnelson
2417078Smjnelson            if oldname in status['removed']:
2427078Smjnelson                self[fname].renamed = True
2437078Smjnelson                if oldname in self:
2447078Smjnelson                    del self[oldname]
2457078Smjnelson
2467078Smjnelson        #
2477078Smjnelson        # Walk the active list setting the change type for each active
2487078Smjnelson        # file.
2497078Smjnelson        #
2507078Smjnelson        # In the case of modified files that are not renames or
2517078Smjnelson        # copies, we do a content comparison, and drop entries that
2527078Smjnelson        # are not actually modified.
2537078Smjnelson        #
2547078Smjnelson        # We walk a copy of the AL such that we can drop entries
2557078Smjnelson        # within the loop.
2567078Smjnelson        #
2577078Smjnelson        for entry in self._active.values():
2587078Smjnelson            if entry.name not in files:
2597078Smjnelson                del self[entry.name]
2607078Smjnelson                continue
2617078Smjnelson
2627078Smjnelson            if entry.name in status['added']:
2637078Smjnelson                entry.change = ActiveEntry.ADDED
2647078Smjnelson            elif entry.name in status['removed']:
2657078Smjnelson                entry.change = ActiveEntry.REMOVED
2667078Smjnelson            elif entry.name in status['modified']:
2677078Smjnelson                entry.change = ActiveEntry.MODIFIED
2687078Smjnelson
2697078Smjnelson            #
2707078Smjnelson            # There are cases during a merge where a file will be in
2717078Smjnelson            # the status return as modified, but in reality be an
2727078Smjnelson            # addition (ie, not in the parenttip).
2737078Smjnelson            #
2747078Smjnelson            # We need to check whether the file is actually present
2757078Smjnelson            # in the parenttip, and set it as an add, if not.
2767078Smjnelson            #
2777078Smjnelson            if entry.name not in self.parenttip.manifest():
2787078Smjnelson                entry.change = ActiveEntry.ADDED
2797078Smjnelson            elif entry.is_modified() and not entry.parentname:
2807078Smjnelson                if not self.filecmp(entry):
2817078Smjnelson                    del self[entry.name]
2827078Smjnelson                    continue
2837078Smjnelson
2847078Smjnelson            assert entry.change
2857078Smjnelson
2867078Smjnelson    def __contains__(self, fname):
2877078Smjnelson        return fname in self._active
2887078Smjnelson
2897078Smjnelson    def __getitem__(self, key):
2907078Smjnelson        return self._active[key]
2917078Smjnelson
2927078Smjnelson    def __setitem__(self, key, value):
2937078Smjnelson        self._active[key] = value
2947078Smjnelson
2957078Smjnelson    def __delitem__(self, key):
2967078Smjnelson        del self._active[key]
2977078Smjnelson
2987078Smjnelson    def __iter__(self):
2997078Smjnelson        for entry in self._active.values():
3007078Smjnelson            yield entry
3017078Smjnelson
3027078Smjnelson    def _addentry(self, fname):
3037078Smjnelson        if fname not in self:
3047078Smjnelson            self[fname] = ActiveEntry(fname)
3057078Smjnelson
3067078Smjnelson    def files(self):
307*10399Srichlowe@richlowe.net        '''Return the list of pathnames of all files touched by this
308*10399Srichlowe@richlowe.net        ActiveList
3097078Smjnelson
310*10399Srichlowe@richlowe.net        Where files have been renamed, this will include both their
311*10399Srichlowe@richlowe.net        current name and the name which they had in the parent tip.
312*10399Srichlowe@richlowe.net        '''
313*10399Srichlowe@richlowe.net
314*10399Srichlowe@richlowe.net        ret = self._active.keys()
3157078Smjnelson        ret.extend([x.parentname for x in self
316*10399Srichlowe@richlowe.net                    if x.is_renamed() and x.parentname not in ret])
3177078Smjnelson        return ret
3187078Smjnelson
3197078Smjnelson    def comments(self):
3207078Smjnelson        return self._comments
3217078Smjnelson
3227078Smjnelson    #
3237078Smjnelson    # It's not uncommon for a child workspace to itself contain the
3247078Smjnelson    # merge of several other children, with initial branch points in
3257078Smjnelson    # the parent (possibly from the cset a project gate was created
3267078Smjnelson    # from, for instance).
3277078Smjnelson    #
3287078Smjnelson    # Immediately after recommit, this leaves us looking like this:
3297078Smjnelson    #
3307078Smjnelson    #     *   <- recommitted changeset (real tip)
3317078Smjnelson    #     |
3327078Smjnelson    #     | *  <- Local tip
3337078Smjnelson    #     |/|
3347078Smjnelson    #     * |  <- parent tip
3357078Smjnelson    #     | |\
3367078Smjnelson    #     | | |
3377078Smjnelson    #     | | |\
3387078Smjnelson    #     | | | |
3397078Smjnelson    #     | * | |  <- Base
3407078Smjnelson    #     |/_/__/
3417078Smjnelson    #
3427078Smjnelson    #     [left-most is parent, next is child, right two being
3437078Smjnelson    #     branches in child, intermediate merges parent->child
3447078Smjnelson    #     omitted]
3457078Smjnelson    #
3467078Smjnelson    # Obviously stripping base (the first child-specific delta on the
3477078Smjnelson    # main child workspace line) doesn't remove the vestigial branches
3487078Smjnelson    # from other workspaces (or in-workspace branches, or whatever)
3497078Smjnelson    #
3507078Smjnelson    # In reality, what we need to strip in a recommit is any
3517078Smjnelson    # child-specific branch descended from the parent (rather than
3527078Smjnelson    # another part of the child).  Note that this by its very nature
3537078Smjnelson    # includes the branch representing the 'main' child workspace.
3547078Smjnelson    #
3557078Smjnelson    # We calculate these by walking from base (which is guaranteed to
3567078Smjnelson    # be the oldest child-local cset) to localtip searching for
3577078Smjnelson    # changesets with only one parent cset, and where that cset is not
3587078Smjnelson    # part of the active list (and is therefore outgoing).
3597078Smjnelson    #
3607078Smjnelson    def bases(self):
3617078Smjnelson        '''Find the bases that in combination define the "old"
3627078Smjnelson        side of a recommitted set of changes, based on AL'''
3637078Smjnelson
3647078Smjnelson        get = util.cachefunc(lambda r: self.ws.repo.changectx(r).changeset())
3657078Smjnelson
3667078Smjnelson        # We don't rebuild the AL So the AL local tip is the old tip
3677078Smjnelson        revrange = "%s:%s" % (self.base.rev(), self.localtip.rev())
3687078Smjnelson
3697078Smjnelson        changeiter = cmdutil.walkchangerevs(self.ws.repo.ui, self.ws.repo,
3707078Smjnelson                                            [], get, {'rev': [revrange]})[0]
3717078Smjnelson
3727078Smjnelson        hold = []
3737078Smjnelson        ret = []
3747078Smjnelson        alrevs = [x.rev() for x in self.revs]
3757078Smjnelson        for st, rev, fns in changeiter:
3767078Smjnelson            n = self.ws.repo.changelog.node(rev)
3777078Smjnelson            if st == 'add':
3787078Smjnelson                if rev in alrevs:
3797078Smjnelson                    hold.append(n)
3807078Smjnelson            elif st == 'iter':
3817078Smjnelson                if n not in hold:
3827078Smjnelson                    continue
3837078Smjnelson
3847078Smjnelson                p = self.ws.repo.changelog.parents(n)
3857078Smjnelson                if p[1] != node.nullid:
3867078Smjnelson                    continue
3877078Smjnelson
3887078Smjnelson                if self.ws.repo.changectx(p[0]).rev() not in alrevs:
3897078Smjnelson                    ret.append(n)
3907078Smjnelson        return ret
3917078Smjnelson
3927078Smjnelson    def tags(self):
3937078Smjnelson        '''Find tags that refer to a changeset in the ActiveList,
394*10399Srichlowe@richlowe.net        returning a list of 3-tuples (tag, node, is_local) for each.
395*10399Srichlowe@richlowe.net
396*10399Srichlowe@richlowe.net        We return all instances of a tag that refer to such a node,
397*10399Srichlowe@richlowe.net        not just that which takes precedence.'''
3987078Smjnelson
399*10399Srichlowe@richlowe.net        def colliding_tags(iterable, nodes, local):
400*10399Srichlowe@richlowe.net            for nd, name in [line.rstrip().split(' ', 1) for line in iterable]:
401*10399Srichlowe@richlowe.net                if nd in nodes:
402*10399Srichlowe@richlowe.net                    yield (name, self.ws.repo.lookup(nd), local)
403*10399Srichlowe@richlowe.net
404*10399Srichlowe@richlowe.net        tags = []
405*10399Srichlowe@richlowe.net        nodes = set(node.hex(ctx.node()) for ctx in self.revs)
4067078Smjnelson
4077078Smjnelson        if os.path.exists(self.ws.repo.join('localtags')):
408*10399Srichlowe@richlowe.net            fh = self.ws.repo.opener('localtags')
409*10399Srichlowe@richlowe.net            tags.extend(colliding_tags(fh, nodes, True))
410*10399Srichlowe@richlowe.net            fh.close()
4117078Smjnelson
4127078Smjnelson        # We want to use the tags file from the localtip
413*10399Srichlowe@richlowe.net        if '.hgtags' in self.localtip:
414*10399Srichlowe@richlowe.net            data = self.localtip.filectx('.hgtags').data().splitlines()
415*10399Srichlowe@richlowe.net            tags.extend(colliding_tags(data, nodes, False))
4167078Smjnelson
4177078Smjnelson        return tags
4187078Smjnelson
419*10399Srichlowe@richlowe.net    def prune_tags(self, data):
420*10399Srichlowe@richlowe.net        '''Return a copy of data, which should correspond to the
421*10399Srichlowe@richlowe.net        contents of a Mercurial tags file, with any tags that refer to
422*10399Srichlowe@richlowe.net        changesets which are components of the ActiveList removed.'''
423*10399Srichlowe@richlowe.net
424*10399Srichlowe@richlowe.net        nodes = set(node.hex(ctx.node()) for ctx in self.revs)
425*10399Srichlowe@richlowe.net        return [t for t in data if t.split(' ', 1)[0] not in nodes]
426*10399Srichlowe@richlowe.net
4277078Smjnelson    def filecmp(self, entry):
4287078Smjnelson        '''Compare two revisions of two files
4297078Smjnelson
4307078Smjnelson        Return True if file changed, False otherwise.
4317078Smjnelson
4327078Smjnelson        The fast path compares file metadata, slow path is a
4337078Smjnelson        real comparison of file content.'''
4347078Smjnelson
4357078Smjnelson        parentfile = self.parenttip.filectx(entry.parentname or entry.name)
4367078Smjnelson        localfile = self.localtip.filectx(entry.name)
4377078Smjnelson
4387078Smjnelson        #
4397078Smjnelson        # NB: Keep these ordered such as to make every attempt
4407078Smjnelson        #     to short-circuit the more time consuming checks.
4417078Smjnelson        #
4427078Smjnelson        if parentfile.size() != localfile.size():
4437078Smjnelson            return True
4447078Smjnelson
445*10399Srichlowe@richlowe.net        if parentfile.flags() != localfile.flags():
4467078Smjnelson            return True
4477078Smjnelson
4487078Smjnelson        if parentfile.cmp(localfile.data()):
4497078Smjnelson            return True
4507078Smjnelson
451*10399Srichlowe@richlowe.net    def context(self, message, user):
452*10399Srichlowe@richlowe.net        '''Return a Mercurial context object representing the entire
453*10399Srichlowe@richlowe.net        ActiveList as one change.'''
454*10399Srichlowe@richlowe.net        return activectx(self, message, user)
455*10399Srichlowe@richlowe.net
456*10399Srichlowe@richlowe.net
457*10399Srichlowe@richlowe.netclass activectx(context.memctx):
458*10399Srichlowe@richlowe.net    '''Represent an ActiveList as a Mercurial context object.
459*10399Srichlowe@richlowe.net
460*10399Srichlowe@richlowe.net    Part of the  WorkSpace.squishdeltas implementation.'''
461*10399Srichlowe@richlowe.net
462*10399Srichlowe@richlowe.net    def __init__(self, active, message, user):
463*10399Srichlowe@richlowe.net        '''Build an activectx object.
464*10399Srichlowe@richlowe.net
465*10399Srichlowe@richlowe.net          active  - The ActiveList object used as the source for all data.
466*10399Srichlowe@richlowe.net          message - Changeset description
467*10399Srichlowe@richlowe.net          user    - Committing user'''
468*10399Srichlowe@richlowe.net
469*10399Srichlowe@richlowe.net        def filectxfn(repository, ctx, fname):
470*10399Srichlowe@richlowe.net            fctx = active.localtip.filectx(fname)
471*10399Srichlowe@richlowe.net            data = fctx.data()
472*10399Srichlowe@richlowe.net
473*10399Srichlowe@richlowe.net            #
474*10399Srichlowe@richlowe.net            # .hgtags is a special case, tags referring to active list
475*10399Srichlowe@richlowe.net            # component changesets should be elided.
476*10399Srichlowe@richlowe.net            #
477*10399Srichlowe@richlowe.net            if fname == '.hgtags':
478*10399Srichlowe@richlowe.net                data = '\n'.join(active.prune_tags(data.splitlines()))
479*10399Srichlowe@richlowe.net
480*10399Srichlowe@richlowe.net            return context.memfilectx(fname, data, 'l' in fctx.flags(),
481*10399Srichlowe@richlowe.net                                      'x' in fctx.flags(),
482*10399Srichlowe@richlowe.net                                      active[fname].parentname)
483*10399Srichlowe@richlowe.net
484*10399Srichlowe@richlowe.net        self.__active = active
485*10399Srichlowe@richlowe.net        parents = (active.parenttip.node(), node.nullid)
486*10399Srichlowe@richlowe.net        extra = {'branch': active.localtip.branch()}
487*10399Srichlowe@richlowe.net        context.memctx.__init__(self, active.ws.repo, parents, message,
488*10399Srichlowe@richlowe.net                                active.files(), filectxfn, user=user,
489*10399Srichlowe@richlowe.net                                extra=extra)
490*10399Srichlowe@richlowe.net
491*10399Srichlowe@richlowe.net    def modified(self):
492*10399Srichlowe@richlowe.net        return [entry.name for entry in self.__active if entry.is_modified()]
493*10399Srichlowe@richlowe.net
494*10399Srichlowe@richlowe.net    def added(self):
495*10399Srichlowe@richlowe.net        return [entry.name for entry in self.__active if entry.is_added()]
496*10399Srichlowe@richlowe.net
497*10399Srichlowe@richlowe.net    def removed(self):
498*10399Srichlowe@richlowe.net        ret = [entry.name for entry in self.__active if entry.is_removed()]
499*10399Srichlowe@richlowe.net        ret.extend([x.parentname for x in self.__active if x.is_renamed()])
500*10399Srichlowe@richlowe.net        return ret
501*10399Srichlowe@richlowe.net
502*10399Srichlowe@richlowe.net    def files(self):
503*10399Srichlowe@richlowe.net        return self.__active.files()
504*10399Srichlowe@richlowe.net
5057078Smjnelson
5067078Smjnelsonclass WorkSpace(object):
5077078Smjnelson
5087078Smjnelson    def __init__(self, repository):
5097078Smjnelson        self.repo = repository
5107078Smjnelson        self.ui = self.repo.ui
5117078Smjnelson        self.name = self.repo.root
5127078Smjnelson
5137078Smjnelson        self.activecache = {}
5147078Smjnelson
5157078Smjnelson    def parent(self, spec=None):
51610263Srichlowe@richlowe.net        '''Return the canonical workspace parent, either SPEC (which
51710263Srichlowe@richlowe.net        will be expanded) if provided or the default parent
51810263Srichlowe@richlowe.net        otherwise.'''
5197078Smjnelson
52010263Srichlowe@richlowe.net        if spec:
52110263Srichlowe@richlowe.net            return self.ui.expandpath(spec)
5227078Smjnelson
52310263Srichlowe@richlowe.net        p = self.ui.expandpath('default')
52410263Srichlowe@richlowe.net        if p == 'default':
52510263Srichlowe@richlowe.net            return None
52610263Srichlowe@richlowe.net        else:
52710263Srichlowe@richlowe.net            return p
5287078Smjnelson
52910263Srichlowe@richlowe.net    def _localtip(self, outgoing, wctx):
53010263Srichlowe@richlowe.net        '''Return the most representative changeset to act as the
53110263Srichlowe@richlowe.net        localtip.
5327078Smjnelson
53310263Srichlowe@richlowe.net        If the working directory is modified (has file changes, is a
53410263Srichlowe@richlowe.net        merge, or has switched branches), this will be a workingctx.
5357078Smjnelson
53610263Srichlowe@richlowe.net        If the working directory is unmodified, this will be the most
53710263Srichlowe@richlowe.net        recent (highest revision number) local (outgoing) head on the
53810263Srichlowe@richlowe.net        current branch, if no heads are determined to be outgoing, it
53910263Srichlowe@richlowe.net        will be the most recent head on the current branch.
54010263Srichlowe@richlowe.net        '''
5417078Smjnelson
5427078Smjnelson        #
54310263Srichlowe@richlowe.net        # A modified working copy is seen as a proto-branch, and thus
54410263Srichlowe@richlowe.net        # our only option as the local tip.
5457078Smjnelson        #
5467078Smjnelson        if (wctx.files() or len(wctx.parents()) > 1 or
5477078Smjnelson            wctx.branch() != wctx.parents()[0].branch()):
54810263Srichlowe@richlowe.net            return wctx
54910263Srichlowe@richlowe.net
55010263Srichlowe@richlowe.net        heads = self.repo.heads(start=wctx.parents()[0].node())
55110263Srichlowe@richlowe.net        headctxs = [self.repo.changectx(n) for n in heads]
55210263Srichlowe@richlowe.net        localctxs = [c for c in headctxs if c.node() in outgoing]
55310263Srichlowe@richlowe.net
55410263Srichlowe@richlowe.net        ltip = sorted(localctxs or headctxs, key=lambda x: x.rev())[-1]
5557078Smjnelson
55610263Srichlowe@richlowe.net        if len(heads) > 1:
55710263Srichlowe@richlowe.net            self.ui.warn('The current branch has more than one head, '
55810263Srichlowe@richlowe.net                         'using %s\n' % ltip.rev())
55910263Srichlowe@richlowe.net
56010263Srichlowe@richlowe.net        return ltip
5617078Smjnelson
56210263Srichlowe@richlowe.net    def _parenttip(self, heads, outgoing):
56310263Srichlowe@richlowe.net        '''Return the highest-numbered, non-outgoing changeset that is
56410263Srichlowe@richlowe.net        an ancestor of a changeset in heads.
5657078Smjnelson
56610263Srichlowe@richlowe.net        This is intended to find the most recent changeset on a given
56710263Srichlowe@richlowe.net        branch that is shared between a parent and child workspace,
56810263Srichlowe@richlowe.net        such that it can act as a stand-in for the parent workspace.
56910263Srichlowe@richlowe.net        '''
5707078Smjnelson
5717078Smjnelson        def tipmost_shared(head, outnodes):
5727078Smjnelson            '''Return the tipmost node on the same branch as head that is not
5737078Smjnelson            in outnodes.
5747078Smjnelson
5757078Smjnelson            We walk from head to the bottom of the workspace (revision
5767078Smjnelson            0) collecting nodes not in outnodes during the add phase
5777078Smjnelson            and return the first node we see in the iter phase that
5787078Smjnelson            was previously collected.
5797078Smjnelson
58010263Srichlowe@richlowe.net            If no node is found (all revisions >= 0 are outgoing), the
58110263Srichlowe@richlowe.net            only possible parenttip is the null node (node.nullid)
58210263Srichlowe@richlowe.net            which is returned explicitly.
58310263Srichlowe@richlowe.net
5847078Smjnelson            See the docstring of mercurial.cmdutil.walkchangerevs()
5857078Smjnelson            for the phased approach to the iterator returned.  The
5867078Smjnelson            important part to note is that the 'add' phase gathers
5877078Smjnelson            nodes, which the 'iter' phase then iterates through.'''
5887078Smjnelson
58910263Srichlowe@richlowe.net            opts = {'rev': ['%s:0' % head.rev()],
59010263Srichlowe@richlowe.net                    'follow': True}
5917078Smjnelson            get = util.cachefunc(lambda r: self.repo.changectx(r).changeset())
5927078Smjnelson            changeiter = cmdutil.walkchangerevs(self.repo.ui, self.repo, [],
59310263Srichlowe@richlowe.net                                                get, opts)[0]
5947078Smjnelson            seen = []
5957078Smjnelson            for st, rev, fns in changeiter:
5967078Smjnelson                n = self.repo.changelog.node(rev)
5977078Smjnelson                if st == 'add':
5987078Smjnelson                    if n not in outnodes:
5997078Smjnelson                        seen.append(n)
6007078Smjnelson                elif st == 'iter':
6017078Smjnelson                    if n in seen:
6027078Smjnelson                        return rev
60310263Srichlowe@richlowe.net            return self.repo.changelog.rev(node.nullid)
6047078Smjnelson
60510263Srichlowe@richlowe.net        nodes = set(outgoing)
60610263Srichlowe@richlowe.net        ptips = map(lambda x: tipmost_shared(x, nodes), heads)
6077078Smjnelson        return self.repo.changectx(sorted(ptips)[-1])
6087078Smjnelson
609*10399Srichlowe@richlowe.net    def status(self, base='.', head=None):
6107078Smjnelson        '''Translate from the hg 6-tuple status format to a hash keyed
6117078Smjnelson        on change-type'''
612*10399Srichlowe@richlowe.net
6137078Smjnelson        states = ['modified', 'added', 'removed', 'deleted', 'unknown',
6147078Smjnelson              'ignored']
6159006Srichlowe@richlowe.net
6167078Smjnelson        chngs = self.repo.status(base, head)
6177078Smjnelson        return dict(zip(states, chngs))
6187078Smjnelson
6197078Smjnelson    def findoutgoing(self, parent):
620*10399Srichlowe@richlowe.net        '''Return the base set of outgoing nodes.
621*10399Srichlowe@richlowe.net
622*10399Srichlowe@richlowe.net        A caching wrapper around mercurial.localrepo.findoutgoing().
623*10399Srichlowe@richlowe.net        Complains (to the user), if the parent workspace is
624*10399Srichlowe@richlowe.net        non-existent or inaccessible'''
625*10399Srichlowe@richlowe.net
626*10399Srichlowe@richlowe.net        self.ui.pushbuffer()
627*10399Srichlowe@richlowe.net        try:
6287078Smjnelson            try:
629*10399Srichlowe@richlowe.net                ui = self.ui
630*10399Srichlowe@richlowe.net                if hasattr(cmdutil, 'remoteui'):
631*10399Srichlowe@richlowe.net                    ui = cmdutil.remoteui(ui, {})
632*10399Srichlowe@richlowe.net                pws = hg.repository(ui, parent)
633*10399Srichlowe@richlowe.net                return self.repo.findoutgoing(pws)
634*10399Srichlowe@richlowe.net            except HgRepoError:
635*10399Srichlowe@richlowe.net                self.ui.warn("Warning: Parent workspace '%s' is not "
636*10399Srichlowe@richlowe.net                             "accessible\n"
637*10399Srichlowe@richlowe.net                             "active list will be incomplete\n\n" % parent)
638*10399Srichlowe@richlowe.net                return []
639*10399Srichlowe@richlowe.net        finally:
6407078Smjnelson            self.ui.popbuffer()
641*10399Srichlowe@richlowe.net    findoutgoing = util.cachefunc(findoutgoing)
6427078Smjnelson
6437078Smjnelson    def modified(self):
6447078Smjnelson        '''Return a list of files modified in the workspace'''
6459006Srichlowe@richlowe.net        wctx = self.workingctx()
6467078Smjnelson        return sorted(wctx.files() + wctx.deleted()) or None
6477078Smjnelson
6487078Smjnelson    def merged(self):
6497078Smjnelson        '''Return boolean indicating whether the workspace has an uncommitted
6507078Smjnelson        merge'''
6519006Srichlowe@richlowe.net        wctx = self.workingctx()
6527078Smjnelson        return len(wctx.parents()) > 1
6537078Smjnelson
6547078Smjnelson    def branched(self):
6557078Smjnelson        '''Return boolean indicating whether the workspace has an
6567078Smjnelson        uncommitted named branch'''
6577078Smjnelson
6589006Srichlowe@richlowe.net        wctx = self.workingctx()
6597078Smjnelson        return wctx.branch() != wctx.parents()[0].branch()
6607078Smjnelson
6617078Smjnelson    def active(self, parent=None):
6627078Smjnelson        '''Return an ActiveList describing changes between workspace
6637078Smjnelson        and parent workspace (including uncommitted changes).
6647078Smjnelson        If workspace has no parent ActiveList will still describe any
6657078Smjnelson        uncommitted changes'''
6667078Smjnelson
6677078Smjnelson        parent = self.parent(parent)
6687078Smjnelson        if parent in self.activecache:
6697078Smjnelson            return self.activecache[parent]
6707078Smjnelson
6717078Smjnelson        if parent:
6727078Smjnelson            outgoing = self.findoutgoing(parent)
67310263Srichlowe@richlowe.net            outnodes = self.repo.changelog.nodesbetween(outgoing)[0]
6747078Smjnelson        else:
6757078Smjnelson            outgoing = []       # No parent, no outgoing nodes
67610263Srichlowe@richlowe.net            outnodes = []
6777078Smjnelson
67810263Srichlowe@richlowe.net        localtip = self._localtip(outnodes, self.workingctx())
6797078Smjnelson
68010263Srichlowe@richlowe.net        if localtip.rev() is None:
68110263Srichlowe@richlowe.net            heads = localtip.parents()
68210263Srichlowe@richlowe.net        else:
68310263Srichlowe@richlowe.net            heads = [localtip]
6847078Smjnelson
68510263Srichlowe@richlowe.net        ctxs = [self.repo.changectx(n) for n in
68610263Srichlowe@richlowe.net                self.repo.changelog.nodesbetween(outgoing,
68710263Srichlowe@richlowe.net                                                 [h.node() for h in heads])[0]]
6887078Smjnelson
68910263Srichlowe@richlowe.net        if localtip.rev() is None:
69010263Srichlowe@richlowe.net            ctxs.append(localtip)
69110263Srichlowe@richlowe.net
69210263Srichlowe@richlowe.net        act = ActiveList(self, self._parenttip(heads, outnodes), ctxs)
6937078Smjnelson
6947078Smjnelson        self.activecache[parent] = act
6957078Smjnelson        return act
6967078Smjnelson
6977298SMark.J.Nelson@Sun.COM    def pdiff(self, pats, opts, parent=None):
6987078Smjnelson        'Return diffs relative to PARENT, as best as we can make out'
6997078Smjnelson
7007078Smjnelson        parent = self.parent(parent)
7017078Smjnelson        act = self.active(parent)
7027078Smjnelson
7037078Smjnelson        #
7047078Smjnelson        # act.localtip maybe nil, in the case of uncommitted local
7057078Smjnelson        # changes.
7067078Smjnelson        #
7077078Smjnelson        if not act.revs:
7087078Smjnelson            return
7097078Smjnelson
710*10399Srichlowe@richlowe.net        matchfunc = cmdutil.match(self.repo, pats, opts)
7117298SMark.J.Nelson@Sun.COM        opts = patch.diffopts(self.ui, opts)
7127298SMark.J.Nelson@Sun.COM
7139006Srichlowe@richlowe.net        return self.diff(act.parenttip.node(), act.localtip.node(),
7149006Srichlowe@richlowe.net                         match=matchfunc, opts=opts)
7157078Smjnelson
7167078Smjnelson    def squishdeltas(self, active, message, user=None):
717*10399Srichlowe@richlowe.net        '''Create a single conglomerate changeset based on a given
718*10399Srichlowe@richlowe.net        active list.  Removes the original changesets comprising the
719*10399Srichlowe@richlowe.net        given active list, and any tags pointing to them.
720*10399Srichlowe@richlowe.net
721*10399Srichlowe@richlowe.net        Operation:
722*10399Srichlowe@richlowe.net
723*10399Srichlowe@richlowe.net          - Commit an activectx object representing the specified
724*10399Srichlowe@richlowe.net            active list,
725*10399Srichlowe@richlowe.net
726*10399Srichlowe@richlowe.net          - Remove any local tags pointing to changesets in the
727*10399Srichlowe@richlowe.net            specified active list.
7287078Smjnelson
729*10399Srichlowe@richlowe.net          - Remove the changesets comprising the specified active
730*10399Srichlowe@richlowe.net            list.
731*10399Srichlowe@richlowe.net
732*10399Srichlowe@richlowe.net          - Remove any metadata that may refer to changesets that were
733*10399Srichlowe@richlowe.net            removed.
7347078Smjnelson
735*10399Srichlowe@richlowe.net        Calling code is expected to hold both the working copy lock
736*10399Srichlowe@richlowe.net        and repository lock of the destination workspace
737*10399Srichlowe@richlowe.net        '''
738*10399Srichlowe@richlowe.net
739*10399Srichlowe@richlowe.net        def strip_local_tags(active):
740*10399Srichlowe@richlowe.net            '''Remove any local tags referring to the specified nodes.'''
7417078Smjnelson
7427078Smjnelson            if os.path.exists(self.repo.join('localtags')):
743*10399Srichlowe@richlowe.net                fh = None
744*10399Srichlowe@richlowe.net                try:
745*10399Srichlowe@richlowe.net                    fh = self.repo.opener('localtags')
746*10399Srichlowe@richlowe.net                    tags = active.prune_tags(fh)
747*10399Srichlowe@richlowe.net                    fh.close()
7487078Smjnelson
749*10399Srichlowe@richlowe.net                    fh = self.repo.opener('localtags', 'w', atomictemp=True)
750*10399Srichlowe@richlowe.net                    fh.writelines(tags)
751*10399Srichlowe@richlowe.net                    fh.rename()
752*10399Srichlowe@richlowe.net                finally:
753*10399Srichlowe@richlowe.net                    if fh and not fh.closed:
754*10399Srichlowe@richlowe.net                        fh.close()
7557078Smjnelson
7567078Smjnelson        if active.files():
757*10399Srichlowe@richlowe.net            for entry in active:
758*10399Srichlowe@richlowe.net                #
759*10399Srichlowe@richlowe.net                # Work around Mercurial issue #1666, if the source
760*10399Srichlowe@richlowe.net                # file of a rename exists in the working copy
761*10399Srichlowe@richlowe.net                # Mercurial will complain, and remove the file.
762*10399Srichlowe@richlowe.net                #
763*10399Srichlowe@richlowe.net                # We preemptively remove the file to avoid the
764*10399Srichlowe@richlowe.net                # complaint (the user was asked about this in
765*10399Srichlowe@richlowe.net                # cdm_recommit)
766*10399Srichlowe@richlowe.net                #
767*10399Srichlowe@richlowe.net                if entry.is_renamed():
768*10399Srichlowe@richlowe.net                    path = self.repo.wjoin(entry.parentname)
769*10399Srichlowe@richlowe.net                    if os.path.exists(path):
770*10399Srichlowe@richlowe.net                        os.unlink(path)
771*10399Srichlowe@richlowe.net
772*10399Srichlowe@richlowe.net            self.repo.commitctx(active.context(message, user))
773*10399Srichlowe@richlowe.net            wsstate = "recommitted"
774*10399Srichlowe@richlowe.net            destination = self.repo.changelog.tip()
7757078Smjnelson        else:
7767078Smjnelson            #
7777078Smjnelson            # If all we're doing is stripping the old nodes, we want to
7787078Smjnelson            # update the working copy such that we're not at a revision
7797078Smjnelson            # that's about to go away.
7807078Smjnelson            #
781*10399Srichlowe@richlowe.net            wsstate = "tip"
782*10399Srichlowe@richlowe.net            destination = active.parenttip.node()
783*10399Srichlowe@richlowe.net
784*10399Srichlowe@richlowe.net        self.clean(destination)
785*10399Srichlowe@richlowe.net
786*10399Srichlowe@richlowe.net        #
787*10399Srichlowe@richlowe.net        # Tags were elided by the activectx object.  Local tags,
788*10399Srichlowe@richlowe.net        # however, must be removed manually.
789*10399Srichlowe@richlowe.net        #
790*10399Srichlowe@richlowe.net        try:
791*10399Srichlowe@richlowe.net            strip_local_tags(active)
792*10399Srichlowe@richlowe.net        except EnvironmentError, e:
793*10399Srichlowe@richlowe.net            raise util.Abort('Could not recommit tags: %s\n' % e)
7947078Smjnelson
7957078Smjnelson        # Silence all the strip and update fun
7967078Smjnelson        self.ui.pushbuffer()
7977078Smjnelson
7987078Smjnelson        #
799*10399Srichlowe@richlowe.net        # Remove the active lists component changesets by stripping
800*10399Srichlowe@richlowe.net        # the base of any active branch (of which there may be
801*10399Srichlowe@richlowe.net        # several)
8027078Smjnelson        #
8037078Smjnelson        bases = active.bases()
8047078Smjnelson        try:
8057078Smjnelson            try:
8067078Smjnelson                for basenode in bases:
807*10399Srichlowe@richlowe.net                    #
808*10399Srichlowe@richlowe.net                    # Any cached information about the repository is
809*10399Srichlowe@richlowe.net                    # likely to be invalid during the strip.  The
810*10399Srichlowe@richlowe.net                    # caching of branch tags is especially
811*10399Srichlowe@richlowe.net                    # problematic.
812*10399Srichlowe@richlowe.net                    #
813*10399Srichlowe@richlowe.net                    self.repo.invalidate()
8147078Smjnelson                    repair.strip(self.ui, self.repo, basenode, backup=False)
8157078Smjnelson            except:
8167078Smjnelson                #
8177078Smjnelson                # If this fails, it may leave us in a surprising place in
8187078Smjnelson                # the history.
8197078Smjnelson                #
8207078Smjnelson                # We want to warn the user that something went wrong,
8217078Smjnelson                # and what will happen next, re-raise the exception, and
8227078Smjnelson                # bring the working copy back into a consistent state
8237078Smjnelson                # (which the finally block will do)
8247078Smjnelson                #
8257078Smjnelson                self.ui.warn("stripping failed, your workspace will have "
8267078Smjnelson                             "superfluous heads.\n"
8277078Smjnelson                             "your workspace has been updated to the "
828*10399Srichlowe@richlowe.net                             "%s changeset.\n" % wsstate)
8297078Smjnelson                raise               # Re-raise the exception
8307078Smjnelson        finally:
831*10399Srichlowe@richlowe.net            self.clean()
832*10399Srichlowe@richlowe.net            self.repo.dirstate.write() # Flush the dirstate
833*10399Srichlowe@richlowe.net            self.repo.invalidate()     # Invalidate caches
834*10399Srichlowe@richlowe.net
8357078Smjnelson            #
8367078Smjnelson            # We need to remove Hg's undo information (used for rollback),
8377078Smjnelson            # since it refers to data that will probably not exist after
8387078Smjnelson            # the strip.
8397078Smjnelson            #
8407078Smjnelson            if os.path.exists(self.repo.sjoin('undo')):
8417078Smjnelson                try:
8427078Smjnelson                    os.unlink(self.repo.sjoin('undo'))
8437078Smjnelson                except EnvironmentError, e:
8447078Smjnelson                    raise util.Abort('failed to remove undo data: %s\n' % e)
8457078Smjnelson
8467078Smjnelson            self.ui.popbuffer()
8477078Smjnelson
8487078Smjnelson    def filepath(self, path):
8497078Smjnelson        'Return the full path to a workspace file.'
8507078Smjnelson        return self.repo.pathto(path)
8517078Smjnelson
8527078Smjnelson    def clean(self, rev=None):
8537078Smjnelson        '''Bring workspace up to REV (or tip) forcefully (discarding in
8547078Smjnelson        progress changes)'''
8559006Srichlowe@richlowe.net
8567078Smjnelson        if rev != None:
8577078Smjnelson            rev = self.repo.lookup(rev)
8587078Smjnelson        else:
8597078Smjnelson            rev = self.repo.changelog.tip()
8607078Smjnelson
8617078Smjnelson        hg.clean(self.repo, rev, show_stats=False)
8627078Smjnelson
8637078Smjnelson    def mq_applied(self):
8647078Smjnelson        '''True if the workspace has Mq patches applied'''
8657078Smjnelson        q = mq.queue(self.ui, self.repo.join(''))
8667078Smjnelson        return q.applied
8679006Srichlowe@richlowe.net
868*10399Srichlowe@richlowe.net    def workingctx(self):
869*10399Srichlowe@richlowe.net        return self.repo.changectx(None)
8709006Srichlowe@richlowe.net
871*10399Srichlowe@richlowe.net    def diff(self, node1=None, node2=None, match=None, opts=None):
872*10399Srichlowe@richlowe.net        ret = cStringIO.StringIO()
873*10399Srichlowe@richlowe.net        try:
8749006Srichlowe@richlowe.net            for chunk in patch.diff(self.repo, node1, node2, match=match,
8759006Srichlowe@richlowe.net                                    opts=opts):
8769006Srichlowe@richlowe.net                ret.write(chunk)
877*10399Srichlowe@richlowe.net        finally:
878*10399Srichlowe@richlowe.net            # Workaround Hg bug 1651
879*10399Srichlowe@richlowe.net            if not Version.at_least("1.3"):
880*10399Srichlowe@richlowe.net                self.repo.dirstate.invalidate()
8819006Srichlowe@richlowe.net
882*10399Srichlowe@richlowe.net        return ret.getvalue()
883