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