Home | History | Annotate | Download | only in Scm
      1 #
      2 #  This program is free software; you can redistribute it and/or modify
      3 #  it under the terms of the GNU General Public License version 2
      4 #  as published by the Free Software Foundation.
      5 #
      6 #  This program is distributed in the hope that it will be useful,
      7 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
      8 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      9 #  GNU General Public License for more details.
     10 #
     11 #  You should have received a copy of the GNU General Public License
     12 #  along with this program; if not, write to the Free Software
     13 #  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
     14 #
     15 
     16 #
     17 # Copyright 2009 Sun Microsystems, Inc.  All rights reserved.
     18 # Use is subject to license terms.
     19 #
     20 
     21 #
     22 # Theory:
     23 #
     24 # Workspaces have a non-binding parent/child relationship.
     25 # All important operations apply to the changes between the two.
     26 #
     27 # However, for the sake of remote operation, the 'parent' of a
     28 # workspace is not seen as a literal entity, instead the figurative
     29 # parent contains the last changeset common to both parent and child,
     30 # as such the 'parent tip' is actually nothing of the sort, but instead a
     31 # convenient imitation.
     32 #
     33 # Any change made to a workspace is a change to a file therein, such
     34 # changes can be represented briefly as whether the file was
     35 # modified/added/removed as compared to the parent workspace, whether
     36 # the file has a different name in the parent and if so, whether it
     37 # was renamed or merely copied.  Each changed file has an
     38 # associated ActiveEntry.
     39 #
     40 # The ActiveList being a list ActiveEntrys can thus present the entire
     41 # change in workspace state between a parent and its child, and is the
     42 # important bit here (in that if it is incorrect, everything else will
     43 # be as incorrect, or more)
     44 #
     45 
     46 import cStringIO
     47 import os
     48 from mercurial import cmdutil, context, hg, node, patch, repair, util
     49 from hgext import mq
     50 
     51 from onbld.Scm import Version
     52 
     53 #
     54 # Mercurial >= 1.2 has its exception types in a mercurial.error
     55 # module, prior versions had them in their associated modules.
     56 #
     57 if Version.at_least("1.2"):
     58     from mercurial import error
     59     HgRepoError = error.RepoError
     60     HgLookupError = error.LookupError
     61 else:
     62     from mercurial import repo, revlog
     63     HgRepoError = repo.RepoError
     64     HgLookupError = revlog.LookupError
     65 
     66 
     67 class ActiveEntry(object):
     68     '''Representation of the changes made to a single file.
     69 
     70     MODIFIED   - Contents changed, but no other changes were made
     71     ADDED      - File is newly created
     72     REMOVED    - File is being removed
     73 
     74     Copies are represented by an Entry whose .parentname is non-nil
     75 
     76     Truly copied files have non-nil .parentname and .renamed = False
     77     Renames have non-nil .parentname and .renamed = True
     78 
     79     Do not access any of this information directly, do so via the
     80 
     81     .is_<change>() methods.'''
     82 
     83     MODIFIED = 1
     84     ADDED = 2
     85     REMOVED = 3
     86 
     87     def __init__(self, name):
     88         self.name = name
     89         self.change = None
     90         self.parentname = None
     91         # As opposed to copied (or neither)
     92         self.renamed = False
     93         self.comments = []
     94 
     95     #
     96     # ActiveEntrys sort by the name of the file they represent.
     97     #
     98     def __cmp__(self, other):
     99         return cmp(self.name, other.name)
    100 
    101     def is_added(self):
    102         return self.change == self.ADDED
    103 
    104     def is_modified(self):
    105         return self.change == self.MODIFIED
    106 
    107     def is_removed(self):
    108         return self.change == self.REMOVED
    109 
    110     def is_renamed(self):
    111         return self.parentname and self.renamed
    112 
    113     def is_copied(self):
    114         return self.parentname and not self.renamed
    115 
    116 
    117 class ActiveList(object):
    118     '''Complete representation of workspace change.
    119 
    120     In practice, a container for ActiveEntrys, and methods to build them,
    121     update them, and deal with them en masse.'''
    122 
    123     def __init__(self, ws, parenttip, revs=None):
    124         self._active = {}
    125         self.ws = ws
    126 
    127         self.revs = revs
    128 
    129         self.base = None
    130         self.parenttip = parenttip
    131 
    132         #
    133         # If we couldn't find a parenttip, the two repositories must
    134         # be unrelated (Hg catches most of this, but this case is valid for it
    135         # but invalid for us)
    136         #
    137         if self.parenttip == None:
    138             raise util.Abort('repository is unrelated')
    139         self.localtip = None
    140 
    141         if revs:
    142             self.base = revs[0]
    143             self.localtip = revs[-1]
    144 
    145         self._comments = []
    146 
    147         self._build(revs)
    148 
    149     def _build(self, revs):
    150         if not revs:
    151             return
    152 
    153         status = self.ws.status(self.parenttip.node(), self.localtip.node())
    154 
    155         files = []
    156         for ctype in status.values():
    157             files.extend(ctype)
    158 
    159         #
    160         # When a file is renamed, two operations actually occur.
    161         # A file copy from source to dest and a removal of source.
    162         #
    163         # These are represented as two distinct entries in the
    164         # changectx and status (one on the dest file for the
    165         # copy, one on the source file for the remove).
    166         #
    167         # Since these are unconnected in both the context and
    168         # status we can only make the association by explicitly
    169         # looking for it.
    170         #
    171         # We deal with this thusly:
    172         #
    173         # We maintain a dict dest -> source of all copies
    174         # (updating dest as appropriate, but leaving source alone).
    175         #
    176         # After all other processing, we mark as renamed any pair
    177         # where source is on the removed list.
    178         #
    179         copies = {}
    180 
    181         #
    182         # Walk revs looking for renames and adding files that
    183         # are in both change context and status to the active
    184         # list.
    185         #
    186         for ctx in revs:
    187             desc = ctx.description().splitlines()
    188 
    189             self._comments.extend(desc)
    190 
    191             for fname in ctx.files():
    192                 #
    193                 # We store comments per-entry as well, for the sake of
    194                 # webrev and similar.  We store twice to avoid the problems
    195                 # of uniquifying comments for the general list (and possibly
    196                 # destroying multi-line entities in the process).
    197                 #
    198                 if fname not in self:
    199                     self._addentry(fname)
    200                 self[fname].comments.extend(desc)
    201 
    202                 try:
    203                     fctx = ctx.filectx(fname)
    204                 except HgLookupError:
    205                     continue
    206 
    207                 #
    208                 # NB: .renamed() is a misnomer, this actually checks
    209                 #     for copies.
    210                 #
    211                 rn = fctx.renamed()
    212                 if rn:
    213                     #
    214                     # If the source file is a known copy we know its
    215                     # ancestry leads us to the parent.
    216                     # Otherwise make sure the source file is known to
    217                     # be in the parent, we need not care otherwise.
    218                     #
    219                     # We detect cycles at a later point.  There is no
    220                     # reason to continuously handle them.
    221                     #
    222                     if rn[0] in copies:
    223                         copies[fname] = copies[rn[0]]
    224                     elif rn[0] in self.parenttip.manifest():
    225                         copies[fname] = rn[0]
    226 
    227         #
    228         # Walk the copy list marking as copied any non-cyclic pair
    229         # where the destination file is still present in the local
    230         # tip (to avoid ephemeral changes)
    231         #
    232         # Where source is removed, mark as renamed, and remove the
    233         # AL entry for the source file
    234         #
    235         for fname, oldname in copies.iteritems():
    236             if fname == oldname or fname not in self.localtip.manifest():
    237                 continue
    238 
    239             self[fname].parentname = oldname
    240 
    241             if oldname in status['removed']:
    242                 self[fname].renamed = True
    243                 if oldname in self:
    244                     del self[oldname]
    245 
    246         #
    247         # Walk the active list setting the change type for each active
    248         # file.
    249         #
    250         # In the case of modified files that are not renames or
    251         # copies, we do a content comparison, and drop entries that
    252         # are not actually modified.
    253         #
    254         # We walk a copy of the AL such that we can drop entries
    255         # within the loop.
    256         #
    257         for entry in self._active.values():
    258             if entry.name not in files:
    259                 del self[entry.name]
    260                 continue
    261 
    262             if entry.name in status['added']:
    263                 entry.change = ActiveEntry.ADDED
    264             elif entry.name in status['removed']:
    265                 entry.change = ActiveEntry.REMOVED
    266             elif entry.name in status['modified']:
    267                 entry.change = ActiveEntry.MODIFIED
    268 
    269             #
    270             # There are cases during a merge where a file will be in
    271             # the status return as modified, but in reality be an
    272             # addition (ie, not in the parenttip).
    273             #
    274             # We need to check whether the file is actually present
    275             # in the parenttip, and set it as an add, if not.
    276             #
    277             if entry.name not in self.parenttip.manifest():
    278                 entry.change = ActiveEntry.ADDED
    279             elif entry.is_modified() and not entry.parentname:
    280                 if not self.filecmp(entry):
    281                     del self[entry.name]
    282                     continue
    283 
    284             assert entry.change
    285 
    286     def __contains__(self, fname):
    287         return fname in self._active
    288 
    289     def __getitem__(self, key):
    290         return self._active[key]
    291 
    292     def __setitem__(self, key, value):
    293         self._active[key] = value
    294 
    295     def __delitem__(self, key):
    296         del self._active[key]
    297 
    298     def __iter__(self):
    299         for entry in self._active.values():
    300             yield entry
    301 
    302     def _addentry(self, fname):
    303         if fname not in self:
    304             self[fname] = ActiveEntry(fname)
    305 
    306     def files(self):
    307         '''Return the list of pathnames of all files touched by this
    308         ActiveList
    309 
    310         Where files have been renamed, this will include both their
    311         current name and the name which they had in the parent tip.
    312         '''
    313 
    314         ret = self._active.keys()
    315         ret.extend([x.parentname for x in self
    316                     if x.is_renamed() and x.parentname not in ret])
    317         return ret
    318 
    319     def comments(self):
    320         return self._comments
    321 
    322     #
    323     # It's not uncommon for a child workspace to itself contain the
    324     # merge of several other children, with initial branch points in
    325     # the parent (possibly from the cset a project gate was created
    326     # from, for instance).
    327     #
    328     # Immediately after recommit, this leaves us looking like this:
    329     #
    330     #     *   <- recommitted changeset (real tip)
    331     #     |
    332     #     | *  <- Local tip
    333     #     |/|
    334     #     * |  <- parent tip
    335     #     | |\
    336     #     | | |
    337     #     | | |\
    338     #     | | | |
    339     #     | * | |  <- Base
    340     #     |/_/__/
    341     #
    342     #     [left-most is parent, next is child, right two being
    343     #     branches in child, intermediate merges parent->child
    344     #     omitted]
    345     #
    346     # Obviously stripping base (the first child-specific delta on the
    347     # main child workspace line) doesn't remove the vestigial branches
    348     # from other workspaces (or in-workspace branches, or whatever)
    349     #
    350     # In reality, what we need to strip in a recommit is any
    351     # child-specific branch descended from the parent (rather than
    352     # another part of the child).  Note that this by its very nature
    353     # includes the branch representing the 'main' child workspace.
    354     #
    355     # We calculate these by walking from base (which is guaranteed to
    356     # be the oldest child-local cset) to localtip searching for
    357     # changesets with only one parent cset, and where that cset is not
    358     # part of the active list (and is therefore outgoing).
    359     #
    360     def bases(self):
    361         '''Find the bases that in combination define the "old"
    362         side of a recommitted set of changes, based on AL'''
    363 
    364         get = util.cachefunc(lambda r: self.ws.repo.changectx(r).changeset())
    365 
    366         # We don't rebuild the AL So the AL local tip is the old tip
    367         revrange = "%s:%s" % (self.base.rev(), self.localtip.rev())
    368 
    369         changeiter = cmdutil.walkchangerevs(self.ws.repo.ui, self.ws.repo,
    370                                             [], get, {'rev': [revrange]})[0]
    371 
    372         hold = []
    373         ret = []
    374         alrevs = [x.rev() for x in self.revs]
    375         for st, rev, fns in changeiter:
    376             n = self.ws.repo.changelog.node(rev)
    377             if st == 'add':
    378                 if rev in alrevs:
    379                     hold.append(n)
    380             elif st == 'iter':
    381                 if n not in hold:
    382                     continue
    383 
    384                 p = self.ws.repo.changelog.parents(n)
    385                 if p[1] != node.nullid:
    386                     continue
    387 
    388                 if self.ws.repo.changectx(p[0]).rev() not in alrevs:
    389                     ret.append(n)
    390         return ret
    391 
    392     def tags(self):
    393         '''Find tags that refer to a changeset in the ActiveList,
    394         returning a list of 3-tuples (tag, node, is_local) for each.
    395 
    396         We return all instances of a tag that refer to such a node,
    397         not just that which takes precedence.'''
    398 
    399         def colliding_tags(iterable, nodes, local):
    400             for nd, name in [line.rstrip().split(' ', 1) for line in iterable]:
    401                 if nd in nodes:
    402                     yield (name, self.ws.repo.lookup(nd), local)
    403 
    404         tags = []
    405         nodes = set(node.hex(ctx.node()) for ctx in self.revs)
    406 
    407         if os.path.exists(self.ws.repo.join('localtags')):
    408             fh = self.ws.repo.opener('localtags')
    409             tags.extend(colliding_tags(fh, nodes, True))
    410             fh.close()
    411 
    412         # We want to use the tags file from the localtip
    413         if '.hgtags' in self.localtip:
    414             data = self.localtip.filectx('.hgtags').data().splitlines()
    415             tags.extend(colliding_tags(data, nodes, False))
    416 
    417         return tags
    418 
    419     def prune_tags(self, data):
    420         '''Return a copy of data, which should correspond to the
    421         contents of a Mercurial tags file, with any tags that refer to
    422         changesets which are components of the ActiveList removed.'''
    423 
    424         nodes = set(node.hex(ctx.node()) for ctx in self.revs)
    425         return [t for t in data if t.split(' ', 1)[0] not in nodes]
    426 
    427     def filecmp(self, entry):
    428         '''Compare two revisions of two files
    429 
    430         Return True if file changed, False otherwise.
    431 
    432         The fast path compares file metadata, slow path is a
    433         real comparison of file content.'''
    434 
    435         parentfile = self.parenttip.filectx(entry.parentname or entry.name)
    436         localfile = self.localtip.filectx(entry.name)
    437 
    438         #
    439         # NB: Keep these ordered such as to make every attempt
    440         #     to short-circuit the more time consuming checks.
    441         #
    442         if parentfile.size() != localfile.size():
    443             return True
    444 
    445         if parentfile.flags() != localfile.flags():
    446             return True
    447 
    448         if parentfile.cmp(localfile.data()):
    449             return True
    450 
    451     def context(self, message, user):
    452         '''Return a Mercurial context object representing the entire
    453         ActiveList as one change.'''
    454         return activectx(self, message, user)
    455 
    456 
    457 class activectx(context.memctx):
    458     '''Represent an ActiveList as a Mercurial context object.
    459 
    460     Part of the  WorkSpace.squishdeltas implementation.'''
    461 
    462     def __init__(self, active, message, user):
    463         '''Build an activectx object.
    464 
    465           active  - The ActiveList object used as the source for all data.
    466           message - Changeset description
    467           user    - Committing user'''
    468 
    469         def filectxfn(repository, ctx, fname):
    470             fctx = active.localtip.filectx(fname)
    471             data = fctx.data()
    472 
    473             #
    474             # .hgtags is a special case, tags referring to active list
    475             # component changesets should be elided.
    476             #
    477             if fname == '.hgtags':
    478                 data = '\n'.join(active.prune_tags(data.splitlines()))
    479 
    480             return context.memfilectx(fname, data, 'l' in fctx.flags(),
    481                                       'x' in fctx.flags(),
    482                                       active[fname].parentname)
    483 
    484         self.__active = active
    485         parents = (active.parenttip.node(), node.nullid)
    486         extra = {'branch': active.localtip.branch()}
    487         context.memctx.__init__(self, active.ws.repo, parents, message,
    488                                 active.files(), filectxfn, user=user,
    489                                 extra=extra)
    490 
    491     def modified(self):
    492         return [entry.name for entry in self.__active if entry.is_modified()]
    493 
    494     def added(self):
    495         return [entry.name for entry in self.__active if entry.is_added()]
    496 
    497     def removed(self):
    498         ret = [entry.name for entry in self.__active if entry.is_removed()]
    499         ret.extend([x.parentname for x in self.__active if x.is_renamed()])
    500         return ret
    501 
    502     def files(self):
    503         return self.__active.files()
    504 
    505 
    506 class WorkSpace(object):
    507 
    508     def __init__(self, repository):
    509         self.repo = repository
    510         self.ui = self.repo.ui
    511         self.name = self.repo.root
    512 
    513         self.activecache = {}
    514 
    515     def parent(self, spec=None):
    516         '''Return the canonical workspace parent, either SPEC (which
    517         will be expanded) if provided or the default parent
    518         otherwise.'''
    519 
    520         if spec:
    521             return self.ui.expandpath(spec)
    522 
    523         p = self.ui.expandpath('default')
    524         if p == 'default':
    525             return None
    526         else:
    527             return p
    528 
    529     def _localtip(self, outgoing, wctx):
    530         '''Return the most representative changeset to act as the
    531         localtip.
    532 
    533         If the working directory is modified (has file changes, is a
    534         merge, or has switched branches), this will be a workingctx.
    535 
    536         If the working directory is unmodified, this will be the most
    537         recent (highest revision number) local (outgoing) head on the
    538         current branch, if no heads are determined to be outgoing, it
    539         will be the most recent head on the current branch.
    540         '''
    541 
    542         #
    543         # A modified working copy is seen as a proto-branch, and thus
    544         # our only option as the local tip.
    545         #
    546         if (wctx.files() or len(wctx.parents()) > 1 or
    547             wctx.branch() != wctx.parents()[0].branch()):
    548             return wctx
    549 
    550         heads = self.repo.heads(start=wctx.parents()[0].node())
    551         headctxs = [self.repo.changectx(n) for n in heads]
    552         localctxs = [c for c in headctxs if c.node() in outgoing]
    553 
    554         ltip = sorted(localctxs or headctxs, key=lambda x: x.rev())[-1]
    555 
    556         if len(heads) > 1:
    557             self.ui.warn('The current branch has more than one head, '
    558                          'using %s\n' % ltip.rev())
    559 
    560         return ltip
    561 
    562     def _parenttip(self, heads, outgoing):
    563         '''Return the highest-numbered, non-outgoing changeset that is
    564         an ancestor of a changeset in heads.
    565 
    566         This is intended to find the most recent changeset on a given
    567         branch that is shared between a parent and child workspace,
    568         such that it can act as a stand-in for the parent workspace.
    569         '''
    570 
    571         def tipmost_shared(head, outnodes):
    572             '''Return the tipmost node on the same branch as head that is not
    573             in outnodes.
    574 
    575             We walk from head to the bottom of the workspace (revision
    576             0) collecting nodes not in outnodes during the add phase
    577             and return the first node we see in the iter phase that
    578             was previously collected.
    579 
    580             If no node is found (all revisions >= 0 are outgoing), the
    581             only possible parenttip is the null node (node.nullid)
    582             which is returned explicitly.
    583 
    584             See the docstring of mercurial.cmdutil.walkchangerevs()
    585             for the phased approach to the iterator returned.  The
    586             important part to note is that the 'add' phase gathers
    587             nodes, which the 'iter' phase then iterates through.'''
    588 
    589             opts = {'rev': ['%s:0' % head.rev()],
    590                     'follow': True}
    591             get = util.cachefunc(lambda r: self.repo.changectx(r).changeset())
    592             changeiter = cmdutil.walkchangerevs(self.repo.ui, self.repo, [],
    593                                                 get, opts)[0]
    594             seen = []
    595             for st, rev, fns in changeiter:
    596                 n = self.repo.changelog.node(rev)
    597                 if st == 'add':
    598                     if n not in outnodes:
    599                         seen.append(n)
    600                 elif st == 'iter':
    601                     if n in seen:
    602                         return rev
    603             return self.repo.changelog.rev(node.nullid)
    604 
    605         nodes = set(outgoing)
    606         ptips = map(lambda x: tipmost_shared(x, nodes), heads)
    607         return self.repo.changectx(sorted(ptips)[-1])
    608 
    609     def status(self, base='.', head=None):
    610         '''Translate from the hg 6-tuple status format to a hash keyed
    611         on change-type'''
    612 
    613         states = ['modified', 'added', 'removed', 'deleted', 'unknown',
    614               'ignored']
    615 
    616         chngs = self.repo.status(base, head)
    617         return dict(zip(states, chngs))
    618 
    619     def findoutgoing(self, parent):
    620         '''Return the base set of outgoing nodes.
    621 
    622         A caching wrapper around mercurial.localrepo.findoutgoing().
    623         Complains (to the user), if the parent workspace is
    624         non-existent or inaccessible'''
    625 
    626         self.ui.pushbuffer()
    627         try:
    628             try:
    629                 ui = self.ui
    630                 if hasattr(cmdutil, 'remoteui'):
    631                     ui = cmdutil.remoteui(ui, {})
    632                 pws = hg.repository(ui, parent)
    633                 return self.repo.findoutgoing(pws)
    634             except HgRepoError:
    635                 self.ui.warn("Warning: Parent workspace '%s' is not "
    636                              "accessible\n"
    637                              "active list will be incomplete\n\n" % parent)
    638                 return []
    639         finally:
    640             self.ui.popbuffer()
    641     findoutgoing = util.cachefunc(findoutgoing)
    642 
    643     def modified(self):
    644         '''Return a list of files modified in the workspace'''
    645         wctx = self.workingctx()
    646         return sorted(wctx.files() + wctx.deleted()) or None
    647 
    648     def merged(self):
    649         '''Return boolean indicating whether the workspace has an uncommitted
    650         merge'''
    651         wctx = self.workingctx()
    652         return len(wctx.parents()) > 1
    653 
    654     def branched(self):
    655         '''Return boolean indicating whether the workspace has an
    656         uncommitted named branch'''
    657 
    658         wctx = self.workingctx()
    659         return wctx.branch() != wctx.parents()[0].branch()
    660 
    661     def active(self, parent=None):
    662         '''Return an ActiveList describing changes between workspace
    663         and parent workspace (including uncommitted changes).
    664         If workspace has no parent ActiveList will still describe any
    665         uncommitted changes'''
    666 
    667         parent = self.parent(parent)
    668         if parent in self.activecache:
    669             return self.activecache[parent]
    670 
    671         if parent:
    672             outgoing = self.findoutgoing(parent)
    673             outnodes = self.repo.changelog.nodesbetween(outgoing)[0]
    674         else:
    675             outgoing = []       # No parent, no outgoing nodes
    676             outnodes = []
    677 
    678         localtip = self._localtip(outnodes, self.workingctx())
    679 
    680         if localtip.rev() is None:
    681             heads = localtip.parents()
    682         else:
    683             heads = [localtip]
    684 
    685         ctxs = [self.repo.changectx(n) for n in
    686                 self.repo.changelog.nodesbetween(outgoing,
    687                                                  [h.node() for h in heads])[0]]
    688 
    689         if localtip.rev() is None:
    690             ctxs.append(localtip)
    691 
    692         act = ActiveList(self, self._parenttip(heads, outnodes), ctxs)
    693 
    694         self.activecache[parent] = act
    695         return act
    696 
    697     def pdiff(self, pats, opts, parent=None):
    698         'Return diffs relative to PARENT, as best as we can make out'
    699 
    700         parent = self.parent(parent)
    701         act = self.active(parent)
    702 
    703         #
    704         # act.localtip maybe nil, in the case of uncommitted local
    705         # changes.
    706         #
    707         if not act.revs:
    708             return
    709 
    710         matchfunc = cmdutil.match(self.repo, pats, opts)
    711         opts = patch.diffopts(self.ui, opts)
    712 
    713         return self.diff(act.parenttip.node(), act.localtip.node(),
    714                          match=matchfunc, opts=opts)
    715 
    716     def squishdeltas(self, active, message, user=None):
    717         '''Create a single conglomerate changeset based on a given
    718         active list.  Removes the original changesets comprising the
    719         given active list, and any tags pointing to them.
    720 
    721         Operation:
    722 
    723           - Commit an activectx object representing the specified
    724             active list,
    725 
    726           - Remove any local tags pointing to changesets in the
    727             specified active list.
    728 
    729           - Remove the changesets comprising the specified active
    730             list.
    731 
    732           - Remove any metadata that may refer to changesets that were
    733             removed.
    734 
    735         Calling code is expected to hold both the working copy lock
    736         and repository lock of the destination workspace
    737         '''
    738 
    739         def strip_local_tags(active):
    740             '''Remove any local tags referring to the specified nodes.'''
    741 
    742             if os.path.exists(self.repo.join('localtags')):
    743                 fh = None
    744                 try:
    745                     fh = self.repo.opener('localtags')
    746                     tags = active.prune_tags(fh)
    747                     fh.close()
    748 
    749                     fh = self.repo.opener('localtags', 'w', atomictemp=True)
    750                     fh.writelines(tags)
    751                     fh.rename()
    752                 finally:
    753                     if fh and not fh.closed:
    754                         fh.close()
    755 
    756         if active.files():
    757             for entry in active:
    758                 #
    759                 # Work around Mercurial issue #1666, if the source
    760                 # file of a rename exists in the working copy
    761                 # Mercurial will complain, and remove the file.
    762                 #
    763                 # We preemptively remove the file to avoid the
    764                 # complaint (the user was asked about this in
    765                 # cdm_recommit)
    766                 #
    767                 if entry.is_renamed():
    768                     path = self.repo.wjoin(entry.parentname)
    769                     if os.path.exists(path):
    770                         os.unlink(path)
    771 
    772             self.repo.commitctx(active.context(message, user))
    773             wsstate = "recommitted"
    774             destination = self.repo.changelog.tip()
    775         else:
    776             #
    777             # If all we're doing is stripping the old nodes, we want to
    778             # update the working copy such that we're not at a revision
    779             # that's about to go away.
    780             #
    781             wsstate = "tip"
    782             destination = active.parenttip.node()
    783 
    784         self.clean(destination)
    785 
    786         #
    787         # Tags were elided by the activectx object.  Local tags,
    788         # however, must be removed manually.
    789         #
    790         try:
    791             strip_local_tags(active)
    792         except EnvironmentError, e:
    793             raise util.Abort('Could not recommit tags: %s\n' % e)
    794 
    795         # Silence all the strip and update fun
    796         self.ui.pushbuffer()
    797 
    798         #
    799         # Remove the active lists component changesets by stripping
    800         # the base of any active branch (of which there may be
    801         # several)
    802         #
    803         bases = active.bases()
    804         try:
    805             try:
    806                 for basenode in bases:
    807                     #
    808                     # Any cached information about the repository is
    809                     # likely to be invalid during the strip.  The
    810                     # caching of branch tags is especially
    811                     # problematic.
    812                     #
    813                     self.repo.invalidate()
    814                     repair.strip(self.ui, self.repo, basenode, backup=False)
    815             except:
    816                 #
    817                 # If this fails, it may leave us in a surprising place in
    818                 # the history.
    819                 #
    820                 # We want to warn the user that something went wrong,
    821                 # and what will happen next, re-raise the exception, and
    822                 # bring the working copy back into a consistent state
    823                 # (which the finally block will do)
    824                 #
    825                 self.ui.warn("stripping failed, your workspace will have "
    826                              "superfluous heads.\n"
    827                              "your workspace has been updated to the "
    828                              "%s changeset.\n" % wsstate)
    829                 raise               # Re-raise the exception
    830         finally:
    831             self.clean()
    832             self.repo.dirstate.write() # Flush the dirstate
    833             self.repo.invalidate()     # Invalidate caches
    834 
    835             #
    836             # We need to remove Hg's undo information (used for rollback),
    837             # since it refers to data that will probably not exist after
    838             # the strip.
    839             #
    840             if os.path.exists(self.repo.sjoin('undo')):
    841                 try:
    842                     os.unlink(self.repo.sjoin('undo'))
    843                 except EnvironmentError, e:
    844                     raise util.Abort('failed to remove undo data: %s\n' % e)
    845 
    846             self.ui.popbuffer()
    847 
    848     def filepath(self, path):
    849         'Return the full path to a workspace file.'
    850         return self.repo.pathto(path)
    851 
    852     def clean(self, rev=None):
    853         '''Bring workspace up to REV (or tip) forcefully (discarding in
    854         progress changes)'''
    855 
    856         if rev != None:
    857             rev = self.repo.lookup(rev)
    858         else:
    859             rev = self.repo.changelog.tip()
    860 
    861         hg.clean(self.repo, rev, show_stats=False)
    862 
    863     def mq_applied(self):
    864         '''True if the workspace has Mq patches applied'''
    865         q = mq.queue(self.ui, self.repo.join(''))
    866         return q.applied
    867 
    868     def workingctx(self):
    869         return self.repo.changectx(None)
    870 
    871     def diff(self, node1=None, node2=None, match=None, opts=None):
    872         ret = cStringIO.StringIO()
    873         try:
    874             for chunk in patch.diff(self.repo, node1, node2, match=match,
    875                                     opts=opts):
    876                 ret.write(chunk)
    877         finally:
    878             # Workaround Hg bug 1651
    879             if not Version.at_least("1.3"):
    880                 self.repo.dirstate.invalidate()
    881 
    882         return ret.getvalue()
    883