17078Smjnelson# 27078Smjnelson# This program is free software; you can redistribute it and/or modify 37078Smjnelson# it under the terms of the GNU General Public License version 2 47078Smjnelson# as published by the Free Software Foundation. 57078Smjnelson# 67078Smjnelson# This program is distributed in the hope that it will be useful, 77078Smjnelson# but WITHOUT ANY WARRANTY; without even the implied warranty of 87078Smjnelson# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 97078Smjnelson# GNU General Public License for more details. 107078Smjnelson# 117078Smjnelson# You should have received a copy of the GNU General Public License 127078Smjnelson# along with this program; if not, write to the Free Software 137078Smjnelson# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 147078Smjnelson# 157078Smjnelson 167078Smjnelson# 179006Srichlowe@richlowe.net# Copyright 2009 Sun Microsystems, Inc. All rights reserved. 187078Smjnelson# Use is subject to license terms. 197078Smjnelson# 207078Smjnelson 217078Smjnelson# 227078Smjnelson# Theory: 237078Smjnelson# 247078Smjnelson# Workspaces have a non-binding parent/child relationship. 257078Smjnelson# All important operations apply to the changes between the two. 267078Smjnelson# 277078Smjnelson# However, for the sake of remote operation, the 'parent' of a 287078Smjnelson# workspace is not seen as a literal entity, instead the figurative 297078Smjnelson# parent contains the last changeset common to both parent and child, 307078Smjnelson# as such the 'parent tip' is actually nothing of the sort, but instead a 317078Smjnelson# convenient imitation. 327078Smjnelson# 337078Smjnelson# Any change made to a workspace is a change to a file therein, such 347078Smjnelson# changes can be represented briefly as whether the file was 357078Smjnelson# modified/added/removed as compared to the parent workspace, whether 367078Smjnelson# the file has a different name in the parent and if so, whether it 377078Smjnelson# was renamed or merely copied. Each changed file has an 387078Smjnelson# associated ActiveEntry. 397078Smjnelson# 407078Smjnelson# The ActiveList being a list ActiveEntrys can thus present the entire 417078Smjnelson# change in workspace state between a parent and its child, and is the 427078Smjnelson# important bit here (in that if it is incorrect, everything else will 437078Smjnelson# be as incorrect, or more) 447078Smjnelson# 457078Smjnelson 467078Smjnelsonimport cStringIO 477078Smjnelsonimport os 48*10399Srichlowe@richlowe.netfrom mercurial import cmdutil, context, hg, node, patch, repair, util 497078Smjnelsonfrom hgext import mq 507078Smjnelson 519006Srichlowe@richlowe.netfrom onbld.Scm import Version 529006Srichlowe@richlowe.net 53*10399Srichlowe@richlowe.net# 54*10399Srichlowe@richlowe.net# Mercurial >= 1.2 has its exception types in a mercurial.error 55*10399Srichlowe@richlowe.net# module, prior versions had them in their associated modules. 56*10399Srichlowe@richlowe.net# 57*10399Srichlowe@richlowe.netif Version.at_least("1.2"): 58*10399Srichlowe@richlowe.net from mercurial import error 59*10399Srichlowe@richlowe.net HgRepoError = error.RepoError 60*10399Srichlowe@richlowe.net HgLookupError = error.LookupError 61*10399Srichlowe@richlowe.netelse: 62*10399Srichlowe@richlowe.net from mercurial import repo, revlog 63*10399Srichlowe@richlowe.net HgRepoError = repo.RepoError 64*10399Srichlowe@richlowe.net HgLookupError = revlog.LookupError 65*10399Srichlowe@richlowe.net 667078Smjnelson 677078Smjnelsonclass ActiveEntry(object): 687078Smjnelson '''Representation of the changes made to a single file. 697078Smjnelson 707078Smjnelson MODIFIED - Contents changed, but no other changes were made 717078Smjnelson ADDED - File is newly created 727078Smjnelson REMOVED - File is being removed 737078Smjnelson 747078Smjnelson Copies are represented by an Entry whose .parentname is non-nil 757078Smjnelson 767078Smjnelson Truly copied files have non-nil .parentname and .renamed = False 777078Smjnelson Renames have non-nil .parentname and .renamed = True 787078Smjnelson 797078Smjnelson Do not access any of this information directly, do so via the 807078Smjnelson 817078Smjnelson .is_<change>() methods.''' 827078Smjnelson 837078Smjnelson MODIFIED = 1 847078Smjnelson ADDED = 2 857078Smjnelson REMOVED = 3 867078Smjnelson 877078Smjnelson def __init__(self, name): 887078Smjnelson self.name = name 897078Smjnelson self.change = None 907078Smjnelson self.parentname = None 917078Smjnelson # As opposed to copied (or neither) 927078Smjnelson self.renamed = False 937078Smjnelson self.comments = [] 947078Smjnelson 957078Smjnelson # 967078Smjnelson # ActiveEntrys sort by the name of the file they represent. 977078Smjnelson # 987078Smjnelson def __cmp__(self, other): 997078Smjnelson return cmp(self.name, other.name) 1007078Smjnelson 1017078Smjnelson def is_added(self): 1027078Smjnelson return self.change == self.ADDED 1037078Smjnelson 1047078Smjnelson def is_modified(self): 1057078Smjnelson return self.change == self.MODIFIED 1067078Smjnelson 1077078Smjnelson def is_removed(self): 1087078Smjnelson return self.change == self.REMOVED 1097078Smjnelson 1107078Smjnelson def is_renamed(self): 1117078Smjnelson return self.parentname and self.renamed 1127078Smjnelson 1137078Smjnelson def is_copied(self): 1147078Smjnelson return self.parentname and not self.renamed 1157078Smjnelson 1167078Smjnelson 1177078Smjnelsonclass ActiveList(object): 1187078Smjnelson '''Complete representation of workspace change. 1197078Smjnelson 1207078Smjnelson In practice, a container for ActiveEntrys, and methods to build them, 1217078Smjnelson update them, and deal with them en masse.''' 1227078Smjnelson 1237078Smjnelson def __init__(self, ws, parenttip, revs=None): 1247078Smjnelson self._active = {} 1257078Smjnelson self.ws = ws 1267078Smjnelson 1277078Smjnelson self.revs = revs 1287078Smjnelson 1297078Smjnelson self.base = None 1307078Smjnelson self.parenttip = parenttip 1317078Smjnelson 1327078Smjnelson # 1337078Smjnelson # If we couldn't find a parenttip, the two repositories must 1347078Smjnelson # be unrelated (Hg catches most of this, but this case is valid for it 1357078Smjnelson # but invalid for us) 1367078Smjnelson # 1377078Smjnelson if self.parenttip == None: 1387078Smjnelson raise util.Abort('repository is unrelated') 1397078Smjnelson self.localtip = None 1407078Smjnelson 1417078Smjnelson if revs: 1427078Smjnelson self.base = revs[0] 1437078Smjnelson self.localtip = revs[-1] 1447078Smjnelson 1457078Smjnelson self._comments = [] 1467078Smjnelson 1477078Smjnelson self._build(revs) 1487078Smjnelson 1497078Smjnelson def _build(self, revs): 1507078Smjnelson if not revs: 1517078Smjnelson return 1527078Smjnelson 1537078Smjnelson status = self.ws.status(self.parenttip.node(), self.localtip.node()) 1547078Smjnelson 1557078Smjnelson files = [] 1567078Smjnelson for ctype in status.values(): 1577078Smjnelson files.extend(ctype) 1587078Smjnelson 1597078Smjnelson # 1607078Smjnelson # When a file is renamed, two operations actually occur. 1617078Smjnelson # A file copy from source to dest and a removal of source. 1627078Smjnelson # 1637078Smjnelson # These are represented as two distinct entries in the 1647078Smjnelson # changectx and status (one on the dest file for the 1657078Smjnelson # copy, one on the source file for the remove). 1667078Smjnelson # 1677078Smjnelson # Since these are unconnected in both the context and 1687078Smjnelson # status we can only make the association by explicitly 1697078Smjnelson # looking for it. 1707078Smjnelson # 1717078Smjnelson # We deal with this thusly: 1727078Smjnelson # 1737078Smjnelson # We maintain a dict dest -> source of all copies 1747078Smjnelson # (updating dest as appropriate, but leaving source alone). 1757078Smjnelson # 1767078Smjnelson # After all other processing, we mark as renamed any pair 1777078Smjnelson # where source is on the removed list. 1787078Smjnelson # 1797078Smjnelson copies = {} 1807078Smjnelson 1817078Smjnelson # 1827078Smjnelson # Walk revs looking for renames and adding files that 1837078Smjnelson # are in both change context and status to the active 1847078Smjnelson # list. 1857078Smjnelson # 1867078Smjnelson for ctx in revs: 1877078Smjnelson desc = ctx.description().splitlines() 1887078Smjnelson 1897078Smjnelson self._comments.extend(desc) 1907078Smjnelson 1917078Smjnelson for fname in ctx.files(): 1927078Smjnelson # 1937078Smjnelson # We store comments per-entry as well, for the sake of 1947078Smjnelson # webrev and similar. We store twice to avoid the problems 1957078Smjnelson # of uniquifying comments for the general list (and possibly 1967078Smjnelson # destroying multi-line entities in the process). 1977078Smjnelson # 1987078Smjnelson if fname not in self: 1997078Smjnelson self._addentry(fname) 2007078Smjnelson self[fname].comments.extend(desc) 2017078Smjnelson 2027078Smjnelson try: 2037078Smjnelson fctx = ctx.filectx(fname) 204*10399Srichlowe@richlowe.net except HgLookupError: 2057078Smjnelson continue 2067078Smjnelson 2077078Smjnelson # 2087078Smjnelson # NB: .renamed() is a misnomer, this actually checks 2097078Smjnelson # for copies. 2107078Smjnelson # 2117078Smjnelson rn = fctx.renamed() 2127078Smjnelson if rn: 2137078Smjnelson # 2147078Smjnelson # If the source file is a known copy we know its 2157078Smjnelson # ancestry leads us to the parent. 2167078Smjnelson # Otherwise make sure the source file is known to 2177078Smjnelson # be in the parent, we need not care otherwise. 2187078Smjnelson # 2197078Smjnelson # We detect cycles at a later point. There is no 2207078Smjnelson # reason to continuously handle them. 2217078Smjnelson # 2227078Smjnelson if rn[0] in copies: 2237078Smjnelson copies[fname] = copies[rn[0]] 2247078Smjnelson elif rn[0] in self.parenttip.manifest(): 2257078Smjnelson copies[fname] = rn[0] 2267078Smjnelson 2277078Smjnelson # 2287078Smjnelson # Walk the copy list marking as copied any non-cyclic pair 2297078Smjnelson # where the destination file is still present in the local 2307078Smjnelson # tip (to avoid ephemeral changes) 2317078Smjnelson # 2327078Smjnelson # Where source is removed, mark as renamed, and remove the 2337078Smjnelson # AL entry for the source file 2347078Smjnelson # 2357078Smjnelson for fname, oldname in copies.iteritems(): 2367078Smjnelson if fname == oldname or fname not in self.localtip.manifest(): 2377078Smjnelson continue 2387078Smjnelson 2397078Smjnelson self[fname].parentname = oldname 2407078Smjnelson 2417078Smjnelson if oldname in status['removed']: 2427078Smjnelson self[fname].renamed = True 2437078Smjnelson if oldname in self: 2447078Smjnelson del self[oldname] 2457078Smjnelson 2467078Smjnelson # 2477078Smjnelson # Walk the active list setting the change type for each active 2487078Smjnelson # file. 2497078Smjnelson # 2507078Smjnelson # In the case of modified files that are not renames or 2517078Smjnelson # copies, we do a content comparison, and drop entries that 2527078Smjnelson # are not actually modified. 2537078Smjnelson # 2547078Smjnelson # We walk a copy of the AL such that we can drop entries 2557078Smjnelson # within the loop. 2567078Smjnelson # 2577078Smjnelson for entry in self._active.values(): 2587078Smjnelson if entry.name not in files: 2597078Smjnelson del self[entry.name] 2607078Smjnelson continue 2617078Smjnelson 2627078Smjnelson if entry.name in status['added']: 2637078Smjnelson entry.change = ActiveEntry.ADDED 2647078Smjnelson elif entry.name in status['removed']: 2657078Smjnelson entry.change = ActiveEntry.REMOVED 2667078Smjnelson elif entry.name in status['modified']: 2677078Smjnelson entry.change = ActiveEntry.MODIFIED 2687078Smjnelson 2697078Smjnelson # 2707078Smjnelson # There are cases during a merge where a file will be in 2717078Smjnelson # the status return as modified, but in reality be an 2727078Smjnelson # addition (ie, not in the parenttip). 2737078Smjnelson # 2747078Smjnelson # We need to check whether the file is actually present 2757078Smjnelson # in the parenttip, and set it as an add, if not. 2767078Smjnelson # 2777078Smjnelson if entry.name not in self.parenttip.manifest(): 2787078Smjnelson entry.change = ActiveEntry.ADDED 2797078Smjnelson elif entry.is_modified() and not entry.parentname: 2807078Smjnelson if not self.filecmp(entry): 2817078Smjnelson del self[entry.name] 2827078Smjnelson continue 2837078Smjnelson 2847078Smjnelson assert entry.change 2857078Smjnelson 2867078Smjnelson def __contains__(self, fname): 2877078Smjnelson return fname in self._active 2887078Smjnelson 2897078Smjnelson def __getitem__(self, key): 2907078Smjnelson return self._active[key] 2917078Smjnelson 2927078Smjnelson def __setitem__(self, key, value): 2937078Smjnelson self._active[key] = value 2947078Smjnelson 2957078Smjnelson def __delitem__(self, key): 2967078Smjnelson del self._active[key] 2977078Smjnelson 2987078Smjnelson def __iter__(self): 2997078Smjnelson for entry in self._active.values(): 3007078Smjnelson yield entry 3017078Smjnelson 3027078Smjnelson def _addentry(self, fname): 3037078Smjnelson if fname not in self: 3047078Smjnelson self[fname] = ActiveEntry(fname) 3057078Smjnelson 3067078Smjnelson def files(self): 307*10399Srichlowe@richlowe.net '''Return the list of pathnames of all files touched by this 308*10399Srichlowe@richlowe.net ActiveList 3097078Smjnelson 310*10399Srichlowe@richlowe.net Where files have been renamed, this will include both their 311*10399Srichlowe@richlowe.net current name and the name which they had in the parent tip. 312*10399Srichlowe@richlowe.net ''' 313*10399Srichlowe@richlowe.net 314*10399Srichlowe@richlowe.net ret = self._active.keys() 3157078Smjnelson ret.extend([x.parentname for x in self 316*10399Srichlowe@richlowe.net if x.is_renamed() and x.parentname not in ret]) 3177078Smjnelson return ret 3187078Smjnelson 3197078Smjnelson def comments(self): 3207078Smjnelson return self._comments 3217078Smjnelson 3227078Smjnelson # 3237078Smjnelson # It's not uncommon for a child workspace to itself contain the 3247078Smjnelson # merge of several other children, with initial branch points in 3257078Smjnelson # the parent (possibly from the cset a project gate was created 3267078Smjnelson # from, for instance). 3277078Smjnelson # 3287078Smjnelson # Immediately after recommit, this leaves us looking like this: 3297078Smjnelson # 3307078Smjnelson # * <- recommitted changeset (real tip) 3317078Smjnelson # | 3327078Smjnelson # | * <- Local tip 3337078Smjnelson # |/| 3347078Smjnelson # * | <- parent tip 3357078Smjnelson # | |\ 3367078Smjnelson # | | | 3377078Smjnelson # | | |\ 3387078Smjnelson # | | | | 3397078Smjnelson # | * | | <- Base 3407078Smjnelson # |/_/__/ 3417078Smjnelson # 3427078Smjnelson # [left-most is parent, next is child, right two being 3437078Smjnelson # branches in child, intermediate merges parent->child 3447078Smjnelson # omitted] 3457078Smjnelson # 3467078Smjnelson # Obviously stripping base (the first child-specific delta on the 3477078Smjnelson # main child workspace line) doesn't remove the vestigial branches 3487078Smjnelson # from other workspaces (or in-workspace branches, or whatever) 3497078Smjnelson # 3507078Smjnelson # In reality, what we need to strip in a recommit is any 3517078Smjnelson # child-specific branch descended from the parent (rather than 3527078Smjnelson # another part of the child). Note that this by its very nature 3537078Smjnelson # includes the branch representing the 'main' child workspace. 3547078Smjnelson # 3557078Smjnelson # We calculate these by walking from base (which is guaranteed to 3567078Smjnelson # be the oldest child-local cset) to localtip searching for 3577078Smjnelson # changesets with only one parent cset, and where that cset is not 3587078Smjnelson # part of the active list (and is therefore outgoing). 3597078Smjnelson # 3607078Smjnelson def bases(self): 3617078Smjnelson '''Find the bases that in combination define the "old" 3627078Smjnelson side of a recommitted set of changes, based on AL''' 3637078Smjnelson 3647078Smjnelson get = util.cachefunc(lambda r: self.ws.repo.changectx(r).changeset()) 3657078Smjnelson 3667078Smjnelson # We don't rebuild the AL So the AL local tip is the old tip 3677078Smjnelson revrange = "%s:%s" % (self.base.rev(), self.localtip.rev()) 3687078Smjnelson 3697078Smjnelson changeiter = cmdutil.walkchangerevs(self.ws.repo.ui, self.ws.repo, 3707078Smjnelson [], get, {'rev': [revrange]})[0] 3717078Smjnelson 3727078Smjnelson hold = [] 3737078Smjnelson ret = [] 3747078Smjnelson alrevs = [x.rev() for x in self.revs] 3757078Smjnelson for st, rev, fns in changeiter: 3767078Smjnelson n = self.ws.repo.changelog.node(rev) 3777078Smjnelson if st == 'add': 3787078Smjnelson if rev in alrevs: 3797078Smjnelson hold.append(n) 3807078Smjnelson elif st == 'iter': 3817078Smjnelson if n not in hold: 3827078Smjnelson continue 3837078Smjnelson 3847078Smjnelson p = self.ws.repo.changelog.parents(n) 3857078Smjnelson if p[1] != node.nullid: 3867078Smjnelson continue 3877078Smjnelson 3887078Smjnelson if self.ws.repo.changectx(p[0]).rev() not in alrevs: 3897078Smjnelson ret.append(n) 3907078Smjnelson return ret 3917078Smjnelson 3927078Smjnelson def tags(self): 3937078Smjnelson '''Find tags that refer to a changeset in the ActiveList, 394*10399Srichlowe@richlowe.net returning a list of 3-tuples (tag, node, is_local) for each. 395*10399Srichlowe@richlowe.net 396*10399Srichlowe@richlowe.net We return all instances of a tag that refer to such a node, 397*10399Srichlowe@richlowe.net not just that which takes precedence.''' 3987078Smjnelson 399*10399Srichlowe@richlowe.net def colliding_tags(iterable, nodes, local): 400*10399Srichlowe@richlowe.net for nd, name in [line.rstrip().split(' ', 1) for line in iterable]: 401*10399Srichlowe@richlowe.net if nd in nodes: 402*10399Srichlowe@richlowe.net yield (name, self.ws.repo.lookup(nd), local) 403*10399Srichlowe@richlowe.net 404*10399Srichlowe@richlowe.net tags = [] 405*10399Srichlowe@richlowe.net nodes = set(node.hex(ctx.node()) for ctx in self.revs) 4067078Smjnelson 4077078Smjnelson if os.path.exists(self.ws.repo.join('localtags')): 408*10399Srichlowe@richlowe.net fh = self.ws.repo.opener('localtags') 409*10399Srichlowe@richlowe.net tags.extend(colliding_tags(fh, nodes, True)) 410*10399Srichlowe@richlowe.net fh.close() 4117078Smjnelson 4127078Smjnelson # We want to use the tags file from the localtip 413*10399Srichlowe@richlowe.net if '.hgtags' in self.localtip: 414*10399Srichlowe@richlowe.net data = self.localtip.filectx('.hgtags').data().splitlines() 415*10399Srichlowe@richlowe.net tags.extend(colliding_tags(data, nodes, False)) 4167078Smjnelson 4177078Smjnelson return tags 4187078Smjnelson 419*10399Srichlowe@richlowe.net def prune_tags(self, data): 420*10399Srichlowe@richlowe.net '''Return a copy of data, which should correspond to the 421*10399Srichlowe@richlowe.net contents of a Mercurial tags file, with any tags that refer to 422*10399Srichlowe@richlowe.net changesets which are components of the ActiveList removed.''' 423*10399Srichlowe@richlowe.net 424*10399Srichlowe@richlowe.net nodes = set(node.hex(ctx.node()) for ctx in self.revs) 425*10399Srichlowe@richlowe.net return [t for t in data if t.split(' ', 1)[0] not in nodes] 426*10399Srichlowe@richlowe.net 4277078Smjnelson def filecmp(self, entry): 4287078Smjnelson '''Compare two revisions of two files 4297078Smjnelson 4307078Smjnelson Return True if file changed, False otherwise. 4317078Smjnelson 4327078Smjnelson The fast path compares file metadata, slow path is a 4337078Smjnelson real comparison of file content.''' 4347078Smjnelson 4357078Smjnelson parentfile = self.parenttip.filectx(entry.parentname or entry.name) 4367078Smjnelson localfile = self.localtip.filectx(entry.name) 4377078Smjnelson 4387078Smjnelson # 4397078Smjnelson # NB: Keep these ordered such as to make every attempt 4407078Smjnelson # to short-circuit the more time consuming checks. 4417078Smjnelson # 4427078Smjnelson if parentfile.size() != localfile.size(): 4437078Smjnelson return True 4447078Smjnelson 445*10399Srichlowe@richlowe.net if parentfile.flags() != localfile.flags(): 4467078Smjnelson return True 4477078Smjnelson 4487078Smjnelson if parentfile.cmp(localfile.data()): 4497078Smjnelson return True 4507078Smjnelson 451*10399Srichlowe@richlowe.net def context(self, message, user): 452*10399Srichlowe@richlowe.net '''Return a Mercurial context object representing the entire 453*10399Srichlowe@richlowe.net ActiveList as one change.''' 454*10399Srichlowe@richlowe.net return activectx(self, message, user) 455*10399Srichlowe@richlowe.net 456*10399Srichlowe@richlowe.net 457*10399Srichlowe@richlowe.netclass activectx(context.memctx): 458*10399Srichlowe@richlowe.net '''Represent an ActiveList as a Mercurial context object. 459*10399Srichlowe@richlowe.net 460*10399Srichlowe@richlowe.net Part of the WorkSpace.squishdeltas implementation.''' 461*10399Srichlowe@richlowe.net 462*10399Srichlowe@richlowe.net def __init__(self, active, message, user): 463*10399Srichlowe@richlowe.net '''Build an activectx object. 464*10399Srichlowe@richlowe.net 465*10399Srichlowe@richlowe.net active - The ActiveList object used as the source for all data. 466*10399Srichlowe@richlowe.net message - Changeset description 467*10399Srichlowe@richlowe.net user - Committing user''' 468*10399Srichlowe@richlowe.net 469*10399Srichlowe@richlowe.net def filectxfn(repository, ctx, fname): 470*10399Srichlowe@richlowe.net fctx = active.localtip.filectx(fname) 471*10399Srichlowe@richlowe.net data = fctx.data() 472*10399Srichlowe@richlowe.net 473*10399Srichlowe@richlowe.net # 474*10399Srichlowe@richlowe.net # .hgtags is a special case, tags referring to active list 475*10399Srichlowe@richlowe.net # component changesets should be elided. 476*10399Srichlowe@richlowe.net # 477*10399Srichlowe@richlowe.net if fname == '.hgtags': 478*10399Srichlowe@richlowe.net data = '\n'.join(active.prune_tags(data.splitlines())) 479*10399Srichlowe@richlowe.net 480*10399Srichlowe@richlowe.net return context.memfilectx(fname, data, 'l' in fctx.flags(), 481*10399Srichlowe@richlowe.net 'x' in fctx.flags(), 482*10399Srichlowe@richlowe.net active[fname].parentname) 483*10399Srichlowe@richlowe.net 484*10399Srichlowe@richlowe.net self.__active = active 485*10399Srichlowe@richlowe.net parents = (active.parenttip.node(), node.nullid) 486*10399Srichlowe@richlowe.net extra = {'branch': active.localtip.branch()} 487*10399Srichlowe@richlowe.net context.memctx.__init__(self, active.ws.repo, parents, message, 488*10399Srichlowe@richlowe.net active.files(), filectxfn, user=user, 489*10399Srichlowe@richlowe.net extra=extra) 490*10399Srichlowe@richlowe.net 491*10399Srichlowe@richlowe.net def modified(self): 492*10399Srichlowe@richlowe.net return [entry.name for entry in self.__active if entry.is_modified()] 493*10399Srichlowe@richlowe.net 494*10399Srichlowe@richlowe.net def added(self): 495*10399Srichlowe@richlowe.net return [entry.name for entry in self.__active if entry.is_added()] 496*10399Srichlowe@richlowe.net 497*10399Srichlowe@richlowe.net def removed(self): 498*10399Srichlowe@richlowe.net ret = [entry.name for entry in self.__active if entry.is_removed()] 499*10399Srichlowe@richlowe.net ret.extend([x.parentname for x in self.__active if x.is_renamed()]) 500*10399Srichlowe@richlowe.net return ret 501*10399Srichlowe@richlowe.net 502*10399Srichlowe@richlowe.net def files(self): 503*10399Srichlowe@richlowe.net return self.__active.files() 504*10399Srichlowe@richlowe.net 5057078Smjnelson 5067078Smjnelsonclass WorkSpace(object): 5077078Smjnelson 5087078Smjnelson def __init__(self, repository): 5097078Smjnelson self.repo = repository 5107078Smjnelson self.ui = self.repo.ui 5117078Smjnelson self.name = self.repo.root 5127078Smjnelson 5137078Smjnelson self.activecache = {} 5147078Smjnelson 5157078Smjnelson def parent(self, spec=None): 51610263Srichlowe@richlowe.net '''Return the canonical workspace parent, either SPEC (which 51710263Srichlowe@richlowe.net will be expanded) if provided or the default parent 51810263Srichlowe@richlowe.net otherwise.''' 5197078Smjnelson 52010263Srichlowe@richlowe.net if spec: 52110263Srichlowe@richlowe.net return self.ui.expandpath(spec) 5227078Smjnelson 52310263Srichlowe@richlowe.net p = self.ui.expandpath('default') 52410263Srichlowe@richlowe.net if p == 'default': 52510263Srichlowe@richlowe.net return None 52610263Srichlowe@richlowe.net else: 52710263Srichlowe@richlowe.net return p 5287078Smjnelson 52910263Srichlowe@richlowe.net def _localtip(self, outgoing, wctx): 53010263Srichlowe@richlowe.net '''Return the most representative changeset to act as the 53110263Srichlowe@richlowe.net localtip. 5327078Smjnelson 53310263Srichlowe@richlowe.net If the working directory is modified (has file changes, is a 53410263Srichlowe@richlowe.net merge, or has switched branches), this will be a workingctx. 5357078Smjnelson 53610263Srichlowe@richlowe.net If the working directory is unmodified, this will be the most 53710263Srichlowe@richlowe.net recent (highest revision number) local (outgoing) head on the 53810263Srichlowe@richlowe.net current branch, if no heads are determined to be outgoing, it 53910263Srichlowe@richlowe.net will be the most recent head on the current branch. 54010263Srichlowe@richlowe.net ''' 5417078Smjnelson 5427078Smjnelson # 54310263Srichlowe@richlowe.net # A modified working copy is seen as a proto-branch, and thus 54410263Srichlowe@richlowe.net # our only option as the local tip. 5457078Smjnelson # 5467078Smjnelson if (wctx.files() or len(wctx.parents()) > 1 or 5477078Smjnelson wctx.branch() != wctx.parents()[0].branch()): 54810263Srichlowe@richlowe.net return wctx 54910263Srichlowe@richlowe.net 55010263Srichlowe@richlowe.net heads = self.repo.heads(start=wctx.parents()[0].node()) 55110263Srichlowe@richlowe.net headctxs = [self.repo.changectx(n) for n in heads] 55210263Srichlowe@richlowe.net localctxs = [c for c in headctxs if c.node() in outgoing] 55310263Srichlowe@richlowe.net 55410263Srichlowe@richlowe.net ltip = sorted(localctxs or headctxs, key=lambda x: x.rev())[-1] 5557078Smjnelson 55610263Srichlowe@richlowe.net if len(heads) > 1: 55710263Srichlowe@richlowe.net self.ui.warn('The current branch has more than one head, ' 55810263Srichlowe@richlowe.net 'using %s\n' % ltip.rev()) 55910263Srichlowe@richlowe.net 56010263Srichlowe@richlowe.net return ltip 5617078Smjnelson 56210263Srichlowe@richlowe.net def _parenttip(self, heads, outgoing): 56310263Srichlowe@richlowe.net '''Return the highest-numbered, non-outgoing changeset that is 56410263Srichlowe@richlowe.net an ancestor of a changeset in heads. 5657078Smjnelson 56610263Srichlowe@richlowe.net This is intended to find the most recent changeset on a given 56710263Srichlowe@richlowe.net branch that is shared between a parent and child workspace, 56810263Srichlowe@richlowe.net such that it can act as a stand-in for the parent workspace. 56910263Srichlowe@richlowe.net ''' 5707078Smjnelson 5717078Smjnelson def tipmost_shared(head, outnodes): 5727078Smjnelson '''Return the tipmost node on the same branch as head that is not 5737078Smjnelson in outnodes. 5747078Smjnelson 5757078Smjnelson We walk from head to the bottom of the workspace (revision 5767078Smjnelson 0) collecting nodes not in outnodes during the add phase 5777078Smjnelson and return the first node we see in the iter phase that 5787078Smjnelson was previously collected. 5797078Smjnelson 58010263Srichlowe@richlowe.net If no node is found (all revisions >= 0 are outgoing), the 58110263Srichlowe@richlowe.net only possible parenttip is the null node (node.nullid) 58210263Srichlowe@richlowe.net which is returned explicitly. 58310263Srichlowe@richlowe.net 5847078Smjnelson See the docstring of mercurial.cmdutil.walkchangerevs() 5857078Smjnelson for the phased approach to the iterator returned. The 5867078Smjnelson important part to note is that the 'add' phase gathers 5877078Smjnelson nodes, which the 'iter' phase then iterates through.''' 5887078Smjnelson 58910263Srichlowe@richlowe.net opts = {'rev': ['%s:0' % head.rev()], 59010263Srichlowe@richlowe.net 'follow': True} 5917078Smjnelson get = util.cachefunc(lambda r: self.repo.changectx(r).changeset()) 5927078Smjnelson changeiter = cmdutil.walkchangerevs(self.repo.ui, self.repo, [], 59310263Srichlowe@richlowe.net get, opts)[0] 5947078Smjnelson seen = [] 5957078Smjnelson for st, rev, fns in changeiter: 5967078Smjnelson n = self.repo.changelog.node(rev) 5977078Smjnelson if st == 'add': 5987078Smjnelson if n not in outnodes: 5997078Smjnelson seen.append(n) 6007078Smjnelson elif st == 'iter': 6017078Smjnelson if n in seen: 6027078Smjnelson return rev 60310263Srichlowe@richlowe.net return self.repo.changelog.rev(node.nullid) 6047078Smjnelson 60510263Srichlowe@richlowe.net nodes = set(outgoing) 60610263Srichlowe@richlowe.net ptips = map(lambda x: tipmost_shared(x, nodes), heads) 6077078Smjnelson return self.repo.changectx(sorted(ptips)[-1]) 6087078Smjnelson 609*10399Srichlowe@richlowe.net def status(self, base='.', head=None): 6107078Smjnelson '''Translate from the hg 6-tuple status format to a hash keyed 6117078Smjnelson on change-type''' 612*10399Srichlowe@richlowe.net 6137078Smjnelson states = ['modified', 'added', 'removed', 'deleted', 'unknown', 6147078Smjnelson 'ignored'] 6159006Srichlowe@richlowe.net 6167078Smjnelson chngs = self.repo.status(base, head) 6177078Smjnelson return dict(zip(states, chngs)) 6187078Smjnelson 6197078Smjnelson def findoutgoing(self, parent): 620*10399Srichlowe@richlowe.net '''Return the base set of outgoing nodes. 621*10399Srichlowe@richlowe.net 622*10399Srichlowe@richlowe.net A caching wrapper around mercurial.localrepo.findoutgoing(). 623*10399Srichlowe@richlowe.net Complains (to the user), if the parent workspace is 624*10399Srichlowe@richlowe.net non-existent or inaccessible''' 625*10399Srichlowe@richlowe.net 626*10399Srichlowe@richlowe.net self.ui.pushbuffer() 627*10399Srichlowe@richlowe.net try: 6287078Smjnelson try: 629*10399Srichlowe@richlowe.net ui = self.ui 630*10399Srichlowe@richlowe.net if hasattr(cmdutil, 'remoteui'): 631*10399Srichlowe@richlowe.net ui = cmdutil.remoteui(ui, {}) 632*10399Srichlowe@richlowe.net pws = hg.repository(ui, parent) 633*10399Srichlowe@richlowe.net return self.repo.findoutgoing(pws) 634*10399Srichlowe@richlowe.net except HgRepoError: 635*10399Srichlowe@richlowe.net self.ui.warn("Warning: Parent workspace '%s' is not " 636*10399Srichlowe@richlowe.net "accessible\n" 637*10399Srichlowe@richlowe.net "active list will be incomplete\n\n" % parent) 638*10399Srichlowe@richlowe.net return [] 639*10399Srichlowe@richlowe.net finally: 6407078Smjnelson self.ui.popbuffer() 641*10399Srichlowe@richlowe.net findoutgoing = util.cachefunc(findoutgoing) 6427078Smjnelson 6437078Smjnelson def modified(self): 6447078Smjnelson '''Return a list of files modified in the workspace''' 6459006Srichlowe@richlowe.net wctx = self.workingctx() 6467078Smjnelson return sorted(wctx.files() + wctx.deleted()) or None 6477078Smjnelson 6487078Smjnelson def merged(self): 6497078Smjnelson '''Return boolean indicating whether the workspace has an uncommitted 6507078Smjnelson merge''' 6519006Srichlowe@richlowe.net wctx = self.workingctx() 6527078Smjnelson return len(wctx.parents()) > 1 6537078Smjnelson 6547078Smjnelson def branched(self): 6557078Smjnelson '''Return boolean indicating whether the workspace has an 6567078Smjnelson uncommitted named branch''' 6577078Smjnelson 6589006Srichlowe@richlowe.net wctx = self.workingctx() 6597078Smjnelson return wctx.branch() != wctx.parents()[0].branch() 6607078Smjnelson 6617078Smjnelson def active(self, parent=None): 6627078Smjnelson '''Return an ActiveList describing changes between workspace 6637078Smjnelson and parent workspace (including uncommitted changes). 6647078Smjnelson If workspace has no parent ActiveList will still describe any 6657078Smjnelson uncommitted changes''' 6667078Smjnelson 6677078Smjnelson parent = self.parent(parent) 6687078Smjnelson if parent in self.activecache: 6697078Smjnelson return self.activecache[parent] 6707078Smjnelson 6717078Smjnelson if parent: 6727078Smjnelson outgoing = self.findoutgoing(parent) 67310263Srichlowe@richlowe.net outnodes = self.repo.changelog.nodesbetween(outgoing)[0] 6747078Smjnelson else: 6757078Smjnelson outgoing = [] # No parent, no outgoing nodes 67610263Srichlowe@richlowe.net outnodes = [] 6777078Smjnelson 67810263Srichlowe@richlowe.net localtip = self._localtip(outnodes, self.workingctx()) 6797078Smjnelson 68010263Srichlowe@richlowe.net if localtip.rev() is None: 68110263Srichlowe@richlowe.net heads = localtip.parents() 68210263Srichlowe@richlowe.net else: 68310263Srichlowe@richlowe.net heads = [localtip] 6847078Smjnelson 68510263Srichlowe@richlowe.net ctxs = [self.repo.changectx(n) for n in 68610263Srichlowe@richlowe.net self.repo.changelog.nodesbetween(outgoing, 68710263Srichlowe@richlowe.net [h.node() for h in heads])[0]] 6887078Smjnelson 68910263Srichlowe@richlowe.net if localtip.rev() is None: 69010263Srichlowe@richlowe.net ctxs.append(localtip) 69110263Srichlowe@richlowe.net 69210263Srichlowe@richlowe.net act = ActiveList(self, self._parenttip(heads, outnodes), ctxs) 6937078Smjnelson 6947078Smjnelson self.activecache[parent] = act 6957078Smjnelson return act 6967078Smjnelson 6977298SMark.J.Nelson@Sun.COM def pdiff(self, pats, opts, parent=None): 6987078Smjnelson 'Return diffs relative to PARENT, as best as we can make out' 6997078Smjnelson 7007078Smjnelson parent = self.parent(parent) 7017078Smjnelson act = self.active(parent) 7027078Smjnelson 7037078Smjnelson # 7047078Smjnelson # act.localtip maybe nil, in the case of uncommitted local 7057078Smjnelson # changes. 7067078Smjnelson # 7077078Smjnelson if not act.revs: 7087078Smjnelson return 7097078Smjnelson 710*10399Srichlowe@richlowe.net matchfunc = cmdutil.match(self.repo, pats, opts) 7117298SMark.J.Nelson@Sun.COM opts = patch.diffopts(self.ui, opts) 7127298SMark.J.Nelson@Sun.COM 7139006Srichlowe@richlowe.net return self.diff(act.parenttip.node(), act.localtip.node(), 7149006Srichlowe@richlowe.net match=matchfunc, opts=opts) 7157078Smjnelson 7167078Smjnelson def squishdeltas(self, active, message, user=None): 717*10399Srichlowe@richlowe.net '''Create a single conglomerate changeset based on a given 718*10399Srichlowe@richlowe.net active list. Removes the original changesets comprising the 719*10399Srichlowe@richlowe.net given active list, and any tags pointing to them. 720*10399Srichlowe@richlowe.net 721*10399Srichlowe@richlowe.net Operation: 722*10399Srichlowe@richlowe.net 723*10399Srichlowe@richlowe.net - Commit an activectx object representing the specified 724*10399Srichlowe@richlowe.net active list, 725*10399Srichlowe@richlowe.net 726*10399Srichlowe@richlowe.net - Remove any local tags pointing to changesets in the 727*10399Srichlowe@richlowe.net specified active list. 7287078Smjnelson 729*10399Srichlowe@richlowe.net - Remove the changesets comprising the specified active 730*10399Srichlowe@richlowe.net list. 731*10399Srichlowe@richlowe.net 732*10399Srichlowe@richlowe.net - Remove any metadata that may refer to changesets that were 733*10399Srichlowe@richlowe.net removed. 7347078Smjnelson 735*10399Srichlowe@richlowe.net Calling code is expected to hold both the working copy lock 736*10399Srichlowe@richlowe.net and repository lock of the destination workspace 737*10399Srichlowe@richlowe.net ''' 738*10399Srichlowe@richlowe.net 739*10399Srichlowe@richlowe.net def strip_local_tags(active): 740*10399Srichlowe@richlowe.net '''Remove any local tags referring to the specified nodes.''' 7417078Smjnelson 7427078Smjnelson if os.path.exists(self.repo.join('localtags')): 743*10399Srichlowe@richlowe.net fh = None 744*10399Srichlowe@richlowe.net try: 745*10399Srichlowe@richlowe.net fh = self.repo.opener('localtags') 746*10399Srichlowe@richlowe.net tags = active.prune_tags(fh) 747*10399Srichlowe@richlowe.net fh.close() 7487078Smjnelson 749*10399Srichlowe@richlowe.net fh = self.repo.opener('localtags', 'w', atomictemp=True) 750*10399Srichlowe@richlowe.net fh.writelines(tags) 751*10399Srichlowe@richlowe.net fh.rename() 752*10399Srichlowe@richlowe.net finally: 753*10399Srichlowe@richlowe.net if fh and not fh.closed: 754*10399Srichlowe@richlowe.net fh.close() 7557078Smjnelson 7567078Smjnelson if active.files(): 757*10399Srichlowe@richlowe.net for entry in active: 758*10399Srichlowe@richlowe.net # 759*10399Srichlowe@richlowe.net # Work around Mercurial issue #1666, if the source 760*10399Srichlowe@richlowe.net # file of a rename exists in the working copy 761*10399Srichlowe@richlowe.net # Mercurial will complain, and remove the file. 762*10399Srichlowe@richlowe.net # 763*10399Srichlowe@richlowe.net # We preemptively remove the file to avoid the 764*10399Srichlowe@richlowe.net # complaint (the user was asked about this in 765*10399Srichlowe@richlowe.net # cdm_recommit) 766*10399Srichlowe@richlowe.net # 767*10399Srichlowe@richlowe.net if entry.is_renamed(): 768*10399Srichlowe@richlowe.net path = self.repo.wjoin(entry.parentname) 769*10399Srichlowe@richlowe.net if os.path.exists(path): 770*10399Srichlowe@richlowe.net os.unlink(path) 771*10399Srichlowe@richlowe.net 772*10399Srichlowe@richlowe.net self.repo.commitctx(active.context(message, user)) 773*10399Srichlowe@richlowe.net wsstate = "recommitted" 774*10399Srichlowe@richlowe.net destination = self.repo.changelog.tip() 7757078Smjnelson else: 7767078Smjnelson # 7777078Smjnelson # If all we're doing is stripping the old nodes, we want to 7787078Smjnelson # update the working copy such that we're not at a revision 7797078Smjnelson # that's about to go away. 7807078Smjnelson # 781*10399Srichlowe@richlowe.net wsstate = "tip" 782*10399Srichlowe@richlowe.net destination = active.parenttip.node() 783*10399Srichlowe@richlowe.net 784*10399Srichlowe@richlowe.net self.clean(destination) 785*10399Srichlowe@richlowe.net 786*10399Srichlowe@richlowe.net # 787*10399Srichlowe@richlowe.net # Tags were elided by the activectx object. Local tags, 788*10399Srichlowe@richlowe.net # however, must be removed manually. 789*10399Srichlowe@richlowe.net # 790*10399Srichlowe@richlowe.net try: 791*10399Srichlowe@richlowe.net strip_local_tags(active) 792*10399Srichlowe@richlowe.net except EnvironmentError, e: 793*10399Srichlowe@richlowe.net raise util.Abort('Could not recommit tags: %s\n' % e) 7947078Smjnelson 7957078Smjnelson # Silence all the strip and update fun 7967078Smjnelson self.ui.pushbuffer() 7977078Smjnelson 7987078Smjnelson # 799*10399Srichlowe@richlowe.net # Remove the active lists component changesets by stripping 800*10399Srichlowe@richlowe.net # the base of any active branch (of which there may be 801*10399Srichlowe@richlowe.net # several) 8027078Smjnelson # 8037078Smjnelson bases = active.bases() 8047078Smjnelson try: 8057078Smjnelson try: 8067078Smjnelson for basenode in bases: 807*10399Srichlowe@richlowe.net # 808*10399Srichlowe@richlowe.net # Any cached information about the repository is 809*10399Srichlowe@richlowe.net # likely to be invalid during the strip. The 810*10399Srichlowe@richlowe.net # caching of branch tags is especially 811*10399Srichlowe@richlowe.net # problematic. 812*10399Srichlowe@richlowe.net # 813*10399Srichlowe@richlowe.net self.repo.invalidate() 8147078Smjnelson repair.strip(self.ui, self.repo, basenode, backup=False) 8157078Smjnelson except: 8167078Smjnelson # 8177078Smjnelson # If this fails, it may leave us in a surprising place in 8187078Smjnelson # the history. 8197078Smjnelson # 8207078Smjnelson # We want to warn the user that something went wrong, 8217078Smjnelson # and what will happen next, re-raise the exception, and 8227078Smjnelson # bring the working copy back into a consistent state 8237078Smjnelson # (which the finally block will do) 8247078Smjnelson # 8257078Smjnelson self.ui.warn("stripping failed, your workspace will have " 8267078Smjnelson "superfluous heads.\n" 8277078Smjnelson "your workspace has been updated to the " 828*10399Srichlowe@richlowe.net "%s changeset.\n" % wsstate) 8297078Smjnelson raise # Re-raise the exception 8307078Smjnelson finally: 831*10399Srichlowe@richlowe.net self.clean() 832*10399Srichlowe@richlowe.net self.repo.dirstate.write() # Flush the dirstate 833*10399Srichlowe@richlowe.net self.repo.invalidate() # Invalidate caches 834*10399Srichlowe@richlowe.net 8357078Smjnelson # 8367078Smjnelson # We need to remove Hg's undo information (used for rollback), 8377078Smjnelson # since it refers to data that will probably not exist after 8387078Smjnelson # the strip. 8397078Smjnelson # 8407078Smjnelson if os.path.exists(self.repo.sjoin('undo')): 8417078Smjnelson try: 8427078Smjnelson os.unlink(self.repo.sjoin('undo')) 8437078Smjnelson except EnvironmentError, e: 8447078Smjnelson raise util.Abort('failed to remove undo data: %s\n' % e) 8457078Smjnelson 8467078Smjnelson self.ui.popbuffer() 8477078Smjnelson 8487078Smjnelson def filepath(self, path): 8497078Smjnelson 'Return the full path to a workspace file.' 8507078Smjnelson return self.repo.pathto(path) 8517078Smjnelson 8527078Smjnelson def clean(self, rev=None): 8537078Smjnelson '''Bring workspace up to REV (or tip) forcefully (discarding in 8547078Smjnelson progress changes)''' 8559006Srichlowe@richlowe.net 8567078Smjnelson if rev != None: 8577078Smjnelson rev = self.repo.lookup(rev) 8587078Smjnelson else: 8597078Smjnelson rev = self.repo.changelog.tip() 8607078Smjnelson 8617078Smjnelson hg.clean(self.repo, rev, show_stats=False) 8627078Smjnelson 8637078Smjnelson def mq_applied(self): 8647078Smjnelson '''True if the workspace has Mq patches applied''' 8657078Smjnelson q = mq.queue(self.ui, self.repo.join('')) 8667078Smjnelson return q.applied 8679006Srichlowe@richlowe.net 868*10399Srichlowe@richlowe.net def workingctx(self): 869*10399Srichlowe@richlowe.net return self.repo.changectx(None) 8709006Srichlowe@richlowe.net 871*10399Srichlowe@richlowe.net def diff(self, node1=None, node2=None, match=None, opts=None): 872*10399Srichlowe@richlowe.net ret = cStringIO.StringIO() 873*10399Srichlowe@richlowe.net try: 8749006Srichlowe@richlowe.net for chunk in patch.diff(self.repo, node1, node2, match=match, 8759006Srichlowe@richlowe.net opts=opts): 8769006Srichlowe@richlowe.net ret.write(chunk) 877*10399Srichlowe@richlowe.net finally: 878*10399Srichlowe@richlowe.net # Workaround Hg bug 1651 879*10399Srichlowe@richlowe.net if not Version.at_least("1.3"): 880*10399Srichlowe@richlowe.net self.repo.dirstate.invalidate() 8819006Srichlowe@richlowe.net 882*10399Srichlowe@richlowe.net return ret.getvalue() 883