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# 1712203c71SRichard Lowe# Copyright 2009 Sun Microsystems, Inc. All rights reserved. 18cdf0c1d5Smjnelson# Use is subject to license terms. 19cdf0c1d5Smjnelson# 20*605a716eSRichard Lowe# Copyright 2008, 2011, Richard Lowe 2187039217SRichard Lowe# 22cdf0c1d5Smjnelson 23*605a716eSRichard Lowe 24cdf0c1d5Smjnelson''' 25cdf0c1d5SmjnelsonWorkspace backup 26cdf0c1d5Smjnelson 27cdf0c1d5SmjnelsonBackup format is: 28cdf0c1d5Smjnelson backupdir/ 29cdf0c1d5Smjnelson wsname/ 30cdf0c1d5Smjnelson generation#/ 31cdf0c1d5Smjnelson dirstate (handled by CdmUncommittedBackup) 3212203c71SRichard Lowe File containing dirstate nodeid (the changeset we need 3312203c71SRichard Lowe to update the workspace to after applying the bundle). 3412203c71SRichard Lowe This is the node to which the working copy changes 3512203c71SRichard Lowe (see 'diff', below) will be applied if applicable. 36cdf0c1d5Smjnelson 37cdf0c1d5Smjnelson bundle (handled by CdmCommittedBackup) 38cdf0c1d5Smjnelson An Hg bundle containing outgoing committed changes. 39cdf0c1d5Smjnelson 40cdf0c1d5Smjnelson nodes (handled by CdmCommittedBackup) 41cdf0c1d5Smjnelson A text file listing the full (hex) nodeid of all nodes in 42cdf0c1d5Smjnelson bundle, used by need_backup. 43cdf0c1d5Smjnelson 44cdf0c1d5Smjnelson diff (handled by CdmUncommittedBackup) 45cdf0c1d5Smjnelson A Git-formatted diff containing uncommitted changes. 46cdf0c1d5Smjnelson 47cdf0c1d5Smjnelson renames (handled by CdmUncommittedBackup) 48cdf0c1d5Smjnelson A list of renames in the working copy that have to be 49cdf0c1d5Smjnelson applied manually, rather than by the diff. 50cdf0c1d5Smjnelson 51cdf0c1d5Smjnelson metadata.tar.gz (handled by CdmMetadataBackup) 52cdf0c1d5Smjnelson $CODEMGR_WS/.hg/hgrc 53cdf0c1d5Smjnelson $CODEMGR_WS/.hg/localtags 54cdf0c1d5Smjnelson $CODEMGR_WS/.hg/patches (Mq data) 55cdf0c1d5Smjnelson 56*605a716eSRichard Lowe clear.tar.gz (handled by CdmClearBackup) 57*605a716eSRichard Lowe <short node>/ 58*605a716eSRichard Lowe copies of each modified or added file, as it is in 59*605a716eSRichard Lowe this head. 60*605a716eSRichard Lowe 61*605a716eSRichard Lowe ... for each outgoing head 62*605a716eSRichard Lowe 63*605a716eSRichard Lowe working/ 64*605a716eSRichard Lowe copies of each modified or added file in the 65*605a716eSRichard Lowe working copy if any. 66*605a716eSRichard Lowe 67cdf0c1d5Smjnelson latest -> generation# 68cdf0c1d5Smjnelson Newest backup generation. 69cdf0c1d5Smjnelson 70cdf0c1d5SmjnelsonAll files in a given backup generation, with the exception of 71cdf0c1d5Smjnelsondirstate, are optional. 72cdf0c1d5Smjnelson''' 73cdf0c1d5Smjnelson 74*605a716eSRichard Loweimport grp, os, pwd, shutil, tarfile, time, traceback 75*605a716eSRichard Lowefrom cStringIO import StringIO 76*605a716eSRichard Lowe 7787039217SRichard Lowefrom mercurial import changegroup, error, node, patch, util 78cdf0c1d5Smjnelson 79cdf0c1d5Smjnelson 8012203c71SRichard Loweclass CdmNodeMissing(util.Abort): 8112203c71SRichard Lowe '''a required node is not present in the destination workspace. 8212203c71SRichard Lowe 8312203c71SRichard Lowe This may occur both in the case where the bundle contains a 8412203c71SRichard Lowe changeset which is a child of a node not present in the 8512203c71SRichard Lowe destination workspace (because the destination workspace is not as 8612203c71SRichard Lowe up-to-date as the source), or because the source and destination 8712203c71SRichard Lowe workspace are not related. 8812203c71SRichard Lowe 8912203c71SRichard Lowe It may also happen in cases where the uncommitted changes need to 9012203c71SRichard Lowe be applied onto a node that the workspace does not possess even 9112203c71SRichard Lowe after application of the bundle (on a branch not present 9212203c71SRichard Lowe in the bundle or destination workspace, for instance)''' 9312203c71SRichard Lowe 9412203c71SRichard Lowe def __init__(self, msg, name): 9512203c71SRichard Lowe # 9612203c71SRichard Lowe # If e.name is a string 20 characters long, it is 9712203c71SRichard Lowe # assumed to be a node. (Mercurial makes this 9812203c71SRichard Lowe # same assumption, when creating a LookupError) 9912203c71SRichard Lowe # 10012203c71SRichard Lowe if isinstance(name, str) and len(name) == 20: 10112203c71SRichard Lowe n = node.short(name) 10212203c71SRichard Lowe else: 10312203c71SRichard Lowe n = name 10412203c71SRichard Lowe 10512203c71SRichard Lowe util.Abort.__init__(self, "%s: changeset '%s' is missing\n" 10612203c71SRichard Lowe "Your workspace is either not " 10712203c71SRichard Lowe "sufficiently up to date,\n" 10812203c71SRichard Lowe "or is unrelated to the workspace from " 10912203c71SRichard Lowe "which the backup was taken.\n" % (msg, n)) 11012203c71SRichard Lowe 11112203c71SRichard Lowe 112*605a716eSRichard Loweclass CdmTarFile(tarfile.TarFile): 113*605a716eSRichard Lowe '''Tar file access + simple comparison to the filesystem, and 114*605a716eSRichard Lowe creation addition of files from Mercurial filectx objects.''' 115*605a716eSRichard Lowe 116*605a716eSRichard Lowe def __init__(self, *args, **kwargs): 117*605a716eSRichard Lowe tarfile.TarFile.__init__(self, *args, **kwargs) 118*605a716eSRichard Lowe self.errorlevel = 2 119*605a716eSRichard Lowe 120*605a716eSRichard Lowe def members_match_fs(self, rootpath): 121*605a716eSRichard Lowe '''Compare the contents of the tar archive to the directory 122*605a716eSRichard Lowe specified by rootpath. Return False if they differ. 123*605a716eSRichard Lowe 124*605a716eSRichard Lowe Every file in the archive must match the equivalent file in 125*605a716eSRichard Lowe the filesystem. 126*605a716eSRichard Lowe 127*605a716eSRichard Lowe The existence, modification time, and size of each file are 128*605a716eSRichard Lowe compared, content is not.''' 129*605a716eSRichard Lowe 130*605a716eSRichard Lowe def _member_matches_fs(member, rootpath): 131*605a716eSRichard Lowe '''Compare a single member to its filesystem counterpart''' 132*605a716eSRichard Lowe fpath = os.path.join(rootpath, member.name) 133*605a716eSRichard Lowe 134*605a716eSRichard Lowe if not os.path.exists(fpath): 135*605a716eSRichard Lowe return False 136*605a716eSRichard Lowe elif ((os.path.isfile(fpath) != member.isfile()) or 137*605a716eSRichard Lowe (os.path.isdir(fpath) != member.isdir()) or 138*605a716eSRichard Lowe (os.path.islink(fpath) != member.issym())): 139*605a716eSRichard Lowe return False 140*605a716eSRichard Lowe 141*605a716eSRichard Lowe # 142*605a716eSRichard Lowe # The filesystem may return a modification time with a 143*605a716eSRichard Lowe # fractional component (as a float), whereas the tar format 144*605a716eSRichard Lowe # only stores it to the whole second, perform the comparison 145*605a716eSRichard Lowe # using integers (truncated, not rounded) 146*605a716eSRichard Lowe # 147*605a716eSRichard Lowe elif member.mtime != int(os.path.getmtime(fpath)): 148*605a716eSRichard Lowe return False 149*605a716eSRichard Lowe elif not member.isdir() and member.size != os.path.getsize(fpath): 150*605a716eSRichard Lowe return False 151*605a716eSRichard Lowe else: 152*605a716eSRichard Lowe return True 153*605a716eSRichard Lowe 154*605a716eSRichard Lowe for elt in self: 155*605a716eSRichard Lowe if not _member_matches_fs(elt, rootpath): 156*605a716eSRichard Lowe return False 157*605a716eSRichard Lowe 158*605a716eSRichard Lowe return True 159*605a716eSRichard Lowe 160*605a716eSRichard Lowe def addfilectx(self, filectx, path=None): 161*605a716eSRichard Lowe '''Add a filectx object to the archive. 162*605a716eSRichard Lowe 163*605a716eSRichard Lowe Use the path specified by the filectx object or, if specified, 164*605a716eSRichard Lowe the PATH argument. 165*605a716eSRichard Lowe 166*605a716eSRichard Lowe The size, modification time, type and permissions of the tar 167*605a716eSRichard Lowe member are taken from the filectx object, user and group id 168*605a716eSRichard Lowe are those of the invoking user, user and group name are those 169*605a716eSRichard Lowe of the invoking user if information is available, or "unknown" 170*605a716eSRichard Lowe if it is not. 171*605a716eSRichard Lowe ''' 172*605a716eSRichard Lowe 173*605a716eSRichard Lowe t = tarfile.TarInfo(path or filectx.path()) 174*605a716eSRichard Lowe t.size = filectx.size() 175*605a716eSRichard Lowe t.mtime = filectx.date()[0] 176*605a716eSRichard Lowe t.uid = os.getuid() 177*605a716eSRichard Lowe t.gid = os.getgid() 178*605a716eSRichard Lowe 179*605a716eSRichard Lowe try: 180*605a716eSRichard Lowe t.uname = pwd.getpwuid(t.uid).pw_name 181*605a716eSRichard Lowe except KeyError: 182*605a716eSRichard Lowe t.uname = "unknown" 183*605a716eSRichard Lowe 184*605a716eSRichard Lowe try: 185*605a716eSRichard Lowe t.gname = grp.getgrgid(t.gid).gr_name 186*605a716eSRichard Lowe except KeyError: 187*605a716eSRichard Lowe t.gname = "unknown" 188*605a716eSRichard Lowe 189*605a716eSRichard Lowe # 190*605a716eSRichard Lowe # Mercurial versions symlinks by setting a flag and storing 191*605a716eSRichard Lowe # the destination path in place of the file content. The 192*605a716eSRichard Lowe # actual contents (in the tar), should be empty. 193*605a716eSRichard Lowe # 194*605a716eSRichard Lowe if 'l' in filectx.flags(): 195*605a716eSRichard Lowe t.type = tarfile.SYMTYPE 196*605a716eSRichard Lowe t.mode = 0777 197*605a716eSRichard Lowe t.linkname = filectx.data() 198*605a716eSRichard Lowe data = None 199*605a716eSRichard Lowe else: 200*605a716eSRichard Lowe t.type = tarfile.REGTYPE 201*605a716eSRichard Lowe t.mode = 'x' in filectx.flags() and 0755 or 0644 202*605a716eSRichard Lowe data = StringIO(filectx.data()) 203*605a716eSRichard Lowe 204*605a716eSRichard Lowe self.addfile(t, data) 205*605a716eSRichard Lowe 206*605a716eSRichard Lowe 207cdf0c1d5Smjnelsonclass CdmCommittedBackup(object): 208cdf0c1d5Smjnelson '''Backup of committed changes''' 209cdf0c1d5Smjnelson 210cdf0c1d5Smjnelson def __init__(self, backup, ws): 211cdf0c1d5Smjnelson self.ws = ws 212cdf0c1d5Smjnelson self.bu = backup 213cdf0c1d5Smjnelson self.files = ('bundle', 'nodes') 214cdf0c1d5Smjnelson 215cdf0c1d5Smjnelson def _outgoing_nodes(self, parent): 216cdf0c1d5Smjnelson '''Return a list of all outgoing nodes in hex format''' 217cdf0c1d5Smjnelson 218cdf0c1d5Smjnelson if parent: 219cdf0c1d5Smjnelson outgoing = self.ws.findoutgoing(parent) 220cdf0c1d5Smjnelson nodes = self.ws.repo.changelog.nodesbetween(outgoing)[0] 221cdf0c1d5Smjnelson return map(node.hex, nodes) 222cdf0c1d5Smjnelson else: 223cdf0c1d5Smjnelson return [] 224cdf0c1d5Smjnelson 225cdf0c1d5Smjnelson def backup(self): 226cdf0c1d5Smjnelson '''Backup committed changes''' 227cdf0c1d5Smjnelson parent = self.ws.parent() 228cdf0c1d5Smjnelson 229cdf0c1d5Smjnelson if not parent: 230cdf0c1d5Smjnelson self.ws.ui.warn('Workspace has no parent, committed changes will ' 231cdf0c1d5Smjnelson 'not be backed up\n') 232cdf0c1d5Smjnelson return 233cdf0c1d5Smjnelson 234cdf0c1d5Smjnelson out = self.ws.findoutgoing(parent) 235cdf0c1d5Smjnelson if not out: 236cdf0c1d5Smjnelson return 237cdf0c1d5Smjnelson 238cdf0c1d5Smjnelson cg = self.ws.repo.changegroup(out, 'bundle') 239cdf0c1d5Smjnelson changegroup.writebundle(cg, self.bu.backupfile('bundle'), 'HG10BZ') 240cdf0c1d5Smjnelson 241cdf0c1d5Smjnelson outnodes = self._outgoing_nodes(parent) 242*605a716eSRichard Lowe if not outnodes: 243*605a716eSRichard Lowe return 244*605a716eSRichard Lowe 245cdf0c1d5Smjnelson fp = None 246cdf0c1d5Smjnelson try: 247cdf0c1d5Smjnelson try: 248*605a716eSRichard Lowe fp = self.bu.open('nodes', 'w') 249cdf0c1d5Smjnelson fp.write('%s\n' % '\n'.join(outnodes)) 250cdf0c1d5Smjnelson except EnvironmentError, e: 251cdf0c1d5Smjnelson raise util.Abort("couldn't store outgoing nodes: %s" % e) 252cdf0c1d5Smjnelson finally: 253cdf0c1d5Smjnelson if fp and not fp.closed: 254cdf0c1d5Smjnelson fp.close() 255cdf0c1d5Smjnelson 256cdf0c1d5Smjnelson def restore(self): 257cdf0c1d5Smjnelson '''Restore committed changes from backup''' 258cdf0c1d5Smjnelson 259*605a716eSRichard Lowe if not self.bu.exists('bundle'): 260*605a716eSRichard Lowe return 261*605a716eSRichard Lowe 262*605a716eSRichard Lowe bpath = self.bu.backupfile('bundle') 263cdf0c1d5Smjnelson f = None 264cdf0c1d5Smjnelson try: 265cdf0c1d5Smjnelson try: 266*605a716eSRichard Lowe f = self.bu.open('bundle') 267*605a716eSRichard Lowe bundle = changegroup.readbundle(f, bpath) 268cdf0c1d5Smjnelson self.ws.repo.addchangegroup(bundle, 'strip', 269*605a716eSRichard Lowe 'bundle:%s' % bpath) 270cdf0c1d5Smjnelson except EnvironmentError, e: 271cdf0c1d5Smjnelson raise util.Abort("couldn't restore committed changes: %s\n" 272*605a716eSRichard Lowe " %s" % (bpath, e)) 27387039217SRichard Lowe except error.LookupError, e: 27412203c71SRichard Lowe raise CdmNodeMissing("couldn't restore committed changes", 27512203c71SRichard Lowe e.name) 276cdf0c1d5Smjnelson finally: 277cdf0c1d5Smjnelson if f and not f.closed: 278cdf0c1d5Smjnelson f.close() 279cdf0c1d5Smjnelson 280cdf0c1d5Smjnelson def need_backup(self): 281cdf0c1d5Smjnelson '''Compare backup of committed changes to workspace''' 282cdf0c1d5Smjnelson 283*605a716eSRichard Lowe if self.bu.exists('nodes'): 284cdf0c1d5Smjnelson f = None 285cdf0c1d5Smjnelson try: 286cdf0c1d5Smjnelson try: 287*605a716eSRichard Lowe f = self.bu.open('nodes') 288*605a716eSRichard Lowe bnodes = set(line.rstrip('\r\n') for line in f.readlines()) 289cdf0c1d5Smjnelson f.close() 290cdf0c1d5Smjnelson except EnvironmentError, e: 291cdf0c1d5Smjnelson raise util.Abort("couldn't open backup node list: %s" % e) 292cdf0c1d5Smjnelson finally: 293cdf0c1d5Smjnelson if f and not f.closed: 294cdf0c1d5Smjnelson f.close() 295cdf0c1d5Smjnelson else: 296cdf0c1d5Smjnelson bnodes = set() 297cdf0c1d5Smjnelson 298cdf0c1d5Smjnelson outnodes = set(self._outgoing_nodes(self.ws.parent())) 299*605a716eSRichard Lowe 300*605a716eSRichard Lowe # 301*605a716eSRichard Lowe # If there are outgoing nodes not in the prior backup we need 302*605a716eSRichard Lowe # to take a new backup; it's fine if there are nodes in the 303*605a716eSRichard Lowe # old backup which are no longer outgoing, however. 304*605a716eSRichard Lowe # 305*605a716eSRichard Lowe if not outnodes <= bnodes: 306cdf0c1d5Smjnelson return True 307cdf0c1d5Smjnelson 308cdf0c1d5Smjnelson return False 309cdf0c1d5Smjnelson 310cdf0c1d5Smjnelson def cleanup(self): 311cdf0c1d5Smjnelson '''Remove backed up committed changes''' 312cdf0c1d5Smjnelson 313*605a716eSRichard Lowe for f in self.files: 314*605a716eSRichard Lowe self.bu.unlink(f) 315cdf0c1d5Smjnelson 316cdf0c1d5Smjnelson 317cdf0c1d5Smjnelsonclass CdmUncommittedBackup(object): 318cdf0c1d5Smjnelson '''Backup of uncommitted changes''' 319cdf0c1d5Smjnelson 320cdf0c1d5Smjnelson def __init__(self, backup, ws): 321cdf0c1d5Smjnelson self.ws = ws 322cdf0c1d5Smjnelson self.bu = backup 323*605a716eSRichard Lowe self.wctx = self.ws.workingctx(worklist=True) 324cdf0c1d5Smjnelson 325cdf0c1d5Smjnelson def _clobbering_renames(self): 326cdf0c1d5Smjnelson '''Return a list of pairs of files representing renames/copies 327*605a716eSRichard Lowe that clobber already versioned files. [(old-name new-name)...] 328*605a716eSRichard Lowe ''' 329cdf0c1d5Smjnelson 330cdf0c1d5Smjnelson # 331cdf0c1d5Smjnelson # Note that this doesn't handle uncommitted merges 332cdf0c1d5Smjnelson # as CdmUncommittedBackup itself doesn't. 333cdf0c1d5Smjnelson # 334*605a716eSRichard Lowe parent = self.wctx.parents()[0] 335cdf0c1d5Smjnelson 336cdf0c1d5Smjnelson ret = [] 337*605a716eSRichard Lowe for fname in self.wctx.added() + self.wctx.modified(): 338*605a716eSRichard Lowe rn = self.wctx.filectx(fname).renamed() 339cdf0c1d5Smjnelson if rn and fname in parent: 340cdf0c1d5Smjnelson ret.append((rn[0], fname)) 341cdf0c1d5Smjnelson return ret 342cdf0c1d5Smjnelson 343cdf0c1d5Smjnelson def backup(self): 344cdf0c1d5Smjnelson '''Backup uncommitted changes''' 345cdf0c1d5Smjnelson 346cdf0c1d5Smjnelson if self.ws.merged(): 347cdf0c1d5Smjnelson raise util.Abort("Unable to backup an uncommitted merge.\n" 348cdf0c1d5Smjnelson "Please complete your merge and commit") 349cdf0c1d5Smjnelson 350*605a716eSRichard Lowe dirstate = node.hex(self.wctx.parents()[0].node()) 351cdf0c1d5Smjnelson 352cdf0c1d5Smjnelson fp = None 353cdf0c1d5Smjnelson try: 354cdf0c1d5Smjnelson try: 355*605a716eSRichard Lowe fp = self.bu.open('dirstate', 'w') 356cdf0c1d5Smjnelson fp.write(dirstate + '\n') 357*605a716eSRichard Lowe fp.close() 358cdf0c1d5Smjnelson except EnvironmentError, e: 359cdf0c1d5Smjnelson raise util.Abort("couldn't save working copy parent: %s" % e) 360cdf0c1d5Smjnelson 361cdf0c1d5Smjnelson try: 362*605a716eSRichard Lowe fp = self.bu.open('renames', 'w') 363cdf0c1d5Smjnelson for cons in self._clobbering_renames(): 364cdf0c1d5Smjnelson fp.write("%s %s\n" % cons) 365*605a716eSRichard Lowe fp.close() 366cdf0c1d5Smjnelson except EnvironmentError, e: 367cdf0c1d5Smjnelson raise util.Abort("couldn't save clobbering copies: %s" % e) 368cdf0c1d5Smjnelson 369cdf0c1d5Smjnelson try: 370*605a716eSRichard Lowe fp = self.bu.open('diff', 'w') 371*605a716eSRichard Lowe match = self.ws.matcher(files=self.wctx.files()) 372*605a716eSRichard Lowe fp.write(self.ws.diff(opts={'git': True}, match=match)) 373cdf0c1d5Smjnelson except EnvironmentError, e: 374cdf0c1d5Smjnelson raise util.Abort("couldn't save working copy diff: %s" % e) 375cdf0c1d5Smjnelson finally: 376cdf0c1d5Smjnelson if fp and not fp.closed: 377cdf0c1d5Smjnelson fp.close() 378cdf0c1d5Smjnelson 379cdf0c1d5Smjnelson def _dirstate(self): 3802b5878deSRich Lowe '''Return the desired working copy node from the backup''' 381cdf0c1d5Smjnelson fp = None 382cdf0c1d5Smjnelson try: 383cdf0c1d5Smjnelson try: 384*605a716eSRichard Lowe fp = self.bu.open('dirstate') 385cdf0c1d5Smjnelson dirstate = fp.readline().strip() 386cdf0c1d5Smjnelson except EnvironmentError, e: 387cdf0c1d5Smjnelson raise util.Abort("couldn't read saved parent: %s" % e) 388cdf0c1d5Smjnelson finally: 389cdf0c1d5Smjnelson if fp and not fp.closed: 390cdf0c1d5Smjnelson fp.close() 391cdf0c1d5Smjnelson 392*605a716eSRichard Lowe return dirstate 393*605a716eSRichard Lowe 394cdf0c1d5Smjnelson def restore(self): 395cdf0c1d5Smjnelson '''Restore uncommitted changes''' 396cdf0c1d5Smjnelson dirstate = self._dirstate() 397cdf0c1d5Smjnelson 3982b5878deSRich Lowe # 3992b5878deSRich Lowe # Check that the patch's parent changeset exists. 4002b5878deSRich Lowe # 401cdf0c1d5Smjnelson try: 4022b5878deSRich Lowe n = node.bin(dirstate) 4032b5878deSRich Lowe self.ws.repo.changelog.lookup(n) 40487039217SRichard Lowe except error.LookupError, e: 40512203c71SRichard Lowe raise CdmNodeMissing("couldn't restore uncommitted changes", 40612203c71SRichard Lowe e.name) 4072b5878deSRich Lowe 4082b5878deSRich Lowe try: 4092b5878deSRich Lowe self.ws.clean(rev=dirstate) 410cdf0c1d5Smjnelson except util.Abort, e: 411cdf0c1d5Smjnelson raise util.Abort("couldn't update to saved node: %s" % e) 412cdf0c1d5Smjnelson 413*605a716eSRichard Lowe if not self.bu.exists('diff'): 414cdf0c1d5Smjnelson return 415cdf0c1d5Smjnelson 416cdf0c1d5Smjnelson # 417cdf0c1d5Smjnelson # There's a race here whereby if the patch (or part thereof) 418cdf0c1d5Smjnelson # is applied within the same second as the clean above (such 419*605a716eSRichard Lowe # that modification time doesn't change) and if the size of 420*605a716eSRichard Lowe # that file does not change, Hg may not see the change. 421cdf0c1d5Smjnelson # 422cdf0c1d5Smjnelson # We sleep a full second to avoid this, as sleeping merely 423cdf0c1d5Smjnelson # until the next second begins would require very close clock 424cdf0c1d5Smjnelson # synchronization on network filesystems. 425cdf0c1d5Smjnelson # 426cdf0c1d5Smjnelson time.sleep(1) 427cdf0c1d5Smjnelson 428cdf0c1d5Smjnelson files = {} 429cdf0c1d5Smjnelson try: 430*605a716eSRichard Lowe diff = self.bu.backupfile('diff') 431cdf0c1d5Smjnelson try: 432cdf0c1d5Smjnelson fuzz = patch.patch(diff, self.ws.ui, strip=1, 433cdf0c1d5Smjnelson cwd=self.ws.repo.root, files=files) 434cdf0c1d5Smjnelson if fuzz: 435cdf0c1d5Smjnelson raise util.Abort('working copy diff applied with fuzz') 436cdf0c1d5Smjnelson except Exception, e: 437cdf0c1d5Smjnelson raise util.Abort("couldn't apply working copy diff: %s\n" 438cdf0c1d5Smjnelson " %s" % (diff, e)) 439cdf0c1d5Smjnelson finally: 440cdf0c1d5Smjnelson patch.updatedir(self.ws.ui, self.ws.repo, files) 441cdf0c1d5Smjnelson 442*605a716eSRichard Lowe if not self.bu.exists('renames'): 443cdf0c1d5Smjnelson return 444cdf0c1d5Smjnelson 445cdf0c1d5Smjnelson # 446cdf0c1d5Smjnelson # We need to re-apply name changes where the new name 447cdf0c1d5Smjnelson # (rename/copy destination) is an already versioned file, as 448cdf0c1d5Smjnelson # Hg would otherwise ignore them. 449cdf0c1d5Smjnelson # 450cdf0c1d5Smjnelson try: 451*605a716eSRichard Lowe fp = self.bu.open('renames') 452cdf0c1d5Smjnelson for line in fp: 453cdf0c1d5Smjnelson source, dest = line.strip().split() 45487039217SRichard Lowe self.ws.copy(source, dest) 455cdf0c1d5Smjnelson except EnvironmentError, e: 456cdf0c1d5Smjnelson raise util.Abort('unable to open renames file: %s' % e) 457cdf0c1d5Smjnelson except ValueError: 458cdf0c1d5Smjnelson raise util.Abort('corrupt renames file: %s' % 459cdf0c1d5Smjnelson self.bu.backupfile('renames')) 460cdf0c1d5Smjnelson 461cdf0c1d5Smjnelson def need_backup(self): 462cdf0c1d5Smjnelson '''Compare backup of uncommitted changes to workspace''' 463*605a716eSRichard Lowe cnode = self.wctx.parents()[0].node() 4642b5878deSRich Lowe if self._dirstate() != node.hex(cnode): 465cdf0c1d5Smjnelson return True 466cdf0c1d5Smjnelson 467*605a716eSRichard Lowe fd = None 468*605a716eSRichard Lowe match = self.ws.matcher(files=self.wctx.files()) 469*605a716eSRichard Lowe curdiff = self.ws.diff(opts={'git': True}, match=match) 4702b5878deSRich Lowe 471cdf0c1d5Smjnelson try: 472*605a716eSRichard Lowe if self.bu.exists('diff'): 473cdf0c1d5Smjnelson try: 474*605a716eSRichard Lowe fd = self.bu.open('diff') 475cdf0c1d5Smjnelson backdiff = fd.read() 476*605a716eSRichard Lowe fd.close() 477cdf0c1d5Smjnelson except EnvironmentError, e: 478cdf0c1d5Smjnelson raise util.Abort("couldn't open backup diff %s\n" 479*605a716eSRichard Lowe " %s" % (self.bu.backupfile('diff'), e)) 480cdf0c1d5Smjnelson else: 481cdf0c1d5Smjnelson backdiff = '' 482cdf0c1d5Smjnelson 4832b5878deSRich Lowe if backdiff != curdiff: 484cdf0c1d5Smjnelson return True 485cdf0c1d5Smjnelson 486cdf0c1d5Smjnelson currrenamed = self._clobbering_renames() 487cdf0c1d5Smjnelson bakrenamed = None 488cdf0c1d5Smjnelson 489*605a716eSRichard Lowe if self.bu.exists('renames'): 490cdf0c1d5Smjnelson try: 491*605a716eSRichard Lowe fd = self.bu.open('renames') 492*605a716eSRichard Lowe bakrenamed = [tuple(line.strip().split(' ')) for line in fd] 493*605a716eSRichard Lowe fd.close() 494cdf0c1d5Smjnelson except EnvironmentError, e: 495cdf0c1d5Smjnelson raise util.Abort("couldn't open renames file %s: %s\n" % 496cdf0c1d5Smjnelson (self.bu.backupfile('renames'), e)) 497cdf0c1d5Smjnelson 498cdf0c1d5Smjnelson if currrenamed != bakrenamed: 499cdf0c1d5Smjnelson return True 500*605a716eSRichard Lowe finally: 501*605a716eSRichard Lowe if fd and not fd.closed: 502*605a716eSRichard Lowe fd.close() 503cdf0c1d5Smjnelson 504cdf0c1d5Smjnelson return False 505cdf0c1d5Smjnelson 506cdf0c1d5Smjnelson def cleanup(self): 507cdf0c1d5Smjnelson '''Remove backed up uncommitted changes''' 508*605a716eSRichard Lowe 509*605a716eSRichard Lowe for f in ('dirstate', 'diff', 'renames'): 510*605a716eSRichard Lowe self.bu.unlink(f) 511cdf0c1d5Smjnelson 512cdf0c1d5Smjnelson 513cdf0c1d5Smjnelsonclass CdmMetadataBackup(object): 514cdf0c1d5Smjnelson '''Backup of workspace metadata''' 515cdf0c1d5Smjnelson 516cdf0c1d5Smjnelson def __init__(self, backup, ws): 517cdf0c1d5Smjnelson self.bu = backup 518cdf0c1d5Smjnelson self.ws = ws 5199a70fc3bSMark J. Nelson self.files = ('hgrc', 'localtags', 'patches', 'cdm') 520cdf0c1d5Smjnelson 521cdf0c1d5Smjnelson def backup(self): 522cdf0c1d5Smjnelson '''Backup workspace metadata''' 523cdf0c1d5Smjnelson 524*605a716eSRichard Lowe tarpath = self.bu.backupfile('metadata.tar.gz') 525*605a716eSRichard Lowe 526*605a716eSRichard Lowe # 527*605a716eSRichard Lowe # Files is a list of tuples (name, path), where name is as in 528*605a716eSRichard Lowe # self.files, and path is the absolute path. 529*605a716eSRichard Lowe # 530*605a716eSRichard Lowe files = filter(lambda (name, path): os.path.exists(path), 531*605a716eSRichard Lowe zip(self.files, map(self.ws.repo.join, self.files))) 532*605a716eSRichard Lowe 533*605a716eSRichard Lowe if not files: 534*605a716eSRichard Lowe return 535cdf0c1d5Smjnelson 536cdf0c1d5Smjnelson try: 537*605a716eSRichard Lowe tar = CdmTarFile.gzopen(tarpath, 'w') 538cdf0c1d5Smjnelson except (EnvironmentError, tarfile.TarError), e: 539cdf0c1d5Smjnelson raise util.Abort("couldn't open %s for writing: %s" % 540*605a716eSRichard Lowe (tarpath, e)) 541cdf0c1d5Smjnelson 542cdf0c1d5Smjnelson try: 543*605a716eSRichard Lowe for name, path in files: 544*605a716eSRichard Lowe try: 545*605a716eSRichard Lowe tar.add(path, name) 546cdf0c1d5Smjnelson except (EnvironmentError, tarfile.TarError), e: 547cdf0c1d5Smjnelson # 548cdf0c1d5Smjnelson # tarfile.TarError doesn't include the tar member or file 549cdf0c1d5Smjnelson # in question, so we have to do so ourselves. 550cdf0c1d5Smjnelson # 551cdf0c1d5Smjnelson if isinstance(e, tarfile.TarError): 552*605a716eSRichard Lowe errstr = "%s: %s" % (name, e) 553cdf0c1d5Smjnelson else: 55487039217SRichard Lowe errstr = str(e) 555cdf0c1d5Smjnelson 556cdf0c1d5Smjnelson raise util.Abort("couldn't backup metadata to %s:\n" 557*605a716eSRichard Lowe " %s" % (tarpath, errstr)) 558cdf0c1d5Smjnelson finally: 559cdf0c1d5Smjnelson tar.close() 560cdf0c1d5Smjnelson 561cdf0c1d5Smjnelson def old_restore(self): 562cdf0c1d5Smjnelson '''Restore workspace metadata from an pre-tar backup''' 563cdf0c1d5Smjnelson 564cdf0c1d5Smjnelson for fname in self.files: 565*605a716eSRichard Lowe if self.bu.exists(fname): 566cdf0c1d5Smjnelson bfile = self.bu.backupfile(fname) 567cdf0c1d5Smjnelson wfile = self.ws.repo.join(fname) 568cdf0c1d5Smjnelson 569cdf0c1d5Smjnelson try: 570cdf0c1d5Smjnelson shutil.copy2(bfile, wfile) 571cdf0c1d5Smjnelson except EnvironmentError, e: 572cdf0c1d5Smjnelson raise util.Abort("couldn't restore metadata from %s:\n" 573cdf0c1d5Smjnelson " %s" % (bfile, e)) 574cdf0c1d5Smjnelson 575cdf0c1d5Smjnelson def tar_restore(self): 576cdf0c1d5Smjnelson '''Restore workspace metadata (from a tar-style backup)''' 577cdf0c1d5Smjnelson 578*605a716eSRichard Lowe if not self.bu.exists('metadata.tar.gz'): 579*605a716eSRichard Lowe return 580*605a716eSRichard Lowe 581*605a716eSRichard Lowe tarpath = self.bu.backupfile('metadata.tar.gz') 582cdf0c1d5Smjnelson 583cdf0c1d5Smjnelson try: 584*605a716eSRichard Lowe tar = CdmTarFile.gzopen(tarpath) 585cdf0c1d5Smjnelson except (EnvironmentError, tarfile.TarError), e: 586*605a716eSRichard Lowe raise util.Abort("couldn't open %s: %s" % (tarpath, e)) 587cdf0c1d5Smjnelson 588cdf0c1d5Smjnelson try: 589cdf0c1d5Smjnelson for elt in tar: 590*605a716eSRichard Lowe try: 591cdf0c1d5Smjnelson tar.extract(elt, path=self.ws.repo.path) 592cdf0c1d5Smjnelson except (EnvironmentError, tarfile.TarError), e: 593cdf0c1d5Smjnelson # Make sure the member name is in the exception message. 594cdf0c1d5Smjnelson if isinstance(e, tarfile.TarError): 59587039217SRichard Lowe errstr = "%s: %s" % (elt.name, e) 596cdf0c1d5Smjnelson else: 59787039217SRichard Lowe errstr = str(e) 598cdf0c1d5Smjnelson 599cdf0c1d5Smjnelson raise util.Abort("couldn't restore metadata from %s:\n" 600cdf0c1d5Smjnelson " %s" % 601*605a716eSRichard Lowe (tarpath, errstr)) 602cdf0c1d5Smjnelson finally: 603cdf0c1d5Smjnelson if tar and not tar.closed: 604cdf0c1d5Smjnelson tar.close() 605cdf0c1d5Smjnelson 606cdf0c1d5Smjnelson def restore(self): 607cdf0c1d5Smjnelson '''Restore workspace metadata''' 608cdf0c1d5Smjnelson 609*605a716eSRichard Lowe if self.bu.exists('hgrc'): 610cdf0c1d5Smjnelson self.old_restore() 611cdf0c1d5Smjnelson else: 612cdf0c1d5Smjnelson self.tar_restore() 613cdf0c1d5Smjnelson 614*605a716eSRichard Lowe def _walk(self): 615*605a716eSRichard Lowe '''Yield the repo-relative path to each file we operate on, 616*605a716eSRichard Lowe including each file within any affected directory''' 617*605a716eSRichard Lowe 618*605a716eSRichard Lowe for elt in self.files: 619*605a716eSRichard Lowe path = self.ws.repo.join(elt) 620*605a716eSRichard Lowe 621*605a716eSRichard Lowe if not os.path.exists(path): 622*605a716eSRichard Lowe continue 623*605a716eSRichard Lowe 624*605a716eSRichard Lowe if os.path.isdir(path): 625*605a716eSRichard Lowe for root, dirs, files in os.walk(path, topdown=True): 626*605a716eSRichard Lowe yield root 627*605a716eSRichard Lowe 628*605a716eSRichard Lowe for f in files: 629*605a716eSRichard Lowe yield os.path.join(root, f) 630*605a716eSRichard Lowe else: 631*605a716eSRichard Lowe yield path 632*605a716eSRichard Lowe 633cdf0c1d5Smjnelson def need_backup(self): 634cdf0c1d5Smjnelson '''Compare backed up workspace metadata to workspace''' 635cdf0c1d5Smjnelson 636*605a716eSRichard Lowe def strip_trailing_pathsep(pathname): 637*605a716eSRichard Lowe '''Remove a possible trailing path separator from PATHNAME''' 638*605a716eSRichard Lowe return pathname.endswith('/') and pathname[:-1] or pathname 639*605a716eSRichard Lowe 640*605a716eSRichard Lowe if self.bu.exists('metadata.tar.gz'): 641*605a716eSRichard Lowe tarpath = self.bu.backupfile('metadata.tar.gz') 642cdf0c1d5Smjnelson try: 643*605a716eSRichard Lowe tar = CdmTarFile.gzopen(tarpath) 644cdf0c1d5Smjnelson except (EnvironmentError, tarfile.TarError), e: 645cdf0c1d5Smjnelson raise util.Abort("couldn't open metadata tarball: %s\n" 646*605a716eSRichard Lowe " %s" % (tarpath, e)) 647cdf0c1d5Smjnelson 648*605a716eSRichard Lowe if not tar.members_match_fs(self.ws.repo.path): 649*605a716eSRichard Lowe tar.close() 650cdf0c1d5Smjnelson return True 651cdf0c1d5Smjnelson 652*605a716eSRichard Lowe tarnames = map(strip_trailing_pathsep, tar.getnames()) 653cdf0c1d5Smjnelson tar.close() 654cdf0c1d5Smjnelson else: 655cdf0c1d5Smjnelson tarnames = [] 656cdf0c1d5Smjnelson 657*605a716eSRichard Lowe repopath = self.ws.repo.path 658*605a716eSRichard Lowe if not repopath.endswith('/'): 659*605a716eSRichard Lowe repopath += '/' 660cdf0c1d5Smjnelson 661*605a716eSRichard Lowe for path in self._walk(): 662*605a716eSRichard Lowe if path.replace(repopath, '', 1) not in tarnames: 663cdf0c1d5Smjnelson return True 664cdf0c1d5Smjnelson 665cdf0c1d5Smjnelson return False 666cdf0c1d5Smjnelson 667cdf0c1d5Smjnelson def cleanup(self): 668cdf0c1d5Smjnelson '''Remove backed up workspace metadata''' 669*605a716eSRichard Lowe self.bu.unlink('metadata.tar.gz') 670*605a716eSRichard Lowe 671*605a716eSRichard Lowe 672*605a716eSRichard Loweclass CdmClearBackup(object): 673*605a716eSRichard Lowe '''A backup (in tar format) of complete source files from every 674*605a716eSRichard Lowe workspace head. 675*605a716eSRichard Lowe 676*605a716eSRichard Lowe Paths in the tarball are prefixed by the revision and node of the 677*605a716eSRichard Lowe head, or "working" for the working directory. 678*605a716eSRichard Lowe 679*605a716eSRichard Lowe This is done purely for the benefit of the user, and as such takes 680*605a716eSRichard Lowe no part in restore or need_backup checking, restore always 681*605a716eSRichard Lowe succeeds, need_backup always returns False 682*605a716eSRichard Lowe ''' 683*605a716eSRichard Lowe 684*605a716eSRichard Lowe def __init__(self, backup, ws): 685*605a716eSRichard Lowe self.bu = backup 686*605a716eSRichard Lowe self.ws = ws 687*605a716eSRichard Lowe 688*605a716eSRichard Lowe def _branch_pairs(self): 689*605a716eSRichard Lowe '''Return a list of tuples (parenttip, localtip) for each 690*605a716eSRichard Lowe outgoing head. If the working copy contains modified files, 691*605a716eSRichard Lowe it is a head, and neither of its parents are. 692*605a716eSRichard Lowe ''' 693*605a716eSRichard Lowe 694*605a716eSRichard Lowe parent = self.ws.parent() 695*605a716eSRichard Lowe 696*605a716eSRichard Lowe if parent: 697*605a716eSRichard Lowe outgoing = self.ws.findoutgoing(parent) 698*605a716eSRichard Lowe outnodes = set(self.ws.repo.changelog.nodesbetween(outgoing)[0]) 699*605a716eSRichard Lowe 700*605a716eSRichard Lowe heads = [self.ws.repo.changectx(n) for n in self.ws.repo.heads() 701*605a716eSRichard Lowe if n in outnodes] 702*605a716eSRichard Lowe else: 703*605a716eSRichard Lowe heads = [] 704*605a716eSRichard Lowe outnodes = [] 705*605a716eSRichard Lowe 706*605a716eSRichard Lowe wctx = self.ws.workingctx() 707*605a716eSRichard Lowe if wctx.files(): # We only care about file changes. 708*605a716eSRichard Lowe heads = filter(lambda x: x not in wctx.parents(), heads) + [wctx] 709*605a716eSRichard Lowe 710*605a716eSRichard Lowe pairs = [] 711*605a716eSRichard Lowe for head in heads: 712*605a716eSRichard Lowe if head.rev() is None: 713*605a716eSRichard Lowe c = head.parents() 714*605a716eSRichard Lowe else: 715*605a716eSRichard Lowe c = [head] 716*605a716eSRichard Lowe 717*605a716eSRichard Lowe pairs.append((self.ws.parenttip(c, outnodes), head)) 718*605a716eSRichard Lowe return pairs 719*605a716eSRichard Lowe 720*605a716eSRichard Lowe def backup(self): 721*605a716eSRichard Lowe '''Save a clear copy of each source file modified between each 722*605a716eSRichard Lowe head and that head's parenttip (see WorkSpace.parenttip). 723*605a716eSRichard Lowe ''' 724*605a716eSRichard Lowe 725*605a716eSRichard Lowe tarpath = self.bu.backupfile('clear.tar.gz') 726*605a716eSRichard Lowe branches = self._branch_pairs() 727*605a716eSRichard Lowe 728*605a716eSRichard Lowe if not branches: 729*605a716eSRichard Lowe return 730*605a716eSRichard Lowe 731*605a716eSRichard Lowe try: 732*605a716eSRichard Lowe tar = CdmTarFile.gzopen(tarpath, 'w') 733*605a716eSRichard Lowe except (EnvironmentError, tarfile.TarError), e: 734*605a716eSRichard Lowe raise util.Abort("Could not open %s for writing: %s" % 735*605a716eSRichard Lowe (tarpath, e)) 736*605a716eSRichard Lowe 737*605a716eSRichard Lowe try: 738*605a716eSRichard Lowe for parent, child in branches: 739*605a716eSRichard Lowe tpath = child.node() and node.short(child.node()) or "working" 740*605a716eSRichard Lowe 741*605a716eSRichard Lowe for fname, change in self.ws.status(parent, child).iteritems(): 742*605a716eSRichard Lowe if change not in ('added', 'modified'): 743*605a716eSRichard Lowe continue 744*605a716eSRichard Lowe 745*605a716eSRichard Lowe try: 746*605a716eSRichard Lowe tar.addfilectx(child.filectx(fname), 747*605a716eSRichard Lowe os.path.join(tpath, fname)) 748*605a716eSRichard Lowe except ValueError, e: 749*605a716eSRichard Lowe crev = child.rev() 750*605a716eSRichard Lowe if crev is None: 751*605a716eSRichard Lowe crev = "working copy" 752*605a716eSRichard Lowe raise util.Abort("Could not backup clear file %s " 753*605a716eSRichard Lowe "from %s: %s\n" % (fname, crev, e)) 754*605a716eSRichard Lowe finally: 755*605a716eSRichard Lowe tar.close() 756*605a716eSRichard Lowe 757*605a716eSRichard Lowe def cleanup(self): 758*605a716eSRichard Lowe '''Cleanup a failed Clear backup. 759*605a716eSRichard Lowe 760*605a716eSRichard Lowe Remove the clear tarball from the backup directory. 761*605a716eSRichard Lowe ''' 762*605a716eSRichard Lowe 763*605a716eSRichard Lowe self.bu.unlink('clear.tar.gz') 764*605a716eSRichard Lowe 765*605a716eSRichard Lowe def restore(self): 766*605a716eSRichard Lowe '''Clear backups are never restored, do nothing''' 767*605a716eSRichard Lowe pass 768*605a716eSRichard Lowe 769*605a716eSRichard Lowe def need_backup(self): 770*605a716eSRichard Lowe '''Clear backups are never compared, return False (no backup needed). 771*605a716eSRichard Lowe 772*605a716eSRichard Lowe Should a backup actually be needed, one of the other 773*605a716eSRichard Lowe implementation classes would notice in any situation we would. 774*605a716eSRichard Lowe ''' 775*605a716eSRichard Lowe 776*605a716eSRichard Lowe return False 777cdf0c1d5Smjnelson 778cdf0c1d5Smjnelson 779cdf0c1d5Smjnelsonclass CdmBackup(object): 780cdf0c1d5Smjnelson '''A backup of a given workspace''' 781cdf0c1d5Smjnelson 782cdf0c1d5Smjnelson def __init__(self, ui, ws, name): 783cdf0c1d5Smjnelson self.ws = ws 784cdf0c1d5Smjnelson self.ui = ui 785cdf0c1d5Smjnelson self.backupdir = self._find_backup_dir(name) 786cdf0c1d5Smjnelson 787cdf0c1d5Smjnelson # 788cdf0c1d5Smjnelson # The order of instances here controls the order the various operations 789cdf0c1d5Smjnelson # are run. 790cdf0c1d5Smjnelson # 791cdf0c1d5Smjnelson # There's some inherent dependence, in that on restore we need 792cdf0c1d5Smjnelson # to restore committed changes prior to uncommitted changes 793cdf0c1d5Smjnelson # (as the parent revision of any uncommitted changes is quite 794cdf0c1d5Smjnelson # likely to not exist until committed changes are restored). 795cdf0c1d5Smjnelson # Metadata restore can happen at any point, but happens last 796cdf0c1d5Smjnelson # as a matter of convention. 797cdf0c1d5Smjnelson # 798cdf0c1d5Smjnelson self.modules = [x(self, ws) for x in [CdmCommittedBackup, 799cdf0c1d5Smjnelson CdmUncommittedBackup, 800*605a716eSRichard Lowe CdmClearBackup, 801cdf0c1d5Smjnelson CdmMetadataBackup]] 802cdf0c1d5Smjnelson 803cdf0c1d5Smjnelson if os.path.exists(os.path.join(self.backupdir, 'latest')): 804cdf0c1d5Smjnelson generation = os.readlink(os.path.join(self.backupdir, 'latest')) 805cdf0c1d5Smjnelson self.generation = int(os.path.split(generation)[1]) 806cdf0c1d5Smjnelson else: 807cdf0c1d5Smjnelson self.generation = 0 808cdf0c1d5Smjnelson 809cdf0c1d5Smjnelson def _find_backup_dir(self, name): 810cdf0c1d5Smjnelson '''Find the path to an appropriate backup directory based on NAME''' 811cdf0c1d5Smjnelson 812cdf0c1d5Smjnelson if os.path.isabs(name): 813cdf0c1d5Smjnelson return name 814cdf0c1d5Smjnelson 815cdf0c1d5Smjnelson if self.ui.config('cdm', 'backupdir'): 816cdf0c1d5Smjnelson backupbase = os.path.expanduser(self.ui.config('cdm', 'backupdir')) 817cdf0c1d5Smjnelson else: 818cdf0c1d5Smjnelson home = None 819cdf0c1d5Smjnelson 820cdf0c1d5Smjnelson try: 821cdf0c1d5Smjnelson home = os.getenv('HOME') or pwd.getpwuid(os.getuid()).pw_dir 822cdf0c1d5Smjnelson except KeyError: 823cdf0c1d5Smjnelson pass # Handled anyway 824cdf0c1d5Smjnelson 825cdf0c1d5Smjnelson if not home: 826cdf0c1d5Smjnelson raise util.Abort('Could not determine your HOME directory to ' 827cdf0c1d5Smjnelson 'find backup path') 828cdf0c1d5Smjnelson 829cdf0c1d5Smjnelson backupbase = os.path.join(home, 'cdm.backup') 830cdf0c1d5Smjnelson 831cdf0c1d5Smjnelson backupdir = os.path.join(backupbase, name) 832cdf0c1d5Smjnelson 833cdf0c1d5Smjnelson # If backupdir exists, it must be a directory. 834cdf0c1d5Smjnelson if (os.path.exists(backupdir) and not os.path.isdir(backupdir)): 835cdf0c1d5Smjnelson raise util.Abort('%s exists but is not a directory' % backupdir) 836cdf0c1d5Smjnelson 837cdf0c1d5Smjnelson return backupdir 838cdf0c1d5Smjnelson 839*605a716eSRichard Lowe def _update_latest(self, gen): 840cdf0c1d5Smjnelson '''Update latest symlink to point to the current generation''' 841cdf0c1d5Smjnelson linkpath = os.path.join(self.backupdir, 'latest') 842cdf0c1d5Smjnelson 843cdf0c1d5Smjnelson if os.path.lexists(linkpath): 844cdf0c1d5Smjnelson os.unlink(linkpath) 845cdf0c1d5Smjnelson 846cdf0c1d5Smjnelson os.symlink(str(gen), linkpath) 847cdf0c1d5Smjnelson 848*605a716eSRichard Lowe def _create_gen(self, gen): 849cdf0c1d5Smjnelson '''Create a new backup generation''' 850cdf0c1d5Smjnelson try: 851cdf0c1d5Smjnelson os.makedirs(os.path.join(self.backupdir, str(gen))) 852*605a716eSRichard Lowe self._update_latest(gen) 853cdf0c1d5Smjnelson except EnvironmentError, e: 854cdf0c1d5Smjnelson raise util.Abort("Couldn't create backup generation %s: %s" % 855cdf0c1d5Smjnelson (os.path.join(self.backupdir, str(gen)), e)) 856cdf0c1d5Smjnelson 857*605a716eSRichard Lowe def backupfile(self, path): 858*605a716eSRichard Lowe '''return full path to backup file FILE at GEN''' 859*605a716eSRichard Lowe return os.path.join(self.backupdir, str(self.generation), path) 860*605a716eSRichard Lowe 861*605a716eSRichard Lowe def unlink(self, name): 862*605a716eSRichard Lowe '''Unlink the specified path from the backup directory. 863*605a716eSRichard Lowe A no-op if the path does not exist. 864*605a716eSRichard Lowe ''' 865*605a716eSRichard Lowe 866*605a716eSRichard Lowe fpath = self.backupfile(name) 867*605a716eSRichard Lowe if os.path.exists(fpath): 868*605a716eSRichard Lowe os.unlink(fpath) 869*605a716eSRichard Lowe 870*605a716eSRichard Lowe def open(self, name, mode='r'): 871*605a716eSRichard Lowe '''Open the specified file in the backup directory''' 872*605a716eSRichard Lowe return open(self.backupfile(name), mode) 873*605a716eSRichard Lowe 874*605a716eSRichard Lowe def exists(self, name): 875*605a716eSRichard Lowe '''Return boolean indicating wether a given file exists in the 876*605a716eSRichard Lowe backup directory.''' 877*605a716eSRichard Lowe return os.path.exists(self.backupfile(name)) 878*605a716eSRichard Lowe 879cdf0c1d5Smjnelson def need_backup(self): 880cdf0c1d5Smjnelson '''Compare backed up changes to workspace''' 881cdf0c1d5Smjnelson # 882cdf0c1d5Smjnelson # If there's no current backup generation, or the last backup was 883cdf0c1d5Smjnelson # invalid (lacking the dirstate file), we need a backup regardless 884cdf0c1d5Smjnelson # of anything else. 885cdf0c1d5Smjnelson # 886*605a716eSRichard Lowe if not self.generation or not self.exists('dirstate'): 887cdf0c1d5Smjnelson return True 888cdf0c1d5Smjnelson 889cdf0c1d5Smjnelson for x in self.modules: 890cdf0c1d5Smjnelson if x.need_backup(): 891cdf0c1d5Smjnelson return True 892cdf0c1d5Smjnelson 893cdf0c1d5Smjnelson return False 894cdf0c1d5Smjnelson 895cdf0c1d5Smjnelson def backup(self): 896c959a081SRichard Lowe '''Take a backup of the current workspace 897c959a081SRichard Lowe 898c959a081SRichard Lowe Calling code is expected to hold both the working copy lock 899c959a081SRichard Lowe and repository lock.''' 900cdf0c1d5Smjnelson 901cdf0c1d5Smjnelson if not os.path.exists(self.backupdir): 902cdf0c1d5Smjnelson try: 903cdf0c1d5Smjnelson os.makedirs(self.backupdir) 904cdf0c1d5Smjnelson except EnvironmentError, e: 905cdf0c1d5Smjnelson raise util.Abort('Could not create backup directory %s: %s' % 906cdf0c1d5Smjnelson (self.backupdir, e)) 907cdf0c1d5Smjnelson 908cdf0c1d5Smjnelson self.generation += 1 909*605a716eSRichard Lowe self._create_gen(self.generation) 910cdf0c1d5Smjnelson 911cdf0c1d5Smjnelson try: 912cdf0c1d5Smjnelson for x in self.modules: 913cdf0c1d5Smjnelson x.backup() 914cdf0c1d5Smjnelson except Exception, e: 915cdf0c1d5Smjnelson if isinstance(e, KeyboardInterrupt): 916cdf0c1d5Smjnelson self.ws.ui.warn("Interrupted\n") 917cdf0c1d5Smjnelson else: 918cdf0c1d5Smjnelson self.ws.ui.warn("Error: %s\n" % e) 919c959a081SRichard Lowe show_traceback = self.ws.ui.configbool('ui', 'traceback', 920c959a081SRichard Lowe False) 921cdf0c1d5Smjnelson 922cdf0c1d5Smjnelson # 923cdf0c1d5Smjnelson # If it's not a 'normal' error, we want to print a stack 924cdf0c1d5Smjnelson # trace now in case the attempt to remove the partial 925cdf0c1d5Smjnelson # backup also fails, and raises a second exception. 926cdf0c1d5Smjnelson # 927cdf0c1d5Smjnelson if (not isinstance(e, (EnvironmentError, util.Abort)) 928c959a081SRichard Lowe or show_traceback): 929cdf0c1d5Smjnelson traceback.print_exc() 930cdf0c1d5Smjnelson 931cdf0c1d5Smjnelson for x in self.modules: 932cdf0c1d5Smjnelson x.cleanup() 933cdf0c1d5Smjnelson 934cdf0c1d5Smjnelson os.rmdir(os.path.join(self.backupdir, str(self.generation))) 935cdf0c1d5Smjnelson self.generation -= 1 936cdf0c1d5Smjnelson 937cdf0c1d5Smjnelson if self.generation != 0: 938*605a716eSRichard Lowe self._update_latest(self.generation) 939cdf0c1d5Smjnelson else: 940cdf0c1d5Smjnelson os.unlink(os.path.join(self.backupdir, 'latest')) 941cdf0c1d5Smjnelson 942cdf0c1d5Smjnelson raise util.Abort('Backup failed') 943cdf0c1d5Smjnelson 944cdf0c1d5Smjnelson def restore(self, gen=None): 945cdf0c1d5Smjnelson '''Restore workspace from backup 946cdf0c1d5Smjnelson 947cdf0c1d5Smjnelson Restores from backup generation GEN (defaulting to the latest) 948c959a081SRichard Lowe into workspace WS. 949cdf0c1d5Smjnelson 950c959a081SRichard Lowe Calling code is expected to hold both the working copy lock 951c959a081SRichard Lowe and repository lock of the destination workspace.''' 952cdf0c1d5Smjnelson 953cdf0c1d5Smjnelson if not os.path.exists(self.backupdir): 954cdf0c1d5Smjnelson raise util.Abort('Backup directory does not exist: %s' % 955cdf0c1d5Smjnelson (self.backupdir)) 956cdf0c1d5Smjnelson 957cdf0c1d5Smjnelson if gen: 958cdf0c1d5Smjnelson if not os.path.exists(os.path.join(self.backupdir, str(gen))): 959cdf0c1d5Smjnelson raise util.Abort('Backup generation does not exist: %s' % 960cdf0c1d5Smjnelson (os.path.join(self.backupdir, str(gen)))) 961cdf0c1d5Smjnelson self.generation = int(gen) 962cdf0c1d5Smjnelson 963*605a716eSRichard Lowe if not self.generation: # This is OK, 0 is not a valid generation 964cdf0c1d5Smjnelson raise util.Abort('Backup has no generations: %s' % self.backupdir) 965cdf0c1d5Smjnelson 966*605a716eSRichard Lowe if not self.exists('dirstate'): 967cdf0c1d5Smjnelson raise util.Abort('Backup %s/%s is incomplete (dirstate missing)' % 968cdf0c1d5Smjnelson (self.backupdir, self.generation)) 969cdf0c1d5Smjnelson 970cdf0c1d5Smjnelson try: 971cdf0c1d5Smjnelson for x in self.modules: 972cdf0c1d5Smjnelson x.restore() 973cdf0c1d5Smjnelson except util.Abort, e: 974cdf0c1d5Smjnelson raise util.Abort('Error restoring workspace:\n' 975cdf0c1d5Smjnelson '%s\n' 97612203c71SRichard Lowe 'Workspace may be partially restored' % e) 977