xref: /titanic_41/usr/src/tools/onbld/Scm/WorkSpace.py (revision 7c2fbfb345896881c631598ee3852ce9ce33fb07)
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 2008 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 hg, patch, cmdutil, util, node, repo
49from mercurial import revlog, repair
50from hgext import mq
51
52
53class ActiveEntry(object):
54    '''Representation of the changes made to a single file.
55
56    MODIFIED   - Contents changed, but no other changes were made
57    ADDED      - File is newly created
58    REMOVED    - File is being removed
59
60    Copies are represented by an Entry whose .parentname is non-nil
61
62    Truly copied files have non-nil .parentname and .renamed = False
63    Renames have non-nil .parentname and .renamed = True
64
65    Do not access any of this information directly, do so via the
66
67    .is_<change>() methods.'''
68
69    MODIFIED = 1
70    ADDED = 2
71    REMOVED = 3
72
73    def __init__(self, name):
74        self.name = name
75        self.change = None
76        self.parentname = None
77        # As opposed to copied (or neither)
78        self.renamed = False
79        self.comments = []
80
81    #
82    # ActiveEntrys sort by the name of the file they represent.
83    #
84    def __cmp__(self, other):
85        return cmp(self.name, other.name)
86
87    def is_added(self):
88        return self.change == self.ADDED
89
90    def is_modified(self):
91        return self.change == self.MODIFIED
92
93    def is_removed(self):
94        return self.change == self.REMOVED
95
96    def is_renamed(self):
97        return self.parentname and self.renamed
98
99    def is_copied(self):
100        return self.parentname and not self.renamed
101
102
103class ActiveList(object):
104    '''Complete representation of workspace change.
105
106    In practice, a container for ActiveEntrys, and methods to build them,
107    update them, and deal with them en masse.'''
108
109    def __init__(self, ws, parenttip, revs=None):
110        self._active = {}
111        self.ws = ws
112
113        self.revs = revs
114
115        self.base = None
116        self.parenttip = parenttip
117
118        #
119        # If we couldn't find a parenttip, the two repositories must
120        # be unrelated (Hg catches most of this, but this case is valid for it
121        # but invalid for us)
122        #
123        if self.parenttip == None:
124            raise util.Abort('repository is unrelated')
125        self.localtip = None
126
127        if revs:
128            self.base = revs[0]
129            self.localtip = revs[-1]
130
131        self._comments = []
132
133        self._build(revs)
134
135    def _build(self, revs):
136        if not revs:
137            return
138
139        status = self.ws.status(self.parenttip.node(), self.localtip.node())
140
141        files = []
142        for ctype in status.values():
143            files.extend(ctype)
144
145        #
146        # When a file is renamed, two operations actually occur.
147        # A file copy from source to dest and a removal of source.
148        #
149        # These are represented as two distinct entries in the
150        # changectx and status (one on the dest file for the
151        # copy, one on the source file for the remove).
152        #
153        # Since these are unconnected in both the context and
154        # status we can only make the association by explicitly
155        # looking for it.
156        #
157        # We deal with this thusly:
158        #
159        # We maintain a dict dest -> source of all copies
160        # (updating dest as appropriate, but leaving source alone).
161        #
162        # After all other processing, we mark as renamed any pair
163        # where source is on the removed list.
164        #
165        copies = {}
166
167        #
168        # Walk revs looking for renames and adding files that
169        # are in both change context and status to the active
170        # list.
171        #
172        for ctx in revs:
173            desc = ctx.description().splitlines()
174
175            self._comments.extend(desc)
176
177            for fname in ctx.files():
178                #
179                # We store comments per-entry as well, for the sake of
180                # webrev and similar.  We store twice to avoid the problems
181                # of uniquifying comments for the general list (and possibly
182                # destroying multi-line entities in the process).
183                #
184                if fname not in self:
185                    self._addentry(fname)
186                self[fname].comments.extend(desc)
187
188                try:
189                    fctx = ctx.filectx(fname)
190                except revlog.LookupError:
191                    continue
192
193                #
194                # NB: .renamed() is a misnomer, this actually checks
195                #     for copies.
196                #
197                rn = fctx.renamed()
198                if rn:
199                    #
200                    # If the source file is a known copy we know its
201                    # ancestry leads us to the parent.
202                    # Otherwise make sure the source file is known to
203                    # be in the parent, we need not care otherwise.
204                    #
205                    # We detect cycles at a later point.  There is no
206                    # reason to continuously handle them.
207                    #
208                    if rn[0] in copies:
209                        copies[fname] = copies[rn[0]]
210                    elif rn[0] in self.parenttip.manifest():
211                        copies[fname] = rn[0]
212
213        #
214        # Walk the copy list marking as copied any non-cyclic pair
215        # where the destination file is still present in the local
216        # tip (to avoid ephemeral changes)
217        #
218        # Where source is removed, mark as renamed, and remove the
219        # AL entry for the source file
220        #
221        for fname, oldname in copies.iteritems():
222            if fname == oldname or fname not in self.localtip.manifest():
223                continue
224
225            self[fname].parentname = oldname
226
227            if oldname in status['removed']:
228                self[fname].renamed = True
229                if oldname in self:
230                    del self[oldname]
231
232        #
233        # Walk the active list setting the change type for each active
234        # file.
235        #
236        # In the case of modified files that are not renames or
237        # copies, we do a content comparison, and drop entries that
238        # are not actually modified.
239        #
240        # We walk a copy of the AL such that we can drop entries
241        # within the loop.
242        #
243        for entry in self._active.values():
244            if entry.name not in files:
245                del self[entry.name]
246                continue
247
248            if entry.name in status['added']:
249                entry.change = ActiveEntry.ADDED
250            elif entry.name in status['removed']:
251                entry.change = ActiveEntry.REMOVED
252            elif entry.name in status['modified']:
253                entry.change = ActiveEntry.MODIFIED
254
255            #
256            # There are cases during a merge where a file will be in
257            # the status return as modified, but in reality be an
258            # addition (ie, not in the parenttip).
259            #
260            # We need to check whether the file is actually present
261            # in the parenttip, and set it as an add, if not.
262            #
263            if entry.name not in self.parenttip.manifest():
264                entry.change = ActiveEntry.ADDED
265            elif entry.is_modified() and not entry.parentname:
266                if not self.filecmp(entry):
267                    del self[entry.name]
268                    continue
269
270            assert entry.change
271
272    def __contains__(self, fname):
273        return fname in self._active
274
275    def __getitem__(self, key):
276        return self._active[key]
277
278    def __setitem__(self, key, value):
279        self._active[key] = value
280
281    def __delitem__(self, key):
282        del self._active[key]
283
284    def __iter__(self):
285        for entry in self._active.values():
286            yield entry
287
288    def _addentry(self, fname):
289        if fname not in self:
290            self[fname] = ActiveEntry(fname)
291
292    #
293    # Return list of files represented in this AL,
294    # including the parent file of a rename
295    #
296    def files(self):
297        ret = self._active.keys()
298
299        ret.extend([x.parentname for x in self
300                    if x.parentname and x.parentname not in ret])
301        return ret
302
303    def comments(self):
304        return self._comments
305
306    #
307    # It's not uncommon for a child workspace to itself contain the
308    # merge of several other children, with initial branch points in
309    # the parent (possibly from the cset a project gate was created
310    # from, for instance).
311    #
312    # Immediately after recommit, this leaves us looking like this:
313    #
314    #     *   <- recommitted changeset (real tip)
315    #     |
316    #     | *  <- Local tip
317    #     |/|
318    #     * |  <- parent tip
319    #     | |\
320    #     | | |
321    #     | | |\
322    #     | | | |
323    #     | * | |  <- Base
324    #     |/_/__/
325    #
326    #     [left-most is parent, next is child, right two being
327    #     branches in child, intermediate merges parent->child
328    #     omitted]
329    #
330    # Obviously stripping base (the first child-specific delta on the
331    # main child workspace line) doesn't remove the vestigial branches
332    # from other workspaces (or in-workspace branches, or whatever)
333    #
334    # In reality, what we need to strip in a recommit is any
335    # child-specific branch descended from the parent (rather than
336    # another part of the child).  Note that this by its very nature
337    # includes the branch representing the 'main' child workspace.
338    #
339    # We calculate these by walking from base (which is guaranteed to
340    # be the oldest child-local cset) to localtip searching for
341    # changesets with only one parent cset, and where that cset is not
342    # part of the active list (and is therefore outgoing).
343    #
344    def bases(self):
345        '''Find the bases that in combination define the "old"
346        side of a recommitted set of changes, based on AL'''
347
348        get = util.cachefunc(lambda r: self.ws.repo.changectx(r).changeset())
349
350        # We don't rebuild the AL So the AL local tip is the old tip
351        revrange = "%s:%s" % (self.base.rev(), self.localtip.rev())
352
353        changeiter = cmdutil.walkchangerevs(self.ws.repo.ui, self.ws.repo,
354                                            [], get, {'rev': [revrange]})[0]
355
356        hold = []
357        ret = []
358        alrevs = [x.rev() for x in self.revs]
359        for st, rev, fns in changeiter:
360            n = self.ws.repo.changelog.node(rev)
361            if st == 'add':
362                if rev in alrevs:
363                    hold.append(n)
364            elif st == 'iter':
365                if n not in hold:
366                    continue
367
368                p = self.ws.repo.changelog.parents(n)
369                if p[1] != node.nullid:
370                    continue
371
372                if self.ws.repo.changectx(p[0]).rev() not in alrevs:
373                    ret.append(n)
374        return ret
375
376    def tags(self):
377        '''Find tags that refer to a changeset in the ActiveList,
378           returning a list of 3-tuples (tag, node, is_local) for each.
379
380           We return all instances of a tag that refer to such a node,
381           not just that which takes precedence.'''
382
383        if os.path.exists(self.ws.repo.join('localtags')):
384            l = self.ws.repo.opener('localtags').readlines()
385            ltags = [x.rstrip().split(' ') for x in l]
386        else:
387            ltags = []
388
389        # We want to use the tags file from the localtip
390        if '.hgtags' in self.localtip.manifest():
391            f = self.localtip.filectx('.hgtags')
392            rtags = [x.rstrip().split(' ') for x in f.data().splitlines()]
393        else:
394            rtags = []
395
396        nodes = [node.hex(n.node()) for n in self.revs]
397        tags = []
398
399        for nd, name in rtags:
400            if nd in nodes:
401                tags.append((name, self.ws.repo.lookup(nd), False))
402
403        for nd, name in ltags:
404            if nd in nodes:
405                tags.append((name, self.ws.repo.lookup(nd), True))
406
407        return tags
408
409    def filecmp(self, entry):
410        '''Compare two revisions of two files
411
412        Return True if file changed, False otherwise.
413
414        The fast path compares file metadata, slow path is a
415        real comparison of file content.'''
416
417        parentfile = self.parenttip.filectx(entry.parentname or entry.name)
418        localfile = self.localtip.filectx(entry.name)
419
420        #
421        # NB: Keep these ordered such as to make every attempt
422        #     to short-circuit the more time consuming checks.
423        #
424        if parentfile.size() != localfile.size():
425            return True
426
427        if parentfile.fileflags() != localfile.fileflags():
428            return True
429
430        if parentfile.cmp(localfile.data()):
431            return True
432
433
434class WorkSpace(object):
435
436    def __init__(self, repository):
437        self.repo = repository
438        self.ui = self.repo.ui
439        self.name = self.repo.root
440
441        parent = self.repo.ui.expandpath('default')
442        if parent == 'default':
443            parent = None
444        self.parentrepo = parent
445
446        self.activecache = {}
447        self.outgoingcache = {}
448
449    def parent(self, spec=None):
450        '''Return canonical workspace parent, either SPEC if passed,
451        or default parent otherwise'''
452        return spec or self.parentrepo
453
454    def _localtip(self, bases, heads):
455        '''Return a tuple (changectx, workingctx) representing the most
456        representative head to act as the local tip.
457
458        If the working directory is modified, the changectx is its
459        tipmost local parent (or tipmost parent, if neither is
460        local), and the workingctx is non-null.
461
462        If the working directory is clean, the workingctx is null.
463        The changectx is the tip-most local head on the current branch.
464        If this can't be determined for some reason (e.g., the parent
465        repo is inacessible), changectx is the tip-most head on the
466        current branch.
467
468        If the workingctx is non-null it is the actual local tip (and would
469        be the local tip in any generated ActiveList, for instance),
470        the better parent revision is returned also to aid callers needing
471        a real changeset to act as a surrogate for an uncommitted change.'''
472
473        def tipmost_of(nodes):
474            return sorted(nodes, cmp=lambda x, y: cmp(x.rev(), y.rev()))[-1]
475
476        #
477        # We need a full set of outgoing nodes such that we can limit
478        # local branch heads to those which are outgoing
479        #
480        outnodes = self.repo.changelog.nodesbetween(bases, heads)[0]
481        wctx = self.repo.workingctx()
482
483        #
484        # A modified working context is seen as a proto-branch, where
485        # the 'heads' from our view are the parent revisions of that
486        # context.
487        # (and the working head is it)
488        #
489        if (wctx.files() or len(wctx.parents()) > 1 or
490            wctx.branch() != wctx.parents()[0].branch()):
491            heads = wctx.parents()
492        else:
493            heads = [self.repo.changectx(n) for n in heads]
494            wctx = None
495
496        localchoices = [n for n in heads if n.node() in outnodes]
497        return (tipmost_of(localchoices or heads), wctx)
498
499    def _parenttip(self, localtip, parent=None):
500        '''Find the closest approximation of the parents tip, as best
501        as we can.
502
503        In parent-less workspaces returns our tip (given the best
504        we can do is deal with uncommitted changes)'''
505
506        def tipmost_shared(head, outnodes):
507            '''Return the tipmost node on the same branch as head that is not
508            in outnodes.
509
510            We walk from head to the bottom of the workspace (revision
511            0) collecting nodes not in outnodes during the add phase
512            and return the first node we see in the iter phase that
513            was previously collected.
514
515            See the docstring of mercurial.cmdutil.walkchangerevs()
516            for the phased approach to the iterator returned.  The
517            important part to note is that the 'add' phase gathers
518            nodes, which the 'iter' phase then iterates through.'''
519
520            get = util.cachefunc(lambda r: self.repo.changectx(r).changeset())
521            changeiter = cmdutil.walkchangerevs(self.repo.ui, self.repo, [],
522                                                get, {'rev': ['%s:0' % head],
523                                                      'follow': True})[0]
524            seen = []
525            for st, rev, fns in changeiter:
526                n = self.repo.changelog.node(rev)
527                if st == 'add':
528                    if n not in outnodes:
529                        seen.append(n)
530                elif st == 'iter':
531                    if n in seen:
532                        return rev
533            return None
534
535        tipctx, wctx = localtip
536        parent = self.parent(parent)
537        outgoing = None
538
539        if parent:
540            outgoing = self.findoutgoing(parent)
541
542        if wctx:
543            possible_branches = wctx.parents()
544        else:
545            possible_branches = [tipctx]
546
547        nodes = self.repo.changelog.nodesbetween(outgoing)[0]
548        ptips = map(lambda x: tipmost_shared(x.rev(), nodes), possible_branches)
549        return self.repo.changectx(sorted(ptips)[-1])
550
551    def status(self, base=None, head=None):
552        '''Translate from the hg 6-tuple status format to a hash keyed
553        on change-type'''
554        states = ['modified', 'added', 'removed', 'deleted', 'unknown',
555              'ignored']
556        chngs = self.repo.status(base, head)
557        return dict(zip(states, chngs))
558
559    #
560    # Cache findoutgoing results
561    #
562    def findoutgoing(self, parent):
563        ret = []
564        if parent in self.outgoingcache:
565            ret = self.outgoingcache[parent]
566        else:
567            self.ui.pushbuffer()
568            try:
569                pws = hg.repository(self.ui, parent)
570                ret = self.repo.findoutgoing(pws)
571            except repo.RepoError:
572                self.ui.warn(
573                    "Warning: Parent workspace %s is not accessible\n" % parent)
574                self.ui.warn("active list will be incomplete\n\n")
575
576            self.outgoingcache[parent] = ret
577            self.ui.popbuffer()
578
579        return ret
580
581    def modified(self):
582        '''Return a list of files modified in the workspace'''
583        wctx = self.repo.workingctx()
584        return sorted(wctx.files() + wctx.deleted()) or None
585
586    def merged(self):
587        '''Return boolean indicating whether the workspace has an uncommitted
588        merge'''
589        wctx = self.repo.workingctx()
590        return len(wctx.parents()) > 1
591
592    def branched(self):
593        '''Return boolean indicating whether the workspace has an
594        uncommitted named branch'''
595
596        wctx = self.repo.workingctx()
597        return wctx.branch() != wctx.parents()[0].branch()
598
599    def active(self, parent=None):
600        '''Return an ActiveList describing changes between workspace
601        and parent workspace (including uncommitted changes).
602        If workspace has no parent ActiveList will still describe any
603        uncommitted changes'''
604
605        parent = self.parent(parent)
606        if parent in self.activecache:
607            return self.activecache[parent]
608
609        if parent:
610            outgoing = self.findoutgoing(parent)
611        else:
612            outgoing = []       # No parent, no outgoing nodes
613
614        branchheads = self.repo.heads(start=self.repo.dirstate.parents()[0])
615        ourhead, workinghead = self._localtip(outgoing, branchheads)
616
617        if len(branchheads) > 1:
618            self.ui.warn('The current branch has more than one head, '
619                         'using %s\n' % ourhead.rev())
620
621        if workinghead:
622            parents = workinghead.parents()
623            ctxs = [self.repo.changectx(n) for n in
624                    self.repo.changelog.nodesbetween(outgoing,
625                                                     [h.node() for h in
626                                                      parents])[0]]
627            ctxs.append(workinghead)
628        else:
629            ctxs = [self.repo.changectx(n) for n in
630                    self.repo.changelog.nodesbetween(outgoing,
631                                                     [ourhead.node()])[0]]
632
633        act = ActiveList(self, self._parenttip((ourhead, workinghead), parent),
634                         ctxs)
635
636        self.activecache[parent] = act
637        return act
638
639    def pdiff(self, pats, opts, parent=None):
640        'Return diffs relative to PARENT, as best as we can make out'
641
642        parent = self.parent(parent)
643        act = self.active(parent)
644
645        #
646        # act.localtip maybe nil, in the case of uncommitted local
647        # changes.
648        #
649        if not act.revs:
650            return
651
652        names, match = cmdutil.matchpats(self.repo, pats, opts)[:2]
653        opts = patch.diffopts(self.ui, opts)
654
655        ret = cStringIO.StringIO()
656        patch.diff(self.repo, act.parenttip.node(), act.localtip.node(),
657                   names, fp=ret, opts=opts, match=match)
658        return ret.getvalue()
659
660    #
661    # Theory:
662    #
663    # We wish to go from a single series of consecutive changesets
664    # (possibly including merges with the parent) to a single
665    # changeset faithfully representing contents and copy history.
666    #
667    # We achieve this in a somewhat confusing fashion.
668    #
669    # - Sanity check the workspace
670    # - Update the workspace to tip
671    # - Enter into the dirstate the sum total of file contents in the
672    #   to-be-squished changesets
673    # - Commit this in-progress change (which has no changes at all,
674    #   in reality) On top of the effective parent tip.
675    # - Strip the child-local branch(es) (see ActiveList.bases())
676    #
677    def squishdeltas(self, active, message, user=None):
678        '''Create a single conglomerate changeset, with log message MESSAGE
679        containing the changes from ACTIVE.  USER, if set, is used
680        as the author name.
681
682        The old changes are removed.'''
683
684        def strip_tags(nodes):
685            '''Remove any tags referring to the specified nodes.'''
686
687            if os.path.exists(self.repo.join('localtags')):
688                fh = self.repo.opener('localtags').readlines()
689                tags = [t for t in fh if t.split(' ')[0] not in nodes]
690                fh = self.repo.opener('localtags', 'w', atomictemp=True)
691                fh.writelines(tags)
692                fh.rename()
693
694            if os.path.exists(self.repo.wjoin('.hgtags')):
695                fh = self.repo.wopener('.hgtags', 'rb').readlines()
696                tags = [t for t in fh if t.split(' ')[0] not in nodes]
697                fh = self.repo.wopener('.hgtags', 'wb', atomictemp=True)
698                fh.writelines(tags)
699                fh.rename()
700
701        wlock = self.repo.wlock()
702        lock = self.repo.lock()
703
704        #
705        # The files involved need to be present in the workspace and
706        # not otherwise molested, rather than the workspace not being
707        # modified we also need to prevent files being deleted (but
708        # left versioned) too.
709        #
710        # The easiest way to achieve this is to update the working
711        # copy to tip.
712        #
713        self.clean()
714
715        try:
716            strip_tags([node.hex(ctx.node()) for ctx in active.revs])
717        except EnvironmentError, e:
718            raise util.Abort('Could not recommit tags: %s\n' % e)
719
720        #
721        # For copied files, we need to enter the copy into the
722        # dirstate before we force the commit such that the
723        # file logs of both branches (old and new) contain
724        # representation of the copy.
725        #
726        parentman = active.parenttip.manifest()
727        for entry in active:
728            if not entry.is_renamed() and not entry.is_copied():
729                continue
730
731            assert entry.parentname in parentman, \
732                ("parentname '%s' (of '%s') not in parent" %
733                 (entry.parentname, entry.name))
734
735            #
736            # If the source file exists, and used to be versioned
737            # this will cause this to become a true copy
738            # (re-introducing the source file)
739            #
740            # We bandaid this, by removing the source file in this
741            # case.  If we're here, the user has already agreed to this
742            # from above.
743            #
744            if (entry.is_renamed() and
745                os.path.exists(self.repo.wjoin(entry.parentname))):
746                os.unlink(self.repo.wjoin(entry.parentname))
747
748            self.repo.copy(entry.parentname, entry.name)
749
750        if active.files():
751            extra = {'branch': active.localtip.branch()}
752            self.repo.commit(files=active.files(), text=message,
753                             user=user, p1=active.parenttip.node(), p2=None,
754                             extra=extra)
755            wsstate = "recommitted changeset"
756            self.clean()
757        else:
758            #
759            # If all we're doing is stripping the old nodes, we want to
760            # update the working copy such that we're not at a revision
761            # that's about to go away.
762            #
763            wsstate = "tip changeset"
764            self.clean(rev=active.parenttip.node())
765
766        # Silence all the strip and update fun
767        self.ui.pushbuffer()
768
769        #
770        # We must strip away the old representation of the child
771        # branch(es).  This may involve stripping a theoretically
772        # large number of branches in certain cases
773        #
774        bases = active.bases()
775        try:
776            try:
777                for basenode in bases:
778                    repair.strip(self.ui, self.repo, basenode, backup=False)
779            except:
780                #
781                # If this fails, it may leave us in a surprising place in
782                # the history.
783                #
784                # We want to warn the user that something went wrong,
785                # and what will happen next, re-raise the exception, and
786                # bring the working copy back into a consistent state
787                # (which the finally block will do)
788                #
789                self.ui.warn("stripping failed, your workspace will have "
790                             "superfluous heads.\n"
791                             "your workspace has been updated to the "
792                             "%s.\n" % wsstate)
793                raise               # Re-raise the exception
794        finally:
795            #
796            # We need to remove Hg's undo information (used for rollback),
797            # since it refers to data that will probably not exist after
798            # the strip.
799            #
800
801            self.clean()
802            self.repo.dirstate.write() # Flush the dirstate
803            self.repo.invalidate()     # Invalidate caches
804
805            if os.path.exists(self.repo.sjoin('undo')):
806                try:
807                    os.unlink(self.repo.sjoin('undo'))
808                except EnvironmentError, e:
809                    raise util.Abort('failed to remove undo data: %s\n' % e)
810
811            self.ui.popbuffer()
812
813    def filepath(self, path):
814        'Return the full path to a workspace file.'
815        return self.repo.pathto(path)
816
817    def clean(self, rev=None):
818        '''Bring workspace up to REV (or tip) forcefully (discarding in
819        progress changes)'''
820        if rev != None:
821            rev = self.repo.lookup(rev)
822        else:
823            rev = self.repo.changelog.tip()
824
825        wlock = self.repo.wlock()
826        hg.clean(self.repo, rev, show_stats=False)
827
828    def mq_applied(self):
829        '''True if the workspace has Mq patches applied'''
830        q = mq.queue(self.ui, self.repo.join(''))
831        return q.applied
832