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