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