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# 17*12776SJames.McPherson@Sun.COM# Copyright (c) 2008, 2010, Oracle and/or its affiliates. All rights reserved. 187078Smjnelson# 197078Smjnelson 207078Smjnelson# 217078Smjnelson# Theory: 227078Smjnelson# 237078Smjnelson# Workspaces have a non-binding parent/child relationship. 247078Smjnelson# All important operations apply to the changes between the two. 257078Smjnelson# 267078Smjnelson# However, for the sake of remote operation, the 'parent' of a 277078Smjnelson# workspace is not seen as a literal entity, instead the figurative 287078Smjnelson# parent contains the last changeset common to both parent and child, 297078Smjnelson# as such the 'parent tip' is actually nothing of the sort, but instead a 307078Smjnelson# convenient imitation. 317078Smjnelson# 327078Smjnelson# Any change made to a workspace is a change to a file therein, such 337078Smjnelson# changes can be represented briefly as whether the file was 347078Smjnelson# modified/added/removed as compared to the parent workspace, whether 357078Smjnelson# the file has a different name in the parent and if so, whether it 367078Smjnelson# was renamed or merely copied. Each changed file has an 377078Smjnelson# associated ActiveEntry. 387078Smjnelson# 397078Smjnelson# The ActiveList being a list ActiveEntrys can thus present the entire 407078Smjnelson# change in workspace state between a parent and its child, and is the 417078Smjnelson# important bit here (in that if it is incorrect, everything else will 427078Smjnelson# be as incorrect, or more) 437078Smjnelson# 447078Smjnelson 457078Smjnelsonimport cStringIO 467078Smjnelsonimport os 4710399Srichlowe@richlowe.netfrom mercurial import cmdutil, context, hg, node, patch, repair, util 487078Smjnelsonfrom hgext import mq 497078Smjnelson 509006Srichlowe@richlowe.netfrom onbld.Scm import Version 519006Srichlowe@richlowe.net 5210399Srichlowe@richlowe.net# 5310399Srichlowe@richlowe.net# Mercurial >= 1.2 has its exception types in a mercurial.error 5410399Srichlowe@richlowe.net# module, prior versions had them in their associated modules. 5510399Srichlowe@richlowe.net# 5610399Srichlowe@richlowe.netif Version.at_least("1.2"): 5710399Srichlowe@richlowe.net from mercurial import error 5810399Srichlowe@richlowe.net HgRepoError = error.RepoError 5910399Srichlowe@richlowe.net HgLookupError = error.LookupError 6010399Srichlowe@richlowe.netelse: 6110399Srichlowe@richlowe.net from mercurial import repo, revlog 6210399Srichlowe@richlowe.net HgRepoError = repo.RepoError 6310399Srichlowe@richlowe.net HgLookupError = revlog.LookupError 6410399Srichlowe@richlowe.net 657078Smjnelson 667078Smjnelsonclass ActiveEntry(object): 677078Smjnelson '''Representation of the changes made to a single file. 687078Smjnelson 697078Smjnelson MODIFIED - Contents changed, but no other changes were made 707078Smjnelson ADDED - File is newly created 717078Smjnelson REMOVED - File is being removed 727078Smjnelson 737078Smjnelson Copies are represented by an Entry whose .parentname is non-nil 747078Smjnelson 757078Smjnelson Truly copied files have non-nil .parentname and .renamed = False 767078Smjnelson Renames have non-nil .parentname and .renamed = True 777078Smjnelson 787078Smjnelson Do not access any of this information directly, do so via the 797078Smjnelson 807078Smjnelson .is_<change>() methods.''' 817078Smjnelson 827078Smjnelson MODIFIED = 1 837078Smjnelson ADDED = 2 847078Smjnelson REMOVED = 3 857078Smjnelson 867078Smjnelson def __init__(self, name): 877078Smjnelson self.name = name 887078Smjnelson self.change = None 897078Smjnelson self.parentname = None 907078Smjnelson # As opposed to copied (or neither) 917078Smjnelson self.renamed = False 927078Smjnelson self.comments = [] 937078Smjnelson 947078Smjnelson # 957078Smjnelson # ActiveEntrys sort by the name of the file they represent. 967078Smjnelson # 977078Smjnelson def __cmp__(self, other): 987078Smjnelson return cmp(self.name, other.name) 997078Smjnelson 1007078Smjnelson def is_added(self): 1017078Smjnelson return self.change == self.ADDED 1027078Smjnelson 1037078Smjnelson def is_modified(self): 1047078Smjnelson return self.change == self.MODIFIED 1057078Smjnelson 1067078Smjnelson def is_removed(self): 1077078Smjnelson return self.change == self.REMOVED 1087078Smjnelson 1097078Smjnelson def is_renamed(self): 1107078Smjnelson return self.parentname and self.renamed 1117078Smjnelson 1127078Smjnelson def is_copied(self): 1137078Smjnelson return self.parentname and not self.renamed 1147078Smjnelson 1157078Smjnelson 1167078Smjnelsonclass ActiveList(object): 1177078Smjnelson '''Complete representation of workspace change. 1187078Smjnelson 1197078Smjnelson In practice, a container for ActiveEntrys, and methods to build them, 1207078Smjnelson update them, and deal with them en masse.''' 1217078Smjnelson 1227078Smjnelson def __init__(self, ws, parenttip, revs=None): 1237078Smjnelson self._active = {} 1247078Smjnelson self.ws = ws 1257078Smjnelson 1267078Smjnelson self.revs = revs 1277078Smjnelson 1287078Smjnelson self.base = None 1297078Smjnelson self.parenttip = parenttip 1307078Smjnelson 1317078Smjnelson # 1327078Smjnelson # If we couldn't find a parenttip, the two repositories must 1337078Smjnelson # be unrelated (Hg catches most of this, but this case is valid for it 1347078Smjnelson # but invalid for us) 1357078Smjnelson # 1367078Smjnelson if self.parenttip == None: 1377078Smjnelson raise util.Abort('repository is unrelated') 1387078Smjnelson self.localtip = None 1397078Smjnelson 1407078Smjnelson if revs: 1417078Smjnelson self.base = revs[0] 1427078Smjnelson self.localtip = revs[-1] 1437078Smjnelson 1447078Smjnelson self._comments = [] 1457078Smjnelson 1467078Smjnelson self._build(revs) 1477078Smjnelson 1487078Smjnelson def _build(self, revs): 1497078Smjnelson if not revs: 1507078Smjnelson return 1517078Smjnelson 1527078Smjnelson status = self.ws.status(self.parenttip.node(), self.localtip.node()) 1537078Smjnelson 1547078Smjnelson files = [] 1557078Smjnelson for ctype in status.values(): 1567078Smjnelson files.extend(ctype) 1577078Smjnelson 1587078Smjnelson # 1597078Smjnelson # When a file is renamed, two operations actually occur. 1607078Smjnelson # A file copy from source to dest and a removal of source. 1617078Smjnelson # 1627078Smjnelson # These are represented as two distinct entries in the 1637078Smjnelson # changectx and status (one on the dest file for the 1647078Smjnelson # copy, one on the source file for the remove). 1657078Smjnelson # 1667078Smjnelson # Since these are unconnected in both the context and 1677078Smjnelson # status we can only make the association by explicitly 1687078Smjnelson # looking for it. 1697078Smjnelson # 1707078Smjnelson # We deal with this thusly: 1717078Smjnelson # 1727078Smjnelson # We maintain a dict dest -> source of all copies 1737078Smjnelson # (updating dest as appropriate, but leaving source alone). 1747078Smjnelson # 1757078Smjnelson # After all other processing, we mark as renamed any pair 1767078Smjnelson # where source is on the removed list. 1777078Smjnelson # 1787078Smjnelson copies = {} 1797078Smjnelson 1807078Smjnelson # 1817078Smjnelson # Walk revs looking for renames and adding files that 1827078Smjnelson # are in both change context and status to the active 1837078Smjnelson # list. 1847078Smjnelson # 1857078Smjnelson for ctx in revs: 1867078Smjnelson desc = ctx.description().splitlines() 1877078Smjnelson 1887078Smjnelson self._comments.extend(desc) 1897078Smjnelson 1907078Smjnelson for fname in ctx.files(): 1917078Smjnelson # 1927078Smjnelson # We store comments per-entry as well, for the sake of 1937078Smjnelson # webrev and similar. We store twice to avoid the problems 1947078Smjnelson # of uniquifying comments for the general list (and possibly 1957078Smjnelson # destroying multi-line entities in the process). 1967078Smjnelson # 1977078Smjnelson if fname not in self: 1987078Smjnelson self._addentry(fname) 1997078Smjnelson self[fname].comments.extend(desc) 2007078Smjnelson 2017078Smjnelson try: 2027078Smjnelson fctx = ctx.filectx(fname) 20310399Srichlowe@richlowe.net except HgLookupError: 2047078Smjnelson continue 2057078Smjnelson 2067078Smjnelson # 2077078Smjnelson # NB: .renamed() is a misnomer, this actually checks 2087078Smjnelson # for copies. 2097078Smjnelson # 2107078Smjnelson rn = fctx.renamed() 2117078Smjnelson if rn: 2127078Smjnelson # 2137078Smjnelson # If the source file is a known copy we know its 2147078Smjnelson # ancestry leads us to the parent. 2157078Smjnelson # Otherwise make sure the source file is known to 2167078Smjnelson # be in the parent, we need not care otherwise. 2177078Smjnelson # 2187078Smjnelson # We detect cycles at a later point. There is no 2197078Smjnelson # reason to continuously handle them. 2207078Smjnelson # 2217078Smjnelson if rn[0] in copies: 2227078Smjnelson copies[fname] = copies[rn[0]] 2237078Smjnelson elif rn[0] in self.parenttip.manifest(): 2247078Smjnelson copies[fname] = rn[0] 2257078Smjnelson 2267078Smjnelson # 2277078Smjnelson # Walk the copy list marking as copied any non-cyclic pair 2287078Smjnelson # where the destination file is still present in the local 2297078Smjnelson # tip (to avoid ephemeral changes) 2307078Smjnelson # 2317078Smjnelson # Where source is removed, mark as renamed, and remove the 2327078Smjnelson # AL entry for the source file 2337078Smjnelson # 2347078Smjnelson for fname, oldname in copies.iteritems(): 2357078Smjnelson if fname == oldname or fname not in self.localtip.manifest(): 2367078Smjnelson continue 2377078Smjnelson 2387078Smjnelson self[fname].parentname = oldname 2397078Smjnelson 2407078Smjnelson if oldname in status['removed']: 2417078Smjnelson self[fname].renamed = True 2427078Smjnelson if oldname in self: 2437078Smjnelson del self[oldname] 2447078Smjnelson 2457078Smjnelson # 2467078Smjnelson # Walk the active list setting the change type for each active 2477078Smjnelson # file. 2487078Smjnelson # 2497078Smjnelson # In the case of modified files that are not renames or 2507078Smjnelson # copies, we do a content comparison, and drop entries that 2517078Smjnelson # are not actually modified. 2527078Smjnelson # 2537078Smjnelson # We walk a copy of the AL such that we can drop entries 2547078Smjnelson # within the loop. 2557078Smjnelson # 2567078Smjnelson for entry in self._active.values(): 2577078Smjnelson if entry.name not in files: 2587078Smjnelson del self[entry.name] 2597078Smjnelson continue 2607078Smjnelson 2617078Smjnelson if entry.name in status['added']: 2627078Smjnelson entry.change = ActiveEntry.ADDED 2637078Smjnelson elif entry.name in status['removed']: 2647078Smjnelson entry.change = ActiveEntry.REMOVED 2657078Smjnelson elif entry.name in status['modified']: 2667078Smjnelson entry.change = ActiveEntry.MODIFIED 2677078Smjnelson 2687078Smjnelson # 2697078Smjnelson # There are cases during a merge where a file will be in 2707078Smjnelson # the status return as modified, but in reality be an 2717078Smjnelson # addition (ie, not in the parenttip). 2727078Smjnelson # 2737078Smjnelson # We need to check whether the file is actually present 2747078Smjnelson # in the parenttip, and set it as an add, if not. 2757078Smjnelson # 2767078Smjnelson if entry.name not in self.parenttip.manifest(): 2777078Smjnelson entry.change = ActiveEntry.ADDED 278*12776SJames.McPherson@Sun.COM elif entry.is_modified(): 279*12776SJames.McPherson@Sun.COM if not self._changed_file(entry.name): 2807078Smjnelson del self[entry.name] 2817078Smjnelson continue 2827078Smjnelson 2837078Smjnelson assert entry.change 2847078Smjnelson 2857078Smjnelson def __contains__(self, fname): 2867078Smjnelson return fname in self._active 2877078Smjnelson 2887078Smjnelson def __getitem__(self, key): 2897078Smjnelson return self._active[key] 2907078Smjnelson 2917078Smjnelson def __setitem__(self, key, value): 2927078Smjnelson self._active[key] = value 2937078Smjnelson 2947078Smjnelson def __delitem__(self, key): 2957078Smjnelson del self._active[key] 2967078Smjnelson 2977078Smjnelson def __iter__(self): 2987078Smjnelson for entry in self._active.values(): 2997078Smjnelson yield entry 3007078Smjnelson 3017078Smjnelson def _addentry(self, fname): 3027078Smjnelson if fname not in self: 3037078Smjnelson self[fname] = ActiveEntry(fname) 3047078Smjnelson 3057078Smjnelson def files(self): 30610399Srichlowe@richlowe.net '''Return the list of pathnames of all files touched by this 30710399Srichlowe@richlowe.net ActiveList 3087078Smjnelson 30910399Srichlowe@richlowe.net Where files have been renamed, this will include both their 31010399Srichlowe@richlowe.net current name and the name which they had in the parent tip. 31110399Srichlowe@richlowe.net ''' 31210399Srichlowe@richlowe.net 31310399Srichlowe@richlowe.net ret = self._active.keys() 3147078Smjnelson ret.extend([x.parentname for x in self 31510399Srichlowe@richlowe.net if x.is_renamed() and x.parentname not in ret]) 3167078Smjnelson return ret 3177078Smjnelson 3187078Smjnelson def comments(self): 3197078Smjnelson return self._comments 3207078Smjnelson 3217078Smjnelson def bases(self): 32211646SJames.McPherson@Sun.COM '''Return the list of changesets that are roots of the ActiveList. 3237078Smjnelson 32411646SJames.McPherson@Sun.COM This is the set of active changesets where neither parent 32511646SJames.McPherson@Sun.COM changeset is itself active.''' 3267078Smjnelson 32711646SJames.McPherson@Sun.COM revset = set(self.revs) 32811646SJames.McPherson@Sun.COM return filter(lambda ctx: not [p for p in ctx.parents() if p in revset], 32911646SJames.McPherson@Sun.COM self.revs) 3307078Smjnelson 3317078Smjnelson def tags(self): 3327078Smjnelson '''Find tags that refer to a changeset in the ActiveList, 33310399Srichlowe@richlowe.net returning a list of 3-tuples (tag, node, is_local) for each. 33410399Srichlowe@richlowe.net 33510399Srichlowe@richlowe.net We return all instances of a tag that refer to such a node, 33610399Srichlowe@richlowe.net not just that which takes precedence.''' 3377078Smjnelson 33810399Srichlowe@richlowe.net def colliding_tags(iterable, nodes, local): 33910399Srichlowe@richlowe.net for nd, name in [line.rstrip().split(' ', 1) for line in iterable]: 34010399Srichlowe@richlowe.net if nd in nodes: 34110399Srichlowe@richlowe.net yield (name, self.ws.repo.lookup(nd), local) 34210399Srichlowe@richlowe.net 34310399Srichlowe@richlowe.net tags = [] 34410399Srichlowe@richlowe.net nodes = set(node.hex(ctx.node()) for ctx in self.revs) 3457078Smjnelson 3467078Smjnelson if os.path.exists(self.ws.repo.join('localtags')): 34710399Srichlowe@richlowe.net fh = self.ws.repo.opener('localtags') 34810399Srichlowe@richlowe.net tags.extend(colliding_tags(fh, nodes, True)) 34910399Srichlowe@richlowe.net fh.close() 3507078Smjnelson 3517078Smjnelson # We want to use the tags file from the localtip 35210399Srichlowe@richlowe.net if '.hgtags' in self.localtip: 35310399Srichlowe@richlowe.net data = self.localtip.filectx('.hgtags').data().splitlines() 35410399Srichlowe@richlowe.net tags.extend(colliding_tags(data, nodes, False)) 3557078Smjnelson 3567078Smjnelson return tags 3577078Smjnelson 35810399Srichlowe@richlowe.net def prune_tags(self, data): 35910399Srichlowe@richlowe.net '''Return a copy of data, which should correspond to the 36010399Srichlowe@richlowe.net contents of a Mercurial tags file, with any tags that refer to 36110399Srichlowe@richlowe.net changesets which are components of the ActiveList removed.''' 36210399Srichlowe@richlowe.net 36310399Srichlowe@richlowe.net nodes = set(node.hex(ctx.node()) for ctx in self.revs) 36410399Srichlowe@richlowe.net return [t for t in data if t.split(' ', 1)[0] not in nodes] 36510399Srichlowe@richlowe.net 366*12776SJames.McPherson@Sun.COM def _changed_file(self, path): 367*12776SJames.McPherson@Sun.COM '''Compare the parent and local versions of a given file. 368*12776SJames.McPherson@Sun.COM Return True if file changed, False otherwise. 3697078Smjnelson 370*12776SJames.McPherson@Sun.COM Note that this compares the given path in both versions, not the given 371*12776SJames.McPherson@Sun.COM entry; renamed and copied files are compared by name, not history. 3727078Smjnelson 3737078Smjnelson The fast path compares file metadata, slow path is a 3747078Smjnelson real comparison of file content.''' 3757078Smjnelson 376*12776SJames.McPherson@Sun.COM # Note that we use localtip.manifest() here because of a bug in 377*12776SJames.McPherson@Sun.COM # Mercurial 1.1.2's workingctx.__contains__ 378*12776SJames.McPherson@Sun.COM if ((path in self.parenttip) != (path in self.localtip.manifest())): 379*12776SJames.McPherson@Sun.COM return True 380*12776SJames.McPherson@Sun.COM 381*12776SJames.McPherson@Sun.COM parentfile = self.parenttip.filectx(path) 382*12776SJames.McPherson@Sun.COM localfile = self.localtip.filectx(path) 3837078Smjnelson 3847078Smjnelson # 3857078Smjnelson # NB: Keep these ordered such as to make every attempt 3867078Smjnelson # to short-circuit the more time consuming checks. 3877078Smjnelson # 3887078Smjnelson if parentfile.size() != localfile.size(): 3897078Smjnelson return True 3907078Smjnelson 39110399Srichlowe@richlowe.net if parentfile.flags() != localfile.flags(): 3927078Smjnelson return True 3937078Smjnelson 3947078Smjnelson if parentfile.cmp(localfile.data()): 3957078Smjnelson return True 3967078Smjnelson 39710399Srichlowe@richlowe.net def context(self, message, user): 39810399Srichlowe@richlowe.net '''Return a Mercurial context object representing the entire 39910399Srichlowe@richlowe.net ActiveList as one change.''' 40010399Srichlowe@richlowe.net return activectx(self, message, user) 40110399Srichlowe@richlowe.net 40210399Srichlowe@richlowe.net 40310399Srichlowe@richlowe.netclass activectx(context.memctx): 40410399Srichlowe@richlowe.net '''Represent an ActiveList as a Mercurial context object. 40510399Srichlowe@richlowe.net 40610399Srichlowe@richlowe.net Part of the WorkSpace.squishdeltas implementation.''' 40710399Srichlowe@richlowe.net 40810399Srichlowe@richlowe.net def __init__(self, active, message, user): 40910399Srichlowe@richlowe.net '''Build an activectx object. 41010399Srichlowe@richlowe.net 41110399Srichlowe@richlowe.net active - The ActiveList object used as the source for all data. 41210399Srichlowe@richlowe.net message - Changeset description 41310399Srichlowe@richlowe.net user - Committing user''' 41410399Srichlowe@richlowe.net 41510399Srichlowe@richlowe.net def filectxfn(repository, ctx, fname): 41610399Srichlowe@richlowe.net fctx = active.localtip.filectx(fname) 41710399Srichlowe@richlowe.net data = fctx.data() 41810399Srichlowe@richlowe.net 41910399Srichlowe@richlowe.net # 42010399Srichlowe@richlowe.net # .hgtags is a special case, tags referring to active list 42110399Srichlowe@richlowe.net # component changesets should be elided. 42210399Srichlowe@richlowe.net # 42310399Srichlowe@richlowe.net if fname == '.hgtags': 42410399Srichlowe@richlowe.net data = '\n'.join(active.prune_tags(data.splitlines())) 42510399Srichlowe@richlowe.net 42610399Srichlowe@richlowe.net return context.memfilectx(fname, data, 'l' in fctx.flags(), 42710399Srichlowe@richlowe.net 'x' in fctx.flags(), 42810399Srichlowe@richlowe.net active[fname].parentname) 42910399Srichlowe@richlowe.net 43010399Srichlowe@richlowe.net self.__active = active 43110399Srichlowe@richlowe.net parents = (active.parenttip.node(), node.nullid) 43210399Srichlowe@richlowe.net extra = {'branch': active.localtip.branch()} 43310399Srichlowe@richlowe.net context.memctx.__init__(self, active.ws.repo, parents, message, 43410399Srichlowe@richlowe.net active.files(), filectxfn, user=user, 43510399Srichlowe@richlowe.net extra=extra) 43610399Srichlowe@richlowe.net 43710399Srichlowe@richlowe.net def modified(self): 43810399Srichlowe@richlowe.net return [entry.name for entry in self.__active if entry.is_modified()] 43910399Srichlowe@richlowe.net 44010399Srichlowe@richlowe.net def added(self): 44110399Srichlowe@richlowe.net return [entry.name for entry in self.__active if entry.is_added()] 44210399Srichlowe@richlowe.net 44310399Srichlowe@richlowe.net def removed(self): 44411733Srichlowe@richlowe.net ret = set(entry.name for entry in self.__active if entry.is_removed()) 44511733Srichlowe@richlowe.net ret.update(set(x.parentname for x in self.__active if x.is_renamed())) 44611733Srichlowe@richlowe.net return list(ret) 44710399Srichlowe@richlowe.net 44810399Srichlowe@richlowe.net def files(self): 44910399Srichlowe@richlowe.net return self.__active.files() 45010399Srichlowe@richlowe.net 4517078Smjnelson 4527078Smjnelsonclass WorkSpace(object): 4537078Smjnelson 4547078Smjnelson def __init__(self, repository): 4557078Smjnelson self.repo = repository 4567078Smjnelson self.ui = self.repo.ui 4577078Smjnelson self.name = self.repo.root 4587078Smjnelson 4597078Smjnelson self.activecache = {} 4607078Smjnelson 4617078Smjnelson def parent(self, spec=None): 46210263Srichlowe@richlowe.net '''Return the canonical workspace parent, either SPEC (which 46310263Srichlowe@richlowe.net will be expanded) if provided or the default parent 46410263Srichlowe@richlowe.net otherwise.''' 4657078Smjnelson 46610263Srichlowe@richlowe.net if spec: 46710263Srichlowe@richlowe.net return self.ui.expandpath(spec) 4687078Smjnelson 46910263Srichlowe@richlowe.net p = self.ui.expandpath('default') 47010263Srichlowe@richlowe.net if p == 'default': 47110263Srichlowe@richlowe.net return None 47210263Srichlowe@richlowe.net else: 47310263Srichlowe@richlowe.net return p 4747078Smjnelson 47510263Srichlowe@richlowe.net def _localtip(self, outgoing, wctx): 47610263Srichlowe@richlowe.net '''Return the most representative changeset to act as the 47710263Srichlowe@richlowe.net localtip. 4787078Smjnelson 47910263Srichlowe@richlowe.net If the working directory is modified (has file changes, is a 48010263Srichlowe@richlowe.net merge, or has switched branches), this will be a workingctx. 4817078Smjnelson 48210263Srichlowe@richlowe.net If the working directory is unmodified, this will be the most 48310263Srichlowe@richlowe.net recent (highest revision number) local (outgoing) head on the 48410263Srichlowe@richlowe.net current branch, if no heads are determined to be outgoing, it 48510263Srichlowe@richlowe.net will be the most recent head on the current branch. 48610263Srichlowe@richlowe.net ''' 4877078Smjnelson 4887078Smjnelson # 48910263Srichlowe@richlowe.net # A modified working copy is seen as a proto-branch, and thus 49010263Srichlowe@richlowe.net # our only option as the local tip. 4917078Smjnelson # 4927078Smjnelson if (wctx.files() or len(wctx.parents()) > 1 or 4937078Smjnelson wctx.branch() != wctx.parents()[0].branch()): 49410263Srichlowe@richlowe.net return wctx 49510263Srichlowe@richlowe.net 49610263Srichlowe@richlowe.net heads = self.repo.heads(start=wctx.parents()[0].node()) 49710263Srichlowe@richlowe.net headctxs = [self.repo.changectx(n) for n in heads] 49810263Srichlowe@richlowe.net localctxs = [c for c in headctxs if c.node() in outgoing] 49910263Srichlowe@richlowe.net 50010263Srichlowe@richlowe.net ltip = sorted(localctxs or headctxs, key=lambda x: x.rev())[-1] 5017078Smjnelson 50210263Srichlowe@richlowe.net if len(heads) > 1: 50310263Srichlowe@richlowe.net self.ui.warn('The current branch has more than one head, ' 50410263Srichlowe@richlowe.net 'using %s\n' % ltip.rev()) 50510263Srichlowe@richlowe.net 50610263Srichlowe@richlowe.net return ltip 5077078Smjnelson 50810263Srichlowe@richlowe.net def _parenttip(self, heads, outgoing): 50910263Srichlowe@richlowe.net '''Return the highest-numbered, non-outgoing changeset that is 51010263Srichlowe@richlowe.net an ancestor of a changeset in heads. 5117078Smjnelson 51210263Srichlowe@richlowe.net This is intended to find the most recent changeset on a given 51310263Srichlowe@richlowe.net branch that is shared between a parent and child workspace, 51410263Srichlowe@richlowe.net such that it can act as a stand-in for the parent workspace. 51510263Srichlowe@richlowe.net ''' 5167078Smjnelson 5177078Smjnelson def tipmost_shared(head, outnodes): 5187078Smjnelson '''Return the tipmost node on the same branch as head that is not 5197078Smjnelson in outnodes. 5207078Smjnelson 5217078Smjnelson We walk from head to the bottom of the workspace (revision 5227078Smjnelson 0) collecting nodes not in outnodes during the add phase 5237078Smjnelson and return the first node we see in the iter phase that 5247078Smjnelson was previously collected. 5257078Smjnelson 52610263Srichlowe@richlowe.net If no node is found (all revisions >= 0 are outgoing), the 52710263Srichlowe@richlowe.net only possible parenttip is the null node (node.nullid) 52810263Srichlowe@richlowe.net which is returned explicitly. 52910263Srichlowe@richlowe.net 5307078Smjnelson See the docstring of mercurial.cmdutil.walkchangerevs() 5317078Smjnelson for the phased approach to the iterator returned. The 5327078Smjnelson important part to note is that the 'add' phase gathers 5337078Smjnelson nodes, which the 'iter' phase then iterates through.''' 5347078Smjnelson 53510263Srichlowe@richlowe.net opts = {'rev': ['%s:0' % head.rev()], 53610263Srichlowe@richlowe.net 'follow': True} 5377078Smjnelson get = util.cachefunc(lambda r: self.repo.changectx(r).changeset()) 5387078Smjnelson changeiter = cmdutil.walkchangerevs(self.repo.ui, self.repo, [], 53910263Srichlowe@richlowe.net get, opts)[0] 5407078Smjnelson seen = [] 5417078Smjnelson for st, rev, fns in changeiter: 5427078Smjnelson n = self.repo.changelog.node(rev) 5437078Smjnelson if st == 'add': 5447078Smjnelson if n not in outnodes: 5457078Smjnelson seen.append(n) 5467078Smjnelson elif st == 'iter': 5477078Smjnelson if n in seen: 5487078Smjnelson return rev 54910263Srichlowe@richlowe.net return self.repo.changelog.rev(node.nullid) 5507078Smjnelson 55110263Srichlowe@richlowe.net nodes = set(outgoing) 55210263Srichlowe@richlowe.net ptips = map(lambda x: tipmost_shared(x, nodes), heads) 5537078Smjnelson return self.repo.changectx(sorted(ptips)[-1]) 5547078Smjnelson 55510399Srichlowe@richlowe.net def status(self, base='.', head=None): 5567078Smjnelson '''Translate from the hg 6-tuple status format to a hash keyed 5577078Smjnelson on change-type''' 55810399Srichlowe@richlowe.net 5597078Smjnelson states = ['modified', 'added', 'removed', 'deleted', 'unknown', 5607078Smjnelson 'ignored'] 5619006Srichlowe@richlowe.net 5627078Smjnelson chngs = self.repo.status(base, head) 5637078Smjnelson return dict(zip(states, chngs)) 5647078Smjnelson 5657078Smjnelson def findoutgoing(self, parent): 56610399Srichlowe@richlowe.net '''Return the base set of outgoing nodes. 56710399Srichlowe@richlowe.net 56810399Srichlowe@richlowe.net A caching wrapper around mercurial.localrepo.findoutgoing(). 56910399Srichlowe@richlowe.net Complains (to the user), if the parent workspace is 57010399Srichlowe@richlowe.net non-existent or inaccessible''' 57110399Srichlowe@richlowe.net 57210399Srichlowe@richlowe.net self.ui.pushbuffer() 57310399Srichlowe@richlowe.net try: 5747078Smjnelson try: 57510399Srichlowe@richlowe.net ui = self.ui 57610399Srichlowe@richlowe.net if hasattr(cmdutil, 'remoteui'): 57710399Srichlowe@richlowe.net ui = cmdutil.remoteui(ui, {}) 57810399Srichlowe@richlowe.net pws = hg.repository(ui, parent) 57910399Srichlowe@richlowe.net return self.repo.findoutgoing(pws) 58010399Srichlowe@richlowe.net except HgRepoError: 58110399Srichlowe@richlowe.net self.ui.warn("Warning: Parent workspace '%s' is not " 58210399Srichlowe@richlowe.net "accessible\n" 58310399Srichlowe@richlowe.net "active list will be incomplete\n\n" % parent) 58410399Srichlowe@richlowe.net return [] 58510399Srichlowe@richlowe.net finally: 5867078Smjnelson self.ui.popbuffer() 58710399Srichlowe@richlowe.net findoutgoing = util.cachefunc(findoutgoing) 5887078Smjnelson 5897078Smjnelson def modified(self): 5907078Smjnelson '''Return a list of files modified in the workspace''' 5919006Srichlowe@richlowe.net wctx = self.workingctx() 5927078Smjnelson return sorted(wctx.files() + wctx.deleted()) or None 5937078Smjnelson 5947078Smjnelson def merged(self): 5957078Smjnelson '''Return boolean indicating whether the workspace has an uncommitted 5967078Smjnelson merge''' 5979006Srichlowe@richlowe.net wctx = self.workingctx() 5987078Smjnelson return len(wctx.parents()) > 1 5997078Smjnelson 6007078Smjnelson def branched(self): 6017078Smjnelson '''Return boolean indicating whether the workspace has an 6027078Smjnelson uncommitted named branch''' 6037078Smjnelson 6049006Srichlowe@richlowe.net wctx = self.workingctx() 6057078Smjnelson return wctx.branch() != wctx.parents()[0].branch() 6067078Smjnelson 6077078Smjnelson def active(self, parent=None): 6087078Smjnelson '''Return an ActiveList describing changes between workspace 6097078Smjnelson and parent workspace (including uncommitted changes). 6107078Smjnelson If workspace has no parent ActiveList will still describe any 6117078Smjnelson uncommitted changes''' 6127078Smjnelson 6137078Smjnelson parent = self.parent(parent) 6147078Smjnelson if parent in self.activecache: 6157078Smjnelson return self.activecache[parent] 6167078Smjnelson 6177078Smjnelson if parent: 6187078Smjnelson outgoing = self.findoutgoing(parent) 61910263Srichlowe@richlowe.net outnodes = self.repo.changelog.nodesbetween(outgoing)[0] 6207078Smjnelson else: 6217078Smjnelson outgoing = [] # No parent, no outgoing nodes 62210263Srichlowe@richlowe.net outnodes = [] 6237078Smjnelson 62410263Srichlowe@richlowe.net localtip = self._localtip(outnodes, self.workingctx()) 6257078Smjnelson 62610263Srichlowe@richlowe.net if localtip.rev() is None: 62710263Srichlowe@richlowe.net heads = localtip.parents() 62810263Srichlowe@richlowe.net else: 62910263Srichlowe@richlowe.net heads = [localtip] 6307078Smjnelson 63110263Srichlowe@richlowe.net ctxs = [self.repo.changectx(n) for n in 63210263Srichlowe@richlowe.net self.repo.changelog.nodesbetween(outgoing, 63310263Srichlowe@richlowe.net [h.node() for h in heads])[0]] 6347078Smjnelson 63510263Srichlowe@richlowe.net if localtip.rev() is None: 63610263Srichlowe@richlowe.net ctxs.append(localtip) 63710263Srichlowe@richlowe.net 63810263Srichlowe@richlowe.net act = ActiveList(self, self._parenttip(heads, outnodes), ctxs) 6397078Smjnelson 6407078Smjnelson self.activecache[parent] = act 6417078Smjnelson return act 6427078Smjnelson 6437298SMark.J.Nelson@Sun.COM def pdiff(self, pats, opts, parent=None): 6447078Smjnelson 'Return diffs relative to PARENT, as best as we can make out' 6457078Smjnelson 6467078Smjnelson parent = self.parent(parent) 6477078Smjnelson act = self.active(parent) 6487078Smjnelson 6497078Smjnelson # 6507078Smjnelson # act.localtip maybe nil, in the case of uncommitted local 6517078Smjnelson # changes. 6527078Smjnelson # 6537078Smjnelson if not act.revs: 6547078Smjnelson return 6557078Smjnelson 65610399Srichlowe@richlowe.net matchfunc = cmdutil.match(self.repo, pats, opts) 6577298SMark.J.Nelson@Sun.COM opts = patch.diffopts(self.ui, opts) 6587298SMark.J.Nelson@Sun.COM 6599006Srichlowe@richlowe.net return self.diff(act.parenttip.node(), act.localtip.node(), 6609006Srichlowe@richlowe.net match=matchfunc, opts=opts) 6617078Smjnelson 6627078Smjnelson def squishdeltas(self, active, message, user=None): 66310399Srichlowe@richlowe.net '''Create a single conglomerate changeset based on a given 66410399Srichlowe@richlowe.net active list. Removes the original changesets comprising the 66510399Srichlowe@richlowe.net given active list, and any tags pointing to them. 66610399Srichlowe@richlowe.net 66710399Srichlowe@richlowe.net Operation: 66810399Srichlowe@richlowe.net 66910399Srichlowe@richlowe.net - Commit an activectx object representing the specified 67010399Srichlowe@richlowe.net active list, 67110399Srichlowe@richlowe.net 67210399Srichlowe@richlowe.net - Remove any local tags pointing to changesets in the 67310399Srichlowe@richlowe.net specified active list. 6747078Smjnelson 67510399Srichlowe@richlowe.net - Remove the changesets comprising the specified active 67610399Srichlowe@richlowe.net list. 67710399Srichlowe@richlowe.net 67810399Srichlowe@richlowe.net - Remove any metadata that may refer to changesets that were 67910399Srichlowe@richlowe.net removed. 6807078Smjnelson 68110399Srichlowe@richlowe.net Calling code is expected to hold both the working copy lock 68210399Srichlowe@richlowe.net and repository lock of the destination workspace 68310399Srichlowe@richlowe.net ''' 68410399Srichlowe@richlowe.net 68510399Srichlowe@richlowe.net def strip_local_tags(active): 68610399Srichlowe@richlowe.net '''Remove any local tags referring to the specified nodes.''' 6877078Smjnelson 6887078Smjnelson if os.path.exists(self.repo.join('localtags')): 68910399Srichlowe@richlowe.net fh = None 69010399Srichlowe@richlowe.net try: 69110399Srichlowe@richlowe.net fh = self.repo.opener('localtags') 69210399Srichlowe@richlowe.net tags = active.prune_tags(fh) 69310399Srichlowe@richlowe.net fh.close() 6947078Smjnelson 69510399Srichlowe@richlowe.net fh = self.repo.opener('localtags', 'w', atomictemp=True) 69610399Srichlowe@richlowe.net fh.writelines(tags) 69710399Srichlowe@richlowe.net fh.rename() 69810399Srichlowe@richlowe.net finally: 69910399Srichlowe@richlowe.net if fh and not fh.closed: 70010399Srichlowe@richlowe.net fh.close() 7017078Smjnelson 7027078Smjnelson if active.files(): 70310399Srichlowe@richlowe.net for entry in active: 70410399Srichlowe@richlowe.net # 70510399Srichlowe@richlowe.net # Work around Mercurial issue #1666, if the source 70610399Srichlowe@richlowe.net # file of a rename exists in the working copy 70710399Srichlowe@richlowe.net # Mercurial will complain, and remove the file. 70810399Srichlowe@richlowe.net # 70910399Srichlowe@richlowe.net # We preemptively remove the file to avoid the 71010399Srichlowe@richlowe.net # complaint (the user was asked about this in 71110399Srichlowe@richlowe.net # cdm_recommit) 71210399Srichlowe@richlowe.net # 71310399Srichlowe@richlowe.net if entry.is_renamed(): 71410399Srichlowe@richlowe.net path = self.repo.wjoin(entry.parentname) 71510399Srichlowe@richlowe.net if os.path.exists(path): 71610399Srichlowe@richlowe.net os.unlink(path) 71710399Srichlowe@richlowe.net 71810399Srichlowe@richlowe.net self.repo.commitctx(active.context(message, user)) 71910399Srichlowe@richlowe.net wsstate = "recommitted" 72010399Srichlowe@richlowe.net destination = self.repo.changelog.tip() 7217078Smjnelson else: 7227078Smjnelson # 7237078Smjnelson # If all we're doing is stripping the old nodes, we want to 7247078Smjnelson # update the working copy such that we're not at a revision 7257078Smjnelson # that's about to go away. 7267078Smjnelson # 72710399Srichlowe@richlowe.net wsstate = "tip" 72810399Srichlowe@richlowe.net destination = active.parenttip.node() 72910399Srichlowe@richlowe.net 73010399Srichlowe@richlowe.net self.clean(destination) 73110399Srichlowe@richlowe.net 73210399Srichlowe@richlowe.net # 73310399Srichlowe@richlowe.net # Tags were elided by the activectx object. Local tags, 73410399Srichlowe@richlowe.net # however, must be removed manually. 73510399Srichlowe@richlowe.net # 73610399Srichlowe@richlowe.net try: 73710399Srichlowe@richlowe.net strip_local_tags(active) 73810399Srichlowe@richlowe.net except EnvironmentError, e: 73910399Srichlowe@richlowe.net raise util.Abort('Could not recommit tags: %s\n' % e) 7407078Smjnelson 7417078Smjnelson # Silence all the strip and update fun 7427078Smjnelson self.ui.pushbuffer() 7437078Smjnelson 7447078Smjnelson # 74510399Srichlowe@richlowe.net # Remove the active lists component changesets by stripping 74610399Srichlowe@richlowe.net # the base of any active branch (of which there may be 74710399Srichlowe@richlowe.net # several) 7487078Smjnelson # 7497078Smjnelson try: 7507078Smjnelson try: 75111646SJames.McPherson@Sun.COM for base in active.bases(): 75210399Srichlowe@richlowe.net # 75310399Srichlowe@richlowe.net # Any cached information about the repository is 75410399Srichlowe@richlowe.net # likely to be invalid during the strip. The 75510399Srichlowe@richlowe.net # caching of branch tags is especially 75610399Srichlowe@richlowe.net # problematic. 75710399Srichlowe@richlowe.net # 75810399Srichlowe@richlowe.net self.repo.invalidate() 75911646SJames.McPherson@Sun.COM repair.strip(self.ui, self.repo, base.node(), backup=False) 7607078Smjnelson except: 7617078Smjnelson # 7627078Smjnelson # If this fails, it may leave us in a surprising place in 7637078Smjnelson # the history. 7647078Smjnelson # 7657078Smjnelson # We want to warn the user that something went wrong, 7667078Smjnelson # and what will happen next, re-raise the exception, and 7677078Smjnelson # bring the working copy back into a consistent state 7687078Smjnelson # (which the finally block will do) 7697078Smjnelson # 7707078Smjnelson self.ui.warn("stripping failed, your workspace will have " 7717078Smjnelson "superfluous heads.\n" 7727078Smjnelson "your workspace has been updated to the " 77310399Srichlowe@richlowe.net "%s changeset.\n" % wsstate) 7747078Smjnelson raise # Re-raise the exception 7757078Smjnelson finally: 77610399Srichlowe@richlowe.net self.clean() 77710399Srichlowe@richlowe.net self.repo.dirstate.write() # Flush the dirstate 77810399Srichlowe@richlowe.net self.repo.invalidate() # Invalidate caches 77910399Srichlowe@richlowe.net 7807078Smjnelson # 7817078Smjnelson # We need to remove Hg's undo information (used for rollback), 7827078Smjnelson # since it refers to data that will probably not exist after 7837078Smjnelson # the strip. 7847078Smjnelson # 7857078Smjnelson if os.path.exists(self.repo.sjoin('undo')): 7867078Smjnelson try: 7877078Smjnelson os.unlink(self.repo.sjoin('undo')) 7887078Smjnelson except EnvironmentError, e: 7897078Smjnelson raise util.Abort('failed to remove undo data: %s\n' % e) 7907078Smjnelson 7917078Smjnelson self.ui.popbuffer() 7927078Smjnelson 7937078Smjnelson def filepath(self, path): 7947078Smjnelson 'Return the full path to a workspace file.' 7957078Smjnelson return self.repo.pathto(path) 7967078Smjnelson 7977078Smjnelson def clean(self, rev=None): 7987078Smjnelson '''Bring workspace up to REV (or tip) forcefully (discarding in 7997078Smjnelson progress changes)''' 8009006Srichlowe@richlowe.net 8017078Smjnelson if rev != None: 8027078Smjnelson rev = self.repo.lookup(rev) 8037078Smjnelson else: 8047078Smjnelson rev = self.repo.changelog.tip() 8057078Smjnelson 8067078Smjnelson hg.clean(self.repo, rev, show_stats=False) 8077078Smjnelson 8087078Smjnelson def mq_applied(self): 8097078Smjnelson '''True if the workspace has Mq patches applied''' 8107078Smjnelson q = mq.queue(self.ui, self.repo.join('')) 8117078Smjnelson return q.applied 8129006Srichlowe@richlowe.net 81310399Srichlowe@richlowe.net def workingctx(self): 81410399Srichlowe@richlowe.net return self.repo.changectx(None) 8159006Srichlowe@richlowe.net 81610399Srichlowe@richlowe.net def diff(self, node1=None, node2=None, match=None, opts=None): 81710399Srichlowe@richlowe.net ret = cStringIO.StringIO() 81810399Srichlowe@richlowe.net try: 8199006Srichlowe@richlowe.net for chunk in patch.diff(self.repo, node1, node2, match=match, 8209006Srichlowe@richlowe.net opts=opts): 8219006Srichlowe@richlowe.net ret.write(chunk) 82210399Srichlowe@richlowe.net finally: 82310399Srichlowe@richlowe.net # Workaround Hg bug 1651 82410399Srichlowe@richlowe.net if not Version.at_least("1.3"): 82510399Srichlowe@richlowe.net self.repo.dirstate.invalidate() 8269006Srichlowe@richlowe.net 82710399Srichlowe@richlowe.net return ret.getvalue() 828