1cdf0c1d5Smjnelson# 2cdf0c1d5Smjnelson# This program is free software; you can redistribute it and/or modify 3cdf0c1d5Smjnelson# it under the terms of the GNU General Public License version 2 4cdf0c1d5Smjnelson# as published by the Free Software Foundation. 5cdf0c1d5Smjnelson# 6cdf0c1d5Smjnelson# This program is distributed in the hope that it will be useful, 7cdf0c1d5Smjnelson# but WITHOUT ANY WARRANTY; without even the implied warranty of 8cdf0c1d5Smjnelson# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9cdf0c1d5Smjnelson# GNU General Public License for more details. 10cdf0c1d5Smjnelson# 11cdf0c1d5Smjnelson# You should have received a copy of the GNU General Public License 12cdf0c1d5Smjnelson# along with this program; if not, write to the Free Software 13cdf0c1d5Smjnelson# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 14cdf0c1d5Smjnelson# 15cdf0c1d5Smjnelson 16cdf0c1d5Smjnelson# 1778add226Sjmcp# Copyright (c) 2008, 2010, Oracle and/or its affiliates. All rights reserved. 18605a716eSRichard Lowe# Copyright 2008, 2011, Richard Lowe 19cdf0c1d5Smjnelson# 20cdf0c1d5Smjnelson 21cdf0c1d5Smjnelson# 22cdf0c1d5Smjnelson# Theory: 23cdf0c1d5Smjnelson# 24cdf0c1d5Smjnelson# Workspaces have a non-binding parent/child relationship. 25cdf0c1d5Smjnelson# All important operations apply to the changes between the two. 26cdf0c1d5Smjnelson# 27cdf0c1d5Smjnelson# However, for the sake of remote operation, the 'parent' of a 28cdf0c1d5Smjnelson# workspace is not seen as a literal entity, instead the figurative 29cdf0c1d5Smjnelson# parent contains the last changeset common to both parent and child, 30cdf0c1d5Smjnelson# as such the 'parent tip' is actually nothing of the sort, but instead a 31cdf0c1d5Smjnelson# convenient imitation. 32cdf0c1d5Smjnelson# 33cdf0c1d5Smjnelson# Any change made to a workspace is a change to a file therein, such 34cdf0c1d5Smjnelson# changes can be represented briefly as whether the file was 35cdf0c1d5Smjnelson# modified/added/removed as compared to the parent workspace, whether 36cdf0c1d5Smjnelson# the file has a different name in the parent and if so, whether it 37cdf0c1d5Smjnelson# was renamed or merely copied. Each changed file has an 38cdf0c1d5Smjnelson# associated ActiveEntry. 39cdf0c1d5Smjnelson# 40605a716eSRichard Lowe# The ActiveList, being a list of ActiveEntry objects, can thus 41605a716eSRichard Lowe# present the entire change in workspace state between a parent and 42605a716eSRichard Lowe# its child and is the important bit here (in that if it is incorrect, 43605a716eSRichard Lowe# everything else will be as incorrect, or more) 44cdf0c1d5Smjnelson# 45cdf0c1d5Smjnelson 46cdf0c1d5Smjnelsonimport cStringIO 47cdf0c1d5Smjnelsonimport os 4887039217SRichard Lowefrom mercurial import cmdutil, context, error, hg, node, patch, repair, util 49cdf0c1d5Smjnelsonfrom hgext import mq 50cdf0c1d5Smjnelson 512b5878deSRich Lowefrom onbld.Scm import Version 522b5878deSRich Lowe 53605a716eSRichard Lowe 54c959a081SRichard Lowe# 5587039217SRichard Lowe# Mercurial 1.6 moves findoutgoing into a discover module 56c959a081SRichard Lowe# 5787039217SRichard Loweif Version.at_least("1.6"): 5887039217SRichard Lowe from mercurial import discovery 59c959a081SRichard Lowe 60cdf0c1d5Smjnelson 61cdf0c1d5Smjnelsonclass ActiveEntry(object): 62cdf0c1d5Smjnelson '''Representation of the changes made to a single file. 63cdf0c1d5Smjnelson 64cdf0c1d5Smjnelson MODIFIED - Contents changed, but no other changes were made 65cdf0c1d5Smjnelson ADDED - File is newly created 66cdf0c1d5Smjnelson REMOVED - File is being removed 67cdf0c1d5Smjnelson 68cdf0c1d5Smjnelson Copies are represented by an Entry whose .parentname is non-nil 69cdf0c1d5Smjnelson 70cdf0c1d5Smjnelson Truly copied files have non-nil .parentname and .renamed = False 71cdf0c1d5Smjnelson Renames have non-nil .parentname and .renamed = True 72cdf0c1d5Smjnelson 73cdf0c1d5Smjnelson Do not access any of this information directly, do so via the 74cdf0c1d5Smjnelson 75cdf0c1d5Smjnelson .is_<change>() methods.''' 76cdf0c1d5Smjnelson 77605a716eSRichard Lowe MODIFIED = intern('modified') 78605a716eSRichard Lowe ADDED = intern('added') 79605a716eSRichard Lowe REMOVED = intern('removed') 80cdf0c1d5Smjnelson 81605a716eSRichard Lowe def __init__(self, name, change): 82cdf0c1d5Smjnelson self.name = name 83605a716eSRichard Lowe self.change = intern(change) 84605a716eSRichard Lowe 85605a716eSRichard Lowe assert change in (self.MODIFIED, self.ADDED, self.REMOVED) 86605a716eSRichard Lowe 87cdf0c1d5Smjnelson self.parentname = None 88cdf0c1d5Smjnelson # As opposed to copied (or neither) 89cdf0c1d5Smjnelson self.renamed = False 90cdf0c1d5Smjnelson self.comments = [] 91cdf0c1d5Smjnelson 92cdf0c1d5Smjnelson def __cmp__(self, other): 93cdf0c1d5Smjnelson return cmp(self.name, other.name) 94cdf0c1d5Smjnelson 95cdf0c1d5Smjnelson def is_added(self): 96605a716eSRichard Lowe '''Return True if this ActiveEntry represents an added file''' 97605a716eSRichard Lowe return self.change is self.ADDED 98cdf0c1d5Smjnelson 99cdf0c1d5Smjnelson def is_modified(self): 100605a716eSRichard Lowe '''Return True if this ActiveEntry represents a modified file''' 101605a716eSRichard Lowe return self.change is self.MODIFIED 102cdf0c1d5Smjnelson 103cdf0c1d5Smjnelson def is_removed(self): 104605a716eSRichard Lowe '''Return True if this ActiveEntry represents a removed file''' 105605a716eSRichard Lowe return self.change is self.REMOVED 106cdf0c1d5Smjnelson 107cdf0c1d5Smjnelson def is_renamed(self): 108605a716eSRichard Lowe '''Return True if this ActiveEntry represents a renamed file''' 109cdf0c1d5Smjnelson return self.parentname and self.renamed 110cdf0c1d5Smjnelson 111cdf0c1d5Smjnelson def is_copied(self): 112605a716eSRichard Lowe '''Return True if this ActiveEntry represents a copied file''' 113cdf0c1d5Smjnelson return self.parentname and not self.renamed 114cdf0c1d5Smjnelson 115cdf0c1d5Smjnelson 116cdf0c1d5Smjnelsonclass ActiveList(object): 117605a716eSRichard Lowe '''Complete representation of change between two changesets. 118cdf0c1d5Smjnelson 119605a716eSRichard Lowe In practice, a container for ActiveEntry objects, and methods to 120605a716eSRichard Lowe create them, and deal with them as a group.''' 121cdf0c1d5Smjnelson 122cdf0c1d5Smjnelson def __init__(self, ws, parenttip, revs=None): 123605a716eSRichard Lowe '''Initialize the ActiveList 124605a716eSRichard Lowe 125605a716eSRichard Lowe parenttip is the revision with which to compare (likely to be 126605a716eSRichard Lowe from the parent), revs is a topologically sorted list of 127605a716eSRichard Lowe revisions ending with the revision to compare with (likely to 128605a716eSRichard Lowe be the child-local revisions).''' 129605a716eSRichard Lowe 130605a716eSRichard Lowe assert parenttip is not None 131605a716eSRichard Lowe 132cdf0c1d5Smjnelson self.ws = ws 133cdf0c1d5Smjnelson self.revs = revs 134cdf0c1d5Smjnelson self.parenttip = parenttip 135cdf0c1d5Smjnelson self.localtip = None 136cdf0c1d5Smjnelson 137605a716eSRichard Lowe self._active = {} 138cdf0c1d5Smjnelson self._comments = [] 139cdf0c1d5Smjnelson 140605a716eSRichard Lowe if revs: 141605a716eSRichard Lowe self.localtip = revs[-1] 142605a716eSRichard Lowe self._build() 143cdf0c1d5Smjnelson 144605a716eSRichard Lowe def _status(self): 145605a716eSRichard Lowe '''Return the status of any file mentioned in any of the 146605a716eSRichard Lowe changesets making up this active list.''' 147cdf0c1d5Smjnelson 148605a716eSRichard Lowe files = set() 149605a716eSRichard Lowe for c in self.revs: 150605a716eSRichard Lowe files.update(c.files()) 151cdf0c1d5Smjnelson 152cdf0c1d5Smjnelson # 153605a716eSRichard Lowe # Any file not in the parenttip or the localtip is ephemeral 154605a716eSRichard Lowe # and can be ignored. Mercurial will complain regarding these 155605a716eSRichard Lowe # files if the localtip is a workingctx, so remove them in 156605a716eSRichard Lowe # that case. 157cdf0c1d5Smjnelson # 158605a716eSRichard Lowe # Compare against the dirstate because a workingctx manifest 159605a716eSRichard Lowe # is created on-demand and is particularly expensive. 160cdf0c1d5Smjnelson # 161605a716eSRichard Lowe if self.localtip.rev() is None: 162605a716eSRichard Lowe for f in files.copy(): 163605a716eSRichard Lowe if f not in self.parenttip and f not in self.ws.repo.dirstate: 164605a716eSRichard Lowe files.remove(f) 165605a716eSRichard Lowe 166605a716eSRichard Lowe return self.ws.status(self.parenttip, self.localtip, files=files) 167605a716eSRichard Lowe 168605a716eSRichard Lowe def _build(self): 169605a716eSRichard Lowe '''Construct ActiveEntry objects for each changed file. 170605a716eSRichard Lowe 171605a716eSRichard Lowe This works in 3 stages: 172605a716eSRichard Lowe 173605a716eSRichard Lowe - Create entries for every changed file with 174605a716eSRichard Lowe semi-appropriate change type 175605a716eSRichard Lowe 176605a716eSRichard Lowe - Track renames/copies, and set change comments (both 177605a716eSRichard Lowe ActiveList-wide, and per-file). 178605a716eSRichard Lowe 179605a716eSRichard Lowe - Cleanup 180605a716eSRichard Lowe - Drop circular renames 181605a716eSRichard Lowe - Drop the removal of the old name of any rename 182605a716eSRichard Lowe - Drop entries for modified files that haven't actually changed''' 183cdf0c1d5Smjnelson 184cdf0c1d5Smjnelson # 185605a716eSRichard Lowe # Keep a cache of filectx objects (keyed on pathname) so that 186605a716eSRichard Lowe # we can avoid opening filelogs numerous times. 187cdf0c1d5Smjnelson # 188605a716eSRichard Lowe fctxcache = {} 189cdf0c1d5Smjnelson 190605a716eSRichard Lowe def oldname(ctx, fname): 191605a716eSRichard Lowe '''Return the name 'fname' held prior to any possible 192605a716eSRichard Lowe rename/copy in the given changeset.''' 193cdf0c1d5Smjnelson try: 194605a716eSRichard Lowe if fname in fctxcache: 195605a716eSRichard Lowe octx = fctxcache[fname] 196605a716eSRichard Lowe fctx = ctx.filectx(fname, filelog=octx.filelog()) 197605a716eSRichard Lowe else: 198cdf0c1d5Smjnelson fctx = ctx.filectx(fname) 199605a716eSRichard Lowe # 200605a716eSRichard Lowe # workingfilectx objects may not refer to the 201605a716eSRichard Lowe # right filelog (in case of rename). Don't cache 202605a716eSRichard Lowe # them. 203605a716eSRichard Lowe # 204605a716eSRichard Lowe if not isinstance(fctx, context.workingfilectx): 205605a716eSRichard Lowe fctxcache[fname] = fctx 20687039217SRichard Lowe except error.LookupError: 207605a716eSRichard Lowe return None 208cdf0c1d5Smjnelson 209cdf0c1d5Smjnelson rn = fctx.renamed() 210605a716eSRichard Lowe return rn and rn[0] or fname 211605a716eSRichard Lowe 212605a716eSRichard Lowe status = self._status() 213605a716eSRichard Lowe self._active = dict((fname, ActiveEntry(fname, kind)) 214605a716eSRichard Lowe for fname, kind in status.iteritems() 215605a716eSRichard Lowe if kind in ('modified', 'added', 'removed')) 216cdf0c1d5Smjnelson 217cdf0c1d5Smjnelson # 218605a716eSRichard Lowe # We do two things: 219605a716eSRichard Lowe # - Gather checkin comments (for the entire ActiveList, and 220605a716eSRichard Lowe # per-file) 221605a716eSRichard Lowe # - Set the .parentname of any copied/renamed file 222cdf0c1d5Smjnelson # 223605a716eSRichard Lowe # renames/copies: 224605a716eSRichard Lowe # We walk the list of revisions backward such that only files 225605a716eSRichard Lowe # that ultimately remain active need be considered. 226cdf0c1d5Smjnelson # 227605a716eSRichard Lowe # At each iteration (revision) we update the .parentname of 228605a716eSRichard Lowe # any active file renamed or copied in that revision (the 229605a716eSRichard Lowe # current .parentname if set, or .name otherwise, reflects 230605a716eSRichard Lowe # the name of a given active file in the revision currently 231605a716eSRichard Lowe # being looked at) 232605a716eSRichard Lowe # 233605a716eSRichard Lowe for ctx in reversed(self.revs): 234605a716eSRichard Lowe desc = ctx.description().splitlines() 235605a716eSRichard Lowe self._comments = desc + self._comments 236605a716eSRichard Lowe cfiles = set(ctx.files()) 237605a716eSRichard Lowe 238605a716eSRichard Lowe for entry in self: 239605a716eSRichard Lowe fname = entry.parentname or entry.name 240605a716eSRichard Lowe if fname not in cfiles: 241cdf0c1d5Smjnelson continue 242cdf0c1d5Smjnelson 243605a716eSRichard Lowe entry.comments = desc + entry.comments 244cdf0c1d5Smjnelson 245cdf0c1d5Smjnelson # 246605a716eSRichard Lowe # We don't care about the name history of any file 247605a716eSRichard Lowe # that ends up being removed, since that trumps any 248605a716eSRichard Lowe # possible renames or copies along the way. 249cdf0c1d5Smjnelson # 250605a716eSRichard Lowe # Changes that we may care about involving an 251605a716eSRichard Lowe # intermediate name of a removed file will appear 252605a716eSRichard Lowe # separately (related to the eventual name along 253605a716eSRichard Lowe # that line) 254cdf0c1d5Smjnelson # 255605a716eSRichard Lowe if not entry.is_removed(): 256605a716eSRichard Lowe entry.parentname = oldname(ctx, fname) 257605a716eSRichard Lowe 258cdf0c1d5Smjnelson for entry in self._active.values(): 259605a716eSRichard Lowe # 260605a716eSRichard Lowe # For any file marked as copied or renamed, clear the 261605a716eSRichard Lowe # .parentname if the copy or rename is cyclic (source == 262605a716eSRichard Lowe # destination) or if the .parentname did not exist in the 263605a716eSRichard Lowe # parenttip. 264605a716eSRichard Lowe # 265605a716eSRichard Lowe # If the parentname is marked as removed, set the renamed 266605a716eSRichard Lowe # flag and remove any ActiveEntry we may have for the 267605a716eSRichard Lowe # .parentname. 268605a716eSRichard Lowe # 269605a716eSRichard Lowe if entry.parentname: 270605a716eSRichard Lowe if (entry.parentname == entry.name or 271605a716eSRichard Lowe entry.parentname not in self.parenttip): 272605a716eSRichard Lowe entry.parentname = None 273605a716eSRichard Lowe elif status.get(entry.parentname) == 'removed': 274605a716eSRichard Lowe entry.renamed = True 275cdf0c1d5Smjnelson 276605a716eSRichard Lowe if entry.parentname in self: 277605a716eSRichard Lowe del self[entry.parentname] 278cdf0c1d5Smjnelson 279cdf0c1d5Smjnelson # 280605a716eSRichard Lowe # There are cases during a merge where a file will be seen 281605a716eSRichard Lowe # as modified by status but in reality be an addition (not 282605a716eSRichard Lowe # in the parenttip), so we have to check whether the file 283605a716eSRichard Lowe # is in the parenttip and set it as an addition, if not. 284cdf0c1d5Smjnelson # 285605a716eSRichard Lowe # If a file is modified (and not a copy or rename), we do 286605a716eSRichard Lowe # a full comparison to the copy in the parenttip and 287605a716eSRichard Lowe # ignore files that are parts of active revisions but 288605a716eSRichard Lowe # unchanged. 289cdf0c1d5Smjnelson # 290605a716eSRichard Lowe if entry.name not in self.parenttip: 291cdf0c1d5Smjnelson entry.change = ActiveEntry.ADDED 29278add226Sjmcp elif entry.is_modified(): 29378add226Sjmcp if not self._changed_file(entry.name): 294cdf0c1d5Smjnelson del self[entry.name] 295cdf0c1d5Smjnelson 296cdf0c1d5Smjnelson def __contains__(self, fname): 297cdf0c1d5Smjnelson return fname in self._active 298cdf0c1d5Smjnelson 299cdf0c1d5Smjnelson def __getitem__(self, key): 300cdf0c1d5Smjnelson return self._active[key] 301cdf0c1d5Smjnelson 302cdf0c1d5Smjnelson def __setitem__(self, key, value): 303cdf0c1d5Smjnelson self._active[key] = value 304cdf0c1d5Smjnelson 305cdf0c1d5Smjnelson def __delitem__(self, key): 306cdf0c1d5Smjnelson del self._active[key] 307cdf0c1d5Smjnelson 308cdf0c1d5Smjnelson def __iter__(self): 309605a716eSRichard Lowe return self._active.itervalues() 310cdf0c1d5Smjnelson 311cdf0c1d5Smjnelson def files(self): 312c959a081SRichard Lowe '''Return the list of pathnames of all files touched by this 313c959a081SRichard Lowe ActiveList 314cdf0c1d5Smjnelson 315c959a081SRichard Lowe Where files have been renamed, this will include both their 316c959a081SRichard Lowe current name and the name which they had in the parent tip. 317c959a081SRichard Lowe ''' 318c959a081SRichard Lowe 319c959a081SRichard Lowe ret = self._active.keys() 320605a716eSRichard Lowe ret.extend(x.parentname for x in self if x.is_renamed()) 321605a716eSRichard Lowe return set(ret) 322cdf0c1d5Smjnelson 323cdf0c1d5Smjnelson def comments(self): 324605a716eSRichard Lowe '''Return the full set of changeset comments associated with 325605a716eSRichard Lowe this ActiveList''' 326605a716eSRichard Lowe 327cdf0c1d5Smjnelson return self._comments 328cdf0c1d5Smjnelson 329cdf0c1d5Smjnelson def bases(self): 330c7f512e4Sjmcp '''Return the list of changesets that are roots of the ActiveList. 331cdf0c1d5Smjnelson 332c7f512e4Sjmcp This is the set of active changesets where neither parent 333c7f512e4Sjmcp changeset is itself active.''' 334cdf0c1d5Smjnelson 335c7f512e4Sjmcp revset = set(self.revs) 336c7f512e4Sjmcp return filter(lambda ctx: not [p for p in ctx.parents() if p in revset], 337c7f512e4Sjmcp self.revs) 338cdf0c1d5Smjnelson 339cdf0c1d5Smjnelson def tags(self): 340cdf0c1d5Smjnelson '''Find tags that refer to a changeset in the ActiveList, 341cdf0c1d5Smjnelson returning a list of 3-tuples (tag, node, is_local) for each. 342cdf0c1d5Smjnelson 343cdf0c1d5Smjnelson We return all instances of a tag that refer to such a node, 344cdf0c1d5Smjnelson not just that which takes precedence.''' 345cdf0c1d5Smjnelson 346c959a081SRichard Lowe def colliding_tags(iterable, nodes, local): 347c959a081SRichard Lowe for nd, name in [line.rstrip().split(' ', 1) for line in iterable]: 348c959a081SRichard Lowe if nd in nodes: 349c959a081SRichard Lowe yield (name, self.ws.repo.lookup(nd), local) 350c959a081SRichard Lowe 351c959a081SRichard Lowe tags = [] 352c959a081SRichard Lowe nodes = set(node.hex(ctx.node()) for ctx in self.revs) 353c959a081SRichard Lowe 354cdf0c1d5Smjnelson if os.path.exists(self.ws.repo.join('localtags')): 355c959a081SRichard Lowe fh = self.ws.repo.opener('localtags') 356c959a081SRichard Lowe tags.extend(colliding_tags(fh, nodes, True)) 357c959a081SRichard Lowe fh.close() 358cdf0c1d5Smjnelson 359cdf0c1d5Smjnelson # We want to use the tags file from the localtip 360c959a081SRichard Lowe if '.hgtags' in self.localtip: 361c959a081SRichard Lowe data = self.localtip.filectx('.hgtags').data().splitlines() 362c959a081SRichard Lowe tags.extend(colliding_tags(data, nodes, False)) 363cdf0c1d5Smjnelson 364cdf0c1d5Smjnelson return tags 365cdf0c1d5Smjnelson 366c959a081SRichard Lowe def prune_tags(self, data): 367c959a081SRichard Lowe '''Return a copy of data, which should correspond to the 368c959a081SRichard Lowe contents of a Mercurial tags file, with any tags that refer to 369c959a081SRichard Lowe changesets which are components of the ActiveList removed.''' 370c959a081SRichard Lowe 371c959a081SRichard Lowe nodes = set(node.hex(ctx.node()) for ctx in self.revs) 372c959a081SRichard Lowe return [t for t in data if t.split(' ', 1)[0] not in nodes] 373c959a081SRichard Lowe 37478add226Sjmcp def _changed_file(self, path): 37578add226Sjmcp '''Compare the parent and local versions of a given file. 376cdf0c1d5Smjnelson Return True if file changed, False otherwise. 377cdf0c1d5Smjnelson 37878add226Sjmcp Note that this compares the given path in both versions, not the given 37978add226Sjmcp entry; renamed and copied files are compared by name, not history. 38078add226Sjmcp 381cdf0c1d5Smjnelson The fast path compares file metadata, slow path is a 382cdf0c1d5Smjnelson real comparison of file content.''' 383cdf0c1d5Smjnelson 38487039217SRichard Lowe if ((path in self.parenttip) != (path in self.localtip)): 38578add226Sjmcp return True 38678add226Sjmcp 38778add226Sjmcp parentfile = self.parenttip.filectx(path) 38878add226Sjmcp localfile = self.localtip.filectx(path) 389cdf0c1d5Smjnelson 390cdf0c1d5Smjnelson # 391cdf0c1d5Smjnelson # NB: Keep these ordered such as to make every attempt 392cdf0c1d5Smjnelson # to short-circuit the more time consuming checks. 393cdf0c1d5Smjnelson # 394cdf0c1d5Smjnelson if parentfile.size() != localfile.size(): 395cdf0c1d5Smjnelson return True 396cdf0c1d5Smjnelson 397c959a081SRichard Lowe if parentfile.flags() != localfile.flags(): 398cdf0c1d5Smjnelson return True 399cdf0c1d5Smjnelson 400*036abacaSRichard Lowe if Version.at_least("1.7"): 401*036abacaSRichard Lowe if parentfile.cmp(localfile): 402*036abacaSRichard Lowe return True 403*036abacaSRichard Lowe else: 404cdf0c1d5Smjnelson if parentfile.cmp(localfile.data()): 405cdf0c1d5Smjnelson return True 406cdf0c1d5Smjnelson 407c959a081SRichard Lowe def context(self, message, user): 408c959a081SRichard Lowe '''Return a Mercurial context object representing the entire 409c959a081SRichard Lowe ActiveList as one change.''' 410c959a081SRichard Lowe return activectx(self, message, user) 411c959a081SRichard Lowe 412605a716eSRichard Lowe def as_text(self, paths): 413605a716eSRichard Lowe '''Return the ActiveList as a block of text in a format 414605a716eSRichard Lowe intended to aid debugging and simplify the test suite. 415605a716eSRichard Lowe 416605a716eSRichard Lowe paths should be a list of paths for which file-level data 417605a716eSRichard Lowe should be included. If it is empty, the whole active list is 418605a716eSRichard Lowe included.''' 419605a716eSRichard Lowe 420605a716eSRichard Lowe cstr = cStringIO.StringIO() 421605a716eSRichard Lowe 422605a716eSRichard Lowe cstr.write('parent tip: %s:%s\n' % (self.parenttip.rev(), 423605a716eSRichard Lowe self.parenttip)) 424605a716eSRichard Lowe if self.localtip: 425605a716eSRichard Lowe rev = self.localtip.rev() 426605a716eSRichard Lowe cstr.write('local tip: %s:%s\n' % 427605a716eSRichard Lowe (rev is None and "working" or rev, self.localtip)) 428605a716eSRichard Lowe else: 429605a716eSRichard Lowe cstr.write('local tip: None\n') 430605a716eSRichard Lowe 431605a716eSRichard Lowe cstr.write('entries:\n') 432605a716eSRichard Lowe for entry in self: 433605a716eSRichard Lowe if paths and self.ws.filepath(entry.name) not in paths: 434605a716eSRichard Lowe continue 435605a716eSRichard Lowe 436605a716eSRichard Lowe cstr.write(' - %s\n' % entry.name) 437605a716eSRichard Lowe cstr.write(' parentname: %s\n' % entry.parentname) 438605a716eSRichard Lowe cstr.write(' change: %s\n' % entry.change) 439605a716eSRichard Lowe cstr.write(' renamed: %s\n' % entry.renamed) 440605a716eSRichard Lowe cstr.write(' comments:\n') 441605a716eSRichard Lowe cstr.write(' ' + '\n '.join(entry.comments) + '\n') 442605a716eSRichard Lowe cstr.write('\n') 443605a716eSRichard Lowe 444605a716eSRichard Lowe return cstr.getvalue() 445605a716eSRichard Lowe 446605a716eSRichard Lowe 447605a716eSRichard Loweclass WorkList(object): 448605a716eSRichard Lowe '''A (user-maintained) list of files changed in this workspace as 449605a716eSRichard Lowe compared to any parent workspace. 450605a716eSRichard Lowe 451605a716eSRichard Lowe Internally, the WorkList is stored in .hg/cdm/worklist as a list 452605a716eSRichard Lowe of file pathnames, one per-line. 453605a716eSRichard Lowe 454605a716eSRichard Lowe This may only safely be used as a hint regarding possible 455605a716eSRichard Lowe modifications to the working copy, it should not be relied upon to 456605a716eSRichard Lowe suggest anything about committed changes.''' 457605a716eSRichard Lowe 458605a716eSRichard Lowe def __init__(self, ws): 459605a716eSRichard Lowe '''Load the WorkList for the specified WorkSpace from disk.''' 460605a716eSRichard Lowe 461605a716eSRichard Lowe self._ws = ws 462605a716eSRichard Lowe self._repo = ws.repo 463605a716eSRichard Lowe self._file = os.path.join('cdm', 'worklist') 464605a716eSRichard Lowe self._files = set() 465605a716eSRichard Lowe self._valid = False 466605a716eSRichard Lowe 467605a716eSRichard Lowe if os.path.exists(self._repo.join(self._file)): 468605a716eSRichard Lowe self.load() 469605a716eSRichard Lowe 470605a716eSRichard Lowe def __nonzero__(self): 471605a716eSRichard Lowe '''A WorkList object is true if it was loaded from disk, 472605a716eSRichard Lowe rather than freshly created. 473605a716eSRichard Lowe ''' 474605a716eSRichard Lowe 475605a716eSRichard Lowe return self._valid 476605a716eSRichard Lowe 477605a716eSRichard Lowe def list(self): 478605a716eSRichard Lowe '''List of pathnames contained in the WorkList 479605a716eSRichard Lowe ''' 480605a716eSRichard Lowe 481605a716eSRichard Lowe return list(self._files) 482605a716eSRichard Lowe 483605a716eSRichard Lowe def status(self): 484605a716eSRichard Lowe '''Return the status (in tuple form) of files from the 485605a716eSRichard Lowe WorkList as they are in the working copy 486605a716eSRichard Lowe ''' 487605a716eSRichard Lowe 488605a716eSRichard Lowe match = self._ws.matcher(files=self.list()) 489605a716eSRichard Lowe return self._repo.status(match=match) 490605a716eSRichard Lowe 491605a716eSRichard Lowe def add(self, fname): 492605a716eSRichard Lowe '''Add FNAME to the WorkList. 493605a716eSRichard Lowe ''' 494605a716eSRichard Lowe 495605a716eSRichard Lowe self._files.add(fname) 496605a716eSRichard Lowe 497605a716eSRichard Lowe def write(self): 498605a716eSRichard Lowe '''Write the WorkList out to disk. 499605a716eSRichard Lowe ''' 500605a716eSRichard Lowe 501605a716eSRichard Lowe dirn = os.path.split(self._file)[0] 502605a716eSRichard Lowe 503605a716eSRichard Lowe if dirn and not os.path.exists(self._repo.join(dirn)): 504605a716eSRichard Lowe try: 505605a716eSRichard Lowe os.makedirs(self._repo.join(dirn)) 506605a716eSRichard Lowe except EnvironmentError, e: 507605a716eSRichard Lowe raise util.Abort("Couldn't create directory %s: %s" % 508605a716eSRichard Lowe (self._repo.join(dirn), e)) 509605a716eSRichard Lowe 510605a716eSRichard Lowe fh = self._repo.opener(self._file, 'w', atomictemp=True) 511605a716eSRichard Lowe 512605a716eSRichard Lowe for name in self._files: 513605a716eSRichard Lowe fh.write("%s\n" % name) 514605a716eSRichard Lowe 515605a716eSRichard Lowe fh.rename() 516605a716eSRichard Lowe fh.close() 517605a716eSRichard Lowe 518605a716eSRichard Lowe def load(self): 519605a716eSRichard Lowe '''Read in the WorkList from disk. 520605a716eSRichard Lowe ''' 521605a716eSRichard Lowe 522605a716eSRichard Lowe fh = self._repo.opener(self._file, 'r') 523605a716eSRichard Lowe self._files = set(l.rstrip('\n') for l in fh) 524605a716eSRichard Lowe self._valid = True 525605a716eSRichard Lowe fh.close() 526605a716eSRichard Lowe 527605a716eSRichard Lowe def delete(self): 528605a716eSRichard Lowe '''Empty the WorkList 529605a716eSRichard Lowe 530605a716eSRichard Lowe Remove the on-disk WorkList and clear the file-list of the 531605a716eSRichard Lowe in-memory copy 532605a716eSRichard Lowe ''' 533605a716eSRichard Lowe 534605a716eSRichard Lowe if os.path.exists(self._repo.join(self._file)): 535605a716eSRichard Lowe os.unlink(self._repo.join(self._file)) 536605a716eSRichard Lowe 537605a716eSRichard Lowe self._files = set() 538605a716eSRichard Lowe self._valid = False 539605a716eSRichard Lowe 540c959a081SRichard Lowe 541c959a081SRichard Loweclass activectx(context.memctx): 542c959a081SRichard Lowe '''Represent an ActiveList as a Mercurial context object. 543c959a081SRichard Lowe 544c959a081SRichard Lowe Part of the WorkSpace.squishdeltas implementation.''' 545c959a081SRichard Lowe 546c959a081SRichard Lowe def __init__(self, active, message, user): 547c959a081SRichard Lowe '''Build an activectx object. 548c959a081SRichard Lowe 549c959a081SRichard Lowe active - The ActiveList object used as the source for all data. 550c959a081SRichard Lowe message - Changeset description 551c959a081SRichard Lowe user - Committing user''' 552c959a081SRichard Lowe 553c959a081SRichard Lowe def filectxfn(repository, ctx, fname): 554c959a081SRichard Lowe fctx = active.localtip.filectx(fname) 555c959a081SRichard Lowe data = fctx.data() 556c959a081SRichard Lowe 557c959a081SRichard Lowe # 558c959a081SRichard Lowe # .hgtags is a special case, tags referring to active list 559c959a081SRichard Lowe # component changesets should be elided. 560c959a081SRichard Lowe # 561c959a081SRichard Lowe if fname == '.hgtags': 562c959a081SRichard Lowe data = '\n'.join(active.prune_tags(data.splitlines())) 563c959a081SRichard Lowe 564c959a081SRichard Lowe return context.memfilectx(fname, data, 'l' in fctx.flags(), 565c959a081SRichard Lowe 'x' in fctx.flags(), 566c959a081SRichard Lowe active[fname].parentname) 567c959a081SRichard Lowe 568c959a081SRichard Lowe self.__active = active 569c959a081SRichard Lowe parents = (active.parenttip.node(), node.nullid) 570c959a081SRichard Lowe extra = {'branch': active.localtip.branch()} 571c959a081SRichard Lowe context.memctx.__init__(self, active.ws.repo, parents, message, 572c959a081SRichard Lowe active.files(), filectxfn, user=user, 573c959a081SRichard Lowe extra=extra) 574c959a081SRichard Lowe 575c959a081SRichard Lowe def modified(self): 576c959a081SRichard Lowe return [entry.name for entry in self.__active if entry.is_modified()] 577c959a081SRichard Lowe 578c959a081SRichard Lowe def added(self): 579c959a081SRichard Lowe return [entry.name for entry in self.__active if entry.is_added()] 580c959a081SRichard Lowe 581c959a081SRichard Lowe def removed(self): 582bb664d7bSRichard Lowe ret = set(entry.name for entry in self.__active if entry.is_removed()) 583bb664d7bSRichard Lowe ret.update(set(x.parentname for x in self.__active if x.is_renamed())) 584bb664d7bSRichard Lowe return list(ret) 585c959a081SRichard Lowe 586c959a081SRichard Lowe def files(self): 587c959a081SRichard Lowe return self.__active.files() 588c959a081SRichard Lowe 589cdf0c1d5Smjnelson 590cdf0c1d5Smjnelsonclass WorkSpace(object): 591cdf0c1d5Smjnelson 592cdf0c1d5Smjnelson def __init__(self, repository): 593cdf0c1d5Smjnelson self.repo = repository 594cdf0c1d5Smjnelson self.ui = self.repo.ui 595cdf0c1d5Smjnelson self.name = self.repo.root 596cdf0c1d5Smjnelson 597cdf0c1d5Smjnelson self.activecache = {} 598cdf0c1d5Smjnelson 599cdf0c1d5Smjnelson def parent(self, spec=None): 6000df7087fSRichard Lowe '''Return the canonical workspace parent, either SPEC (which 6010df7087fSRichard Lowe will be expanded) if provided or the default parent 6020df7087fSRichard Lowe otherwise.''' 603cdf0c1d5Smjnelson 6040df7087fSRichard Lowe if spec: 6050df7087fSRichard Lowe return self.ui.expandpath(spec) 606cdf0c1d5Smjnelson 6070df7087fSRichard Lowe p = self.ui.expandpath('default') 6080df7087fSRichard Lowe if p == 'default': 6090df7087fSRichard Lowe return None 6100df7087fSRichard Lowe else: 6110df7087fSRichard Lowe return p 612cdf0c1d5Smjnelson 6130df7087fSRichard Lowe def _localtip(self, outgoing, wctx): 6140df7087fSRichard Lowe '''Return the most representative changeset to act as the 6150df7087fSRichard Lowe localtip. 616cdf0c1d5Smjnelson 6170df7087fSRichard Lowe If the working directory is modified (has file changes, is a 6180df7087fSRichard Lowe merge, or has switched branches), this will be a workingctx. 619cdf0c1d5Smjnelson 6200df7087fSRichard Lowe If the working directory is unmodified, this will be the most 6210df7087fSRichard Lowe recent (highest revision number) local (outgoing) head on the 6220df7087fSRichard Lowe current branch, if no heads are determined to be outgoing, it 6230df7087fSRichard Lowe will be the most recent head on the current branch. 6240df7087fSRichard Lowe ''' 625cdf0c1d5Smjnelson 626cdf0c1d5Smjnelson if (wctx.files() or len(wctx.parents()) > 1 or 627cdf0c1d5Smjnelson wctx.branch() != wctx.parents()[0].branch()): 6280df7087fSRichard Lowe return wctx 629cdf0c1d5Smjnelson 6300df7087fSRichard Lowe heads = self.repo.heads(start=wctx.parents()[0].node()) 6310df7087fSRichard Lowe headctxs = [self.repo.changectx(n) for n in heads] 6320df7087fSRichard Lowe localctxs = [c for c in headctxs if c.node() in outgoing] 633cdf0c1d5Smjnelson 6340df7087fSRichard Lowe ltip = sorted(localctxs or headctxs, key=lambda x: x.rev())[-1] 635cdf0c1d5Smjnelson 6360df7087fSRichard Lowe if len(heads) > 1: 6370df7087fSRichard Lowe self.ui.warn('The current branch has more than one head, ' 6380df7087fSRichard Lowe 'using %s\n' % ltip.rev()) 6390df7087fSRichard Lowe 6400df7087fSRichard Lowe return ltip 6410df7087fSRichard Lowe 642605a716eSRichard Lowe def parenttip(self, heads, outgoing): 6430df7087fSRichard Lowe '''Return the highest-numbered, non-outgoing changeset that is 6440df7087fSRichard Lowe an ancestor of a changeset in heads. 6450df7087fSRichard Lowe 646605a716eSRichard Lowe This returns the most recent changeset on a given branch that 647605a716eSRichard Lowe is shared between a parent and child workspace, in effect the 648605a716eSRichard Lowe common ancestor of the chosen local tip and the parent 649605a716eSRichard Lowe workspace. 6500df7087fSRichard Lowe ''' 651cdf0c1d5Smjnelson 652cdf0c1d5Smjnelson def tipmost_shared(head, outnodes): 65387039217SRichard Lowe '''Return the changeset on the same branch as head that is 65487039217SRichard Lowe not in outnodes and is closest to the tip. 655cdf0c1d5Smjnelson 65687039217SRichard Lowe Walk outgoing changesets from head to the bottom of the 65787039217SRichard Lowe workspace (revision 0) and return the the first changeset 65887039217SRichard Lowe we see that is not in outnodes. 659cdf0c1d5Smjnelson 66087039217SRichard Lowe If none is found (all revisions >= 0 are outgoing), the 6610df7087fSRichard Lowe only possible parenttip is the null node (node.nullid) 6620df7087fSRichard Lowe which is returned explicitly. 66387039217SRichard Lowe ''' 66487039217SRichard Lowe for ctx in self._walkctxs(head, self.repo.changectx(0), 66587039217SRichard Lowe follow=True, 66687039217SRichard Lowe pick=lambda c: c.node() not in outnodes): 66787039217SRichard Lowe return ctx 6680df7087fSRichard Lowe 66987039217SRichard Lowe return self.repo.changectx(node.nullid) 670cdf0c1d5Smjnelson 6710df7087fSRichard Lowe nodes = set(outgoing) 6720df7087fSRichard Lowe ptips = map(lambda x: tipmost_shared(x, nodes), heads) 67387039217SRichard Lowe return sorted(ptips, key=lambda x: x.rev(), reverse=True)[0] 674cdf0c1d5Smjnelson 675605a716eSRichard Lowe def status(self, base='.', head=None, files=None): 676cdf0c1d5Smjnelson '''Translate from the hg 6-tuple status format to a hash keyed 677cdf0c1d5Smjnelson on change-type''' 678c959a081SRichard Lowe 679cdf0c1d5Smjnelson states = ['modified', 'added', 'removed', 'deleted', 'unknown', 680cdf0c1d5Smjnelson 'ignored'] 6812b5878deSRich Lowe 682605a716eSRichard Lowe match = self.matcher(files=files) 683605a716eSRichard Lowe chngs = self.repo.status(base, head, match=match) 684605a716eSRichard Lowe 685605a716eSRichard Lowe ret = {} 686605a716eSRichard Lowe for paths, change in zip(chngs, states): 687605a716eSRichard Lowe ret.update((f, change) for f in paths) 688605a716eSRichard Lowe return ret 689cdf0c1d5Smjnelson 690cdf0c1d5Smjnelson def findoutgoing(self, parent): 691c959a081SRichard Lowe '''Return the base set of outgoing nodes. 692c959a081SRichard Lowe 693c959a081SRichard Lowe A caching wrapper around mercurial.localrepo.findoutgoing(). 694c959a081SRichard Lowe Complains (to the user), if the parent workspace is 695c959a081SRichard Lowe non-existent or inaccessible''' 696c959a081SRichard Lowe 697cdf0c1d5Smjnelson self.ui.pushbuffer() 698cdf0c1d5Smjnelson try: 699c959a081SRichard Lowe try: 700c959a081SRichard Lowe ui = self.ui 701c959a081SRichard Lowe if hasattr(cmdutil, 'remoteui'): 702c959a081SRichard Lowe ui = cmdutil.remoteui(ui, {}) 703c959a081SRichard Lowe pws = hg.repository(ui, parent) 70487039217SRichard Lowe if Version.at_least("1.6"): 70587039217SRichard Lowe return discovery.findoutgoing(self.repo, pws) 70687039217SRichard Lowe else: 707c959a081SRichard Lowe return self.repo.findoutgoing(pws) 70887039217SRichard Lowe except error.RepoError: 709c959a081SRichard Lowe self.ui.warn("Warning: Parent workspace '%s' is not " 710c959a081SRichard Lowe "accessible\n" 711c959a081SRichard Lowe "active list will be incomplete\n\n" % parent) 712c959a081SRichard Lowe return [] 713c959a081SRichard Lowe finally: 714cdf0c1d5Smjnelson self.ui.popbuffer() 715c959a081SRichard Lowe findoutgoing = util.cachefunc(findoutgoing) 716cdf0c1d5Smjnelson 717cdf0c1d5Smjnelson def modified(self): 718cdf0c1d5Smjnelson '''Return a list of files modified in the workspace''' 719605a716eSRichard Lowe 7202b5878deSRich Lowe wctx = self.workingctx() 721cdf0c1d5Smjnelson return sorted(wctx.files() + wctx.deleted()) or None 722cdf0c1d5Smjnelson 723cdf0c1d5Smjnelson def merged(self): 724cdf0c1d5Smjnelson '''Return boolean indicating whether the workspace has an uncommitted 725cdf0c1d5Smjnelson merge''' 726605a716eSRichard Lowe 7272b5878deSRich Lowe wctx = self.workingctx() 728cdf0c1d5Smjnelson return len(wctx.parents()) > 1 729cdf0c1d5Smjnelson 730cdf0c1d5Smjnelson def branched(self): 731cdf0c1d5Smjnelson '''Return boolean indicating whether the workspace has an 732cdf0c1d5Smjnelson uncommitted named branch''' 733cdf0c1d5Smjnelson 7342b5878deSRich Lowe wctx = self.workingctx() 735cdf0c1d5Smjnelson return wctx.branch() != wctx.parents()[0].branch() 736cdf0c1d5Smjnelson 737605a716eSRichard Lowe def active(self, parent=None, thorough=False): 738cdf0c1d5Smjnelson '''Return an ActiveList describing changes between workspace 739cdf0c1d5Smjnelson and parent workspace (including uncommitted changes). 740605a716eSRichard Lowe If the workspace has no parent, ActiveList will still describe any 741605a716eSRichard Lowe uncommitted changes. 742605a716eSRichard Lowe 743605a716eSRichard Lowe If thorough is True use neither the WorkList nor any cached 744605a716eSRichard Lowe results (though the result of this call will be cached for 745605a716eSRichard Lowe future, non-thorough, calls).''' 746cdf0c1d5Smjnelson 747cdf0c1d5Smjnelson parent = self.parent(parent) 748605a716eSRichard Lowe 749605a716eSRichard Lowe # 750605a716eSRichard Lowe # Use the cached copy if we can (we have one, and weren't 751605a716eSRichard Lowe # asked to be thorough) 752605a716eSRichard Lowe # 753605a716eSRichard Lowe if not thorough and parent in self.activecache: 754cdf0c1d5Smjnelson return self.activecache[parent] 755cdf0c1d5Smjnelson 756605a716eSRichard Lowe # 757605a716eSRichard Lowe # outbases: The set of outgoing nodes with no outgoing ancestors 758605a716eSRichard Lowe # outnodes: The full set of outgoing nodes 759605a716eSRichard Lowe # 760cdf0c1d5Smjnelson if parent: 761605a716eSRichard Lowe outbases = self.findoutgoing(parent) 762605a716eSRichard Lowe outnodes = self.repo.changelog.nodesbetween(outbases)[0] 763605a716eSRichard Lowe else: # No parent, no outgoing nodes 764605a716eSRichard Lowe outbases = [] 7650df7087fSRichard Lowe outnodes = [] 766cdf0c1d5Smjnelson 767605a716eSRichard Lowe wctx = self.workingctx(worklist=not thorough) 768605a716eSRichard Lowe localtip = self._localtip(outnodes, wctx) 769cdf0c1d5Smjnelson 7700df7087fSRichard Lowe if localtip.rev() is None: 7710df7087fSRichard Lowe heads = localtip.parents() 772cdf0c1d5Smjnelson else: 7730df7087fSRichard Lowe heads = [localtip] 7740df7087fSRichard Lowe 775605a716eSRichard Lowe parenttip = self.parenttip(heads, outnodes) 776605a716eSRichard Lowe 777605a716eSRichard Lowe # 778605a716eSRichard Lowe # If we couldn't find a parenttip, the two repositories must 779605a716eSRichard Lowe # be unrelated (Hg catches most of this, but this case is 780605a716eSRichard Lowe # valid for it but invalid for us) 781605a716eSRichard Lowe # 782605a716eSRichard Lowe if parenttip == None: 783605a716eSRichard Lowe raise util.Abort('repository is unrelated') 784605a716eSRichard Lowe 785605a716eSRichard Lowe headnodes = [h.node() for h in heads] 786cdf0c1d5Smjnelson ctxs = [self.repo.changectx(n) for n in 787605a716eSRichard Lowe self.repo.changelog.nodesbetween(outbases, headnodes)[0]] 788cdf0c1d5Smjnelson 7890df7087fSRichard Lowe if localtip.rev() is None: 7900df7087fSRichard Lowe ctxs.append(localtip) 7910df7087fSRichard Lowe 792605a716eSRichard Lowe act = ActiveList(self, parenttip, ctxs) 793cdf0c1d5Smjnelson self.activecache[parent] = act 794605a716eSRichard Lowe 795cdf0c1d5Smjnelson return act 796cdf0c1d5Smjnelson 797cdf0c1d5Smjnelson def squishdeltas(self, active, message, user=None): 798c959a081SRichard Lowe '''Create a single conglomerate changeset based on a given 799c959a081SRichard Lowe active list. Removes the original changesets comprising the 800c959a081SRichard Lowe given active list, and any tags pointing to them. 801cdf0c1d5Smjnelson 802c959a081SRichard Lowe Operation: 803cdf0c1d5Smjnelson 804c959a081SRichard Lowe - Commit an activectx object representing the specified 805c959a081SRichard Lowe active list, 806c959a081SRichard Lowe 807c959a081SRichard Lowe - Remove any local tags pointing to changesets in the 808c959a081SRichard Lowe specified active list. 809c959a081SRichard Lowe 810c959a081SRichard Lowe - Remove the changesets comprising the specified active 811c959a081SRichard Lowe list. 812c959a081SRichard Lowe 813c959a081SRichard Lowe - Remove any metadata that may refer to changesets that were 814c959a081SRichard Lowe removed. 815c959a081SRichard Lowe 816c959a081SRichard Lowe Calling code is expected to hold both the working copy lock 817c959a081SRichard Lowe and repository lock of the destination workspace 818c959a081SRichard Lowe ''' 819c959a081SRichard Lowe 820c959a081SRichard Lowe def strip_local_tags(active): 821c959a081SRichard Lowe '''Remove any local tags referring to the specified nodes.''' 822cdf0c1d5Smjnelson 823cdf0c1d5Smjnelson if os.path.exists(self.repo.join('localtags')): 824c959a081SRichard Lowe fh = None 825c959a081SRichard Lowe try: 826c959a081SRichard Lowe fh = self.repo.opener('localtags') 827c959a081SRichard Lowe tags = active.prune_tags(fh) 828c959a081SRichard Lowe fh.close() 829c959a081SRichard Lowe 830cdf0c1d5Smjnelson fh = self.repo.opener('localtags', 'w', atomictemp=True) 831cdf0c1d5Smjnelson fh.writelines(tags) 832cdf0c1d5Smjnelson fh.rename() 833c959a081SRichard Lowe finally: 834c959a081SRichard Lowe if fh and not fh.closed: 835c959a081SRichard Lowe fh.close() 836cdf0c1d5Smjnelson 837cdf0c1d5Smjnelson if active.files(): 838c959a081SRichard Lowe for entry in active: 839c959a081SRichard Lowe # 840c959a081SRichard Lowe # Work around Mercurial issue #1666, if the source 841c959a081SRichard Lowe # file of a rename exists in the working copy 842c959a081SRichard Lowe # Mercurial will complain, and remove the file. 843c959a081SRichard Lowe # 844c959a081SRichard Lowe # We preemptively remove the file to avoid the 845c959a081SRichard Lowe # complaint (the user was asked about this in 846c959a081SRichard Lowe # cdm_recommit) 847c959a081SRichard Lowe # 848c959a081SRichard Lowe if entry.is_renamed(): 849c959a081SRichard Lowe path = self.repo.wjoin(entry.parentname) 850c959a081SRichard Lowe if os.path.exists(path): 851c959a081SRichard Lowe os.unlink(path) 852c959a081SRichard Lowe 853c959a081SRichard Lowe self.repo.commitctx(active.context(message, user)) 854c959a081SRichard Lowe wsstate = "recommitted" 855c959a081SRichard Lowe destination = self.repo.changelog.tip() 856cdf0c1d5Smjnelson else: 857cdf0c1d5Smjnelson # 858cdf0c1d5Smjnelson # If all we're doing is stripping the old nodes, we want to 859cdf0c1d5Smjnelson # update the working copy such that we're not at a revision 860cdf0c1d5Smjnelson # that's about to go away. 861cdf0c1d5Smjnelson # 862c959a081SRichard Lowe wsstate = "tip" 863c959a081SRichard Lowe destination = active.parenttip.node() 864c959a081SRichard Lowe 865c959a081SRichard Lowe self.clean(destination) 866c959a081SRichard Lowe 867c959a081SRichard Lowe # 868c959a081SRichard Lowe # Tags were elided by the activectx object. Local tags, 869c959a081SRichard Lowe # however, must be removed manually. 870c959a081SRichard Lowe # 871c959a081SRichard Lowe try: 872c959a081SRichard Lowe strip_local_tags(active) 873c959a081SRichard Lowe except EnvironmentError, e: 874c959a081SRichard Lowe raise util.Abort('Could not recommit tags: %s\n' % e) 875cdf0c1d5Smjnelson 876cdf0c1d5Smjnelson # Silence all the strip and update fun 877cdf0c1d5Smjnelson self.ui.pushbuffer() 878cdf0c1d5Smjnelson 879cdf0c1d5Smjnelson # 880605a716eSRichard Lowe # Remove the previous child-local changes by stripping the 881605a716eSRichard Lowe # nodes that form the base of the ActiveList (removing their 882605a716eSRichard Lowe # children in the process). 883cdf0c1d5Smjnelson # 884cdf0c1d5Smjnelson try: 885cdf0c1d5Smjnelson try: 886c7f512e4Sjmcp for base in active.bases(): 887c959a081SRichard Lowe # 888c959a081SRichard Lowe # Any cached information about the repository is 889c959a081SRichard Lowe # likely to be invalid during the strip. The 890c959a081SRichard Lowe # caching of branch tags is especially 891c959a081SRichard Lowe # problematic. 892c959a081SRichard Lowe # 893c959a081SRichard Lowe self.repo.invalidate() 894c7f512e4Sjmcp repair.strip(self.ui, self.repo, base.node(), backup=False) 895cdf0c1d5Smjnelson except: 896cdf0c1d5Smjnelson # 897cdf0c1d5Smjnelson # If this fails, it may leave us in a surprising place in 898cdf0c1d5Smjnelson # the history. 899cdf0c1d5Smjnelson # 900cdf0c1d5Smjnelson # We want to warn the user that something went wrong, 901cdf0c1d5Smjnelson # and what will happen next, re-raise the exception, and 902cdf0c1d5Smjnelson # bring the working copy back into a consistent state 903cdf0c1d5Smjnelson # (which the finally block will do) 904cdf0c1d5Smjnelson # 905cdf0c1d5Smjnelson self.ui.warn("stripping failed, your workspace will have " 906cdf0c1d5Smjnelson "superfluous heads.\n" 907cdf0c1d5Smjnelson "your workspace has been updated to the " 908c959a081SRichard Lowe "%s changeset.\n" % wsstate) 909cdf0c1d5Smjnelson raise # Re-raise the exception 910cdf0c1d5Smjnelson finally: 911c959a081SRichard Lowe self.clean() 912c959a081SRichard Lowe self.repo.dirstate.write() # Flush the dirstate 913c959a081SRichard Lowe self.repo.invalidate() # Invalidate caches 914c959a081SRichard Lowe 915cdf0c1d5Smjnelson # 916cdf0c1d5Smjnelson # We need to remove Hg's undo information (used for rollback), 917cdf0c1d5Smjnelson # since it refers to data that will probably not exist after 918cdf0c1d5Smjnelson # the strip. 919cdf0c1d5Smjnelson # 920cdf0c1d5Smjnelson if os.path.exists(self.repo.sjoin('undo')): 921cdf0c1d5Smjnelson try: 922cdf0c1d5Smjnelson os.unlink(self.repo.sjoin('undo')) 923cdf0c1d5Smjnelson except EnvironmentError, e: 924cdf0c1d5Smjnelson raise util.Abort('failed to remove undo data: %s\n' % e) 925cdf0c1d5Smjnelson 926cdf0c1d5Smjnelson self.ui.popbuffer() 927cdf0c1d5Smjnelson 928cdf0c1d5Smjnelson def filepath(self, path): 929cdf0c1d5Smjnelson 'Return the full path to a workspace file.' 930605a716eSRichard Lowe 931cdf0c1d5Smjnelson return self.repo.pathto(path) 932cdf0c1d5Smjnelson 933cdf0c1d5Smjnelson def clean(self, rev=None): 934cdf0c1d5Smjnelson '''Bring workspace up to REV (or tip) forcefully (discarding in 935cdf0c1d5Smjnelson progress changes)''' 9362b5878deSRich Lowe 937cdf0c1d5Smjnelson if rev != None: 938cdf0c1d5Smjnelson rev = self.repo.lookup(rev) 939cdf0c1d5Smjnelson else: 940cdf0c1d5Smjnelson rev = self.repo.changelog.tip() 941cdf0c1d5Smjnelson 942cdf0c1d5Smjnelson hg.clean(self.repo, rev, show_stats=False) 943cdf0c1d5Smjnelson 944cdf0c1d5Smjnelson def mq_applied(self): 945cdf0c1d5Smjnelson '''True if the workspace has Mq patches applied''' 946605a716eSRichard Lowe 947cdf0c1d5Smjnelson q = mq.queue(self.ui, self.repo.join('')) 948cdf0c1d5Smjnelson return q.applied 9492b5878deSRich Lowe 950605a716eSRichard Lowe def workingctx(self, worklist=False): 951605a716eSRichard Lowe '''Return a workingctx object representing the working copy. 952605a716eSRichard Lowe 953605a716eSRichard Lowe If worklist is true, return a workingctx object created based 954605a716eSRichard Lowe on the status of files in the workspace's worklist.''' 955605a716eSRichard Lowe 956605a716eSRichard Lowe wl = WorkList(self) 957605a716eSRichard Lowe 958605a716eSRichard Lowe if worklist and wl: 959605a716eSRichard Lowe return context.workingctx(self.repo, changes=wl.status()) 960605a716eSRichard Lowe else: 9612b5878deSRich Lowe return self.repo.changectx(None) 9622b5878deSRich Lowe 963605a716eSRichard Lowe def matcher(self, pats=None, opts=None, files=None): 964605a716eSRichard Lowe '''Return a match object suitable for Mercurial based on 965605a716eSRichard Lowe specified criteria. 966605a716eSRichard Lowe 967605a716eSRichard Lowe If files is specified it is a list of pathnames relative to 968605a716eSRichard Lowe the repository root to be matched precisely. 969605a716eSRichard Lowe 970605a716eSRichard Lowe If pats and/or opts are specified, these are as to 971605a716eSRichard Lowe cmdutil.match''' 972605a716eSRichard Lowe 973605a716eSRichard Lowe of_patterns = pats is not None or opts is not None 974605a716eSRichard Lowe of_files = files is not None 975605a716eSRichard Lowe opts = opts or {} # must be a dict 976605a716eSRichard Lowe 977605a716eSRichard Lowe assert not (of_patterns and of_files) 978605a716eSRichard Lowe 979605a716eSRichard Lowe if of_patterns: 980605a716eSRichard Lowe return cmdutil.match(self.repo, pats, opts) 981605a716eSRichard Lowe elif of_files: 982605a716eSRichard Lowe return cmdutil.matchfiles(self.repo, files) 983605a716eSRichard Lowe else: 984605a716eSRichard Lowe return cmdutil.matchall(self.repo) 985605a716eSRichard Lowe 9862b5878deSRich Lowe def diff(self, node1=None, node2=None, match=None, opts=None): 987605a716eSRichard Lowe '''Return the diff of changes between two changesets as a string''' 988605a716eSRichard Lowe 989605a716eSRichard Lowe # 990605a716eSRichard Lowe # Retain compatibility by only calling diffopts() if it 991605a716eSRichard Lowe # obviously has not already been done. 992605a716eSRichard Lowe # 993605a716eSRichard Lowe if isinstance(opts, dict): 994605a716eSRichard Lowe opts = patch.diffopts(self.ui, opts) 995605a716eSRichard Lowe 9962b5878deSRich Lowe ret = cStringIO.StringIO() 9972b5878deSRich Lowe for chunk in patch.diff(self.repo, node1, node2, match=match, 9982b5878deSRich Lowe opts=opts): 9992b5878deSRich Lowe ret.write(chunk) 10002b5878deSRich Lowe 10012b5878deSRich Lowe return ret.getvalue() 100287039217SRichard Lowe 100387039217SRichard Lowe if Version.at_least("1.6"): 100487039217SRichard Lowe def copy(self, src, dest): 100587039217SRichard Lowe '''Copy a file from src to dest 100687039217SRichard Lowe ''' 100787039217SRichard Lowe 100887039217SRichard Lowe self.workingctx().copy(src, dest) 100987039217SRichard Lowe else: 101087039217SRichard Lowe def copy(self, src, dest): 101187039217SRichard Lowe '''Copy a file from src to dest 101287039217SRichard Lowe ''' 101387039217SRichard Lowe 101487039217SRichard Lowe self.repo.copy(src, dest) 101587039217SRichard Lowe 101687039217SRichard Lowe 101787039217SRichard Lowe if Version.at_least("1.4"): 101887039217SRichard Lowe 101987039217SRichard Lowe def _walkctxs(self, base, head, follow=False, pick=None): 102087039217SRichard Lowe '''Generate changectxs between BASE and HEAD. 102187039217SRichard Lowe 102287039217SRichard Lowe Walk changesets between BASE and HEAD (in the order implied by 102387039217SRichard Lowe their relation), following a given branch if FOLLOW is a true 102487039217SRichard Lowe value, yielding changectxs where PICK (if specified) returns a 102587039217SRichard Lowe true value. 102687039217SRichard Lowe 102787039217SRichard Lowe PICK is a function of one argument, a changectx.''' 102887039217SRichard Lowe 102987039217SRichard Lowe chosen = {} 103087039217SRichard Lowe 103187039217SRichard Lowe def prep(ctx, fns): 103287039217SRichard Lowe chosen[ctx.rev()] = not pick or pick(ctx) 103387039217SRichard Lowe 103487039217SRichard Lowe opts = {'rev': ['%s:%s' % (base.rev(), head.rev())], 103587039217SRichard Lowe 'follow': follow} 103687039217SRichard Lowe matcher = cmdutil.matchall(self.repo) 103787039217SRichard Lowe 103887039217SRichard Lowe for ctx in cmdutil.walkchangerevs(self.repo, matcher, opts, prep): 103987039217SRichard Lowe if chosen[ctx.rev()]: 104087039217SRichard Lowe yield ctx 104187039217SRichard Lowe else: 104287039217SRichard Lowe 104387039217SRichard Lowe def _walkctxs(self, base, head, follow=False, pick=None): 104487039217SRichard Lowe '''Generate changectxs between BASE and HEAD. 104587039217SRichard Lowe 104687039217SRichard Lowe Walk changesets between BASE and HEAD (in the order implied by 104787039217SRichard Lowe their relation), following a given branch if FOLLOW is a true 104887039217SRichard Lowe value, yielding changectxs where PICK (if specified) returns a 104987039217SRichard Lowe true value. 105087039217SRichard Lowe 105187039217SRichard Lowe PICK is a function of one argument, a changectx.''' 105287039217SRichard Lowe 105387039217SRichard Lowe opts = {'rev': ['%s:%s' % (base.rev(), head.rev())], 105487039217SRichard Lowe 'follow': follow} 105587039217SRichard Lowe 105687039217SRichard Lowe changectx = self.repo.changectx 105787039217SRichard Lowe getcset = util.cachefunc(lambda r: changectx(r).changeset()) 105887039217SRichard Lowe 105987039217SRichard Lowe # 106087039217SRichard Lowe # See the docstring of mercurial.cmdutil.walkchangerevs() for 106187039217SRichard Lowe # the phased approach to the iterator returned. The important 106287039217SRichard Lowe # part to note is that the 'add' phase gathers nodes, which 106387039217SRichard Lowe # the 'iter' phase then iterates through. 106487039217SRichard Lowe # 106587039217SRichard Lowe changeiter = cmdutil.walkchangerevs(self.ui, self.repo, 106687039217SRichard Lowe [], getcset, opts)[0] 106787039217SRichard Lowe 106887039217SRichard Lowe matched = {} 106987039217SRichard Lowe for st, rev, fns in changeiter: 107087039217SRichard Lowe if st == 'add': 107187039217SRichard Lowe ctx = changectx(rev) 107287039217SRichard Lowe if not pick or pick(ctx): 107387039217SRichard Lowe matched[rev] = ctx 107487039217SRichard Lowe elif st == 'iter': 107587039217SRichard Lowe if rev in matched: 107687039217SRichard Lowe yield matched[rev] 1077