xref: /onnv-gate/usr/src/tools/onbld/Scm/WorkSpace.py (revision 12776:0a254b4b5ca4)
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#
17*12776SJames.McPherson@Sun.COM# Copyright (c) 2008, 2010, Oracle and/or its affiliates. All rights reserved.
187078Smjnelson#
197078Smjnelson
207078Smjnelson#
217078Smjnelson# Theory:
227078Smjnelson#
237078Smjnelson# Workspaces have a non-binding parent/child relationship.
247078Smjnelson# All important operations apply to the changes between the two.
257078Smjnelson#
267078Smjnelson# However, for the sake of remote operation, the 'parent' of a
277078Smjnelson# workspace is not seen as a literal entity, instead the figurative
287078Smjnelson# parent contains the last changeset common to both parent and child,
297078Smjnelson# as such the 'parent tip' is actually nothing of the sort, but instead a
307078Smjnelson# convenient imitation.
317078Smjnelson#
327078Smjnelson# Any change made to a workspace is a change to a file therein, such
337078Smjnelson# changes can be represented briefly as whether the file was
347078Smjnelson# modified/added/removed as compared to the parent workspace, whether
357078Smjnelson# the file has a different name in the parent and if so, whether it
367078Smjnelson# was renamed or merely copied.  Each changed file has an
377078Smjnelson# associated ActiveEntry.
387078Smjnelson#
397078Smjnelson# The ActiveList being a list ActiveEntrys can thus present the entire
407078Smjnelson# change in workspace state between a parent and its child, and is the
417078Smjnelson# important bit here (in that if it is incorrect, everything else will
427078Smjnelson# be as incorrect, or more)
437078Smjnelson#
447078Smjnelson
457078Smjnelsonimport cStringIO
467078Smjnelsonimport os
4710399Srichlowe@richlowe.netfrom mercurial import cmdutil, context, hg, node, patch, repair, util
487078Smjnelsonfrom hgext import mq
497078Smjnelson
509006Srichlowe@richlowe.netfrom onbld.Scm import Version
519006Srichlowe@richlowe.net
5210399Srichlowe@richlowe.net#
5310399Srichlowe@richlowe.net# Mercurial >= 1.2 has its exception types in a mercurial.error
5410399Srichlowe@richlowe.net# module, prior versions had them in their associated modules.
5510399Srichlowe@richlowe.net#
5610399Srichlowe@richlowe.netif Version.at_least("1.2"):
5710399Srichlowe@richlowe.net    from mercurial import error
5810399Srichlowe@richlowe.net    HgRepoError = error.RepoError
5910399Srichlowe@richlowe.net    HgLookupError = error.LookupError
6010399Srichlowe@richlowe.netelse:
6110399Srichlowe@richlowe.net    from mercurial import repo, revlog
6210399Srichlowe@richlowe.net    HgRepoError = repo.RepoError
6310399Srichlowe@richlowe.net    HgLookupError = revlog.LookupError
6410399Srichlowe@richlowe.net
657078Smjnelson
667078Smjnelsonclass ActiveEntry(object):
677078Smjnelson    '''Representation of the changes made to a single file.
687078Smjnelson
697078Smjnelson    MODIFIED   - Contents changed, but no other changes were made
707078Smjnelson    ADDED      - File is newly created
717078Smjnelson    REMOVED    - File is being removed
727078Smjnelson
737078Smjnelson    Copies are represented by an Entry whose .parentname is non-nil
747078Smjnelson
757078Smjnelson    Truly copied files have non-nil .parentname and .renamed = False
767078Smjnelson    Renames have non-nil .parentname and .renamed = True
777078Smjnelson
787078Smjnelson    Do not access any of this information directly, do so via the
797078Smjnelson
807078Smjnelson    .is_<change>() methods.'''
817078Smjnelson
827078Smjnelson    MODIFIED = 1
837078Smjnelson    ADDED = 2
847078Smjnelson    REMOVED = 3
857078Smjnelson
867078Smjnelson    def __init__(self, name):
877078Smjnelson        self.name = name
887078Smjnelson        self.change = None
897078Smjnelson        self.parentname = None
907078Smjnelson        # As opposed to copied (or neither)
917078Smjnelson        self.renamed = False
927078Smjnelson        self.comments = []
937078Smjnelson
947078Smjnelson    #
957078Smjnelson    # ActiveEntrys sort by the name of the file they represent.
967078Smjnelson    #
977078Smjnelson    def __cmp__(self, other):
987078Smjnelson        return cmp(self.name, other.name)
997078Smjnelson
1007078Smjnelson    def is_added(self):
1017078Smjnelson        return self.change == self.ADDED
1027078Smjnelson
1037078Smjnelson    def is_modified(self):
1047078Smjnelson        return self.change == self.MODIFIED
1057078Smjnelson
1067078Smjnelson    def is_removed(self):
1077078Smjnelson        return self.change == self.REMOVED
1087078Smjnelson
1097078Smjnelson    def is_renamed(self):
1107078Smjnelson        return self.parentname and self.renamed
1117078Smjnelson
1127078Smjnelson    def is_copied(self):
1137078Smjnelson        return self.parentname and not self.renamed
1147078Smjnelson
1157078Smjnelson
1167078Smjnelsonclass ActiveList(object):
1177078Smjnelson    '''Complete representation of workspace change.
1187078Smjnelson
1197078Smjnelson    In practice, a container for ActiveEntrys, and methods to build them,
1207078Smjnelson    update them, and deal with them en masse.'''
1217078Smjnelson
1227078Smjnelson    def __init__(self, ws, parenttip, revs=None):
1237078Smjnelson        self._active = {}
1247078Smjnelson        self.ws = ws
1257078Smjnelson
1267078Smjnelson        self.revs = revs
1277078Smjnelson
1287078Smjnelson        self.base = None
1297078Smjnelson        self.parenttip = parenttip
1307078Smjnelson
1317078Smjnelson        #
1327078Smjnelson        # If we couldn't find a parenttip, the two repositories must
1337078Smjnelson        # be unrelated (Hg catches most of this, but this case is valid for it
1347078Smjnelson        # but invalid for us)
1357078Smjnelson        #
1367078Smjnelson        if self.parenttip == None:
1377078Smjnelson            raise util.Abort('repository is unrelated')
1387078Smjnelson        self.localtip = None
1397078Smjnelson
1407078Smjnelson        if revs:
1417078Smjnelson            self.base = revs[0]
1427078Smjnelson            self.localtip = revs[-1]
1437078Smjnelson
1447078Smjnelson        self._comments = []
1457078Smjnelson
1467078Smjnelson        self._build(revs)
1477078Smjnelson
1487078Smjnelson    def _build(self, revs):
1497078Smjnelson        if not revs:
1507078Smjnelson            return
1517078Smjnelson
1527078Smjnelson        status = self.ws.status(self.parenttip.node(), self.localtip.node())
1537078Smjnelson
1547078Smjnelson        files = []
1557078Smjnelson        for ctype in status.values():
1567078Smjnelson            files.extend(ctype)
1577078Smjnelson
1587078Smjnelson        #
1597078Smjnelson        # When a file is renamed, two operations actually occur.
1607078Smjnelson        # A file copy from source to dest and a removal of source.
1617078Smjnelson        #
1627078Smjnelson        # These are represented as two distinct entries in the
1637078Smjnelson        # changectx and status (one on the dest file for the
1647078Smjnelson        # copy, one on the source file for the remove).
1657078Smjnelson        #
1667078Smjnelson        # Since these are unconnected in both the context and
1677078Smjnelson        # status we can only make the association by explicitly
1687078Smjnelson        # looking for it.
1697078Smjnelson        #
1707078Smjnelson        # We deal with this thusly:
1717078Smjnelson        #
1727078Smjnelson        # We maintain a dict dest -> source of all copies
1737078Smjnelson        # (updating dest as appropriate, but leaving source alone).
1747078Smjnelson        #
1757078Smjnelson        # After all other processing, we mark as renamed any pair
1767078Smjnelson        # where source is on the removed list.
1777078Smjnelson        #
1787078Smjnelson        copies = {}
1797078Smjnelson
1807078Smjnelson        #
1817078Smjnelson        # Walk revs looking for renames and adding files that
1827078Smjnelson        # are in both change context and status to the active
1837078Smjnelson        # list.
1847078Smjnelson        #
1857078Smjnelson        for ctx in revs:
1867078Smjnelson            desc = ctx.description().splitlines()
1877078Smjnelson
1887078Smjnelson            self._comments.extend(desc)
1897078Smjnelson
1907078Smjnelson            for fname in ctx.files():
1917078Smjnelson                #
1927078Smjnelson                # We store comments per-entry as well, for the sake of
1937078Smjnelson                # webrev and similar.  We store twice to avoid the problems
1947078Smjnelson                # of uniquifying comments for the general list (and possibly
1957078Smjnelson                # destroying multi-line entities in the process).
1967078Smjnelson                #
1977078Smjnelson                if fname not in self:
1987078Smjnelson                    self._addentry(fname)
1997078Smjnelson                self[fname].comments.extend(desc)
2007078Smjnelson
2017078Smjnelson                try:
2027078Smjnelson                    fctx = ctx.filectx(fname)
20310399Srichlowe@richlowe.net                except HgLookupError:
2047078Smjnelson                    continue
2057078Smjnelson
2067078Smjnelson                #
2077078Smjnelson                # NB: .renamed() is a misnomer, this actually checks
2087078Smjnelson                #     for copies.
2097078Smjnelson                #
2107078Smjnelson                rn = fctx.renamed()
2117078Smjnelson                if rn:
2127078Smjnelson                    #
2137078Smjnelson                    # If the source file is a known copy we know its
2147078Smjnelson                    # ancestry leads us to the parent.
2157078Smjnelson                    # Otherwise make sure the source file is known to
2167078Smjnelson                    # be in the parent, we need not care otherwise.
2177078Smjnelson                    #
2187078Smjnelson                    # We detect cycles at a later point.  There is no
2197078Smjnelson                    # reason to continuously handle them.
2207078Smjnelson                    #
2217078Smjnelson                    if rn[0] in copies:
2227078Smjnelson                        copies[fname] = copies[rn[0]]
2237078Smjnelson                    elif rn[0] in self.parenttip.manifest():
2247078Smjnelson                        copies[fname] = rn[0]
2257078Smjnelson
2267078Smjnelson        #
2277078Smjnelson        # Walk the copy list marking as copied any non-cyclic pair
2287078Smjnelson        # where the destination file is still present in the local
2297078Smjnelson        # tip (to avoid ephemeral changes)
2307078Smjnelson        #
2317078Smjnelson        # Where source is removed, mark as renamed, and remove the
2327078Smjnelson        # AL entry for the source file
2337078Smjnelson        #
2347078Smjnelson        for fname, oldname in copies.iteritems():
2357078Smjnelson            if fname == oldname or fname not in self.localtip.manifest():
2367078Smjnelson                continue
2377078Smjnelson
2387078Smjnelson            self[fname].parentname = oldname
2397078Smjnelson
2407078Smjnelson            if oldname in status['removed']:
2417078Smjnelson                self[fname].renamed = True
2427078Smjnelson                if oldname in self:
2437078Smjnelson                    del self[oldname]
2447078Smjnelson
2457078Smjnelson        #
2467078Smjnelson        # Walk the active list setting the change type for each active
2477078Smjnelson        # file.
2487078Smjnelson        #
2497078Smjnelson        # In the case of modified files that are not renames or
2507078Smjnelson        # copies, we do a content comparison, and drop entries that
2517078Smjnelson        # are not actually modified.
2527078Smjnelson        #
2537078Smjnelson        # We walk a copy of the AL such that we can drop entries
2547078Smjnelson        # within the loop.
2557078Smjnelson        #
2567078Smjnelson        for entry in self._active.values():
2577078Smjnelson            if entry.name not in files:
2587078Smjnelson                del self[entry.name]
2597078Smjnelson                continue
2607078Smjnelson
2617078Smjnelson            if entry.name in status['added']:
2627078Smjnelson                entry.change = ActiveEntry.ADDED
2637078Smjnelson            elif entry.name in status['removed']:
2647078Smjnelson                entry.change = ActiveEntry.REMOVED
2657078Smjnelson            elif entry.name in status['modified']:
2667078Smjnelson                entry.change = ActiveEntry.MODIFIED
2677078Smjnelson
2687078Smjnelson            #
2697078Smjnelson            # There are cases during a merge where a file will be in
2707078Smjnelson            # the status return as modified, but in reality be an
2717078Smjnelson            # addition (ie, not in the parenttip).
2727078Smjnelson            #
2737078Smjnelson            # We need to check whether the file is actually present
2747078Smjnelson            # in the parenttip, and set it as an add, if not.
2757078Smjnelson            #
2767078Smjnelson            if entry.name not in self.parenttip.manifest():
2777078Smjnelson                entry.change = ActiveEntry.ADDED
278*12776SJames.McPherson@Sun.COM            elif entry.is_modified():
279*12776SJames.McPherson@Sun.COM                if not self._changed_file(entry.name):
2807078Smjnelson                    del self[entry.name]
2817078Smjnelson                    continue
2827078Smjnelson
2837078Smjnelson            assert entry.change
2847078Smjnelson
2857078Smjnelson    def __contains__(self, fname):
2867078Smjnelson        return fname in self._active
2877078Smjnelson
2887078Smjnelson    def __getitem__(self, key):
2897078Smjnelson        return self._active[key]
2907078Smjnelson
2917078Smjnelson    def __setitem__(self, key, value):
2927078Smjnelson        self._active[key] = value
2937078Smjnelson
2947078Smjnelson    def __delitem__(self, key):
2957078Smjnelson        del self._active[key]
2967078Smjnelson
2977078Smjnelson    def __iter__(self):
2987078Smjnelson        for entry in self._active.values():
2997078Smjnelson            yield entry
3007078Smjnelson
3017078Smjnelson    def _addentry(self, fname):
3027078Smjnelson        if fname not in self:
3037078Smjnelson            self[fname] = ActiveEntry(fname)
3047078Smjnelson
3057078Smjnelson    def files(self):
30610399Srichlowe@richlowe.net        '''Return the list of pathnames of all files touched by this
30710399Srichlowe@richlowe.net        ActiveList
3087078Smjnelson
30910399Srichlowe@richlowe.net        Where files have been renamed, this will include both their
31010399Srichlowe@richlowe.net        current name and the name which they had in the parent tip.
31110399Srichlowe@richlowe.net        '''
31210399Srichlowe@richlowe.net
31310399Srichlowe@richlowe.net        ret = self._active.keys()
3147078Smjnelson        ret.extend([x.parentname for x in self
31510399Srichlowe@richlowe.net                    if x.is_renamed() and x.parentname not in ret])
3167078Smjnelson        return ret
3177078Smjnelson
3187078Smjnelson    def comments(self):
3197078Smjnelson        return self._comments
3207078Smjnelson
3217078Smjnelson    def bases(self):
32211646SJames.McPherson@Sun.COM        '''Return the list of changesets that are roots of the ActiveList.
3237078Smjnelson
32411646SJames.McPherson@Sun.COM        This is the set of active changesets where neither parent
32511646SJames.McPherson@Sun.COM        changeset is itself active.'''
3267078Smjnelson
32711646SJames.McPherson@Sun.COM        revset = set(self.revs)
32811646SJames.McPherson@Sun.COM        return filter(lambda ctx: not [p for p in ctx.parents() if p in revset],
32911646SJames.McPherson@Sun.COM                      self.revs)
3307078Smjnelson
3317078Smjnelson    def tags(self):
3327078Smjnelson        '''Find tags that refer to a changeset in the ActiveList,
33310399Srichlowe@richlowe.net        returning a list of 3-tuples (tag, node, is_local) for each.
33410399Srichlowe@richlowe.net
33510399Srichlowe@richlowe.net        We return all instances of a tag that refer to such a node,
33610399Srichlowe@richlowe.net        not just that which takes precedence.'''
3377078Smjnelson
33810399Srichlowe@richlowe.net        def colliding_tags(iterable, nodes, local):
33910399Srichlowe@richlowe.net            for nd, name in [line.rstrip().split(' ', 1) for line in iterable]:
34010399Srichlowe@richlowe.net                if nd in nodes:
34110399Srichlowe@richlowe.net                    yield (name, self.ws.repo.lookup(nd), local)
34210399Srichlowe@richlowe.net
34310399Srichlowe@richlowe.net        tags = []
34410399Srichlowe@richlowe.net        nodes = set(node.hex(ctx.node()) for ctx in self.revs)
3457078Smjnelson
3467078Smjnelson        if os.path.exists(self.ws.repo.join('localtags')):
34710399Srichlowe@richlowe.net            fh = self.ws.repo.opener('localtags')
34810399Srichlowe@richlowe.net            tags.extend(colliding_tags(fh, nodes, True))
34910399Srichlowe@richlowe.net            fh.close()
3507078Smjnelson
3517078Smjnelson        # We want to use the tags file from the localtip
35210399Srichlowe@richlowe.net        if '.hgtags' in self.localtip:
35310399Srichlowe@richlowe.net            data = self.localtip.filectx('.hgtags').data().splitlines()
35410399Srichlowe@richlowe.net            tags.extend(colliding_tags(data, nodes, False))
3557078Smjnelson
3567078Smjnelson        return tags
3577078Smjnelson
35810399Srichlowe@richlowe.net    def prune_tags(self, data):
35910399Srichlowe@richlowe.net        '''Return a copy of data, which should correspond to the
36010399Srichlowe@richlowe.net        contents of a Mercurial tags file, with any tags that refer to
36110399Srichlowe@richlowe.net        changesets which are components of the ActiveList removed.'''
36210399Srichlowe@richlowe.net
36310399Srichlowe@richlowe.net        nodes = set(node.hex(ctx.node()) for ctx in self.revs)
36410399Srichlowe@richlowe.net        return [t for t in data if t.split(' ', 1)[0] not in nodes]
36510399Srichlowe@richlowe.net
366*12776SJames.McPherson@Sun.COM    def _changed_file(self, path):
367*12776SJames.McPherson@Sun.COM        '''Compare the parent and local versions of a given file.
368*12776SJames.McPherson@Sun.COM        Return True if file changed, False otherwise.
3697078Smjnelson
370*12776SJames.McPherson@Sun.COM        Note that this compares the given path in both versions, not the given
371*12776SJames.McPherson@Sun.COM        entry; renamed and copied files are compared by name, not history.
3727078Smjnelson
3737078Smjnelson        The fast path compares file metadata, slow path is a
3747078Smjnelson        real comparison of file content.'''
3757078Smjnelson
376*12776SJames.McPherson@Sun.COM        # Note that we use localtip.manifest() here because of a bug in
377*12776SJames.McPherson@Sun.COM        # Mercurial 1.1.2's workingctx.__contains__
378*12776SJames.McPherson@Sun.COM        if ((path in self.parenttip) != (path in self.localtip.manifest())):
379*12776SJames.McPherson@Sun.COM            return True
380*12776SJames.McPherson@Sun.COM
381*12776SJames.McPherson@Sun.COM        parentfile = self.parenttip.filectx(path)
382*12776SJames.McPherson@Sun.COM        localfile = self.localtip.filectx(path)
3837078Smjnelson
3847078Smjnelson        #
3857078Smjnelson        # NB: Keep these ordered such as to make every attempt
3867078Smjnelson        #     to short-circuit the more time consuming checks.
3877078Smjnelson        #
3887078Smjnelson        if parentfile.size() != localfile.size():
3897078Smjnelson            return True
3907078Smjnelson
39110399Srichlowe@richlowe.net        if parentfile.flags() != localfile.flags():
3927078Smjnelson            return True
3937078Smjnelson
3947078Smjnelson        if parentfile.cmp(localfile.data()):
3957078Smjnelson            return True
3967078Smjnelson
39710399Srichlowe@richlowe.net    def context(self, message, user):
39810399Srichlowe@richlowe.net        '''Return a Mercurial context object representing the entire
39910399Srichlowe@richlowe.net        ActiveList as one change.'''
40010399Srichlowe@richlowe.net        return activectx(self, message, user)
40110399Srichlowe@richlowe.net
40210399Srichlowe@richlowe.net
40310399Srichlowe@richlowe.netclass activectx(context.memctx):
40410399Srichlowe@richlowe.net    '''Represent an ActiveList as a Mercurial context object.
40510399Srichlowe@richlowe.net
40610399Srichlowe@richlowe.net    Part of the  WorkSpace.squishdeltas implementation.'''
40710399Srichlowe@richlowe.net
40810399Srichlowe@richlowe.net    def __init__(self, active, message, user):
40910399Srichlowe@richlowe.net        '''Build an activectx object.
41010399Srichlowe@richlowe.net
41110399Srichlowe@richlowe.net          active  - The ActiveList object used as the source for all data.
41210399Srichlowe@richlowe.net          message - Changeset description
41310399Srichlowe@richlowe.net          user    - Committing user'''
41410399Srichlowe@richlowe.net
41510399Srichlowe@richlowe.net        def filectxfn(repository, ctx, fname):
41610399Srichlowe@richlowe.net            fctx = active.localtip.filectx(fname)
41710399Srichlowe@richlowe.net            data = fctx.data()
41810399Srichlowe@richlowe.net
41910399Srichlowe@richlowe.net            #
42010399Srichlowe@richlowe.net            # .hgtags is a special case, tags referring to active list
42110399Srichlowe@richlowe.net            # component changesets should be elided.
42210399Srichlowe@richlowe.net            #
42310399Srichlowe@richlowe.net            if fname == '.hgtags':
42410399Srichlowe@richlowe.net                data = '\n'.join(active.prune_tags(data.splitlines()))
42510399Srichlowe@richlowe.net
42610399Srichlowe@richlowe.net            return context.memfilectx(fname, data, 'l' in fctx.flags(),
42710399Srichlowe@richlowe.net                                      'x' in fctx.flags(),
42810399Srichlowe@richlowe.net                                      active[fname].parentname)
42910399Srichlowe@richlowe.net
43010399Srichlowe@richlowe.net        self.__active = active
43110399Srichlowe@richlowe.net        parents = (active.parenttip.node(), node.nullid)
43210399Srichlowe@richlowe.net        extra = {'branch': active.localtip.branch()}
43310399Srichlowe@richlowe.net        context.memctx.__init__(self, active.ws.repo, parents, message,
43410399Srichlowe@richlowe.net                                active.files(), filectxfn, user=user,
43510399Srichlowe@richlowe.net                                extra=extra)
43610399Srichlowe@richlowe.net
43710399Srichlowe@richlowe.net    def modified(self):
43810399Srichlowe@richlowe.net        return [entry.name for entry in self.__active if entry.is_modified()]
43910399Srichlowe@richlowe.net
44010399Srichlowe@richlowe.net    def added(self):
44110399Srichlowe@richlowe.net        return [entry.name for entry in self.__active if entry.is_added()]
44210399Srichlowe@richlowe.net
44310399Srichlowe@richlowe.net    def removed(self):
44411733Srichlowe@richlowe.net        ret = set(entry.name for entry in self.__active if entry.is_removed())
44511733Srichlowe@richlowe.net        ret.update(set(x.parentname for x in self.__active if x.is_renamed()))
44611733Srichlowe@richlowe.net        return list(ret)
44710399Srichlowe@richlowe.net
44810399Srichlowe@richlowe.net    def files(self):
44910399Srichlowe@richlowe.net        return self.__active.files()
45010399Srichlowe@richlowe.net
4517078Smjnelson
4527078Smjnelsonclass WorkSpace(object):
4537078Smjnelson
4547078Smjnelson    def __init__(self, repository):
4557078Smjnelson        self.repo = repository
4567078Smjnelson        self.ui = self.repo.ui
4577078Smjnelson        self.name = self.repo.root
4587078Smjnelson
4597078Smjnelson        self.activecache = {}
4607078Smjnelson
4617078Smjnelson    def parent(self, spec=None):
46210263Srichlowe@richlowe.net        '''Return the canonical workspace parent, either SPEC (which
46310263Srichlowe@richlowe.net        will be expanded) if provided or the default parent
46410263Srichlowe@richlowe.net        otherwise.'''
4657078Smjnelson
46610263Srichlowe@richlowe.net        if spec:
46710263Srichlowe@richlowe.net            return self.ui.expandpath(spec)
4687078Smjnelson
46910263Srichlowe@richlowe.net        p = self.ui.expandpath('default')
47010263Srichlowe@richlowe.net        if p == 'default':
47110263Srichlowe@richlowe.net            return None
47210263Srichlowe@richlowe.net        else:
47310263Srichlowe@richlowe.net            return p
4747078Smjnelson
47510263Srichlowe@richlowe.net    def _localtip(self, outgoing, wctx):
47610263Srichlowe@richlowe.net        '''Return the most representative changeset to act as the
47710263Srichlowe@richlowe.net        localtip.
4787078Smjnelson
47910263Srichlowe@richlowe.net        If the working directory is modified (has file changes, is a
48010263Srichlowe@richlowe.net        merge, or has switched branches), this will be a workingctx.
4817078Smjnelson
48210263Srichlowe@richlowe.net        If the working directory is unmodified, this will be the most
48310263Srichlowe@richlowe.net        recent (highest revision number) local (outgoing) head on the
48410263Srichlowe@richlowe.net        current branch, if no heads are determined to be outgoing, it
48510263Srichlowe@richlowe.net        will be the most recent head on the current branch.
48610263Srichlowe@richlowe.net        '''
4877078Smjnelson
4887078Smjnelson        #
48910263Srichlowe@richlowe.net        # A modified working copy is seen as a proto-branch, and thus
49010263Srichlowe@richlowe.net        # our only option as the local tip.
4917078Smjnelson        #
4927078Smjnelson        if (wctx.files() or len(wctx.parents()) > 1 or
4937078Smjnelson            wctx.branch() != wctx.parents()[0].branch()):
49410263Srichlowe@richlowe.net            return wctx
49510263Srichlowe@richlowe.net
49610263Srichlowe@richlowe.net        heads = self.repo.heads(start=wctx.parents()[0].node())
49710263Srichlowe@richlowe.net        headctxs = [self.repo.changectx(n) for n in heads]
49810263Srichlowe@richlowe.net        localctxs = [c for c in headctxs if c.node() in outgoing]
49910263Srichlowe@richlowe.net
50010263Srichlowe@richlowe.net        ltip = sorted(localctxs or headctxs, key=lambda x: x.rev())[-1]
5017078Smjnelson
50210263Srichlowe@richlowe.net        if len(heads) > 1:
50310263Srichlowe@richlowe.net            self.ui.warn('The current branch has more than one head, '
50410263Srichlowe@richlowe.net                         'using %s\n' % ltip.rev())
50510263Srichlowe@richlowe.net
50610263Srichlowe@richlowe.net        return ltip
5077078Smjnelson
50810263Srichlowe@richlowe.net    def _parenttip(self, heads, outgoing):
50910263Srichlowe@richlowe.net        '''Return the highest-numbered, non-outgoing changeset that is
51010263Srichlowe@richlowe.net        an ancestor of a changeset in heads.
5117078Smjnelson
51210263Srichlowe@richlowe.net        This is intended to find the most recent changeset on a given
51310263Srichlowe@richlowe.net        branch that is shared between a parent and child workspace,
51410263Srichlowe@richlowe.net        such that it can act as a stand-in for the parent workspace.
51510263Srichlowe@richlowe.net        '''
5167078Smjnelson
5177078Smjnelson        def tipmost_shared(head, outnodes):
5187078Smjnelson            '''Return the tipmost node on the same branch as head that is not
5197078Smjnelson            in outnodes.
5207078Smjnelson
5217078Smjnelson            We walk from head to the bottom of the workspace (revision
5227078Smjnelson            0) collecting nodes not in outnodes during the add phase
5237078Smjnelson            and return the first node we see in the iter phase that
5247078Smjnelson            was previously collected.
5257078Smjnelson
52610263Srichlowe@richlowe.net            If no node is found (all revisions >= 0 are outgoing), the
52710263Srichlowe@richlowe.net            only possible parenttip is the null node (node.nullid)
52810263Srichlowe@richlowe.net            which is returned explicitly.
52910263Srichlowe@richlowe.net
5307078Smjnelson            See the docstring of mercurial.cmdutil.walkchangerevs()
5317078Smjnelson            for the phased approach to the iterator returned.  The
5327078Smjnelson            important part to note is that the 'add' phase gathers
5337078Smjnelson            nodes, which the 'iter' phase then iterates through.'''
5347078Smjnelson
53510263Srichlowe@richlowe.net            opts = {'rev': ['%s:0' % head.rev()],
53610263Srichlowe@richlowe.net                    'follow': True}
5377078Smjnelson            get = util.cachefunc(lambda r: self.repo.changectx(r).changeset())
5387078Smjnelson            changeiter = cmdutil.walkchangerevs(self.repo.ui, self.repo, [],
53910263Srichlowe@richlowe.net                                                get, opts)[0]
5407078Smjnelson            seen = []
5417078Smjnelson            for st, rev, fns in changeiter:
5427078Smjnelson                n = self.repo.changelog.node(rev)
5437078Smjnelson                if st == 'add':
5447078Smjnelson                    if n not in outnodes:
5457078Smjnelson                        seen.append(n)
5467078Smjnelson                elif st == 'iter':
5477078Smjnelson                    if n in seen:
5487078Smjnelson                        return rev
54910263Srichlowe@richlowe.net            return self.repo.changelog.rev(node.nullid)
5507078Smjnelson
55110263Srichlowe@richlowe.net        nodes = set(outgoing)
55210263Srichlowe@richlowe.net        ptips = map(lambda x: tipmost_shared(x, nodes), heads)
5537078Smjnelson        return self.repo.changectx(sorted(ptips)[-1])
5547078Smjnelson
55510399Srichlowe@richlowe.net    def status(self, base='.', head=None):
5567078Smjnelson        '''Translate from the hg 6-tuple status format to a hash keyed
5577078Smjnelson        on change-type'''
55810399Srichlowe@richlowe.net
5597078Smjnelson        states = ['modified', 'added', 'removed', 'deleted', 'unknown',
5607078Smjnelson              'ignored']
5619006Srichlowe@richlowe.net
5627078Smjnelson        chngs = self.repo.status(base, head)
5637078Smjnelson        return dict(zip(states, chngs))
5647078Smjnelson
5657078Smjnelson    def findoutgoing(self, parent):
56610399Srichlowe@richlowe.net        '''Return the base set of outgoing nodes.
56710399Srichlowe@richlowe.net
56810399Srichlowe@richlowe.net        A caching wrapper around mercurial.localrepo.findoutgoing().
56910399Srichlowe@richlowe.net        Complains (to the user), if the parent workspace is
57010399Srichlowe@richlowe.net        non-existent or inaccessible'''
57110399Srichlowe@richlowe.net
57210399Srichlowe@richlowe.net        self.ui.pushbuffer()
57310399Srichlowe@richlowe.net        try:
5747078Smjnelson            try:
57510399Srichlowe@richlowe.net                ui = self.ui
57610399Srichlowe@richlowe.net                if hasattr(cmdutil, 'remoteui'):
57710399Srichlowe@richlowe.net                    ui = cmdutil.remoteui(ui, {})
57810399Srichlowe@richlowe.net                pws = hg.repository(ui, parent)
57910399Srichlowe@richlowe.net                return self.repo.findoutgoing(pws)
58010399Srichlowe@richlowe.net            except HgRepoError:
58110399Srichlowe@richlowe.net                self.ui.warn("Warning: Parent workspace '%s' is not "
58210399Srichlowe@richlowe.net                             "accessible\n"
58310399Srichlowe@richlowe.net                             "active list will be incomplete\n\n" % parent)
58410399Srichlowe@richlowe.net                return []
58510399Srichlowe@richlowe.net        finally:
5867078Smjnelson            self.ui.popbuffer()
58710399Srichlowe@richlowe.net    findoutgoing = util.cachefunc(findoutgoing)
5887078Smjnelson
5897078Smjnelson    def modified(self):
5907078Smjnelson        '''Return a list of files modified in the workspace'''
5919006Srichlowe@richlowe.net        wctx = self.workingctx()
5927078Smjnelson        return sorted(wctx.files() + wctx.deleted()) or None
5937078Smjnelson
5947078Smjnelson    def merged(self):
5957078Smjnelson        '''Return boolean indicating whether the workspace has an uncommitted
5967078Smjnelson        merge'''
5979006Srichlowe@richlowe.net        wctx = self.workingctx()
5987078Smjnelson        return len(wctx.parents()) > 1
5997078Smjnelson
6007078Smjnelson    def branched(self):
6017078Smjnelson        '''Return boolean indicating whether the workspace has an
6027078Smjnelson        uncommitted named branch'''
6037078Smjnelson
6049006Srichlowe@richlowe.net        wctx = self.workingctx()
6057078Smjnelson        return wctx.branch() != wctx.parents()[0].branch()
6067078Smjnelson
6077078Smjnelson    def active(self, parent=None):
6087078Smjnelson        '''Return an ActiveList describing changes between workspace
6097078Smjnelson        and parent workspace (including uncommitted changes).
6107078Smjnelson        If workspace has no parent ActiveList will still describe any
6117078Smjnelson        uncommitted changes'''
6127078Smjnelson
6137078Smjnelson        parent = self.parent(parent)
6147078Smjnelson        if parent in self.activecache:
6157078Smjnelson            return self.activecache[parent]
6167078Smjnelson
6177078Smjnelson        if parent:
6187078Smjnelson            outgoing = self.findoutgoing(parent)
61910263Srichlowe@richlowe.net            outnodes = self.repo.changelog.nodesbetween(outgoing)[0]
6207078Smjnelson        else:
6217078Smjnelson            outgoing = []       # No parent, no outgoing nodes
62210263Srichlowe@richlowe.net            outnodes = []
6237078Smjnelson
62410263Srichlowe@richlowe.net        localtip = self._localtip(outnodes, self.workingctx())
6257078Smjnelson
62610263Srichlowe@richlowe.net        if localtip.rev() is None:
62710263Srichlowe@richlowe.net            heads = localtip.parents()
62810263Srichlowe@richlowe.net        else:
62910263Srichlowe@richlowe.net            heads = [localtip]
6307078Smjnelson
63110263Srichlowe@richlowe.net        ctxs = [self.repo.changectx(n) for n in
63210263Srichlowe@richlowe.net                self.repo.changelog.nodesbetween(outgoing,
63310263Srichlowe@richlowe.net                                                 [h.node() for h in heads])[0]]
6347078Smjnelson
63510263Srichlowe@richlowe.net        if localtip.rev() is None:
63610263Srichlowe@richlowe.net            ctxs.append(localtip)
63710263Srichlowe@richlowe.net
63810263Srichlowe@richlowe.net        act = ActiveList(self, self._parenttip(heads, outnodes), ctxs)
6397078Smjnelson
6407078Smjnelson        self.activecache[parent] = act
6417078Smjnelson        return act
6427078Smjnelson
6437298SMark.J.Nelson@Sun.COM    def pdiff(self, pats, opts, parent=None):
6447078Smjnelson        'Return diffs relative to PARENT, as best as we can make out'
6457078Smjnelson
6467078Smjnelson        parent = self.parent(parent)
6477078Smjnelson        act = self.active(parent)
6487078Smjnelson
6497078Smjnelson        #
6507078Smjnelson        # act.localtip maybe nil, in the case of uncommitted local
6517078Smjnelson        # changes.
6527078Smjnelson        #
6537078Smjnelson        if not act.revs:
6547078Smjnelson            return
6557078Smjnelson
65610399Srichlowe@richlowe.net        matchfunc = cmdutil.match(self.repo, pats, opts)
6577298SMark.J.Nelson@Sun.COM        opts = patch.diffopts(self.ui, opts)
6587298SMark.J.Nelson@Sun.COM
6599006Srichlowe@richlowe.net        return self.diff(act.parenttip.node(), act.localtip.node(),
6609006Srichlowe@richlowe.net                         match=matchfunc, opts=opts)
6617078Smjnelson
6627078Smjnelson    def squishdeltas(self, active, message, user=None):
66310399Srichlowe@richlowe.net        '''Create a single conglomerate changeset based on a given
66410399Srichlowe@richlowe.net        active list.  Removes the original changesets comprising the
66510399Srichlowe@richlowe.net        given active list, and any tags pointing to them.
66610399Srichlowe@richlowe.net
66710399Srichlowe@richlowe.net        Operation:
66810399Srichlowe@richlowe.net
66910399Srichlowe@richlowe.net          - Commit an activectx object representing the specified
67010399Srichlowe@richlowe.net            active list,
67110399Srichlowe@richlowe.net
67210399Srichlowe@richlowe.net          - Remove any local tags pointing to changesets in the
67310399Srichlowe@richlowe.net            specified active list.
6747078Smjnelson
67510399Srichlowe@richlowe.net          - Remove the changesets comprising the specified active
67610399Srichlowe@richlowe.net            list.
67710399Srichlowe@richlowe.net
67810399Srichlowe@richlowe.net          - Remove any metadata that may refer to changesets that were
67910399Srichlowe@richlowe.net            removed.
6807078Smjnelson
68110399Srichlowe@richlowe.net        Calling code is expected to hold both the working copy lock
68210399Srichlowe@richlowe.net        and repository lock of the destination workspace
68310399Srichlowe@richlowe.net        '''
68410399Srichlowe@richlowe.net
68510399Srichlowe@richlowe.net        def strip_local_tags(active):
68610399Srichlowe@richlowe.net            '''Remove any local tags referring to the specified nodes.'''
6877078Smjnelson
6887078Smjnelson            if os.path.exists(self.repo.join('localtags')):
68910399Srichlowe@richlowe.net                fh = None
69010399Srichlowe@richlowe.net                try:
69110399Srichlowe@richlowe.net                    fh = self.repo.opener('localtags')
69210399Srichlowe@richlowe.net                    tags = active.prune_tags(fh)
69310399Srichlowe@richlowe.net                    fh.close()
6947078Smjnelson
69510399Srichlowe@richlowe.net                    fh = self.repo.opener('localtags', 'w', atomictemp=True)
69610399Srichlowe@richlowe.net                    fh.writelines(tags)
69710399Srichlowe@richlowe.net                    fh.rename()
69810399Srichlowe@richlowe.net                finally:
69910399Srichlowe@richlowe.net                    if fh and not fh.closed:
70010399Srichlowe@richlowe.net                        fh.close()
7017078Smjnelson
7027078Smjnelson        if active.files():
70310399Srichlowe@richlowe.net            for entry in active:
70410399Srichlowe@richlowe.net                #
70510399Srichlowe@richlowe.net                # Work around Mercurial issue #1666, if the source
70610399Srichlowe@richlowe.net                # file of a rename exists in the working copy
70710399Srichlowe@richlowe.net                # Mercurial will complain, and remove the file.
70810399Srichlowe@richlowe.net                #
70910399Srichlowe@richlowe.net                # We preemptively remove the file to avoid the
71010399Srichlowe@richlowe.net                # complaint (the user was asked about this in
71110399Srichlowe@richlowe.net                # cdm_recommit)
71210399Srichlowe@richlowe.net                #
71310399Srichlowe@richlowe.net                if entry.is_renamed():
71410399Srichlowe@richlowe.net                    path = self.repo.wjoin(entry.parentname)
71510399Srichlowe@richlowe.net                    if os.path.exists(path):
71610399Srichlowe@richlowe.net                        os.unlink(path)
71710399Srichlowe@richlowe.net
71810399Srichlowe@richlowe.net            self.repo.commitctx(active.context(message, user))
71910399Srichlowe@richlowe.net            wsstate = "recommitted"
72010399Srichlowe@richlowe.net            destination = self.repo.changelog.tip()
7217078Smjnelson        else:
7227078Smjnelson            #
7237078Smjnelson            # If all we're doing is stripping the old nodes, we want to
7247078Smjnelson            # update the working copy such that we're not at a revision
7257078Smjnelson            # that's about to go away.
7267078Smjnelson            #
72710399Srichlowe@richlowe.net            wsstate = "tip"
72810399Srichlowe@richlowe.net            destination = active.parenttip.node()
72910399Srichlowe@richlowe.net
73010399Srichlowe@richlowe.net        self.clean(destination)
73110399Srichlowe@richlowe.net
73210399Srichlowe@richlowe.net        #
73310399Srichlowe@richlowe.net        # Tags were elided by the activectx object.  Local tags,
73410399Srichlowe@richlowe.net        # however, must be removed manually.
73510399Srichlowe@richlowe.net        #
73610399Srichlowe@richlowe.net        try:
73710399Srichlowe@richlowe.net            strip_local_tags(active)
73810399Srichlowe@richlowe.net        except EnvironmentError, e:
73910399Srichlowe@richlowe.net            raise util.Abort('Could not recommit tags: %s\n' % e)
7407078Smjnelson
7417078Smjnelson        # Silence all the strip and update fun
7427078Smjnelson        self.ui.pushbuffer()
7437078Smjnelson
7447078Smjnelson        #
74510399Srichlowe@richlowe.net        # Remove the active lists component changesets by stripping
74610399Srichlowe@richlowe.net        # the base of any active branch (of which there may be
74710399Srichlowe@richlowe.net        # several)
7487078Smjnelson        #
7497078Smjnelson        try:
7507078Smjnelson            try:
75111646SJames.McPherson@Sun.COM                for base in active.bases():
75210399Srichlowe@richlowe.net                    #
75310399Srichlowe@richlowe.net                    # Any cached information about the repository is
75410399Srichlowe@richlowe.net                    # likely to be invalid during the strip.  The
75510399Srichlowe@richlowe.net                    # caching of branch tags is especially
75610399Srichlowe@richlowe.net                    # problematic.
75710399Srichlowe@richlowe.net                    #
75810399Srichlowe@richlowe.net                    self.repo.invalidate()
75911646SJames.McPherson@Sun.COM                    repair.strip(self.ui, self.repo, base.node(), backup=False)
7607078Smjnelson            except:
7617078Smjnelson                #
7627078Smjnelson                # If this fails, it may leave us in a surprising place in
7637078Smjnelson                # the history.
7647078Smjnelson                #
7657078Smjnelson                # We want to warn the user that something went wrong,
7667078Smjnelson                # and what will happen next, re-raise the exception, and
7677078Smjnelson                # bring the working copy back into a consistent state
7687078Smjnelson                # (which the finally block will do)
7697078Smjnelson                #
7707078Smjnelson                self.ui.warn("stripping failed, your workspace will have "
7717078Smjnelson                             "superfluous heads.\n"
7727078Smjnelson                             "your workspace has been updated to the "
77310399Srichlowe@richlowe.net                             "%s changeset.\n" % wsstate)
7747078Smjnelson                raise               # Re-raise the exception
7757078Smjnelson        finally:
77610399Srichlowe@richlowe.net            self.clean()
77710399Srichlowe@richlowe.net            self.repo.dirstate.write() # Flush the dirstate
77810399Srichlowe@richlowe.net            self.repo.invalidate()     # Invalidate caches
77910399Srichlowe@richlowe.net
7807078Smjnelson            #
7817078Smjnelson            # We need to remove Hg's undo information (used for rollback),
7827078Smjnelson            # since it refers to data that will probably not exist after
7837078Smjnelson            # the strip.
7847078Smjnelson            #
7857078Smjnelson            if os.path.exists(self.repo.sjoin('undo')):
7867078Smjnelson                try:
7877078Smjnelson                    os.unlink(self.repo.sjoin('undo'))
7887078Smjnelson                except EnvironmentError, e:
7897078Smjnelson                    raise util.Abort('failed to remove undo data: %s\n' % e)
7907078Smjnelson
7917078Smjnelson            self.ui.popbuffer()
7927078Smjnelson
7937078Smjnelson    def filepath(self, path):
7947078Smjnelson        'Return the full path to a workspace file.'
7957078Smjnelson        return self.repo.pathto(path)
7967078Smjnelson
7977078Smjnelson    def clean(self, rev=None):
7987078Smjnelson        '''Bring workspace up to REV (or tip) forcefully (discarding in
7997078Smjnelson        progress changes)'''
8009006Srichlowe@richlowe.net
8017078Smjnelson        if rev != None:
8027078Smjnelson            rev = self.repo.lookup(rev)
8037078Smjnelson        else:
8047078Smjnelson            rev = self.repo.changelog.tip()
8057078Smjnelson
8067078Smjnelson        hg.clean(self.repo, rev, show_stats=False)
8077078Smjnelson
8087078Smjnelson    def mq_applied(self):
8097078Smjnelson        '''True if the workspace has Mq patches applied'''
8107078Smjnelson        q = mq.queue(self.ui, self.repo.join(''))
8117078Smjnelson        return q.applied
8129006Srichlowe@richlowe.net
81310399Srichlowe@richlowe.net    def workingctx(self):
81410399Srichlowe@richlowe.net        return self.repo.changectx(None)
8159006Srichlowe@richlowe.net
81610399Srichlowe@richlowe.net    def diff(self, node1=None, node2=None, match=None, opts=None):
81710399Srichlowe@richlowe.net        ret = cStringIO.StringIO()
81810399Srichlowe@richlowe.net        try:
8199006Srichlowe@richlowe.net            for chunk in patch.diff(self.repo, node1, node2, match=match,
8209006Srichlowe@richlowe.net                                    opts=opts):
8219006Srichlowe@richlowe.net                ret.write(chunk)
82210399Srichlowe@richlowe.net        finally:
82310399Srichlowe@richlowe.net            # Workaround Hg bug 1651
82410399Srichlowe@richlowe.net            if not Version.at_least("1.3"):
82510399Srichlowe@richlowe.net                self.repo.dirstate.invalidate()
8269006Srichlowe@richlowe.net
82710399Srichlowe@richlowe.net        return ret.getvalue()
828