xref: /titanic_41/usr/src/tools/onbld/Scm/WorkSpace.py (revision 4df55fde49134f9735f84011f23a767c75e393c7)
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
46import cStringIO
47import os
48from mercurial import cmdutil, context, hg, node, patch, repair, util
49from hgext import mq
50
51from 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#
57if Version.at_least("1.2"):
58    from mercurial import error
59    HgRepoError = error.RepoError
60    HgLookupError = error.LookupError
61else:
62    from mercurial import repo, revlog
63    HgRepoError = repo.RepoError
64    HgLookupError = revlog.LookupError
65
66
67class 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
117class 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
457class 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
506class 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