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