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*87039217SRichard Lowe# Copyright 2008, 2010, Richard Lowe 21*87039217SRichard Lowe# 22cdf0c1d5Smjnelson 23cdf0c1d5Smjnelson''' 24cdf0c1d5SmjnelsonWorkspace backup 25cdf0c1d5Smjnelson 26cdf0c1d5SmjnelsonBackup format is: 27cdf0c1d5Smjnelson backupdir/ 28cdf0c1d5Smjnelson wsname/ 29cdf0c1d5Smjnelson generation#/ 30cdf0c1d5Smjnelson dirstate (handled by CdmUncommittedBackup) 3112203c71SRichard Lowe File containing dirstate nodeid (the changeset we need 3212203c71SRichard Lowe to update the workspace to after applying the bundle). 3312203c71SRichard Lowe This is the node to which the working copy changes 3412203c71SRichard Lowe (see 'diff', below) will be applied if applicable. 35cdf0c1d5Smjnelson 36cdf0c1d5Smjnelson bundle (handled by CdmCommittedBackup) 37cdf0c1d5Smjnelson An Hg bundle containing outgoing committed changes. 38cdf0c1d5Smjnelson 39cdf0c1d5Smjnelson nodes (handled by CdmCommittedBackup) 40cdf0c1d5Smjnelson A text file listing the full (hex) nodeid of all nodes in 41cdf0c1d5Smjnelson bundle, used by need_backup. 42cdf0c1d5Smjnelson 43cdf0c1d5Smjnelson diff (handled by CdmUncommittedBackup) 44cdf0c1d5Smjnelson A Git-formatted diff containing uncommitted changes. 45cdf0c1d5Smjnelson 46cdf0c1d5Smjnelson renames (handled by CdmUncommittedBackup) 47cdf0c1d5Smjnelson A list of renames in the working copy that have to be 48cdf0c1d5Smjnelson applied manually, rather than by the diff. 49cdf0c1d5Smjnelson 50cdf0c1d5Smjnelson metadata.tar.gz (handled by CdmMetadataBackup) 51cdf0c1d5Smjnelson $CODEMGR_WS/.hg/hgrc 52cdf0c1d5Smjnelson $CODEMGR_WS/.hg/localtags 53cdf0c1d5Smjnelson $CODEMGR_WS/.hg/patches (Mq data) 54cdf0c1d5Smjnelson 55cdf0c1d5Smjnelson latest -> generation# 56cdf0c1d5Smjnelson Newest backup generation. 57cdf0c1d5Smjnelson 58cdf0c1d5SmjnelsonAll files in a given backup generation, with the exception of 59cdf0c1d5Smjnelsondirstate, are optional. 60cdf0c1d5Smjnelson''' 61cdf0c1d5Smjnelson 62c959a081SRichard Loweimport os, pwd, shutil, tarfile, time, traceback 63*87039217SRichard Lowefrom mercurial import changegroup, error, node, patch, util 64cdf0c1d5Smjnelson 65cdf0c1d5Smjnelson 6612203c71SRichard Loweclass CdmNodeMissing(util.Abort): 6712203c71SRichard Lowe '''a required node is not present in the destination workspace. 6812203c71SRichard Lowe 6912203c71SRichard Lowe This may occur both in the case where the bundle contains a 7012203c71SRichard Lowe changeset which is a child of a node not present in the 7112203c71SRichard Lowe destination workspace (because the destination workspace is not as 7212203c71SRichard Lowe up-to-date as the source), or because the source and destination 7312203c71SRichard Lowe workspace are not related. 7412203c71SRichard Lowe 7512203c71SRichard Lowe It may also happen in cases where the uncommitted changes need to 7612203c71SRichard Lowe be applied onto a node that the workspace does not possess even 7712203c71SRichard Lowe after application of the bundle (on a branch not present 7812203c71SRichard Lowe in the bundle or destination workspace, for instance)''' 7912203c71SRichard Lowe 8012203c71SRichard Lowe def __init__(self, msg, name): 8112203c71SRichard Lowe # 8212203c71SRichard Lowe # If e.name is a string 20 characters long, it is 8312203c71SRichard Lowe # assumed to be a node. (Mercurial makes this 8412203c71SRichard Lowe # same assumption, when creating a LookupError) 8512203c71SRichard Lowe # 8612203c71SRichard Lowe if isinstance(name, str) and len(name) == 20: 8712203c71SRichard Lowe n = node.short(name) 8812203c71SRichard Lowe else: 8912203c71SRichard Lowe n = name 9012203c71SRichard Lowe 9112203c71SRichard Lowe util.Abort.__init__(self, "%s: changeset '%s' is missing\n" 9212203c71SRichard Lowe "Your workspace is either not " 9312203c71SRichard Lowe "sufficiently up to date,\n" 9412203c71SRichard Lowe "or is unrelated to the workspace from " 9512203c71SRichard Lowe "which the backup was taken.\n" % (msg, n)) 9612203c71SRichard Lowe 9712203c71SRichard Lowe 98cdf0c1d5Smjnelsonclass CdmCommittedBackup(object): 99cdf0c1d5Smjnelson '''Backup of committed changes''' 100cdf0c1d5Smjnelson 101cdf0c1d5Smjnelson def __init__(self, backup, ws): 102cdf0c1d5Smjnelson self.ws = ws 103cdf0c1d5Smjnelson self.bu = backup 104cdf0c1d5Smjnelson self.files = ('bundle', 'nodes') 105cdf0c1d5Smjnelson 106cdf0c1d5Smjnelson def _outgoing_nodes(self, parent): 107cdf0c1d5Smjnelson '''Return a list of all outgoing nodes in hex format''' 108cdf0c1d5Smjnelson 109cdf0c1d5Smjnelson if parent: 110cdf0c1d5Smjnelson outgoing = self.ws.findoutgoing(parent) 111cdf0c1d5Smjnelson nodes = self.ws.repo.changelog.nodesbetween(outgoing)[0] 112cdf0c1d5Smjnelson return map(node.hex, nodes) 113cdf0c1d5Smjnelson else: 114cdf0c1d5Smjnelson return [] 115cdf0c1d5Smjnelson 116cdf0c1d5Smjnelson def backup(self): 117cdf0c1d5Smjnelson '''Backup committed changes''' 118cdf0c1d5Smjnelson parent = self.ws.parent() 119cdf0c1d5Smjnelson 120cdf0c1d5Smjnelson if not parent: 121cdf0c1d5Smjnelson self.ws.ui.warn('Workspace has no parent, committed changes will ' 122cdf0c1d5Smjnelson 'not be backed up\n') 123cdf0c1d5Smjnelson return 124cdf0c1d5Smjnelson 125cdf0c1d5Smjnelson out = self.ws.findoutgoing(parent) 126cdf0c1d5Smjnelson if not out: 127cdf0c1d5Smjnelson return 128cdf0c1d5Smjnelson 129cdf0c1d5Smjnelson cg = self.ws.repo.changegroup(out, 'bundle') 130cdf0c1d5Smjnelson changegroup.writebundle(cg, self.bu.backupfile('bundle'), 'HG10BZ') 131cdf0c1d5Smjnelson 132cdf0c1d5Smjnelson outnodes = self._outgoing_nodes(parent) 133cdf0c1d5Smjnelson if outnodes: 134cdf0c1d5Smjnelson fp = None 135cdf0c1d5Smjnelson try: 136cdf0c1d5Smjnelson try: 137cdf0c1d5Smjnelson fp = open(self.bu.backupfile('nodes'), 'w') 138cdf0c1d5Smjnelson fp.write('%s\n' % '\n'.join(outnodes)) 139cdf0c1d5Smjnelson except EnvironmentError, e: 140cdf0c1d5Smjnelson raise util.Abort("couldn't store outgoing nodes: %s" % e) 141cdf0c1d5Smjnelson finally: 142cdf0c1d5Smjnelson if fp and not fp.closed: 143cdf0c1d5Smjnelson fp.close() 144cdf0c1d5Smjnelson 145cdf0c1d5Smjnelson def restore(self): 146cdf0c1d5Smjnelson '''Restore committed changes from backup''' 147cdf0c1d5Smjnelson bfile = self.bu.backupfile('bundle') 148cdf0c1d5Smjnelson 149cdf0c1d5Smjnelson if os.path.exists(bfile): 150cdf0c1d5Smjnelson f = None 151cdf0c1d5Smjnelson try: 152cdf0c1d5Smjnelson try: 153cdf0c1d5Smjnelson f = open(bfile, 'r') 154cdf0c1d5Smjnelson bundle = changegroup.readbundle(f, bfile) 155cdf0c1d5Smjnelson self.ws.repo.addchangegroup(bundle, 'strip', 156cdf0c1d5Smjnelson 'bundle:%s' % bfile) 157cdf0c1d5Smjnelson except EnvironmentError, e: 158cdf0c1d5Smjnelson raise util.Abort("couldn't restore committed changes: %s\n" 159cdf0c1d5Smjnelson " %s" % (bfile, e)) 160*87039217SRichard Lowe except error.LookupError, e: 16112203c71SRichard Lowe raise CdmNodeMissing("couldn't restore committed changes", 16212203c71SRichard Lowe e.name) 163cdf0c1d5Smjnelson finally: 164cdf0c1d5Smjnelson if f and not f.closed: 165cdf0c1d5Smjnelson f.close() 166cdf0c1d5Smjnelson 167cdf0c1d5Smjnelson def need_backup(self): 168cdf0c1d5Smjnelson '''Compare backup of committed changes to workspace''' 169cdf0c1d5Smjnelson 170cdf0c1d5Smjnelson if os.path.exists(self.bu.backupfile('nodes')): 171cdf0c1d5Smjnelson f = None 172cdf0c1d5Smjnelson try: 173cdf0c1d5Smjnelson try: 174cdf0c1d5Smjnelson f = open(self.bu.backupfile('nodes')) 175cdf0c1d5Smjnelson bnodes = set([line.rstrip('\r\n') 176cdf0c1d5Smjnelson for line in f.readlines()]) 177cdf0c1d5Smjnelson f.close() 178cdf0c1d5Smjnelson except EnvironmentError, e: 179cdf0c1d5Smjnelson raise util.Abort("couldn't open backup node list: %s" % e) 180cdf0c1d5Smjnelson finally: 181cdf0c1d5Smjnelson if f and not f.closed: 182cdf0c1d5Smjnelson f.close() 183cdf0c1d5Smjnelson else: 184cdf0c1d5Smjnelson bnodes = set() 185cdf0c1d5Smjnelson 186cdf0c1d5Smjnelson outnodes = set(self._outgoing_nodes(self.ws.parent())) 187cdf0c1d5Smjnelson if outnodes != bnodes: 188cdf0c1d5Smjnelson return True 189cdf0c1d5Smjnelson 190cdf0c1d5Smjnelson return False 191cdf0c1d5Smjnelson 192cdf0c1d5Smjnelson def cleanup(self): 193cdf0c1d5Smjnelson '''Remove backed up committed changes''' 194cdf0c1d5Smjnelson 195cdf0c1d5Smjnelson for fname in self.files: 196cdf0c1d5Smjnelson if os.path.exists(self.bu.backupfile(fname)): 197cdf0c1d5Smjnelson os.unlink(self.bu.backupfile(fname)) 198cdf0c1d5Smjnelson 199cdf0c1d5Smjnelson 200cdf0c1d5Smjnelsonclass CdmUncommittedBackup(object): 201cdf0c1d5Smjnelson '''Backup of uncommitted changes''' 202cdf0c1d5Smjnelson 203cdf0c1d5Smjnelson def __init__(self, backup, ws): 204cdf0c1d5Smjnelson self.ws = ws 205cdf0c1d5Smjnelson self.bu = backup 206cdf0c1d5Smjnelson 207cdf0c1d5Smjnelson def _clobbering_renames(self): 208cdf0c1d5Smjnelson '''Return a list of pairs of files representing renames/copies 209cdf0c1d5Smjnelson that clobber already versioned files. [(oldname newname)...]''' 210cdf0c1d5Smjnelson 211cdf0c1d5Smjnelson # 212cdf0c1d5Smjnelson # Note that this doesn't handle uncommitted merges 213cdf0c1d5Smjnelson # as CdmUncommittedBackup itself doesn't. 214cdf0c1d5Smjnelson # 2152b5878deSRich Lowe wctx = self.ws.workingctx() 216cdf0c1d5Smjnelson parent = wctx.parents()[0] 217cdf0c1d5Smjnelson 218cdf0c1d5Smjnelson ret = [] 219cdf0c1d5Smjnelson for fname in wctx.added() + wctx.modified(): 220cdf0c1d5Smjnelson rn = wctx.filectx(fname).renamed() 221cdf0c1d5Smjnelson if rn and fname in parent: 222cdf0c1d5Smjnelson ret.append((rn[0], fname)) 223cdf0c1d5Smjnelson return ret 224cdf0c1d5Smjnelson 225cdf0c1d5Smjnelson def backup(self): 226cdf0c1d5Smjnelson '''Backup uncommitted changes''' 227cdf0c1d5Smjnelson 228cdf0c1d5Smjnelson if self.ws.merged(): 229cdf0c1d5Smjnelson raise util.Abort("Unable to backup an uncommitted merge.\n" 230cdf0c1d5Smjnelson "Please complete your merge and commit") 231cdf0c1d5Smjnelson 2322b5878deSRich Lowe dirstate = node.hex(self.ws.workingctx().parents()[0].node()) 233cdf0c1d5Smjnelson 234cdf0c1d5Smjnelson fp = None 235cdf0c1d5Smjnelson try: 236cdf0c1d5Smjnelson try: 237cdf0c1d5Smjnelson fp = open(self.bu.backupfile('dirstate'), 'w') 238cdf0c1d5Smjnelson fp.write(dirstate + '\n') 239cdf0c1d5Smjnelson except EnvironmentError, e: 240cdf0c1d5Smjnelson raise util.Abort("couldn't save working copy parent: %s" % e) 241cdf0c1d5Smjnelson finally: 242cdf0c1d5Smjnelson if fp and not fp.closed: 243cdf0c1d5Smjnelson fp.close() 244cdf0c1d5Smjnelson 245cdf0c1d5Smjnelson try: 246cdf0c1d5Smjnelson try: 247cdf0c1d5Smjnelson fp = open(self.bu.backupfile('renames'), 'w') 248cdf0c1d5Smjnelson for cons in self._clobbering_renames(): 249cdf0c1d5Smjnelson fp.write("%s %s\n" % cons) 250cdf0c1d5Smjnelson except EnvironmentError, e: 251cdf0c1d5Smjnelson raise util.Abort("couldn't save clobbering copies: %s" % e) 252cdf0c1d5Smjnelson finally: 253cdf0c1d5Smjnelson if fp and not fp.closed: 254cdf0c1d5Smjnelson fp.close() 255cdf0c1d5Smjnelson 256cdf0c1d5Smjnelson try: 257cdf0c1d5Smjnelson try: 258cdf0c1d5Smjnelson fp = open(self.bu.backupfile('diff'), 'w') 2592b5878deSRich Lowe opts = patch.diffopts(self.ws.ui, opts={'git': True}) 2602b5878deSRich Lowe fp.write(self.ws.diff(opts=opts)) 261cdf0c1d5Smjnelson except EnvironmentError, e: 262cdf0c1d5Smjnelson raise util.Abort("couldn't save working copy diff: %s" % e) 263cdf0c1d5Smjnelson finally: 264cdf0c1d5Smjnelson if fp and not fp.closed: 265cdf0c1d5Smjnelson fp.close() 266cdf0c1d5Smjnelson 267cdf0c1d5Smjnelson def _dirstate(self): 2682b5878deSRich Lowe '''Return the desired working copy node from the backup''' 269cdf0c1d5Smjnelson fp = None 270cdf0c1d5Smjnelson try: 271cdf0c1d5Smjnelson try: 272cdf0c1d5Smjnelson fp = open(self.bu.backupfile('dirstate')) 273cdf0c1d5Smjnelson dirstate = fp.readline().strip() 274cdf0c1d5Smjnelson return dirstate 275cdf0c1d5Smjnelson except EnvironmentError, e: 276cdf0c1d5Smjnelson raise util.Abort("couldn't read saved parent: %s" % e) 277cdf0c1d5Smjnelson finally: 278cdf0c1d5Smjnelson if fp and not fp.closed: 279cdf0c1d5Smjnelson fp.close() 280cdf0c1d5Smjnelson 281cdf0c1d5Smjnelson def restore(self): 282cdf0c1d5Smjnelson '''Restore uncommitted changes''' 283cdf0c1d5Smjnelson diff = self.bu.backupfile('diff') 284cdf0c1d5Smjnelson dirstate = self._dirstate() 285cdf0c1d5Smjnelson 2862b5878deSRich Lowe # 2872b5878deSRich Lowe # Check that the patch's parent changeset exists. 2882b5878deSRich Lowe # 289cdf0c1d5Smjnelson try: 2902b5878deSRich Lowe n = node.bin(dirstate) 2912b5878deSRich Lowe self.ws.repo.changelog.lookup(n) 292*87039217SRichard Lowe except error.LookupError, e: 29312203c71SRichard Lowe raise CdmNodeMissing("couldn't restore uncommitted changes", 29412203c71SRichard Lowe e.name) 2952b5878deSRich Lowe 2962b5878deSRich Lowe try: 2972b5878deSRich Lowe self.ws.clean(rev=dirstate) 298cdf0c1d5Smjnelson except util.Abort, e: 299cdf0c1d5Smjnelson raise util.Abort("couldn't update to saved node: %s" % e) 300cdf0c1d5Smjnelson 301cdf0c1d5Smjnelson if not os.path.exists(diff): 302cdf0c1d5Smjnelson return 303cdf0c1d5Smjnelson 304cdf0c1d5Smjnelson # 305cdf0c1d5Smjnelson # There's a race here whereby if the patch (or part thereof) 306cdf0c1d5Smjnelson # is applied within the same second as the clean above (such 307cdf0c1d5Smjnelson # that mtime doesn't change) and if the size of that file 308cdf0c1d5Smjnelson # does not change, Hg may not see the change. 309cdf0c1d5Smjnelson # 310cdf0c1d5Smjnelson # We sleep a full second to avoid this, as sleeping merely 311cdf0c1d5Smjnelson # until the next second begins would require very close clock 312cdf0c1d5Smjnelson # synchronization on network filesystems. 313cdf0c1d5Smjnelson # 314cdf0c1d5Smjnelson time.sleep(1) 315cdf0c1d5Smjnelson 316cdf0c1d5Smjnelson files = {} 317cdf0c1d5Smjnelson try: 318cdf0c1d5Smjnelson try: 319cdf0c1d5Smjnelson fuzz = patch.patch(diff, self.ws.ui, strip=1, 320cdf0c1d5Smjnelson cwd=self.ws.repo.root, files=files) 321cdf0c1d5Smjnelson if fuzz: 322cdf0c1d5Smjnelson raise util.Abort('working copy diff applied with fuzz') 323cdf0c1d5Smjnelson except Exception, e: 324cdf0c1d5Smjnelson raise util.Abort("couldn't apply working copy diff: %s\n" 325cdf0c1d5Smjnelson " %s" % (diff, e)) 326cdf0c1d5Smjnelson finally: 327cdf0c1d5Smjnelson patch.updatedir(self.ws.ui, self.ws.repo, files) 328cdf0c1d5Smjnelson 329cdf0c1d5Smjnelson if not os.path.exists(self.bu.backupfile('renames')): 330cdf0c1d5Smjnelson return 331cdf0c1d5Smjnelson 332cdf0c1d5Smjnelson # 333cdf0c1d5Smjnelson # We need to re-apply name changes where the new name 334cdf0c1d5Smjnelson # (rename/copy destination) is an already versioned file, as 335cdf0c1d5Smjnelson # Hg would otherwise ignore them. 336cdf0c1d5Smjnelson # 337cdf0c1d5Smjnelson try: 338cdf0c1d5Smjnelson fp = open(self.bu.backupfile('renames')) 339cdf0c1d5Smjnelson for line in fp: 340cdf0c1d5Smjnelson source, dest = line.strip().split() 341*87039217SRichard Lowe self.ws.copy(source, dest) 342cdf0c1d5Smjnelson except EnvironmentError, e: 343cdf0c1d5Smjnelson raise util.Abort('unable to open renames file: %s' % e) 344cdf0c1d5Smjnelson except ValueError: 345cdf0c1d5Smjnelson raise util.Abort('corrupt renames file: %s' % 346cdf0c1d5Smjnelson self.bu.backupfile('renames')) 347cdf0c1d5Smjnelson 348cdf0c1d5Smjnelson def need_backup(self): 349cdf0c1d5Smjnelson '''Compare backup of uncommitted changes to workspace''' 3502b5878deSRich Lowe cnode = self.ws.workingctx().parents()[0].node() 3512b5878deSRich Lowe if self._dirstate() != node.hex(cnode): 352cdf0c1d5Smjnelson return True 353cdf0c1d5Smjnelson 3542b5878deSRich Lowe opts = patch.diffopts(self.ws.ui, opts={'git': True}) 3552b5878deSRich Lowe curdiff = self.ws.diff(opts=opts) 3562b5878deSRich Lowe 357cdf0c1d5Smjnelson diff = self.bu.backupfile('diff') 358cdf0c1d5Smjnelson if os.path.exists(diff): 359cdf0c1d5Smjnelson try: 360cdf0c1d5Smjnelson try: 361cdf0c1d5Smjnelson fd = open(diff) 362cdf0c1d5Smjnelson backdiff = fd.read() 363cdf0c1d5Smjnelson except EnvironmentError, e: 364cdf0c1d5Smjnelson raise util.Abort("couldn't open backup diff %s\n" 365cdf0c1d5Smjnelson " %s" % (diff, e)) 366cdf0c1d5Smjnelson finally: 367cdf0c1d5Smjnelson if fd and not fd.closed: 368cdf0c1d5Smjnelson fd.close() 369cdf0c1d5Smjnelson else: 370cdf0c1d5Smjnelson backdiff = '' 371cdf0c1d5Smjnelson 3722b5878deSRich Lowe if backdiff != curdiff: 373cdf0c1d5Smjnelson return True 374cdf0c1d5Smjnelson 375cdf0c1d5Smjnelson 376cdf0c1d5Smjnelson currrenamed = self._clobbering_renames() 377cdf0c1d5Smjnelson bakrenamed = None 378cdf0c1d5Smjnelson 379cdf0c1d5Smjnelson if os.path.exists(self.bu.backupfile('renames')): 380cdf0c1d5Smjnelson try: 381cdf0c1d5Smjnelson try: 382cdf0c1d5Smjnelson fd = open(self.bu.backupfile('renames')) 383cdf0c1d5Smjnelson bakrenamed = [line.strip().split(' ') for line in fd] 384cdf0c1d5Smjnelson except EnvironmentError, e: 385cdf0c1d5Smjnelson raise util.Abort("couldn't open renames file %s: %s\n" % 386cdf0c1d5Smjnelson (self.bu.backupfile('renames'), e)) 387cdf0c1d5Smjnelson finally: 388cdf0c1d5Smjnelson if fd and not fd.closed: 389cdf0c1d5Smjnelson fd.close() 390cdf0c1d5Smjnelson 391cdf0c1d5Smjnelson if currrenamed != bakrenamed: 392cdf0c1d5Smjnelson return True 393cdf0c1d5Smjnelson 394cdf0c1d5Smjnelson return False 395cdf0c1d5Smjnelson 396cdf0c1d5Smjnelson def cleanup(self): 397cdf0c1d5Smjnelson '''Remove backed up uncommitted changes''' 398cdf0c1d5Smjnelson for fname in ('dirstate', 'diff', 'renames'): 399cdf0c1d5Smjnelson if os.path.exists(self.bu.backupfile(fname)): 400cdf0c1d5Smjnelson os.unlink(self.bu.backupfile(fname)) 401cdf0c1d5Smjnelson 402cdf0c1d5Smjnelson 403cdf0c1d5Smjnelsonclass CdmMetadataBackup(object): 404cdf0c1d5Smjnelson '''Backup of workspace metadata''' 405cdf0c1d5Smjnelson 406cdf0c1d5Smjnelson def __init__(self, backup, ws): 407cdf0c1d5Smjnelson self.bu = backup 408cdf0c1d5Smjnelson self.ws = ws 4099a70fc3bSMark J. Nelson self.files = ('hgrc', 'localtags', 'patches', 'cdm') 410cdf0c1d5Smjnelson 411cdf0c1d5Smjnelson def backup(self): 412cdf0c1d5Smjnelson '''Backup workspace metadata''' 413cdf0c1d5Smjnelson 414cdf0c1d5Smjnelson tar = None 415cdf0c1d5Smjnelson 416cdf0c1d5Smjnelson try: 417cdf0c1d5Smjnelson try: 418cdf0c1d5Smjnelson tar = tarfile.open(self.bu.backupfile('metadata.tar.gz'), 419cdf0c1d5Smjnelson 'w:gz') 420cdf0c1d5Smjnelson tar.errorlevel = 2 421cdf0c1d5Smjnelson except (EnvironmentError, tarfile.TarError), e: 422cdf0c1d5Smjnelson raise util.Abort("couldn't open %s for writing: %s" % 423cdf0c1d5Smjnelson (self.bu.backupfile('metadata.tar.gz'), e)) 424cdf0c1d5Smjnelson 425cdf0c1d5Smjnelson try: 426cdf0c1d5Smjnelson for elt in self.files: 427cdf0c1d5Smjnelson fpath = self.ws.repo.join(elt) 428cdf0c1d5Smjnelson if os.path.exists(fpath): 429cdf0c1d5Smjnelson tar.add(fpath, elt) 430cdf0c1d5Smjnelson except (EnvironmentError, tarfile.TarError), e: 431cdf0c1d5Smjnelson # 432cdf0c1d5Smjnelson # tarfile.TarError doesn't include the tar member or file 433cdf0c1d5Smjnelson # in question, so we have to do so ourselves. 434cdf0c1d5Smjnelson # 435cdf0c1d5Smjnelson if isinstance(e, tarfile.TarError): 436*87039217SRichard Lowe errstr = "%s: %s" % (elt, e) 437cdf0c1d5Smjnelson else: 438*87039217SRichard Lowe errstr = str(e) 439cdf0c1d5Smjnelson 440cdf0c1d5Smjnelson raise util.Abort("couldn't backup metadata to %s:\n" 441cdf0c1d5Smjnelson " %s" % 442cdf0c1d5Smjnelson (self.bu.backupfile('metadata.tar.gz'), 443*87039217SRichard Lowe errstr)) 444cdf0c1d5Smjnelson finally: 445cdf0c1d5Smjnelson if tar and not tar.closed: 446cdf0c1d5Smjnelson tar.close() 447cdf0c1d5Smjnelson 448cdf0c1d5Smjnelson def old_restore(self): 449cdf0c1d5Smjnelson '''Restore workspace metadata from an pre-tar backup''' 450cdf0c1d5Smjnelson 451cdf0c1d5Smjnelson for fname in self.files: 452cdf0c1d5Smjnelson bfile = self.bu.backupfile(fname) 453cdf0c1d5Smjnelson wfile = self.ws.repo.join(fname) 454cdf0c1d5Smjnelson 455cdf0c1d5Smjnelson if os.path.exists(bfile): 456cdf0c1d5Smjnelson try: 457cdf0c1d5Smjnelson shutil.copy2(bfile, wfile) 458cdf0c1d5Smjnelson except EnvironmentError, e: 459cdf0c1d5Smjnelson raise util.Abort("couldn't restore metadata from %s:\n" 460cdf0c1d5Smjnelson " %s" % (bfile, e)) 461cdf0c1d5Smjnelson 462cdf0c1d5Smjnelson def tar_restore(self): 463cdf0c1d5Smjnelson '''Restore workspace metadata (from a tar-style backup)''' 464cdf0c1d5Smjnelson 465cdf0c1d5Smjnelson if os.path.exists(self.bu.backupfile('metadata.tar.gz')): 466cdf0c1d5Smjnelson tar = None 467cdf0c1d5Smjnelson 468cdf0c1d5Smjnelson try: 469cdf0c1d5Smjnelson try: 470cdf0c1d5Smjnelson tar = tarfile.open(self.bu.backupfile('metadata.tar.gz')) 471cdf0c1d5Smjnelson tar.errorlevel = 2 472cdf0c1d5Smjnelson except (EnvironmentError, tarfile.TarError), e: 473cdf0c1d5Smjnelson raise util.Abort("couldn't open %s: %s" % 474cdf0c1d5Smjnelson (self.bu.backupfile('metadata.tar.gz'), e)) 475cdf0c1d5Smjnelson 476cdf0c1d5Smjnelson try: 477cdf0c1d5Smjnelson for elt in tar: 478cdf0c1d5Smjnelson tar.extract(elt, path=self.ws.repo.path) 479cdf0c1d5Smjnelson except (EnvironmentError, tarfile.TarError), e: 480cdf0c1d5Smjnelson # Make sure the member name is in the exception message. 481cdf0c1d5Smjnelson if isinstance(e, tarfile.TarError): 482*87039217SRichard Lowe errstr = "%s: %s" % (elt.name, e) 483cdf0c1d5Smjnelson else: 484*87039217SRichard Lowe errstr = str(e) 485cdf0c1d5Smjnelson 486cdf0c1d5Smjnelson raise util.Abort("couldn't restore metadata from %s:\n" 487cdf0c1d5Smjnelson " %s" % 488cdf0c1d5Smjnelson (self.bu.backupfile('metadata.tar.gz'), 489*87039217SRichard Lowe errstr)) 490cdf0c1d5Smjnelson finally: 491cdf0c1d5Smjnelson if tar and not tar.closed: 492cdf0c1d5Smjnelson tar.close() 493cdf0c1d5Smjnelson 494cdf0c1d5Smjnelson def restore(self): 495cdf0c1d5Smjnelson '''Restore workspace metadata''' 496cdf0c1d5Smjnelson 497cdf0c1d5Smjnelson if os.path.exists(self.bu.backupfile('hgrc')): 498cdf0c1d5Smjnelson self.old_restore() 499cdf0c1d5Smjnelson else: 500cdf0c1d5Smjnelson self.tar_restore() 501cdf0c1d5Smjnelson 502cdf0c1d5Smjnelson def need_backup(self): 503cdf0c1d5Smjnelson '''Compare backed up workspace metadata to workspace''' 504cdf0c1d5Smjnelson 505cdf0c1d5Smjnelson if os.path.exists(self.bu.backupfile('metadata.tar.gz')): 506cdf0c1d5Smjnelson try: 507cdf0c1d5Smjnelson tar = tarfile.open(self.bu.backupfile('metadata.tar.gz')) 508cdf0c1d5Smjnelson tar.errorlevel = 2 509cdf0c1d5Smjnelson except (EnvironmentError, tarfile.TarError), e: 510cdf0c1d5Smjnelson raise util.Abort("couldn't open metadata tarball: %s\n" 511cdf0c1d5Smjnelson " %s" % 512cdf0c1d5Smjnelson (self.bu.backupfile('metadata.tar.gz'), e)) 513cdf0c1d5Smjnelson 514cdf0c1d5Smjnelson for elt in tar: 515cdf0c1d5Smjnelson fpath = self.ws.repo.join(elt.name) 516cdf0c1d5Smjnelson if not os.path.exists(fpath): 517cdf0c1d5Smjnelson return True # File in tar, not workspace 518cdf0c1d5Smjnelson 519cdf0c1d5Smjnelson if elt.isdir(): # Don't care about directories 520cdf0c1d5Smjnelson continue 521cdf0c1d5Smjnelson 5222b5878deSRich Lowe # 5232b5878deSRich Lowe # The filesystem can give us mtime with fractional seconds 5242b5878deSRich Lowe # (as a float), whereas tar files only keep it to the second. 5252b5878deSRich Lowe # 5262b5878deSRich Lowe # Always compare to the integer (second-granularity) mtime. 5272b5878deSRich Lowe # 5282b5878deSRich Lowe if (elt.mtime != int(os.path.getmtime(fpath)) or 529cdf0c1d5Smjnelson elt.size != os.path.getsize(fpath)): 530cdf0c1d5Smjnelson return True 531cdf0c1d5Smjnelson 532cdf0c1d5Smjnelson tarnames = tar.getnames() 533cdf0c1d5Smjnelson tar.close() 534cdf0c1d5Smjnelson else: 535cdf0c1d5Smjnelson tarnames = [] 536cdf0c1d5Smjnelson 537cdf0c1d5Smjnelson for mfile in self.files: 538cdf0c1d5Smjnelson fpath = self.ws.repo.join(mfile) 539cdf0c1d5Smjnelson 540cdf0c1d5Smjnelson if os.path.isdir(fpath): 541cdf0c1d5Smjnelson # Directories in tarfile always end with a '/' 542cdf0c1d5Smjnelson if not mfile.endswith('/'): 543cdf0c1d5Smjnelson mfile += '/' 544cdf0c1d5Smjnelson 545cdf0c1d5Smjnelson if mfile not in tarnames: 546cdf0c1d5Smjnelson return True 547cdf0c1d5Smjnelson 548cdf0c1d5Smjnelson for root, dirs, files in os.walk(fpath, topdown=True): 549cdf0c1d5Smjnelson for elt in files: 550cdf0c1d5Smjnelson path = os.path.join(root, elt) 551cdf0c1d5Smjnelson 552cdf0c1d5Smjnelson rpath = self.ws.repo.path 553cdf0c1d5Smjnelson if not rpath.endswith('/'): 554cdf0c1d5Smjnelson rpath += '/' 555cdf0c1d5Smjnelson 556cdf0c1d5Smjnelson path = path.replace(rpath, '', 1) 557cdf0c1d5Smjnelson if path not in tarnames: 558cdf0c1d5Smjnelson return True # In workspace not tar 559cdf0c1d5Smjnelson else: 560cdf0c1d5Smjnelson if os.path.exists(fpath) and mfile not in tarnames: 561cdf0c1d5Smjnelson return True 562cdf0c1d5Smjnelson 563cdf0c1d5Smjnelson return False 564cdf0c1d5Smjnelson 565cdf0c1d5Smjnelson def cleanup(self): 566cdf0c1d5Smjnelson '''Remove backed up workspace metadata''' 567cdf0c1d5Smjnelson if os.path.exists(self.bu.backupfile('metadata.tar.gz')): 568cdf0c1d5Smjnelson os.unlink(self.bu.backupfile('metadata.tar.gz')) 569cdf0c1d5Smjnelson 570cdf0c1d5Smjnelson 571cdf0c1d5Smjnelsonclass CdmBackup(object): 572cdf0c1d5Smjnelson '''A backup of a given workspace''' 573cdf0c1d5Smjnelson 574cdf0c1d5Smjnelson def __init__(self, ui, ws, name): 575cdf0c1d5Smjnelson self.ws = ws 576cdf0c1d5Smjnelson self.ui = ui 577cdf0c1d5Smjnelson self.backupdir = self._find_backup_dir(name) 578cdf0c1d5Smjnelson 579cdf0c1d5Smjnelson # 580cdf0c1d5Smjnelson # The order of instances here controls the order the various operations 581cdf0c1d5Smjnelson # are run. 582cdf0c1d5Smjnelson # 583cdf0c1d5Smjnelson # There's some inherent dependence, in that on restore we need 584cdf0c1d5Smjnelson # to restore committed changes prior to uncommitted changes 585cdf0c1d5Smjnelson # (as the parent revision of any uncommitted changes is quite 586cdf0c1d5Smjnelson # likely to not exist until committed changes are restored). 587cdf0c1d5Smjnelson # Metadata restore can happen at any point, but happens last 588cdf0c1d5Smjnelson # as a matter of convention. 589cdf0c1d5Smjnelson # 590cdf0c1d5Smjnelson self.modules = [x(self, ws) for x in [CdmCommittedBackup, 591cdf0c1d5Smjnelson CdmUncommittedBackup, 592cdf0c1d5Smjnelson CdmMetadataBackup]] 593cdf0c1d5Smjnelson 594cdf0c1d5Smjnelson 595cdf0c1d5Smjnelson if os.path.exists(os.path.join(self.backupdir, 'latest')): 596cdf0c1d5Smjnelson generation = os.readlink(os.path.join(self.backupdir, 'latest')) 597cdf0c1d5Smjnelson self.generation = int(os.path.split(generation)[1]) 598cdf0c1d5Smjnelson else: 599cdf0c1d5Smjnelson self.generation = 0 600cdf0c1d5Smjnelson 601cdf0c1d5Smjnelson def _find_backup_dir(self, name): 602cdf0c1d5Smjnelson '''Find the path to an appropriate backup directory based on NAME''' 603cdf0c1d5Smjnelson backupdir = None 604cdf0c1d5Smjnelson backupbase = None 605cdf0c1d5Smjnelson 606cdf0c1d5Smjnelson if os.path.isabs(name): 607cdf0c1d5Smjnelson return name 608cdf0c1d5Smjnelson 609cdf0c1d5Smjnelson if self.ui.config('cdm', 'backupdir'): 610cdf0c1d5Smjnelson backupbase = os.path.expanduser(self.ui.config('cdm', 'backupdir')) 611cdf0c1d5Smjnelson else: 612cdf0c1d5Smjnelson home = None 613cdf0c1d5Smjnelson 614cdf0c1d5Smjnelson try: 615cdf0c1d5Smjnelson home = os.getenv('HOME') or pwd.getpwuid(os.getuid()).pw_dir 616cdf0c1d5Smjnelson except KeyError: 617cdf0c1d5Smjnelson pass # Handled anyway 618cdf0c1d5Smjnelson 619cdf0c1d5Smjnelson if not home: 620cdf0c1d5Smjnelson raise util.Abort('Could not determine your HOME directory to ' 621cdf0c1d5Smjnelson 'find backup path') 622cdf0c1d5Smjnelson 623cdf0c1d5Smjnelson backupbase = os.path.join(home, 'cdm.backup') 624cdf0c1d5Smjnelson 625cdf0c1d5Smjnelson backupdir = os.path.join(backupbase, name) 626cdf0c1d5Smjnelson 627cdf0c1d5Smjnelson # If backupdir exists, it must be a directory. 628cdf0c1d5Smjnelson if (os.path.exists(backupdir) and not os.path.isdir(backupdir)): 629cdf0c1d5Smjnelson raise util.Abort('%s exists but is not a directory' % backupdir) 630cdf0c1d5Smjnelson 631cdf0c1d5Smjnelson return backupdir 632cdf0c1d5Smjnelson 633cdf0c1d5Smjnelson def backupfile(self, path): 634cdf0c1d5Smjnelson '''return full path to backup file FILE at GEN''' 635cdf0c1d5Smjnelson return os.path.join(self.backupdir, str(self.generation), path) 636cdf0c1d5Smjnelson 637cdf0c1d5Smjnelson def update_latest(self, gen): 638cdf0c1d5Smjnelson '''Update latest symlink to point to the current generation''' 639cdf0c1d5Smjnelson linkpath = os.path.join(self.backupdir, 'latest') 640cdf0c1d5Smjnelson 641cdf0c1d5Smjnelson if os.path.lexists(linkpath): 642cdf0c1d5Smjnelson os.unlink(linkpath) 643cdf0c1d5Smjnelson 644cdf0c1d5Smjnelson os.symlink(str(gen), linkpath) 645cdf0c1d5Smjnelson 646cdf0c1d5Smjnelson def create_gen(self, gen): 647cdf0c1d5Smjnelson '''Create a new backup generation''' 648cdf0c1d5Smjnelson try: 649cdf0c1d5Smjnelson os.makedirs(os.path.join(self.backupdir, str(gen))) 650cdf0c1d5Smjnelson self.update_latest(gen) 651cdf0c1d5Smjnelson except EnvironmentError, e: 652cdf0c1d5Smjnelson raise util.Abort("Couldn't create backup generation %s: %s" % 653cdf0c1d5Smjnelson (os.path.join(self.backupdir, str(gen)), e)) 654cdf0c1d5Smjnelson 655cdf0c1d5Smjnelson def need_backup(self): 656cdf0c1d5Smjnelson '''Compare backed up changes to workspace''' 657cdf0c1d5Smjnelson # 658cdf0c1d5Smjnelson # If there's no current backup generation, or the last backup was 659cdf0c1d5Smjnelson # invalid (lacking the dirstate file), we need a backup regardless 660cdf0c1d5Smjnelson # of anything else. 661cdf0c1d5Smjnelson # 662cdf0c1d5Smjnelson if (not self.generation or 663cdf0c1d5Smjnelson not os.path.exists(self.backupfile('dirstate'))): 664cdf0c1d5Smjnelson return True 665cdf0c1d5Smjnelson 666cdf0c1d5Smjnelson for x in self.modules: 667cdf0c1d5Smjnelson if x.need_backup(): 668cdf0c1d5Smjnelson return True 669cdf0c1d5Smjnelson 670cdf0c1d5Smjnelson return False 671cdf0c1d5Smjnelson 672cdf0c1d5Smjnelson def backup(self): 673c959a081SRichard Lowe '''Take a backup of the current workspace 674c959a081SRichard Lowe 675c959a081SRichard Lowe Calling code is expected to hold both the working copy lock 676c959a081SRichard Lowe and repository lock.''' 677cdf0c1d5Smjnelson 678cdf0c1d5Smjnelson if not os.path.exists(self.backupdir): 679cdf0c1d5Smjnelson try: 680cdf0c1d5Smjnelson os.makedirs(self.backupdir) 681cdf0c1d5Smjnelson except EnvironmentError, e: 682cdf0c1d5Smjnelson raise util.Abort('Could not create backup directory %s: %s' % 683cdf0c1d5Smjnelson (self.backupdir, e)) 684cdf0c1d5Smjnelson 685cdf0c1d5Smjnelson self.generation += 1 686cdf0c1d5Smjnelson self.create_gen(self.generation) 687cdf0c1d5Smjnelson 688cdf0c1d5Smjnelson try: 689cdf0c1d5Smjnelson for x in self.modules: 690cdf0c1d5Smjnelson x.backup() 691cdf0c1d5Smjnelson except Exception, e: 692cdf0c1d5Smjnelson if isinstance(e, KeyboardInterrupt): 693cdf0c1d5Smjnelson self.ws.ui.warn("Interrupted\n") 694cdf0c1d5Smjnelson else: 695cdf0c1d5Smjnelson self.ws.ui.warn("Error: %s\n" % e) 696c959a081SRichard Lowe show_traceback = self.ws.ui.configbool('ui', 'traceback', 697c959a081SRichard Lowe False) 698cdf0c1d5Smjnelson 699cdf0c1d5Smjnelson # 700cdf0c1d5Smjnelson # If it's not a 'normal' error, we want to print a stack 701cdf0c1d5Smjnelson # trace now in case the attempt to remove the partial 702cdf0c1d5Smjnelson # backup also fails, and raises a second exception. 703cdf0c1d5Smjnelson # 704cdf0c1d5Smjnelson if (not isinstance(e, (EnvironmentError, util.Abort)) 705c959a081SRichard Lowe or show_traceback): 706cdf0c1d5Smjnelson traceback.print_exc() 707cdf0c1d5Smjnelson 708cdf0c1d5Smjnelson for x in self.modules: 709cdf0c1d5Smjnelson x.cleanup() 710cdf0c1d5Smjnelson 711cdf0c1d5Smjnelson os.rmdir(os.path.join(self.backupdir, str(self.generation))) 712cdf0c1d5Smjnelson self.generation -= 1 713cdf0c1d5Smjnelson 714cdf0c1d5Smjnelson if self.generation != 0: 715cdf0c1d5Smjnelson self.update_latest(self.generation) 716cdf0c1d5Smjnelson else: 717cdf0c1d5Smjnelson os.unlink(os.path.join(self.backupdir, 'latest')) 718cdf0c1d5Smjnelson 719cdf0c1d5Smjnelson raise util.Abort('Backup failed') 720cdf0c1d5Smjnelson 721cdf0c1d5Smjnelson def restore(self, gen=None): 722cdf0c1d5Smjnelson '''Restore workspace from backup 723cdf0c1d5Smjnelson 724cdf0c1d5Smjnelson Restores from backup generation GEN (defaulting to the latest) 725c959a081SRichard Lowe into workspace WS. 726cdf0c1d5Smjnelson 727c959a081SRichard Lowe Calling code is expected to hold both the working copy lock 728c959a081SRichard Lowe and repository lock of the destination workspace.''' 729cdf0c1d5Smjnelson 730cdf0c1d5Smjnelson if not os.path.exists(self.backupdir): 731cdf0c1d5Smjnelson raise util.Abort('Backup directory does not exist: %s' % 732cdf0c1d5Smjnelson (self.backupdir)) 733cdf0c1d5Smjnelson 734cdf0c1d5Smjnelson if gen: 735cdf0c1d5Smjnelson if not os.path.exists(os.path.join(self.backupdir, str(gen))): 736cdf0c1d5Smjnelson raise util.Abort('Backup generation does not exist: %s' % 737cdf0c1d5Smjnelson (os.path.join(self.backupdir, str(gen)))) 738cdf0c1d5Smjnelson self.generation = int(gen) 739cdf0c1d5Smjnelson 740cdf0c1d5Smjnelson if not self.generation: # This is ok, 0 is not a valid generation 741cdf0c1d5Smjnelson raise util.Abort('Backup has no generations: %s' % self.backupdir) 742cdf0c1d5Smjnelson 743cdf0c1d5Smjnelson if not os.path.exists(self.backupfile('dirstate')): 744cdf0c1d5Smjnelson raise util.Abort('Backup %s/%s is incomplete (dirstate missing)' % 745cdf0c1d5Smjnelson (self.backupdir, self.generation)) 746cdf0c1d5Smjnelson 747cdf0c1d5Smjnelson try: 748cdf0c1d5Smjnelson for x in self.modules: 749cdf0c1d5Smjnelson x.restore() 750cdf0c1d5Smjnelson except util.Abort, e: 751cdf0c1d5Smjnelson raise util.Abort('Error restoring workspace:\n' 752cdf0c1d5Smjnelson '%s\n' 75312203c71SRichard Lowe 'Workspace may be partially restored' % e) 754