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 (c) 2008, 2010, Oracle and/or its affiliates. All rights reserved.
  18 # Copyright 2008, 2011, Richard Lowe
  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 of ActiveEntry objects, can thus
  41 # present the entire change in workspace state between a parent and
  42 # its child and is the important bit here (in that if it is incorrect,
  43 # everything else will be as incorrect, or more)
  44 #
  45 
  46 import cStringIO
  47 import os
  48 from mercurial import cmdutil, context, error, hg, node, patch, repair, util
  49 from hgext import mq
  50 
  51 from onbld.Scm import Version
  52 
  53 
  54 #
  55 # Mercurial 1.6 moves findoutgoing into a discover module
  56 #
  57 if Version.at_least("1.6"):
  58     from mercurial import discovery
  59 
  60 
  61 class ActiveEntry(object):
  62     '''Representation of the changes made to a single file.
  63 
  64     MODIFIED   - Contents changed, but no other changes were made
  65     ADDED      - File is newly created
  66     REMOVED    - File is being removed
  67 
  68     Copies are represented by an Entry whose .parentname is non-nil
  69 
  70     Truly copied files have non-nil .parentname and .renamed = False
  71     Renames have non-nil .parentname and .renamed = True
  72 
  73     Do not access any of this information directly, do so via the
  74 
  75     .is_<change>() methods.'''
  76 
  77     MODIFIED = intern('modified')
  78     ADDED = intern('added')
  79     REMOVED = intern('removed')
  80 
  81     def __init__(self, name, change):
  82         self.name = name
  83         self.change = intern(change)
  84 
  85         assert change in (self.MODIFIED, self.ADDED, self.REMOVED)
  86 
  87         self.parentname = None
  88         # As opposed to copied (or neither)
  89         self.renamed = False
  90         self.comments = []
  91 
  92     def __cmp__(self, other):
  93         return cmp(self.name, other.name)
  94 
  95     def is_added(self):
  96         '''Return True if this ActiveEntry represents an added file'''
  97         return self.change is self.ADDED
  98 
  99     def is_modified(self):
 100         '''Return True if this ActiveEntry represents a modified file'''
 101         return self.change is self.MODIFIED
 102 
 103     def is_removed(self):
 104         '''Return True if this ActiveEntry represents a removed file'''
 105         return self.change is self.REMOVED
 106 
 107     def is_renamed(self):
 108         '''Return True if this ActiveEntry represents a renamed file'''
 109         return self.parentname and self.renamed
 110 
 111     def is_copied(self):
 112         '''Return True if this ActiveEntry represents a copied file'''
 113         return self.parentname and not self.renamed
 114 
 115 
 116 class ActiveList(object):
 117     '''Complete representation of change between two changesets.
 118 
 119     In practice, a container for ActiveEntry objects, and methods to
 120     create them, and deal with them as a group.'''
 121 
 122     def __init__(self, ws, parenttip, revs=None):
 123         '''Initialize the ActiveList
 124 
 125         parenttip is the revision with which to compare (likely to be
 126         from the parent), revs is a topologically sorted list of
 127         revisions ending with the revision to compare with (likely to
 128         be the child-local revisions).'''
 129 
 130         assert parenttip is not None
 131 
 132         self.ws = ws
 133         self.revs = revs
 134         self.parenttip = parenttip
 135         self.localtip = None
 136 
 137         self._active = {}
 138         self._comments = []
 139 
 140         if revs:
 141             self.localtip = revs[-1]
 142             self._build()
 143 
 144     def _status(self):
 145         '''Return the status of any file mentioned in any of the
 146         changesets making up this active list.'''
 147 
 148         files = set()
 149         for c in self.revs:
 150             files.update(c.files())
 151 
 152         #
 153         # Any file not in the parenttip or the localtip is ephemeral
 154         # and can be ignored. Mercurial will complain regarding these
 155         # files if the localtip is a workingctx, so remove them in
 156         # that case.
 157         #
 158         # Compare against the dirstate because a workingctx manifest
 159         # is created on-demand and is particularly expensive.
 160         #
 161         if self.localtip.rev() is None:
 162             for f in files.copy():
 163                 if f not in self.parenttip and f not in self.ws.repo.dirstate:
 164                     files.remove(f)
 165 
 166         return self.ws.status(self.parenttip, self.localtip, files=files)
 167 
 168     def _build(self):
 169         '''Construct ActiveEntry objects for each changed file.
 170 
 171         This works in 3 stages:
 172 
 173           - Create entries for every changed file with
 174             semi-appropriate change type
 175 
 176           - Track renames/copies, and set change comments (both
 177             ActiveList-wide, and per-file).
 178 
 179           - Cleanup
 180             - Drop circular renames
 181             - Drop the removal of the old name of any rename
 182             - Drop entries for modified files that haven't actually changed'''
 183 
 184         #
 185         # Keep a cache of filectx objects (keyed on pathname) so that
 186         # we can avoid opening filelogs numerous times.
 187         #
 188         fctxcache = {}
 189 
 190         def oldname(ctx, fname):
 191             '''Return the name 'fname' held prior to any possible
 192             rename/copy in the given changeset.'''
 193             try:
 194                 if fname in fctxcache:
 195                     octx = fctxcache[fname]
 196                     fctx = ctx.filectx(fname, filelog=octx.filelog())
 197                 else:
 198                     fctx = ctx.filectx(fname)
 199                     #
 200                     # workingfilectx objects may not refer to the
 201                     # right filelog (in case of rename).  Don't cache
 202                     # them.
 203                     #
 204                     if not isinstance(fctx, context.workingfilectx):
 205                         fctxcache[fname] = fctx
 206             except error.LookupError:
 207                 return None
 208 
 209             rn = fctx.renamed()
 210             return rn and rn[0] or fname
 211 
 212         status = self._status()
 213         self._active = dict((fname, ActiveEntry(fname, kind))
 214                             for fname, kind in status.iteritems()
 215                             if kind in ('modified', 'added', 'removed'))
 216 
 217         #
 218         # We do two things:
 219         #    - Gather checkin comments (for the entire ActiveList, and
 220         #      per-file)
 221         #    - Set the .parentname of any copied/renamed file
 222         #
 223         # renames/copies:
 224         #   We walk the list of revisions backward such that only files
 225         #   that ultimately remain active need be considered.
 226         #
 227         #   At each iteration (revision) we update the .parentname of
 228         #   any active file renamed or copied in that revision (the
 229         #   current .parentname if set, or .name otherwise, reflects
 230         #   the name of a given active file in the revision currently
 231         #   being looked at)
 232         #
 233         for ctx in reversed(self.revs):
 234             desc = ctx.description().splitlines()
 235             self._comments = desc + self._comments
 236             cfiles = set(ctx.files())
 237 
 238             for entry in self:
 239                 fname = entry.parentname or entry.name
 240                 if fname not in cfiles:
 241                     continue
 242 
 243                 entry.comments = desc + entry.comments
 244 
 245                 #
 246                 # We don't care about the name history of any file
 247                 # that ends up being removed, since that trumps any
 248                 # possible renames or copies along the way.
 249                 #
 250                 # Changes that we may care about involving an
 251                 # intermediate name of a removed file will appear
 252                 # separately (related to the eventual name along
 253                 # that line)
 254                 #
 255                 if not entry.is_removed():
 256                     entry.parentname = oldname(ctx, fname)
 257 
 258         for entry in self._active.values():
 259             #
 260             # For any file marked as copied or renamed, clear the
 261             # .parentname if the copy or rename is cyclic (source ==
 262             # destination) or if the .parentname did not exist in the
 263             # parenttip.
 264             #
 265             # If the parentname is marked as removed, set the renamed
 266             # flag and remove any ActiveEntry we may have for the
 267             # .parentname.
 268             #
 269             if entry.parentname:
 270                 if (entry.parentname == entry.name or
 271                     entry.parentname not in self.parenttip):
 272                     entry.parentname = None
 273                 elif status.get(entry.parentname) == 'removed':
 274                     entry.renamed = True
 275 
 276                     if entry.parentname in self:
 277                         del self[entry.parentname]
 278 
 279             #
 280             # There are cases during a merge where a file will be seen
 281             # as modified by status but in reality be an addition (not
 282             # in the parenttip), so we have to check whether the file
 283             # is in the parenttip and set it as an addition, if not.
 284             #
 285             # If a file is modified (and not a copy or rename), we do
 286             # a full comparison to the copy in the parenttip and
 287             # ignore files that are parts of active revisions but
 288             # unchanged.
 289             #
 290             if entry.name not in self.parenttip:
 291                 entry.change = ActiveEntry.ADDED
 292             elif entry.is_modified():
 293                 if not self._changed_file(entry.name):
 294                     del self[entry.name]
 295 
 296     def __contains__(self, fname):
 297         return fname in self._active
 298 
 299     def __getitem__(self, key):
 300         return self._active[key]
 301 
 302     def __setitem__(self, key, value):
 303         self._active[key] = value
 304 
 305     def __delitem__(self, key):
 306         del self._active[key]
 307 
 308     def __iter__(self):
 309         return self._active.itervalues()
 310 
 311     def files(self):
 312         '''Return the list of pathnames of all files touched by this
 313         ActiveList
 314 
 315         Where files have been renamed, this will include both their
 316         current name and the name which they had in the parent tip.
 317         '''
 318 
 319         ret = self._active.keys()
 320         ret.extend(x.parentname for x in self if x.is_renamed())
 321         return set(ret)
 322 
 323     def comments(self):
 324         '''Return the full set of changeset comments associated with
 325         this ActiveList'''
 326 
 327         return self._comments
 328 
 329     def bases(self):
 330         '''Return the list of changesets that are roots of the ActiveList.
 331 
 332         This is the set of active changesets where neither parent
 333         changeset is itself active.'''
 334 
 335         revset = set(self.revs)
 336         return filter(lambda ctx: not [p for p in ctx.parents() if p in revset],
 337                       self.revs)
 338 
 339     def tags(self):
 340         '''Find tags that refer to a changeset in the ActiveList,
 341         returning a list of 3-tuples (tag, node, is_local) for each.
 342 
 343         We return all instances of a tag that refer to such a node,
 344         not just that which takes precedence.'''
 345 
 346         def colliding_tags(iterable, nodes, local):
 347             for nd, name in [line.rstrip().split(' ', 1) for line in iterable]:
 348                 if nd in nodes:
 349                     yield (name, self.ws.repo.lookup(nd), local)
 350 
 351         tags = []
 352         nodes = set(node.hex(ctx.node()) for ctx in self.revs)
 353 
 354         if os.path.exists(self.ws.repo.join('localtags')):
 355             fh = self.ws.repo.opener('localtags')
 356             tags.extend(colliding_tags(fh, nodes, True))
 357             fh.close()
 358 
 359         # We want to use the tags file from the localtip
 360         if '.hgtags' in self.localtip:
 361             data = self.localtip.filectx('.hgtags').data().splitlines()
 362             tags.extend(colliding_tags(data, nodes, False))
 363 
 364         return tags
 365 
 366     def prune_tags(self, data):
 367         '''Return a copy of data, which should correspond to the
 368         contents of a Mercurial tags file, with any tags that refer to
 369         changesets which are components of the ActiveList removed.'''
 370 
 371         nodes = set(node.hex(ctx.node()) for ctx in self.revs)
 372         return [t for t in data if t.split(' ', 1)[0] not in nodes]
 373 
 374     def _changed_file(self, path):
 375         '''Compare the parent and local versions of a given file.
 376         Return True if file changed, False otherwise.
 377 
 378         Note that this compares the given path in both versions, not the given
 379         entry; renamed and copied files are compared by name, not history.
 380 
 381         The fast path compares file metadata, slow path is a
 382         real comparison of file content.'''
 383 
 384         if ((path in self.parenttip) != (path in self.localtip)):
 385             return True
 386 
 387         parentfile = self.parenttip.filectx(path)
 388         localfile = self.localtip.filectx(path)
 389 
 390         #
 391         # NB: Keep these ordered such as to make every attempt
 392         #     to short-circuit the more time consuming checks.
 393         #
 394         if parentfile.size() != localfile.size():
 395             return True
 396 
 397         if parentfile.flags() != localfile.flags():
 398             return True
 399 
 400         if Version.at_least("1.7"):
 401             if parentfile.cmp(localfile):
 402                 return True
 403         else:
 404             if parentfile.cmp(localfile.data()):
 405                 return True
 406 
 407     def context(self, message, user):
 408         '''Return a Mercurial context object representing the entire
 409         ActiveList as one change.'''
 410         return activectx(self, message, user)
 411 
 412     def as_text(self, paths):
 413         '''Return the ActiveList as a block of text in a format
 414         intended to aid debugging and simplify the test suite.
 415 
 416         paths should be a list of paths for which file-level data
 417         should be included.  If it is empty, the whole active list is
 418         included.'''
 419 
 420         cstr = cStringIO.StringIO()
 421 
 422         cstr.write('parent tip: %s:%s\n' % (self.parenttip.rev(),
 423                                             self.parenttip))
 424         if self.localtip:
 425             rev = self.localtip.rev()
 426             cstr.write('local tip:  %s:%s\n' %
 427                        (rev is None and "working" or rev, self.localtip))
 428         else:
 429             cstr.write('local tip:  None\n')
 430 
 431         cstr.write('entries:\n')
 432         for entry in self:
 433             if paths and self.ws.filepath(entry.name) not in paths:
 434                 continue
 435 
 436             cstr.write('  - %s\n' % entry.name)
 437             cstr.write('    parentname: %s\n' % entry.parentname)
 438             cstr.write('    change: %s\n' % entry.change)
 439             cstr.write('    renamed: %s\n' % entry.renamed)
 440             cstr.write('    comments:\n')
 441             cstr.write('      ' + '\n      '.join(entry.comments) + '\n')
 442             cstr.write('\n')
 443 
 444         return cstr.getvalue()
 445 
 446 
 447 class WorkList(object):
 448     '''A (user-maintained) list of files changed in this workspace as
 449     compared to any parent workspace.
 450 
 451     Internally, the WorkList is stored in .hg/cdm/worklist as a list
 452     of file pathnames, one per-line.
 453 
 454     This may only safely be used as a hint regarding possible
 455     modifications to the working copy, it should not be relied upon to
 456     suggest anything about committed changes.'''
 457 
 458     def __init__(self, ws):
 459         '''Load the WorkList for the specified WorkSpace from disk.'''
 460 
 461         self._ws = ws
 462         self._repo = ws.repo
 463         self._file = os.path.join('cdm', 'worklist')
 464         self._files = set()
 465         self._valid = False
 466 
 467         if os.path.exists(self._repo.join(self._file)):
 468             self.load()
 469 
 470     def __nonzero__(self):
 471         '''A WorkList object is true if it was loaded from disk,
 472         rather than freshly created.
 473         '''
 474 
 475         return self._valid
 476 
 477     def list(self):
 478         '''List of pathnames contained in the WorkList
 479         '''
 480 
 481         return list(self._files)
 482 
 483     def status(self):
 484         '''Return the status (in tuple form) of files from the
 485         WorkList as they are in the working copy
 486         '''
 487 
 488         match = self._ws.matcher(files=self.list())
 489         return self._repo.status(match=match)
 490 
 491     def add(self, fname):
 492         '''Add FNAME to the WorkList.
 493         '''
 494 
 495         self._files.add(fname)
 496 
 497     def write(self):
 498         '''Write the WorkList out to disk.
 499         '''
 500 
 501         dirn = os.path.split(self._file)[0]
 502 
 503         if dirn and not os.path.exists(self._repo.join(dirn)):
 504             try:
 505                 os.makedirs(self._repo.join(dirn))
 506             except EnvironmentError, e:
 507                 raise util.Abort("Couldn't create directory %s: %s" %
 508                                  (self._repo.join(dirn), e))
 509 
 510         fh = self._repo.opener(self._file, 'w', atomictemp=True)
 511 
 512         for name in self._files:
 513             fh.write("%s\n" % name)
 514 
 515         fh.rename()
 516         fh.close()
 517 
 518     def load(self):
 519         '''Read in the WorkList from disk.
 520         '''
 521 
 522         fh = self._repo.opener(self._file, 'r')
 523         self._files = set(l.rstrip('\n') for l in fh)
 524         self._valid = True
 525         fh.close()
 526 
 527     def delete(self):
 528         '''Empty the WorkList
 529 
 530         Remove the on-disk WorkList and clear the file-list of the
 531         in-memory copy
 532         '''
 533 
 534         if os.path.exists(self._repo.join(self._file)):
 535             os.unlink(self._repo.join(self._file))
 536 
 537         self._files = set()
 538         self._valid = False
 539 
 540 
 541 class activectx(context.memctx):
 542     '''Represent an ActiveList as a Mercurial context object.
 543 
 544     Part of the  WorkSpace.squishdeltas implementation.'''
 545 
 546     def __init__(self, active, message, user):
 547         '''Build an activectx object.
 548 
 549           active  - The ActiveList object used as the source for all data.
 550           message - Changeset description
 551           user    - Committing user'''
 552 
 553         def filectxfn(repository, ctx, fname):
 554             fctx = active.localtip.filectx(fname)
 555             data = fctx.data()
 556 
 557             #
 558             # .hgtags is a special case, tags referring to active list
 559             # component changesets should be elided.
 560             #
 561             if fname == '.hgtags':
 562                 data = '\n'.join(active.prune_tags(data.splitlines()))
 563 
 564             return context.memfilectx(fname, data, 'l' in fctx.flags(),
 565                                       'x' in fctx.flags(),
 566                                       active[fname].parentname)
 567 
 568         self.__active = active
 569         parents = (active.parenttip.node(), node.nullid)
 570         extra = {'branch': active.localtip.branch()}
 571         context.memctx.__init__(self, active.ws.repo, parents, message,
 572                                 active.files(), filectxfn, user=user,
 573                                 extra=extra)
 574 
 575     def modified(self):
 576         return [entry.name for entry in self.__active if entry.is_modified()]
 577 
 578     def added(self):
 579         return [entry.name for entry in self.__active if entry.is_added()]
 580 
 581     def removed(self):
 582         ret = set(entry.name for entry in self.__active if entry.is_removed())
 583         ret.update(set(x.parentname for x in self.__active if x.is_renamed()))
 584         return list(ret)
 585 
 586     def files(self):
 587         return self.__active.files()
 588 
 589 
 590 class WorkSpace(object):
 591 
 592     def __init__(self, repository):
 593         self.repo = repository
 594         self.ui = self.repo.ui
 595         self.name = self.repo.root
 596 
 597         self.activecache = {}
 598 
 599     def parent(self, spec=None):
 600         '''Return the canonical workspace parent, either SPEC (which
 601         will be expanded) if provided or the default parent
 602         otherwise.'''
 603 
 604         if spec:
 605             return self.ui.expandpath(spec)
 606 
 607         p = self.ui.expandpath('default')
 608         if p == 'default':
 609             return None
 610         else:
 611             return p
 612 
 613     def _localtip(self, outgoing, wctx):
 614         '''Return the most representative changeset to act as the
 615         localtip.
 616 
 617         If the working directory is modified (has file changes, is a
 618         merge, or has switched branches), this will be a workingctx.
 619 
 620         If the working directory is unmodified, this will be the most
 621         recent (highest revision number) local (outgoing) head on the
 622         current branch, if no heads are determined to be outgoing, it
 623         will be the most recent head on the current branch.
 624         '''
 625 
 626         if (wctx.files() or len(wctx.parents()) > 1 or
 627             wctx.branch() != wctx.parents()[0].branch()):
 628             return wctx
 629 
 630         heads = self.repo.heads(start=wctx.parents()[0].node())
 631         headctxs = [self.repo.changectx(n) for n in heads]
 632         localctxs = [c for c in headctxs if c.node() in outgoing]
 633 
 634         ltip = sorted(localctxs or headctxs, key=lambda x: x.rev())[-1]
 635 
 636         if len(heads) > 1:
 637             self.ui.warn('The current branch has more than one head, '
 638                          'using %s\n' % ltip.rev())
 639 
 640         return ltip
 641 
 642     def parenttip(self, heads, outgoing):
 643         '''Return the highest-numbered, non-outgoing changeset that is
 644         an ancestor of a changeset in heads.
 645 
 646         This returns the most recent changeset on a given branch that
 647         is shared between a parent and child workspace, in effect the
 648         common ancestor of the chosen local tip and the parent
 649         workspace.
 650         '''
 651 
 652         def tipmost_shared(head, outnodes):
 653             '''Return the changeset on the same branch as head that is
 654             not in outnodes and is closest to the tip.
 655 
 656             Walk outgoing changesets from head to the bottom of the
 657             workspace (revision 0) and return the the first changeset
 658             we see that is not in outnodes.
 659 
 660             If none is found (all revisions >= 0 are outgoing), the
 661             only possible parenttip is the null node (node.nullid)
 662             which is returned explicitly.
 663             '''
 664             for ctx in self._walkctxs(head, self.repo.changectx(0),
 665                                       follow=True,
 666                                       pick=lambda c: c.node() not in outnodes):
 667                 return ctx
 668 
 669             return self.repo.changectx(node.nullid)
 670 
 671         nodes = set(outgoing)
 672         ptips = map(lambda x: tipmost_shared(x, nodes), heads)
 673         return sorted(ptips, key=lambda x: x.rev(), reverse=True)[0]
 674 
 675     def status(self, base='.', head=None, files=None):
 676         '''Translate from the hg 6-tuple status format to a hash keyed
 677         on change-type'''
 678 
 679         states = ['modified', 'added', 'removed', 'deleted', 'unknown',
 680                   'ignored']
 681 
 682         match = self.matcher(files=files)
 683         chngs = self.repo.status(base, head, match=match)
 684 
 685         ret = {}
 686         for paths, change in zip(chngs, states):
 687             ret.update((f, change) for f in paths)
 688         return ret
 689 
 690     def findoutgoing(self, parent):
 691         '''Return the base set of outgoing nodes.
 692 
 693         A caching wrapper around mercurial.localrepo.findoutgoing().
 694         Complains (to the user), if the parent workspace is
 695         non-existent or inaccessible'''
 696 
 697         self.ui.pushbuffer()
 698         try:
 699             try:
 700                 ui = self.ui
 701                 if hasattr(cmdutil, 'remoteui'):
 702                     ui = cmdutil.remoteui(ui, {})
 703                 pws = hg.repository(ui, parent)
 704                 if Version.at_least("1.6"):
 705                     return discovery.findoutgoing(self.repo, pws)
 706                 else:
 707                     return self.repo.findoutgoing(pws)
 708             except error.RepoError:
 709                 self.ui.warn("Warning: Parent workspace '%s' is not "
 710                              "accessible\n"
 711                              "active list will be incomplete\n\n" % parent)
 712                 return []
 713         finally:
 714             self.ui.popbuffer()
 715     findoutgoing = util.cachefunc(findoutgoing)
 716 
 717     def modified(self):
 718         '''Return a list of files modified in the workspace'''
 719 
 720         wctx = self.workingctx()
 721         return sorted(wctx.files() + wctx.deleted()) or None
 722 
 723     def merged(self):
 724         '''Return boolean indicating whether the workspace has an uncommitted
 725         merge'''
 726 
 727         wctx = self.workingctx()
 728         return len(wctx.parents()) > 1
 729 
 730     def branched(self):
 731         '''Return boolean indicating whether the workspace has an
 732         uncommitted named branch'''
 733 
 734         wctx = self.workingctx()
 735         return wctx.branch() != wctx.parents()[0].branch()
 736 
 737     def active(self, parent=None, thorough=False):
 738         '''Return an ActiveList describing changes between workspace
 739         and parent workspace (including uncommitted changes).
 740         If the workspace has no parent, ActiveList will still describe any
 741         uncommitted changes.
 742 
 743         If thorough is True use neither the WorkList nor any cached
 744         results (though the result of this call will be cached for
 745         future, non-thorough, calls).'''
 746 
 747         parent = self.parent(parent)
 748 
 749         #
 750         # Use the cached copy if we can (we have one, and weren't
 751         # asked to be thorough)
 752         #
 753         if not thorough and parent in self.activecache:
 754             return self.activecache[parent]
 755 
 756         #
 757         # outbases: The set of outgoing nodes with no outgoing ancestors
 758         # outnodes: The full set of outgoing nodes
 759         #
 760         if parent:
 761             outbases = self.findoutgoing(parent)
 762             outnodes = self.repo.changelog.nodesbetween(outbases)[0]
 763         else:               # No parent, no outgoing nodes
 764             outbases = []
 765             outnodes = []
 766 
 767         wctx = self.workingctx(worklist=not thorough)
 768         localtip = self._localtip(outnodes, wctx)
 769 
 770         if localtip.rev() is None:
 771             heads = localtip.parents()
 772         else:
 773             heads = [localtip]
 774 
 775         parenttip = self.parenttip(heads, outnodes)
 776 
 777         #
 778         # If we couldn't find a parenttip, the two repositories must
 779         # be unrelated (Hg catches most of this, but this case is
 780         # valid for it but invalid for us)
 781         #
 782         if parenttip == None:
 783             raise util.Abort('repository is unrelated')
 784 
 785         headnodes = [h.node() for h in heads]
 786         ctxs = [self.repo.changectx(n) for n in
 787                 self.repo.changelog.nodesbetween(outbases, headnodes)[0]]
 788 
 789         if localtip.rev() is None:
 790             ctxs.append(localtip)
 791 
 792         act = ActiveList(self, parenttip, ctxs)
 793         self.activecache[parent] = act
 794 
 795         return act
 796 
 797     def squishdeltas(self, active, message, user=None):
 798         '''Create a single conglomerate changeset based on a given
 799         active list.  Removes the original changesets comprising the
 800         given active list, and any tags pointing to them.
 801 
 802         Operation:
 803 
 804           - Commit an activectx object representing the specified
 805             active list,
 806 
 807           - Remove any local tags pointing to changesets in the
 808             specified active list.
 809 
 810           - Remove the changesets comprising the specified active
 811             list.
 812 
 813           - Remove any metadata that may refer to changesets that were
 814             removed.
 815 
 816         Calling code is expected to hold both the working copy lock
 817         and repository lock of the destination workspace
 818         '''
 819 
 820         def strip_local_tags(active):
 821             '''Remove any local tags referring to the specified nodes.'''
 822 
 823             if os.path.exists(self.repo.join('localtags')):
 824                 fh = None
 825                 try:
 826                     fh = self.repo.opener('localtags')
 827                     tags = active.prune_tags(fh)
 828                     fh.close()
 829 
 830                     fh = self.repo.opener('localtags', 'w', atomictemp=True)
 831                     fh.writelines(tags)
 832                     fh.rename()
 833                 finally:
 834                     if fh and not fh.closed:
 835                         fh.close()
 836 
 837         if active.files():
 838             for entry in active:
 839                 #
 840                 # Work around Mercurial issue #1666, if the source
 841                 # file of a rename exists in the working copy
 842                 # Mercurial will complain, and remove the file.
 843                 #
 844                 # We preemptively remove the file to avoid the
 845                 # complaint (the user was asked about this in
 846                 # cdm_recommit)
 847                 #
 848                 if entry.is_renamed():
 849                     path = self.repo.wjoin(entry.parentname)
 850                     if os.path.exists(path):
 851                         os.unlink(path)
 852 
 853             self.repo.commitctx(active.context(message, user))
 854             wsstate = "recommitted"
 855             destination = self.repo.changelog.tip()
 856         else:
 857             #
 858             # If all we're doing is stripping the old nodes, we want to
 859             # update the working copy such that we're not at a revision
 860             # that's about to go away.
 861             #
 862             wsstate = "tip"
 863             destination = active.parenttip.node()
 864 
 865         self.clean(destination)
 866 
 867         #
 868         # Tags were elided by the activectx object.  Local tags,
 869         # however, must be removed manually.
 870         #
 871         try:
 872             strip_local_tags(active)
 873         except EnvironmentError, e:
 874             raise util.Abort('Could not recommit tags: %s\n' % e)
 875 
 876         # Silence all the strip and update fun
 877         self.ui.pushbuffer()
 878 
 879         #
 880         # Remove the previous child-local changes by stripping the
 881         # nodes that form the base of the ActiveList (removing their
 882         # children in the process).
 883         #
 884         try:
 885             try:
 886                 for base in active.bases():
 887                     #
 888                     # Any cached information about the repository is
 889                     # likely to be invalid during the strip.  The
 890                     # caching of branch tags is especially
 891                     # problematic.
 892                     #
 893                     self.repo.invalidate()
 894                     repair.strip(self.ui, self.repo, base.node(), backup=False)
 895             except:
 896                 #
 897                 # If this fails, it may leave us in a surprising place in
 898                 # the history.
 899                 #
 900                 # We want to warn the user that something went wrong,
 901                 # and what will happen next, re-raise the exception, and
 902                 # bring the working copy back into a consistent state
 903                 # (which the finally block will do)
 904                 #
 905                 self.ui.warn("stripping failed, your workspace will have "
 906                              "superfluous heads.\n"
 907                              "your workspace has been updated to the "
 908                              "%s changeset.\n" % wsstate)
 909                 raise               # Re-raise the exception
 910         finally:
 911             self.clean()
 912             self.repo.dirstate.write() # Flush the dirstate
 913             self.repo.invalidate()     # Invalidate caches
 914 
 915             #
 916             # We need to remove Hg's undo information (used for rollback),
 917             # since it refers to data that will probably not exist after
 918             # the strip.
 919             #
 920             if os.path.exists(self.repo.sjoin('undo')):
 921                 try:
 922                     os.unlink(self.repo.sjoin('undo'))
 923                 except EnvironmentError, e:
 924                     raise util.Abort('failed to remove undo data: %s\n' % e)
 925 
 926             self.ui.popbuffer()
 927 
 928     def filepath(self, path):
 929         'Return the full path to a workspace file.'
 930 
 931         return self.repo.pathto(path)
 932 
 933     def clean(self, rev=None):
 934         '''Bring workspace up to REV (or tip) forcefully (discarding in
 935         progress changes)'''
 936 
 937         if rev != None:
 938             rev = self.repo.lookup(rev)
 939         else:
 940             rev = self.repo.changelog.tip()
 941 
 942         hg.clean(self.repo, rev, show_stats=False)
 943 
 944     def mq_applied(self):
 945         '''True if the workspace has Mq patches applied'''
 946 
 947         q = mq.queue(self.ui, self.repo.join(''))
 948         return q.applied
 949 
 950     def workingctx(self, worklist=False):
 951         '''Return a workingctx object representing the working copy.
 952 
 953         If worklist is true, return a workingctx object created based
 954         on the status of files in the workspace's worklist.'''
 955 
 956         wl = WorkList(self)
 957 
 958         if worklist and wl:
 959             return context.workingctx(self.repo, changes=wl.status())
 960         else:
 961             return self.repo.changectx(None)
 962 
 963     def matcher(self, pats=None, opts=None, files=None):
 964         '''Return a match object suitable for Mercurial based on
 965         specified criteria.
 966 
 967         If files is specified it is a list of pathnames relative to
 968         the repository root to be matched precisely.
 969 
 970         If pats and/or opts are specified, these are as to
 971         cmdutil.match'''
 972 
 973         of_patterns = pats is not None or opts is not None
 974         of_files = files is not None
 975         opts = opts or {}       # must be a dict
 976 
 977         assert not (of_patterns and of_files)
 978 
 979         if of_patterns:
 980             return cmdutil.match(self.repo, pats, opts)
 981         elif of_files:
 982             return cmdutil.matchfiles(self.repo, files)
 983         else:
 984             return cmdutil.matchall(self.repo)
 985 
 986     def diff(self, node1=None, node2=None, match=None, opts=None):
 987         '''Return the diff of changes between two changesets as a string'''
 988 
 989         #
 990         # Retain compatibility by only calling diffopts() if it
 991         # obviously has not already been done.
 992         #
 993         if isinstance(opts, dict):
 994             opts = patch.diffopts(self.ui, opts)
 995 
 996         ret = cStringIO.StringIO()
 997         for chunk in patch.diff(self.repo, node1, node2, match=match,
 998                                 opts=opts):
 999             ret.write(chunk)
1000 
1001         return ret.getvalue()
1002 
1003     if Version.at_least("1.6"):
1004         def copy(self, src, dest):
1005             '''Copy a file from src to dest
1006             '''
1007 
1008             self.workingctx().copy(src, dest)
1009     else:
1010         def copy(self, src, dest):
1011             '''Copy a file from src to dest
1012             '''
1013 
1014             self.repo.copy(src, dest)
1015 
1016 
1017     if Version.at_least("1.4"):
1018 
1019         def _walkctxs(self, base, head, follow=False, pick=None):
1020             '''Generate changectxs between BASE and HEAD.
1021 
1022             Walk changesets between BASE and HEAD (in the order implied by
1023             their relation), following a given branch if FOLLOW is a true
1024             value, yielding changectxs where PICK (if specified) returns a
1025             true value.
1026 
1027             PICK is a function of one argument, a changectx.'''
1028 
1029             chosen = {}
1030 
1031             def prep(ctx, fns):
1032                 chosen[ctx.rev()] = not pick or pick(ctx)
1033 
1034             opts = {'rev': ['%s:%s' % (base.rev(), head.rev())],
1035                     'follow': follow}
1036             matcher = cmdutil.matchall(self.repo)
1037 
1038             for ctx in cmdutil.walkchangerevs(self.repo, matcher, opts, prep):
1039                 if chosen[ctx.rev()]:
1040                     yield ctx
1041     else:
1042 
1043         def _walkctxs(self, base, head, follow=False, pick=None):
1044             '''Generate changectxs between BASE and HEAD.
1045 
1046             Walk changesets between BASE and HEAD (in the order implied by
1047             their relation), following a given branch if FOLLOW is a true
1048             value, yielding changectxs where PICK (if specified) returns a
1049             true value.
1050 
1051             PICK is a function of one argument, a changectx.'''
1052 
1053             opts = {'rev': ['%s:%s' % (base.rev(), head.rev())],
1054                     'follow': follow}
1055 
1056             changectx = self.repo.changectx
1057             getcset = util.cachefunc(lambda r: changectx(r).changeset())
1058 
1059             #
1060             # See the docstring of mercurial.cmdutil.walkchangerevs() for
1061             # the phased approach to the iterator returned.  The important
1062             # part to note is that the 'add' phase gathers nodes, which
1063             # the 'iter' phase then iterates through.
1064             #
1065             changeiter = cmdutil.walkchangerevs(self.ui, self.repo,
1066                                                 [], getcset, opts)[0]
1067 
1068             matched = {}
1069             for st, rev, fns in changeiter:
1070                 if st == 'add':
1071                     ctx = changectx(rev)
1072                     if not pick or pick(ctx):
1073                         matched[rev] = ctx
1074                 elif st == 'iter':
1075                     if rev in matched:
1076                         yield matched[rev]