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