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# 20605a716eSRichard Lowe# Copyright 2008, 2011, Richard Lowe 2187039217SRichard Lowe# 22cdf0c1d5Smjnelson 23605a716eSRichard 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 56605a716eSRichard Lowe clear.tar.gz (handled by CdmClearBackup) 57605a716eSRichard Lowe <short node>/ 58605a716eSRichard Lowe copies of each modified or added file, as it is in 59605a716eSRichard Lowe this head. 60605a716eSRichard Lowe 61605a716eSRichard Lowe ... for each outgoing head 62605a716eSRichard Lowe 63605a716eSRichard Lowe working/ 64605a716eSRichard Lowe copies of each modified or added file in the 65605a716eSRichard Lowe working copy if any. 66605a716eSRichard 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 74605a716eSRichard Loweimport grp, os, pwd, shutil, tarfile, time, traceback 75605a716eSRichard Lowefrom cStringIO import StringIO 76605a716eSRichard Lowe 77*036abacaSRichard Lowefrom mercurial import changegroup, cmdutil, error, node, patch, util 78*036abacaSRichard Lowefrom onbld.Scm import Version 79cdf0c1d5Smjnelson 80cdf0c1d5Smjnelson 8112203c71SRichard Loweclass CdmNodeMissing(util.Abort): 8212203c71SRichard Lowe '''a required node is not present in the destination workspace. 8312203c71SRichard Lowe 8412203c71SRichard Lowe This may occur both in the case where the bundle contains a 8512203c71SRichard Lowe changeset which is a child of a node not present in the 8612203c71SRichard Lowe destination workspace (because the destination workspace is not as 8712203c71SRichard Lowe up-to-date as the source), or because the source and destination 8812203c71SRichard Lowe workspace are not related. 8912203c71SRichard Lowe 9012203c71SRichard Lowe It may also happen in cases where the uncommitted changes need to 9112203c71SRichard Lowe be applied onto a node that the workspace does not possess even 9212203c71SRichard Lowe after application of the bundle (on a branch not present 9312203c71SRichard Lowe in the bundle or destination workspace, for instance)''' 9412203c71SRichard Lowe 9512203c71SRichard Lowe def __init__(self, msg, name): 9612203c71SRichard Lowe # 9712203c71SRichard Lowe # If e.name is a string 20 characters long, it is 9812203c71SRichard Lowe # assumed to be a node. (Mercurial makes this 9912203c71SRichard Lowe # same assumption, when creating a LookupError) 10012203c71SRichard Lowe # 10112203c71SRichard Lowe if isinstance(name, str) and len(name) == 20: 10212203c71SRichard Lowe n = node.short(name) 10312203c71SRichard Lowe else: 10412203c71SRichard Lowe n = name 10512203c71SRichard Lowe 10612203c71SRichard Lowe util.Abort.__init__(self, "%s: changeset '%s' is missing\n" 10712203c71SRichard Lowe "Your workspace is either not " 10812203c71SRichard Lowe "sufficiently up to date,\n" 10912203c71SRichard Lowe "or is unrelated to the workspace from " 11012203c71SRichard Lowe "which the backup was taken.\n" % (msg, n)) 11112203c71SRichard Lowe 11212203c71SRichard Lowe 113605a716eSRichard Loweclass CdmTarFile(tarfile.TarFile): 114605a716eSRichard Lowe '''Tar file access + simple comparison to the filesystem, and 115605a716eSRichard Lowe creation addition of files from Mercurial filectx objects.''' 116605a716eSRichard Lowe 117605a716eSRichard Lowe def __init__(self, *args, **kwargs): 118605a716eSRichard Lowe tarfile.TarFile.__init__(self, *args, **kwargs) 119605a716eSRichard Lowe self.errorlevel = 2 120605a716eSRichard Lowe 121605a716eSRichard Lowe def members_match_fs(self, rootpath): 122605a716eSRichard Lowe '''Compare the contents of the tar archive to the directory 123605a716eSRichard Lowe specified by rootpath. Return False if they differ. 124605a716eSRichard Lowe 125605a716eSRichard Lowe Every file in the archive must match the equivalent file in 126605a716eSRichard Lowe the filesystem. 127605a716eSRichard Lowe 128605a716eSRichard Lowe The existence, modification time, and size of each file are 129605a716eSRichard Lowe compared, content is not.''' 130605a716eSRichard Lowe 131605a716eSRichard Lowe def _member_matches_fs(member, rootpath): 132605a716eSRichard Lowe '''Compare a single member to its filesystem counterpart''' 133605a716eSRichard Lowe fpath = os.path.join(rootpath, member.name) 134605a716eSRichard Lowe 135605a716eSRichard Lowe if not os.path.exists(fpath): 136605a716eSRichard Lowe return False 137605a716eSRichard Lowe elif ((os.path.isfile(fpath) != member.isfile()) or 138605a716eSRichard Lowe (os.path.isdir(fpath) != member.isdir()) or 139605a716eSRichard Lowe (os.path.islink(fpath) != member.issym())): 140605a716eSRichard Lowe return False 141605a716eSRichard Lowe 142605a716eSRichard Lowe # 143605a716eSRichard Lowe # The filesystem may return a modification time with a 144605a716eSRichard Lowe # fractional component (as a float), whereas the tar format 145605a716eSRichard Lowe # only stores it to the whole second, perform the comparison 146605a716eSRichard Lowe # using integers (truncated, not rounded) 147605a716eSRichard Lowe # 148605a716eSRichard Lowe elif member.mtime != int(os.path.getmtime(fpath)): 149605a716eSRichard Lowe return False 150605a716eSRichard Lowe elif not member.isdir() and member.size != os.path.getsize(fpath): 151605a716eSRichard Lowe return False 152605a716eSRichard Lowe else: 153605a716eSRichard Lowe return True 154605a716eSRichard Lowe 155605a716eSRichard Lowe for elt in self: 156605a716eSRichard Lowe if not _member_matches_fs(elt, rootpath): 157605a716eSRichard Lowe return False 158605a716eSRichard Lowe 159605a716eSRichard Lowe return True 160605a716eSRichard Lowe 161605a716eSRichard Lowe def addfilectx(self, filectx, path=None): 162605a716eSRichard Lowe '''Add a filectx object to the archive. 163605a716eSRichard Lowe 164605a716eSRichard Lowe Use the path specified by the filectx object or, if specified, 165605a716eSRichard Lowe the PATH argument. 166605a716eSRichard Lowe 167605a716eSRichard Lowe The size, modification time, type and permissions of the tar 168605a716eSRichard Lowe member are taken from the filectx object, user and group id 169605a716eSRichard Lowe are those of the invoking user, user and group name are those 170605a716eSRichard Lowe of the invoking user if information is available, or "unknown" 171605a716eSRichard Lowe if it is not. 172605a716eSRichard Lowe ''' 173605a716eSRichard Lowe 174605a716eSRichard Lowe t = tarfile.TarInfo(path or filectx.path()) 175605a716eSRichard Lowe t.size = filectx.size() 176605a716eSRichard Lowe t.mtime = filectx.date()[0] 177605a716eSRichard Lowe t.uid = os.getuid() 178605a716eSRichard Lowe t.gid = os.getgid() 179605a716eSRichard Lowe 180605a716eSRichard Lowe try: 181605a716eSRichard Lowe t.uname = pwd.getpwuid(t.uid).pw_name 182605a716eSRichard Lowe except KeyError: 183605a716eSRichard Lowe t.uname = "unknown" 184605a716eSRichard Lowe 185605a716eSRichard Lowe try: 186605a716eSRichard Lowe t.gname = grp.getgrgid(t.gid).gr_name 187605a716eSRichard Lowe except KeyError: 188605a716eSRichard Lowe t.gname = "unknown" 189605a716eSRichard Lowe 190605a716eSRichard Lowe # 191605a716eSRichard Lowe # Mercurial versions symlinks by setting a flag and storing 192605a716eSRichard Lowe # the destination path in place of the file content. The 193605a716eSRichard Lowe # actual contents (in the tar), should be empty. 194605a716eSRichard Lowe # 195605a716eSRichard Lowe if 'l' in filectx.flags(): 196605a716eSRichard Lowe t.type = tarfile.SYMTYPE 197605a716eSRichard Lowe t.mode = 0777 198605a716eSRichard Lowe t.linkname = filectx.data() 199605a716eSRichard Lowe data = None 200605a716eSRichard Lowe else: 201605a716eSRichard Lowe t.type = tarfile.REGTYPE 202605a716eSRichard Lowe t.mode = 'x' in filectx.flags() and 0755 or 0644 203605a716eSRichard Lowe data = StringIO(filectx.data()) 204605a716eSRichard Lowe 205605a716eSRichard Lowe self.addfile(t, data) 206605a716eSRichard Lowe 207605a716eSRichard Lowe 208cdf0c1d5Smjnelsonclass CdmCommittedBackup(object): 209cdf0c1d5Smjnelson '''Backup of committed changes''' 210cdf0c1d5Smjnelson 211cdf0c1d5Smjnelson def __init__(self, backup, ws): 212cdf0c1d5Smjnelson self.ws = ws 213cdf0c1d5Smjnelson self.bu = backup 214cdf0c1d5Smjnelson self.files = ('bundle', 'nodes') 215cdf0c1d5Smjnelson 216cdf0c1d5Smjnelson def _outgoing_nodes(self, parent): 217cdf0c1d5Smjnelson '''Return a list of all outgoing nodes in hex format''' 218cdf0c1d5Smjnelson 219cdf0c1d5Smjnelson if parent: 220cdf0c1d5Smjnelson outgoing = self.ws.findoutgoing(parent) 221cdf0c1d5Smjnelson nodes = self.ws.repo.changelog.nodesbetween(outgoing)[0] 222cdf0c1d5Smjnelson return map(node.hex, nodes) 223cdf0c1d5Smjnelson else: 224cdf0c1d5Smjnelson return [] 225cdf0c1d5Smjnelson 226cdf0c1d5Smjnelson def backup(self): 227cdf0c1d5Smjnelson '''Backup committed changes''' 228cdf0c1d5Smjnelson parent = self.ws.parent() 229cdf0c1d5Smjnelson 230cdf0c1d5Smjnelson if not parent: 231cdf0c1d5Smjnelson self.ws.ui.warn('Workspace has no parent, committed changes will ' 232cdf0c1d5Smjnelson 'not be backed up\n') 233cdf0c1d5Smjnelson return 234cdf0c1d5Smjnelson 235cdf0c1d5Smjnelson out = self.ws.findoutgoing(parent) 236cdf0c1d5Smjnelson if not out: 237cdf0c1d5Smjnelson return 238cdf0c1d5Smjnelson 239cdf0c1d5Smjnelson cg = self.ws.repo.changegroup(out, 'bundle') 240cdf0c1d5Smjnelson changegroup.writebundle(cg, self.bu.backupfile('bundle'), 'HG10BZ') 241cdf0c1d5Smjnelson 242cdf0c1d5Smjnelson outnodes = self._outgoing_nodes(parent) 243605a716eSRichard Lowe if not outnodes: 244605a716eSRichard Lowe return 245605a716eSRichard Lowe 246cdf0c1d5Smjnelson fp = None 247cdf0c1d5Smjnelson try: 248cdf0c1d5Smjnelson try: 249605a716eSRichard Lowe fp = self.bu.open('nodes', 'w') 250cdf0c1d5Smjnelson fp.write('%s\n' % '\n'.join(outnodes)) 251cdf0c1d5Smjnelson except EnvironmentError, e: 252cdf0c1d5Smjnelson raise util.Abort("couldn't store outgoing nodes: %s" % e) 253cdf0c1d5Smjnelson finally: 254cdf0c1d5Smjnelson if fp and not fp.closed: 255cdf0c1d5Smjnelson fp.close() 256cdf0c1d5Smjnelson 257cdf0c1d5Smjnelson def restore(self): 258cdf0c1d5Smjnelson '''Restore committed changes from backup''' 259cdf0c1d5Smjnelson 260605a716eSRichard Lowe if not self.bu.exists('bundle'): 261605a716eSRichard Lowe return 262605a716eSRichard Lowe 263605a716eSRichard Lowe bpath = self.bu.backupfile('bundle') 264cdf0c1d5Smjnelson f = None 265cdf0c1d5Smjnelson try: 266cdf0c1d5Smjnelson try: 267605a716eSRichard Lowe f = self.bu.open('bundle') 268605a716eSRichard Lowe bundle = changegroup.readbundle(f, bpath) 269cdf0c1d5Smjnelson self.ws.repo.addchangegroup(bundle, 'strip', 270605a716eSRichard Lowe 'bundle:%s' % bpath) 271cdf0c1d5Smjnelson except EnvironmentError, e: 272cdf0c1d5Smjnelson raise util.Abort("couldn't restore committed changes: %s\n" 273605a716eSRichard Lowe " %s" % (bpath, e)) 27487039217SRichard Lowe except error.LookupError, e: 27512203c71SRichard Lowe raise CdmNodeMissing("couldn't restore committed changes", 27612203c71SRichard Lowe e.name) 277cdf0c1d5Smjnelson finally: 278cdf0c1d5Smjnelson if f and not f.closed: 279cdf0c1d5Smjnelson f.close() 280cdf0c1d5Smjnelson 281cdf0c1d5Smjnelson def need_backup(self): 282cdf0c1d5Smjnelson '''Compare backup of committed changes to workspace''' 283cdf0c1d5Smjnelson 284605a716eSRichard Lowe if self.bu.exists('nodes'): 285cdf0c1d5Smjnelson f = None 286cdf0c1d5Smjnelson try: 287cdf0c1d5Smjnelson try: 288605a716eSRichard Lowe f = self.bu.open('nodes') 289605a716eSRichard Lowe bnodes = set(line.rstrip('\r\n') for line in f.readlines()) 290cdf0c1d5Smjnelson f.close() 291cdf0c1d5Smjnelson except EnvironmentError, e: 292cdf0c1d5Smjnelson raise util.Abort("couldn't open backup node list: %s" % e) 293cdf0c1d5Smjnelson finally: 294cdf0c1d5Smjnelson if f and not f.closed: 295cdf0c1d5Smjnelson f.close() 296cdf0c1d5Smjnelson else: 297cdf0c1d5Smjnelson bnodes = set() 298cdf0c1d5Smjnelson 299cdf0c1d5Smjnelson outnodes = set(self._outgoing_nodes(self.ws.parent())) 300605a716eSRichard Lowe 301605a716eSRichard Lowe # 302605a716eSRichard Lowe # If there are outgoing nodes not in the prior backup we need 303605a716eSRichard Lowe # to take a new backup; it's fine if there are nodes in the 304605a716eSRichard Lowe # old backup which are no longer outgoing, however. 305605a716eSRichard Lowe # 306605a716eSRichard Lowe if not outnodes <= bnodes: 307cdf0c1d5Smjnelson return True 308cdf0c1d5Smjnelson 309cdf0c1d5Smjnelson return False 310cdf0c1d5Smjnelson 311cdf0c1d5Smjnelson def cleanup(self): 312cdf0c1d5Smjnelson '''Remove backed up committed changes''' 313cdf0c1d5Smjnelson 314605a716eSRichard Lowe for f in self.files: 315605a716eSRichard Lowe self.bu.unlink(f) 316cdf0c1d5Smjnelson 317cdf0c1d5Smjnelson 318cdf0c1d5Smjnelsonclass CdmUncommittedBackup(object): 319cdf0c1d5Smjnelson '''Backup of uncommitted changes''' 320cdf0c1d5Smjnelson 321cdf0c1d5Smjnelson def __init__(self, backup, ws): 322cdf0c1d5Smjnelson self.ws = ws 323cdf0c1d5Smjnelson self.bu = backup 324605a716eSRichard Lowe self.wctx = self.ws.workingctx(worklist=True) 325cdf0c1d5Smjnelson 326cdf0c1d5Smjnelson def _clobbering_renames(self): 327cdf0c1d5Smjnelson '''Return a list of pairs of files representing renames/copies 328605a716eSRichard Lowe that clobber already versioned files. [(old-name new-name)...] 329605a716eSRichard Lowe ''' 330cdf0c1d5Smjnelson 331cdf0c1d5Smjnelson # 332cdf0c1d5Smjnelson # Note that this doesn't handle uncommitted merges 333cdf0c1d5Smjnelson # as CdmUncommittedBackup itself doesn't. 334cdf0c1d5Smjnelson # 335605a716eSRichard Lowe parent = self.wctx.parents()[0] 336cdf0c1d5Smjnelson 337cdf0c1d5Smjnelson ret = [] 338605a716eSRichard Lowe for fname in self.wctx.added() + self.wctx.modified(): 339605a716eSRichard Lowe rn = self.wctx.filectx(fname).renamed() 340cdf0c1d5Smjnelson if rn and fname in parent: 341cdf0c1d5Smjnelson ret.append((rn[0], fname)) 342cdf0c1d5Smjnelson return ret 343cdf0c1d5Smjnelson 344cdf0c1d5Smjnelson def backup(self): 345cdf0c1d5Smjnelson '''Backup uncommitted changes''' 346cdf0c1d5Smjnelson 347cdf0c1d5Smjnelson if self.ws.merged(): 348cdf0c1d5Smjnelson raise util.Abort("Unable to backup an uncommitted merge.\n" 349cdf0c1d5Smjnelson "Please complete your merge and commit") 350cdf0c1d5Smjnelson 351605a716eSRichard Lowe dirstate = node.hex(self.wctx.parents()[0].node()) 352cdf0c1d5Smjnelson 353cdf0c1d5Smjnelson fp = None 354cdf0c1d5Smjnelson try: 355cdf0c1d5Smjnelson try: 356605a716eSRichard Lowe fp = self.bu.open('dirstate', 'w') 357cdf0c1d5Smjnelson fp.write(dirstate + '\n') 358605a716eSRichard Lowe fp.close() 359cdf0c1d5Smjnelson except EnvironmentError, e: 360cdf0c1d5Smjnelson raise util.Abort("couldn't save working copy parent: %s" % e) 361cdf0c1d5Smjnelson 362cdf0c1d5Smjnelson try: 363605a716eSRichard Lowe fp = self.bu.open('renames', 'w') 364cdf0c1d5Smjnelson for cons in self._clobbering_renames(): 365cdf0c1d5Smjnelson fp.write("%s %s\n" % cons) 366605a716eSRichard Lowe fp.close() 367cdf0c1d5Smjnelson except EnvironmentError, e: 368cdf0c1d5Smjnelson raise util.Abort("couldn't save clobbering copies: %s" % e) 369cdf0c1d5Smjnelson 370cdf0c1d5Smjnelson try: 371605a716eSRichard Lowe fp = self.bu.open('diff', 'w') 372605a716eSRichard Lowe match = self.ws.matcher(files=self.wctx.files()) 373605a716eSRichard Lowe fp.write(self.ws.diff(opts={'git': True}, match=match)) 374cdf0c1d5Smjnelson except EnvironmentError, e: 375cdf0c1d5Smjnelson raise util.Abort("couldn't save working copy diff: %s" % e) 376cdf0c1d5Smjnelson finally: 377cdf0c1d5Smjnelson if fp and not fp.closed: 378cdf0c1d5Smjnelson fp.close() 379cdf0c1d5Smjnelson 380cdf0c1d5Smjnelson def _dirstate(self): 3812b5878deSRich Lowe '''Return the desired working copy node from the backup''' 382cdf0c1d5Smjnelson fp = None 383cdf0c1d5Smjnelson try: 384cdf0c1d5Smjnelson try: 385605a716eSRichard Lowe fp = self.bu.open('dirstate') 386cdf0c1d5Smjnelson dirstate = fp.readline().strip() 387cdf0c1d5Smjnelson except EnvironmentError, e: 388cdf0c1d5Smjnelson raise util.Abort("couldn't read saved parent: %s" % e) 389cdf0c1d5Smjnelson finally: 390cdf0c1d5Smjnelson if fp and not fp.closed: 391cdf0c1d5Smjnelson fp.close() 392cdf0c1d5Smjnelson 393605a716eSRichard Lowe return dirstate 394605a716eSRichard Lowe 395cdf0c1d5Smjnelson def restore(self): 396cdf0c1d5Smjnelson '''Restore uncommitted changes''' 397cdf0c1d5Smjnelson dirstate = self._dirstate() 398cdf0c1d5Smjnelson 3992b5878deSRich Lowe # 4002b5878deSRich Lowe # Check that the patch's parent changeset exists. 4012b5878deSRich Lowe # 402cdf0c1d5Smjnelson try: 4032b5878deSRich Lowe n = node.bin(dirstate) 4042b5878deSRich Lowe self.ws.repo.changelog.lookup(n) 40587039217SRichard Lowe except error.LookupError, e: 40612203c71SRichard Lowe raise CdmNodeMissing("couldn't restore uncommitted changes", 40712203c71SRichard Lowe e.name) 4082b5878deSRich Lowe 4092b5878deSRich Lowe try: 4102b5878deSRich Lowe self.ws.clean(rev=dirstate) 411cdf0c1d5Smjnelson except util.Abort, e: 412cdf0c1d5Smjnelson raise util.Abort("couldn't update to saved node: %s" % e) 413cdf0c1d5Smjnelson 414605a716eSRichard Lowe if not self.bu.exists('diff'): 415cdf0c1d5Smjnelson return 416cdf0c1d5Smjnelson 417cdf0c1d5Smjnelson # 418cdf0c1d5Smjnelson # There's a race here whereby if the patch (or part thereof) 419cdf0c1d5Smjnelson # is applied within the same second as the clean above (such 420605a716eSRichard Lowe # that modification time doesn't change) and if the size of 421605a716eSRichard Lowe # that file does not change, Hg may not see the change. 422cdf0c1d5Smjnelson # 423cdf0c1d5Smjnelson # We sleep a full second to avoid this, as sleeping merely 424cdf0c1d5Smjnelson # until the next second begins would require very close clock 425cdf0c1d5Smjnelson # synchronization on network filesystems. 426cdf0c1d5Smjnelson # 427cdf0c1d5Smjnelson time.sleep(1) 428cdf0c1d5Smjnelson 429cdf0c1d5Smjnelson files = {} 430cdf0c1d5Smjnelson try: 431605a716eSRichard Lowe diff = self.bu.backupfile('diff') 432cdf0c1d5Smjnelson try: 433cdf0c1d5Smjnelson fuzz = patch.patch(diff, self.ws.ui, strip=1, 434cdf0c1d5Smjnelson cwd=self.ws.repo.root, files=files) 435cdf0c1d5Smjnelson if fuzz: 436cdf0c1d5Smjnelson raise util.Abort('working copy diff applied with fuzz') 437cdf0c1d5Smjnelson except Exception, e: 438cdf0c1d5Smjnelson raise util.Abort("couldn't apply working copy diff: %s\n" 439cdf0c1d5Smjnelson " %s" % (diff, e)) 440cdf0c1d5Smjnelson finally: 441*036abacaSRichard Lowe if Version.at_least("1.7"): 442*036abacaSRichard Lowe cmdutil.updatedir(self.ws.ui, self.ws.repo, files) 443*036abacaSRichard Lowe else: 444cdf0c1d5Smjnelson patch.updatedir(self.ws.ui, self.ws.repo, files) 445cdf0c1d5Smjnelson 446605a716eSRichard Lowe if not self.bu.exists('renames'): 447cdf0c1d5Smjnelson return 448cdf0c1d5Smjnelson 449cdf0c1d5Smjnelson # 450cdf0c1d5Smjnelson # We need to re-apply name changes where the new name 451cdf0c1d5Smjnelson # (rename/copy destination) is an already versioned file, as 452cdf0c1d5Smjnelson # Hg would otherwise ignore them. 453cdf0c1d5Smjnelson # 454cdf0c1d5Smjnelson try: 455605a716eSRichard Lowe fp = self.bu.open('renames') 456cdf0c1d5Smjnelson for line in fp: 457cdf0c1d5Smjnelson source, dest = line.strip().split() 45887039217SRichard Lowe self.ws.copy(source, dest) 459cdf0c1d5Smjnelson except EnvironmentError, e: 460cdf0c1d5Smjnelson raise util.Abort('unable to open renames file: %s' % e) 461cdf0c1d5Smjnelson except ValueError: 462cdf0c1d5Smjnelson raise util.Abort('corrupt renames file: %s' % 463cdf0c1d5Smjnelson self.bu.backupfile('renames')) 464cdf0c1d5Smjnelson 465cdf0c1d5Smjnelson def need_backup(self): 466cdf0c1d5Smjnelson '''Compare backup of uncommitted changes to workspace''' 467605a716eSRichard Lowe cnode = self.wctx.parents()[0].node() 4682b5878deSRich Lowe if self._dirstate() != node.hex(cnode): 469cdf0c1d5Smjnelson return True 470cdf0c1d5Smjnelson 471605a716eSRichard Lowe fd = None 472605a716eSRichard Lowe match = self.ws.matcher(files=self.wctx.files()) 473605a716eSRichard Lowe curdiff = self.ws.diff(opts={'git': True}, match=match) 4742b5878deSRich Lowe 475cdf0c1d5Smjnelson try: 476605a716eSRichard Lowe if self.bu.exists('diff'): 477cdf0c1d5Smjnelson try: 478605a716eSRichard Lowe fd = self.bu.open('diff') 479cdf0c1d5Smjnelson backdiff = fd.read() 480605a716eSRichard Lowe fd.close() 481cdf0c1d5Smjnelson except EnvironmentError, e: 482cdf0c1d5Smjnelson raise util.Abort("couldn't open backup diff %s\n" 483605a716eSRichard Lowe " %s" % (self.bu.backupfile('diff'), e)) 484cdf0c1d5Smjnelson else: 485cdf0c1d5Smjnelson backdiff = '' 486cdf0c1d5Smjnelson 4872b5878deSRich Lowe if backdiff != curdiff: 488cdf0c1d5Smjnelson return True 489cdf0c1d5Smjnelson 490cdf0c1d5Smjnelson currrenamed = self._clobbering_renames() 491cdf0c1d5Smjnelson bakrenamed = None 492cdf0c1d5Smjnelson 493605a716eSRichard Lowe if self.bu.exists('renames'): 494cdf0c1d5Smjnelson try: 495605a716eSRichard Lowe fd = self.bu.open('renames') 496605a716eSRichard Lowe bakrenamed = [tuple(line.strip().split(' ')) for line in fd] 497605a716eSRichard Lowe fd.close() 498cdf0c1d5Smjnelson except EnvironmentError, e: 499cdf0c1d5Smjnelson raise util.Abort("couldn't open renames file %s: %s\n" % 500cdf0c1d5Smjnelson (self.bu.backupfile('renames'), e)) 501cdf0c1d5Smjnelson 502cdf0c1d5Smjnelson if currrenamed != bakrenamed: 503cdf0c1d5Smjnelson return True 504605a716eSRichard Lowe finally: 505605a716eSRichard Lowe if fd and not fd.closed: 506605a716eSRichard Lowe fd.close() 507cdf0c1d5Smjnelson 508cdf0c1d5Smjnelson return False 509cdf0c1d5Smjnelson 510cdf0c1d5Smjnelson def cleanup(self): 511cdf0c1d5Smjnelson '''Remove backed up uncommitted changes''' 512605a716eSRichard Lowe 513605a716eSRichard Lowe for f in ('dirstate', 'diff', 'renames'): 514605a716eSRichard Lowe self.bu.unlink(f) 515cdf0c1d5Smjnelson 516cdf0c1d5Smjnelson 517cdf0c1d5Smjnelsonclass CdmMetadataBackup(object): 518cdf0c1d5Smjnelson '''Backup of workspace metadata''' 519cdf0c1d5Smjnelson 520cdf0c1d5Smjnelson def __init__(self, backup, ws): 521cdf0c1d5Smjnelson self.bu = backup 522cdf0c1d5Smjnelson self.ws = ws 5239a70fc3bSMark J. Nelson self.files = ('hgrc', 'localtags', 'patches', 'cdm') 524cdf0c1d5Smjnelson 525cdf0c1d5Smjnelson def backup(self): 526cdf0c1d5Smjnelson '''Backup workspace metadata''' 527cdf0c1d5Smjnelson 528605a716eSRichard Lowe tarpath = self.bu.backupfile('metadata.tar.gz') 529605a716eSRichard Lowe 530605a716eSRichard Lowe # 531605a716eSRichard Lowe # Files is a list of tuples (name, path), where name is as in 532605a716eSRichard Lowe # self.files, and path is the absolute path. 533605a716eSRichard Lowe # 534605a716eSRichard Lowe files = filter(lambda (name, path): os.path.exists(path), 535605a716eSRichard Lowe zip(self.files, map(self.ws.repo.join, self.files))) 536605a716eSRichard Lowe 537605a716eSRichard Lowe if not files: 538605a716eSRichard Lowe return 539cdf0c1d5Smjnelson 540cdf0c1d5Smjnelson try: 541605a716eSRichard Lowe tar = CdmTarFile.gzopen(tarpath, 'w') 542cdf0c1d5Smjnelson except (EnvironmentError, tarfile.TarError), e: 543cdf0c1d5Smjnelson raise util.Abort("couldn't open %s for writing: %s" % 544605a716eSRichard Lowe (tarpath, e)) 545cdf0c1d5Smjnelson 546cdf0c1d5Smjnelson try: 547605a716eSRichard Lowe for name, path in files: 548605a716eSRichard Lowe try: 549605a716eSRichard Lowe tar.add(path, name) 550cdf0c1d5Smjnelson except (EnvironmentError, tarfile.TarError), e: 551cdf0c1d5Smjnelson # 552cdf0c1d5Smjnelson # tarfile.TarError doesn't include the tar member or file 553cdf0c1d5Smjnelson # in question, so we have to do so ourselves. 554cdf0c1d5Smjnelson # 555cdf0c1d5Smjnelson if isinstance(e, tarfile.TarError): 556605a716eSRichard Lowe errstr = "%s: %s" % (name, e) 557cdf0c1d5Smjnelson else: 55887039217SRichard Lowe errstr = str(e) 559cdf0c1d5Smjnelson 560cdf0c1d5Smjnelson raise util.Abort("couldn't backup metadata to %s:\n" 561605a716eSRichard Lowe " %s" % (tarpath, errstr)) 562cdf0c1d5Smjnelson finally: 563cdf0c1d5Smjnelson tar.close() 564cdf0c1d5Smjnelson 565cdf0c1d5Smjnelson def old_restore(self): 566cdf0c1d5Smjnelson '''Restore workspace metadata from an pre-tar backup''' 567cdf0c1d5Smjnelson 568cdf0c1d5Smjnelson for fname in self.files: 569605a716eSRichard Lowe if self.bu.exists(fname): 570cdf0c1d5Smjnelson bfile = self.bu.backupfile(fname) 571cdf0c1d5Smjnelson wfile = self.ws.repo.join(fname) 572cdf0c1d5Smjnelson 573cdf0c1d5Smjnelson try: 574cdf0c1d5Smjnelson shutil.copy2(bfile, wfile) 575cdf0c1d5Smjnelson except EnvironmentError, e: 576cdf0c1d5Smjnelson raise util.Abort("couldn't restore metadata from %s:\n" 577cdf0c1d5Smjnelson " %s" % (bfile, e)) 578cdf0c1d5Smjnelson 579cdf0c1d5Smjnelson def tar_restore(self): 580cdf0c1d5Smjnelson '''Restore workspace metadata (from a tar-style backup)''' 581cdf0c1d5Smjnelson 582605a716eSRichard Lowe if not self.bu.exists('metadata.tar.gz'): 583605a716eSRichard Lowe return 584605a716eSRichard Lowe 585605a716eSRichard Lowe tarpath = self.bu.backupfile('metadata.tar.gz') 586cdf0c1d5Smjnelson 587cdf0c1d5Smjnelson try: 588605a716eSRichard Lowe tar = CdmTarFile.gzopen(tarpath) 589cdf0c1d5Smjnelson except (EnvironmentError, tarfile.TarError), e: 590605a716eSRichard Lowe raise util.Abort("couldn't open %s: %s" % (tarpath, e)) 591cdf0c1d5Smjnelson 592cdf0c1d5Smjnelson try: 593cdf0c1d5Smjnelson for elt in tar: 594605a716eSRichard Lowe try: 595cdf0c1d5Smjnelson tar.extract(elt, path=self.ws.repo.path) 596cdf0c1d5Smjnelson except (EnvironmentError, tarfile.TarError), e: 597cdf0c1d5Smjnelson # Make sure the member name is in the exception message. 598cdf0c1d5Smjnelson if isinstance(e, tarfile.TarError): 59987039217SRichard Lowe errstr = "%s: %s" % (elt.name, e) 600cdf0c1d5Smjnelson else: 60187039217SRichard Lowe errstr = str(e) 602cdf0c1d5Smjnelson 603cdf0c1d5Smjnelson raise util.Abort("couldn't restore metadata from %s:\n" 604cdf0c1d5Smjnelson " %s" % 605605a716eSRichard Lowe (tarpath, errstr)) 606cdf0c1d5Smjnelson finally: 607cdf0c1d5Smjnelson if tar and not tar.closed: 608cdf0c1d5Smjnelson tar.close() 609cdf0c1d5Smjnelson 610cdf0c1d5Smjnelson def restore(self): 611cdf0c1d5Smjnelson '''Restore workspace metadata''' 612cdf0c1d5Smjnelson 613605a716eSRichard Lowe if self.bu.exists('hgrc'): 614cdf0c1d5Smjnelson self.old_restore() 615cdf0c1d5Smjnelson else: 616cdf0c1d5Smjnelson self.tar_restore() 617cdf0c1d5Smjnelson 618605a716eSRichard Lowe def _walk(self): 619605a716eSRichard Lowe '''Yield the repo-relative path to each file we operate on, 620605a716eSRichard Lowe including each file within any affected directory''' 621605a716eSRichard Lowe 622605a716eSRichard Lowe for elt in self.files: 623605a716eSRichard Lowe path = self.ws.repo.join(elt) 624605a716eSRichard Lowe 625605a716eSRichard Lowe if not os.path.exists(path): 626605a716eSRichard Lowe continue 627605a716eSRichard Lowe 628605a716eSRichard Lowe if os.path.isdir(path): 629605a716eSRichard Lowe for root, dirs, files in os.walk(path, topdown=True): 630605a716eSRichard Lowe yield root 631605a716eSRichard Lowe 632605a716eSRichard Lowe for f in files: 633605a716eSRichard Lowe yield os.path.join(root, f) 634605a716eSRichard Lowe else: 635605a716eSRichard Lowe yield path 636605a716eSRichard Lowe 637cdf0c1d5Smjnelson def need_backup(self): 638cdf0c1d5Smjnelson '''Compare backed up workspace metadata to workspace''' 639cdf0c1d5Smjnelson 640605a716eSRichard Lowe def strip_trailing_pathsep(pathname): 641605a716eSRichard Lowe '''Remove a possible trailing path separator from PATHNAME''' 642605a716eSRichard Lowe return pathname.endswith('/') and pathname[:-1] or pathname 643605a716eSRichard Lowe 644605a716eSRichard Lowe if self.bu.exists('metadata.tar.gz'): 645605a716eSRichard Lowe tarpath = self.bu.backupfile('metadata.tar.gz') 646cdf0c1d5Smjnelson try: 647605a716eSRichard Lowe tar = CdmTarFile.gzopen(tarpath) 648cdf0c1d5Smjnelson except (EnvironmentError, tarfile.TarError), e: 649cdf0c1d5Smjnelson raise util.Abort("couldn't open metadata tarball: %s\n" 650605a716eSRichard Lowe " %s" % (tarpath, e)) 651cdf0c1d5Smjnelson 652605a716eSRichard Lowe if not tar.members_match_fs(self.ws.repo.path): 653605a716eSRichard Lowe tar.close() 654cdf0c1d5Smjnelson return True 655cdf0c1d5Smjnelson 656605a716eSRichard Lowe tarnames = map(strip_trailing_pathsep, tar.getnames()) 657cdf0c1d5Smjnelson tar.close() 658cdf0c1d5Smjnelson else: 659cdf0c1d5Smjnelson tarnames = [] 660cdf0c1d5Smjnelson 661605a716eSRichard Lowe repopath = self.ws.repo.path 662605a716eSRichard Lowe if not repopath.endswith('/'): 663605a716eSRichard Lowe repopath += '/' 664cdf0c1d5Smjnelson 665605a716eSRichard Lowe for path in self._walk(): 666605a716eSRichard Lowe if path.replace(repopath, '', 1) not in tarnames: 667cdf0c1d5Smjnelson return True 668cdf0c1d5Smjnelson 669cdf0c1d5Smjnelson return False 670cdf0c1d5Smjnelson 671cdf0c1d5Smjnelson def cleanup(self): 672cdf0c1d5Smjnelson '''Remove backed up workspace metadata''' 673605a716eSRichard Lowe self.bu.unlink('metadata.tar.gz') 674605a716eSRichard Lowe 675605a716eSRichard Lowe 676605a716eSRichard Loweclass CdmClearBackup(object): 677605a716eSRichard Lowe '''A backup (in tar format) of complete source files from every 678605a716eSRichard Lowe workspace head. 679605a716eSRichard Lowe 680605a716eSRichard Lowe Paths in the tarball are prefixed by the revision and node of the 681605a716eSRichard Lowe head, or "working" for the working directory. 682605a716eSRichard Lowe 683605a716eSRichard Lowe This is done purely for the benefit of the user, and as such takes 684605a716eSRichard Lowe no part in restore or need_backup checking, restore always 685605a716eSRichard Lowe succeeds, need_backup always returns False 686605a716eSRichard Lowe ''' 687605a716eSRichard Lowe 688605a716eSRichard Lowe def __init__(self, backup, ws): 689605a716eSRichard Lowe self.bu = backup 690605a716eSRichard Lowe self.ws = ws 691605a716eSRichard Lowe 692605a716eSRichard Lowe def _branch_pairs(self): 693605a716eSRichard Lowe '''Return a list of tuples (parenttip, localtip) for each 694605a716eSRichard Lowe outgoing head. If the working copy contains modified files, 695605a716eSRichard Lowe it is a head, and neither of its parents are. 696605a716eSRichard Lowe ''' 697605a716eSRichard Lowe 698605a716eSRichard Lowe parent = self.ws.parent() 699605a716eSRichard Lowe 700605a716eSRichard Lowe if parent: 701605a716eSRichard Lowe outgoing = self.ws.findoutgoing(parent) 702605a716eSRichard Lowe outnodes = set(self.ws.repo.changelog.nodesbetween(outgoing)[0]) 703605a716eSRichard Lowe 704605a716eSRichard Lowe heads = [self.ws.repo.changectx(n) for n in self.ws.repo.heads() 705605a716eSRichard Lowe if n in outnodes] 706605a716eSRichard Lowe else: 707605a716eSRichard Lowe heads = [] 708605a716eSRichard Lowe outnodes = [] 709605a716eSRichard Lowe 710605a716eSRichard Lowe wctx = self.ws.workingctx() 711605a716eSRichard Lowe if wctx.files(): # We only care about file changes. 712605a716eSRichard Lowe heads = filter(lambda x: x not in wctx.parents(), heads) + [wctx] 713605a716eSRichard Lowe 714605a716eSRichard Lowe pairs = [] 715605a716eSRichard Lowe for head in heads: 716605a716eSRichard Lowe if head.rev() is None: 717605a716eSRichard Lowe c = head.parents() 718605a716eSRichard Lowe else: 719605a716eSRichard Lowe c = [head] 720605a716eSRichard Lowe 721605a716eSRichard Lowe pairs.append((self.ws.parenttip(c, outnodes), head)) 722605a716eSRichard Lowe return pairs 723605a716eSRichard Lowe 724605a716eSRichard Lowe def backup(self): 725605a716eSRichard Lowe '''Save a clear copy of each source file modified between each 726605a716eSRichard Lowe head and that head's parenttip (see WorkSpace.parenttip). 727605a716eSRichard Lowe ''' 728605a716eSRichard Lowe 729605a716eSRichard Lowe tarpath = self.bu.backupfile('clear.tar.gz') 730605a716eSRichard Lowe branches = self._branch_pairs() 731605a716eSRichard Lowe 732605a716eSRichard Lowe if not branches: 733605a716eSRichard Lowe return 734605a716eSRichard Lowe 735605a716eSRichard Lowe try: 736605a716eSRichard Lowe tar = CdmTarFile.gzopen(tarpath, 'w') 737605a716eSRichard Lowe except (EnvironmentError, tarfile.TarError), e: 738605a716eSRichard Lowe raise util.Abort("Could not open %s for writing: %s" % 739605a716eSRichard Lowe (tarpath, e)) 740605a716eSRichard Lowe 741605a716eSRichard Lowe try: 742605a716eSRichard Lowe for parent, child in branches: 743605a716eSRichard Lowe tpath = child.node() and node.short(child.node()) or "working" 744605a716eSRichard Lowe 745605a716eSRichard Lowe for fname, change in self.ws.status(parent, child).iteritems(): 746605a716eSRichard Lowe if change not in ('added', 'modified'): 747605a716eSRichard Lowe continue 748605a716eSRichard Lowe 749605a716eSRichard Lowe try: 750605a716eSRichard Lowe tar.addfilectx(child.filectx(fname), 751605a716eSRichard Lowe os.path.join(tpath, fname)) 752605a716eSRichard Lowe except ValueError, e: 753605a716eSRichard Lowe crev = child.rev() 754605a716eSRichard Lowe if crev is None: 755605a716eSRichard Lowe crev = "working copy" 756605a716eSRichard Lowe raise util.Abort("Could not backup clear file %s " 757605a716eSRichard Lowe "from %s: %s\n" % (fname, crev, e)) 758605a716eSRichard Lowe finally: 759605a716eSRichard Lowe tar.close() 760605a716eSRichard Lowe 761605a716eSRichard Lowe def cleanup(self): 762605a716eSRichard Lowe '''Cleanup a failed Clear backup. 763605a716eSRichard Lowe 764605a716eSRichard Lowe Remove the clear tarball from the backup directory. 765605a716eSRichard Lowe ''' 766605a716eSRichard Lowe 767605a716eSRichard Lowe self.bu.unlink('clear.tar.gz') 768605a716eSRichard Lowe 769605a716eSRichard Lowe def restore(self): 770605a716eSRichard Lowe '''Clear backups are never restored, do nothing''' 771605a716eSRichard Lowe pass 772605a716eSRichard Lowe 773605a716eSRichard Lowe def need_backup(self): 774605a716eSRichard Lowe '''Clear backups are never compared, return False (no backup needed). 775605a716eSRichard Lowe 776605a716eSRichard Lowe Should a backup actually be needed, one of the other 777605a716eSRichard Lowe implementation classes would notice in any situation we would. 778605a716eSRichard Lowe ''' 779605a716eSRichard Lowe 780605a716eSRichard Lowe return False 781cdf0c1d5Smjnelson 782cdf0c1d5Smjnelson 783cdf0c1d5Smjnelsonclass CdmBackup(object): 784cdf0c1d5Smjnelson '''A backup of a given workspace''' 785cdf0c1d5Smjnelson 786cdf0c1d5Smjnelson def __init__(self, ui, ws, name): 787cdf0c1d5Smjnelson self.ws = ws 788cdf0c1d5Smjnelson self.ui = ui 789cdf0c1d5Smjnelson self.backupdir = self._find_backup_dir(name) 790cdf0c1d5Smjnelson 791cdf0c1d5Smjnelson # 792cdf0c1d5Smjnelson # The order of instances here controls the order the various operations 793cdf0c1d5Smjnelson # are run. 794cdf0c1d5Smjnelson # 795cdf0c1d5Smjnelson # There's some inherent dependence, in that on restore we need 796cdf0c1d5Smjnelson # to restore committed changes prior to uncommitted changes 797cdf0c1d5Smjnelson # (as the parent revision of any uncommitted changes is quite 798cdf0c1d5Smjnelson # likely to not exist until committed changes are restored). 799cdf0c1d5Smjnelson # Metadata restore can happen at any point, but happens last 800cdf0c1d5Smjnelson # as a matter of convention. 801cdf0c1d5Smjnelson # 802cdf0c1d5Smjnelson self.modules = [x(self, ws) for x in [CdmCommittedBackup, 803cdf0c1d5Smjnelson CdmUncommittedBackup, 804605a716eSRichard Lowe CdmClearBackup, 805cdf0c1d5Smjnelson CdmMetadataBackup]] 806cdf0c1d5Smjnelson 807cdf0c1d5Smjnelson if os.path.exists(os.path.join(self.backupdir, 'latest')): 808cdf0c1d5Smjnelson generation = os.readlink(os.path.join(self.backupdir, 'latest')) 809cdf0c1d5Smjnelson self.generation = int(os.path.split(generation)[1]) 810cdf0c1d5Smjnelson else: 811cdf0c1d5Smjnelson self.generation = 0 812cdf0c1d5Smjnelson 813cdf0c1d5Smjnelson def _find_backup_dir(self, name): 814cdf0c1d5Smjnelson '''Find the path to an appropriate backup directory based on NAME''' 815cdf0c1d5Smjnelson 816cdf0c1d5Smjnelson if os.path.isabs(name): 817cdf0c1d5Smjnelson return name 818cdf0c1d5Smjnelson 819cdf0c1d5Smjnelson if self.ui.config('cdm', 'backupdir'): 820cdf0c1d5Smjnelson backupbase = os.path.expanduser(self.ui.config('cdm', 'backupdir')) 821cdf0c1d5Smjnelson else: 822cdf0c1d5Smjnelson home = None 823cdf0c1d5Smjnelson 824cdf0c1d5Smjnelson try: 825cdf0c1d5Smjnelson home = os.getenv('HOME') or pwd.getpwuid(os.getuid()).pw_dir 826cdf0c1d5Smjnelson except KeyError: 827cdf0c1d5Smjnelson pass # Handled anyway 828cdf0c1d5Smjnelson 829cdf0c1d5Smjnelson if not home: 830cdf0c1d5Smjnelson raise util.Abort('Could not determine your HOME directory to ' 831cdf0c1d5Smjnelson 'find backup path') 832cdf0c1d5Smjnelson 833cdf0c1d5Smjnelson backupbase = os.path.join(home, 'cdm.backup') 834cdf0c1d5Smjnelson 835cdf0c1d5Smjnelson backupdir = os.path.join(backupbase, name) 836cdf0c1d5Smjnelson 837cdf0c1d5Smjnelson # If backupdir exists, it must be a directory. 838cdf0c1d5Smjnelson if (os.path.exists(backupdir) and not os.path.isdir(backupdir)): 839cdf0c1d5Smjnelson raise util.Abort('%s exists but is not a directory' % backupdir) 840cdf0c1d5Smjnelson 841cdf0c1d5Smjnelson return backupdir 842cdf0c1d5Smjnelson 843605a716eSRichard Lowe def _update_latest(self, gen): 844cdf0c1d5Smjnelson '''Update latest symlink to point to the current generation''' 845cdf0c1d5Smjnelson linkpath = os.path.join(self.backupdir, 'latest') 846cdf0c1d5Smjnelson 847cdf0c1d5Smjnelson if os.path.lexists(linkpath): 848cdf0c1d5Smjnelson os.unlink(linkpath) 849cdf0c1d5Smjnelson 850cdf0c1d5Smjnelson os.symlink(str(gen), linkpath) 851cdf0c1d5Smjnelson 852605a716eSRichard Lowe def _create_gen(self, gen): 853cdf0c1d5Smjnelson '''Create a new backup generation''' 854cdf0c1d5Smjnelson try: 855cdf0c1d5Smjnelson os.makedirs(os.path.join(self.backupdir, str(gen))) 856605a716eSRichard Lowe self._update_latest(gen) 857cdf0c1d5Smjnelson except EnvironmentError, e: 858cdf0c1d5Smjnelson raise util.Abort("Couldn't create backup generation %s: %s" % 859cdf0c1d5Smjnelson (os.path.join(self.backupdir, str(gen)), e)) 860cdf0c1d5Smjnelson 861605a716eSRichard Lowe def backupfile(self, path): 862605a716eSRichard Lowe '''return full path to backup file FILE at GEN''' 863605a716eSRichard Lowe return os.path.join(self.backupdir, str(self.generation), path) 864605a716eSRichard Lowe 865605a716eSRichard Lowe def unlink(self, name): 866605a716eSRichard Lowe '''Unlink the specified path from the backup directory. 867605a716eSRichard Lowe A no-op if the path does not exist. 868605a716eSRichard Lowe ''' 869605a716eSRichard Lowe 870605a716eSRichard Lowe fpath = self.backupfile(name) 871605a716eSRichard Lowe if os.path.exists(fpath): 872605a716eSRichard Lowe os.unlink(fpath) 873605a716eSRichard Lowe 874605a716eSRichard Lowe def open(self, name, mode='r'): 875605a716eSRichard Lowe '''Open the specified file in the backup directory''' 876605a716eSRichard Lowe return open(self.backupfile(name), mode) 877605a716eSRichard Lowe 878605a716eSRichard Lowe def exists(self, name): 879605a716eSRichard Lowe '''Return boolean indicating wether a given file exists in the 880605a716eSRichard Lowe backup directory.''' 881605a716eSRichard Lowe return os.path.exists(self.backupfile(name)) 882605a716eSRichard Lowe 883cdf0c1d5Smjnelson def need_backup(self): 884cdf0c1d5Smjnelson '''Compare backed up changes to workspace''' 885cdf0c1d5Smjnelson # 886cdf0c1d5Smjnelson # If there's no current backup generation, or the last backup was 887cdf0c1d5Smjnelson # invalid (lacking the dirstate file), we need a backup regardless 888cdf0c1d5Smjnelson # of anything else. 889cdf0c1d5Smjnelson # 890605a716eSRichard Lowe if not self.generation or not self.exists('dirstate'): 891cdf0c1d5Smjnelson return True 892cdf0c1d5Smjnelson 893cdf0c1d5Smjnelson for x in self.modules: 894cdf0c1d5Smjnelson if x.need_backup(): 895cdf0c1d5Smjnelson return True 896cdf0c1d5Smjnelson 897cdf0c1d5Smjnelson return False 898cdf0c1d5Smjnelson 899cdf0c1d5Smjnelson def backup(self): 900c959a081SRichard Lowe '''Take a backup of the current workspace 901c959a081SRichard Lowe 902c959a081SRichard Lowe Calling code is expected to hold both the working copy lock 903c959a081SRichard Lowe and repository lock.''' 904cdf0c1d5Smjnelson 905cdf0c1d5Smjnelson if not os.path.exists(self.backupdir): 906cdf0c1d5Smjnelson try: 907cdf0c1d5Smjnelson os.makedirs(self.backupdir) 908cdf0c1d5Smjnelson except EnvironmentError, e: 909cdf0c1d5Smjnelson raise util.Abort('Could not create backup directory %s: %s' % 910cdf0c1d5Smjnelson (self.backupdir, e)) 911cdf0c1d5Smjnelson 912cdf0c1d5Smjnelson self.generation += 1 913605a716eSRichard Lowe self._create_gen(self.generation) 914cdf0c1d5Smjnelson 915cdf0c1d5Smjnelson try: 916cdf0c1d5Smjnelson for x in self.modules: 917cdf0c1d5Smjnelson x.backup() 918cdf0c1d5Smjnelson except Exception, e: 919cdf0c1d5Smjnelson if isinstance(e, KeyboardInterrupt): 920cdf0c1d5Smjnelson self.ws.ui.warn("Interrupted\n") 921cdf0c1d5Smjnelson else: 922cdf0c1d5Smjnelson self.ws.ui.warn("Error: %s\n" % e) 923c959a081SRichard Lowe show_traceback = self.ws.ui.configbool('ui', 'traceback', 924c959a081SRichard Lowe False) 925cdf0c1d5Smjnelson 926cdf0c1d5Smjnelson # 927cdf0c1d5Smjnelson # If it's not a 'normal' error, we want to print a stack 928cdf0c1d5Smjnelson # trace now in case the attempt to remove the partial 929cdf0c1d5Smjnelson # backup also fails, and raises a second exception. 930cdf0c1d5Smjnelson # 931cdf0c1d5Smjnelson if (not isinstance(e, (EnvironmentError, util.Abort)) 932c959a081SRichard Lowe or show_traceback): 933cdf0c1d5Smjnelson traceback.print_exc() 934cdf0c1d5Smjnelson 935cdf0c1d5Smjnelson for x in self.modules: 936cdf0c1d5Smjnelson x.cleanup() 937cdf0c1d5Smjnelson 938cdf0c1d5Smjnelson os.rmdir(os.path.join(self.backupdir, str(self.generation))) 939cdf0c1d5Smjnelson self.generation -= 1 940cdf0c1d5Smjnelson 941cdf0c1d5Smjnelson if self.generation != 0: 942605a716eSRichard Lowe self._update_latest(self.generation) 943cdf0c1d5Smjnelson else: 944cdf0c1d5Smjnelson os.unlink(os.path.join(self.backupdir, 'latest')) 945cdf0c1d5Smjnelson 946cdf0c1d5Smjnelson raise util.Abort('Backup failed') 947cdf0c1d5Smjnelson 948cdf0c1d5Smjnelson def restore(self, gen=None): 949cdf0c1d5Smjnelson '''Restore workspace from backup 950cdf0c1d5Smjnelson 951cdf0c1d5Smjnelson Restores from backup generation GEN (defaulting to the latest) 952c959a081SRichard Lowe into workspace WS. 953cdf0c1d5Smjnelson 954c959a081SRichard Lowe Calling code is expected to hold both the working copy lock 955c959a081SRichard Lowe and repository lock of the destination workspace.''' 956cdf0c1d5Smjnelson 957cdf0c1d5Smjnelson if not os.path.exists(self.backupdir): 958cdf0c1d5Smjnelson raise util.Abort('Backup directory does not exist: %s' % 959cdf0c1d5Smjnelson (self.backupdir)) 960cdf0c1d5Smjnelson 961cdf0c1d5Smjnelson if gen: 962cdf0c1d5Smjnelson if not os.path.exists(os.path.join(self.backupdir, str(gen))): 963cdf0c1d5Smjnelson raise util.Abort('Backup generation does not exist: %s' % 964cdf0c1d5Smjnelson (os.path.join(self.backupdir, str(gen)))) 965cdf0c1d5Smjnelson self.generation = int(gen) 966cdf0c1d5Smjnelson 967605a716eSRichard Lowe if not self.generation: # This is OK, 0 is not a valid generation 968cdf0c1d5Smjnelson raise util.Abort('Backup has no generations: %s' % self.backupdir) 969cdf0c1d5Smjnelson 970605a716eSRichard Lowe if not self.exists('dirstate'): 971cdf0c1d5Smjnelson raise util.Abort('Backup %s/%s is incomplete (dirstate missing)' % 972cdf0c1d5Smjnelson (self.backupdir, self.generation)) 973cdf0c1d5Smjnelson 974cdf0c1d5Smjnelson try: 975cdf0c1d5Smjnelson for x in self.modules: 976cdf0c1d5Smjnelson x.restore() 977cdf0c1d5Smjnelson except util.Abort, e: 978cdf0c1d5Smjnelson raise util.Abort('Error restoring workspace:\n' 979cdf0c1d5Smjnelson '%s\n' 98012203c71SRichard Lowe 'Workspace may be partially restored' % e) 981