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# 17cdf0c1d5Smjnelson# Copyright 2008 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) 29cdf0c1d5Smjnelson File containing dirstate nodeid (the tip we expect to be at 30cdf0c1d5Smjnelson after applying the bundle). 31cdf0c1d5Smjnelson 32cdf0c1d5Smjnelson bundle (handled by CdmCommittedBackup) 33cdf0c1d5Smjnelson An Hg bundle containing outgoing committed changes. 34cdf0c1d5Smjnelson 35cdf0c1d5Smjnelson nodes (handled by CdmCommittedBackup) 36cdf0c1d5Smjnelson A text file listing the full (hex) nodeid of all nodes in 37cdf0c1d5Smjnelson bundle, used by need_backup. 38cdf0c1d5Smjnelson 39cdf0c1d5Smjnelson diff (handled by CdmUncommittedBackup) 40cdf0c1d5Smjnelson A Git-formatted diff containing uncommitted changes. 41cdf0c1d5Smjnelson 42cdf0c1d5Smjnelson renames (handled by CdmUncommittedBackup) 43cdf0c1d5Smjnelson A list of renames in the working copy that have to be 44cdf0c1d5Smjnelson applied manually, rather than by the diff. 45cdf0c1d5Smjnelson 46cdf0c1d5Smjnelson metadata.tar.gz (handled by CdmMetadataBackup) 47cdf0c1d5Smjnelson $CODEMGR_WS/.hg/hgrc 48cdf0c1d5Smjnelson $CODEMGR_WS/.hg/localtags 49cdf0c1d5Smjnelson $CODEMGR_WS/.hg/patches (Mq data) 50cdf0c1d5Smjnelson 51cdf0c1d5Smjnelson latest -> generation# 52cdf0c1d5Smjnelson Newest backup generation. 53cdf0c1d5Smjnelson 54cdf0c1d5SmjnelsonAll files in a given backup generation, with the exception of 55cdf0c1d5Smjnelsondirstate, are optional. 56cdf0c1d5Smjnelson''' 57cdf0c1d5Smjnelson 58cdf0c1d5Smjnelsonimport os, pwd, shutil, traceback, tarfile, time 59cdf0c1d5Smjnelsonfrom mercurial import changegroup, patch, node, util 60cdf0c1d5Smjnelsonfrom cStringIO import StringIO 61cdf0c1d5Smjnelson 62cdf0c1d5Smjnelson 63cdf0c1d5Smjnelsonclass CdmCommittedBackup(object): 64cdf0c1d5Smjnelson '''Backup of committed changes''' 65cdf0c1d5Smjnelson 66cdf0c1d5Smjnelson def __init__(self, backup, ws): 67cdf0c1d5Smjnelson self.ws = ws 68cdf0c1d5Smjnelson self.bu = backup 69cdf0c1d5Smjnelson self.files = ('bundle', 'nodes') 70cdf0c1d5Smjnelson 71cdf0c1d5Smjnelson def _outgoing_nodes(self, parent): 72cdf0c1d5Smjnelson '''Return a list of all outgoing nodes in hex format''' 73cdf0c1d5Smjnelson 74cdf0c1d5Smjnelson if parent: 75cdf0c1d5Smjnelson outgoing = self.ws.findoutgoing(parent) 76cdf0c1d5Smjnelson nodes = self.ws.repo.changelog.nodesbetween(outgoing)[0] 77cdf0c1d5Smjnelson return map(node.hex, nodes) 78cdf0c1d5Smjnelson else: 79cdf0c1d5Smjnelson return [] 80cdf0c1d5Smjnelson 81cdf0c1d5Smjnelson def backup(self): 82cdf0c1d5Smjnelson '''Backup committed changes''' 83cdf0c1d5Smjnelson parent = self.ws.parent() 84cdf0c1d5Smjnelson 85cdf0c1d5Smjnelson if not parent: 86cdf0c1d5Smjnelson self.ws.ui.warn('Workspace has no parent, committed changes will ' 87cdf0c1d5Smjnelson 'not be backed up\n') 88cdf0c1d5Smjnelson return 89cdf0c1d5Smjnelson 90cdf0c1d5Smjnelson out = self.ws.findoutgoing(parent) 91cdf0c1d5Smjnelson if not out: 92cdf0c1d5Smjnelson return 93cdf0c1d5Smjnelson 94cdf0c1d5Smjnelson cg = self.ws.repo.changegroup(out, 'bundle') 95cdf0c1d5Smjnelson changegroup.writebundle(cg, self.bu.backupfile('bundle'), 'HG10BZ') 96cdf0c1d5Smjnelson 97cdf0c1d5Smjnelson outnodes = self._outgoing_nodes(parent) 98cdf0c1d5Smjnelson if outnodes: 99cdf0c1d5Smjnelson fp = None 100cdf0c1d5Smjnelson try: 101cdf0c1d5Smjnelson try: 102cdf0c1d5Smjnelson fp = open(self.bu.backupfile('nodes'), 'w') 103cdf0c1d5Smjnelson fp.write('%s\n' % '\n'.join(outnodes)) 104cdf0c1d5Smjnelson except EnvironmentError, e: 105cdf0c1d5Smjnelson raise util.Abort("couldn't store outgoing nodes: %s" % e) 106cdf0c1d5Smjnelson finally: 107cdf0c1d5Smjnelson if fp and not fp.closed: 108cdf0c1d5Smjnelson fp.close() 109cdf0c1d5Smjnelson 110cdf0c1d5Smjnelson def restore(self): 111cdf0c1d5Smjnelson '''Restore committed changes from backup''' 112cdf0c1d5Smjnelson bfile = self.bu.backupfile('bundle') 113cdf0c1d5Smjnelson 114cdf0c1d5Smjnelson if os.path.exists(bfile): 115cdf0c1d5Smjnelson f = None 116cdf0c1d5Smjnelson try: 117cdf0c1d5Smjnelson try: 118cdf0c1d5Smjnelson f = open(bfile, 'r') 119cdf0c1d5Smjnelson bundle = changegroup.readbundle(f, bfile) 120cdf0c1d5Smjnelson self.ws.repo.addchangegroup(bundle, 'strip', 121cdf0c1d5Smjnelson 'bundle:%s' % bfile) 122cdf0c1d5Smjnelson except EnvironmentError, e: 123cdf0c1d5Smjnelson raise util.Abort("couldn't restore committed changes: %s\n" 124cdf0c1d5Smjnelson " %s" % (bfile, e)) 125cdf0c1d5Smjnelson finally: 126cdf0c1d5Smjnelson if f and not f.closed: 127cdf0c1d5Smjnelson f.close() 128cdf0c1d5Smjnelson 129cdf0c1d5Smjnelson def need_backup(self): 130cdf0c1d5Smjnelson '''Compare backup of committed changes to workspace''' 131cdf0c1d5Smjnelson 132cdf0c1d5Smjnelson if os.path.exists(self.bu.backupfile('nodes')): 133cdf0c1d5Smjnelson f = None 134cdf0c1d5Smjnelson try: 135cdf0c1d5Smjnelson try: 136cdf0c1d5Smjnelson f = open(self.bu.backupfile('nodes')) 137cdf0c1d5Smjnelson bnodes = set([line.rstrip('\r\n') 138cdf0c1d5Smjnelson for line in f.readlines()]) 139cdf0c1d5Smjnelson f.close() 140cdf0c1d5Smjnelson except EnvironmentError, e: 141cdf0c1d5Smjnelson raise util.Abort("couldn't open backup node list: %s" % e) 142cdf0c1d5Smjnelson finally: 143cdf0c1d5Smjnelson if f and not f.closed: 144cdf0c1d5Smjnelson f.close() 145cdf0c1d5Smjnelson else: 146cdf0c1d5Smjnelson bnodes = set() 147cdf0c1d5Smjnelson 148cdf0c1d5Smjnelson outnodes = set(self._outgoing_nodes(self.ws.parent())) 149cdf0c1d5Smjnelson if outnodes != bnodes: 150cdf0c1d5Smjnelson return True 151cdf0c1d5Smjnelson 152cdf0c1d5Smjnelson return False 153cdf0c1d5Smjnelson 154cdf0c1d5Smjnelson def cleanup(self): 155cdf0c1d5Smjnelson '''Remove backed up committed changes''' 156cdf0c1d5Smjnelson 157cdf0c1d5Smjnelson for fname in self.files: 158cdf0c1d5Smjnelson if os.path.exists(self.bu.backupfile(fname)): 159cdf0c1d5Smjnelson os.unlink(self.bu.backupfile(fname)) 160cdf0c1d5Smjnelson 161cdf0c1d5Smjnelson 162cdf0c1d5Smjnelsonclass CdmUncommittedBackup(object): 163cdf0c1d5Smjnelson '''Backup of uncommitted changes''' 164cdf0c1d5Smjnelson 165cdf0c1d5Smjnelson def __init__(self, backup, ws): 166cdf0c1d5Smjnelson self.ws = ws 167cdf0c1d5Smjnelson self.bu = backup 168cdf0c1d5Smjnelson 169cdf0c1d5Smjnelson def _clobbering_renames(self): 170cdf0c1d5Smjnelson '''Return a list of pairs of files representing renames/copies 171cdf0c1d5Smjnelson that clobber already versioned files. [(oldname newname)...]''' 172cdf0c1d5Smjnelson 173cdf0c1d5Smjnelson # 174cdf0c1d5Smjnelson # Note that this doesn't handle uncommitted merges 175cdf0c1d5Smjnelson # as CdmUncommittedBackup itself doesn't. 176cdf0c1d5Smjnelson # 177cdf0c1d5Smjnelson wctx = self.ws.repo.workingctx() 178cdf0c1d5Smjnelson parent = wctx.parents()[0] 179cdf0c1d5Smjnelson 180cdf0c1d5Smjnelson ret = [] 181cdf0c1d5Smjnelson for fname in wctx.added() + wctx.modified(): 182cdf0c1d5Smjnelson rn = wctx.filectx(fname).renamed() 183cdf0c1d5Smjnelson if rn and fname in parent: 184cdf0c1d5Smjnelson ret.append((rn[0], fname)) 185cdf0c1d5Smjnelson return ret 186cdf0c1d5Smjnelson 187cdf0c1d5Smjnelson def backup(self): 188cdf0c1d5Smjnelson '''Backup uncommitted changes''' 189cdf0c1d5Smjnelson 190cdf0c1d5Smjnelson if self.ws.merged(): 191cdf0c1d5Smjnelson raise util.Abort("Unable to backup an uncommitted merge.\n" 192cdf0c1d5Smjnelson "Please complete your merge and commit") 193cdf0c1d5Smjnelson 194cdf0c1d5Smjnelson dirstate = node.hex(self.ws.repo.changectx().node()) 195cdf0c1d5Smjnelson 196cdf0c1d5Smjnelson fp = None 197cdf0c1d5Smjnelson try: 198cdf0c1d5Smjnelson try: 199cdf0c1d5Smjnelson fp = open(self.bu.backupfile('dirstate'), 'w') 200cdf0c1d5Smjnelson fp.write(dirstate + '\n') 201cdf0c1d5Smjnelson except EnvironmentError, e: 202cdf0c1d5Smjnelson raise util.Abort("couldn't save working copy parent: %s" % e) 203cdf0c1d5Smjnelson finally: 204cdf0c1d5Smjnelson if fp and not fp.closed: 205cdf0c1d5Smjnelson fp.close() 206cdf0c1d5Smjnelson 207cdf0c1d5Smjnelson try: 208cdf0c1d5Smjnelson try: 209cdf0c1d5Smjnelson fp = open(self.bu.backupfile('renames'), 'w') 210cdf0c1d5Smjnelson for cons in self._clobbering_renames(): 211cdf0c1d5Smjnelson fp.write("%s %s\n" % cons) 212cdf0c1d5Smjnelson except EnvironmentError, e: 213cdf0c1d5Smjnelson raise util.Abort("couldn't save clobbering copies: %s" % e) 214cdf0c1d5Smjnelson finally: 215cdf0c1d5Smjnelson if fp and not fp.closed: 216cdf0c1d5Smjnelson fp.close() 217cdf0c1d5Smjnelson 218cdf0c1d5Smjnelson try: 219cdf0c1d5Smjnelson try: 220cdf0c1d5Smjnelson fp = open(self.bu.backupfile('diff'), 'w') 221cdf0c1d5Smjnelson patch.diff(self.ws.repo, fp=fp, 222cdf0c1d5Smjnelson opts=patch.diffopts(self.ws.ui, opts={'git': True})) 223cdf0c1d5Smjnelson except EnvironmentError, e: 224cdf0c1d5Smjnelson raise util.Abort("couldn't save working copy diff: %s" % e) 225cdf0c1d5Smjnelson finally: 226cdf0c1d5Smjnelson if fp and not fp.closed: 227cdf0c1d5Smjnelson fp.close() 228cdf0c1d5Smjnelson 229cdf0c1d5Smjnelson def _dirstate(self): 230cdf0c1d5Smjnelson '''Return the current working copy node''' 231cdf0c1d5Smjnelson fp = None 232cdf0c1d5Smjnelson try: 233cdf0c1d5Smjnelson try: 234cdf0c1d5Smjnelson fp = open(self.bu.backupfile('dirstate')) 235cdf0c1d5Smjnelson dirstate = fp.readline().strip() 236cdf0c1d5Smjnelson return dirstate 237cdf0c1d5Smjnelson except EnvironmentError, e: 238cdf0c1d5Smjnelson raise util.Abort("couldn't read saved parent: %s" % e) 239cdf0c1d5Smjnelson finally: 240cdf0c1d5Smjnelson if fp and not fp.closed: 241cdf0c1d5Smjnelson fp.close() 242cdf0c1d5Smjnelson 243cdf0c1d5Smjnelson def restore(self): 244cdf0c1d5Smjnelson '''Restore uncommitted changes''' 245cdf0c1d5Smjnelson diff = self.bu.backupfile('diff') 246cdf0c1d5Smjnelson dirstate = self._dirstate() 247cdf0c1d5Smjnelson 248cdf0c1d5Smjnelson try: 249cdf0c1d5Smjnelson self.ws.clean(rev=dirstate) 250cdf0c1d5Smjnelson except util.Abort, e: 251cdf0c1d5Smjnelson raise util.Abort("couldn't update to saved node: %s" % e) 252cdf0c1d5Smjnelson 253cdf0c1d5Smjnelson if not os.path.exists(diff): 254cdf0c1d5Smjnelson return 255cdf0c1d5Smjnelson 256cdf0c1d5Smjnelson # 257cdf0c1d5Smjnelson # There's a race here whereby if the patch (or part thereof) 258cdf0c1d5Smjnelson # is applied within the same second as the clean above (such 259cdf0c1d5Smjnelson # that mtime doesn't change) and if the size of that file 260cdf0c1d5Smjnelson # does not change, Hg may not see the change. 261cdf0c1d5Smjnelson # 262cdf0c1d5Smjnelson # We sleep a full second to avoid this, as sleeping merely 263cdf0c1d5Smjnelson # until the next second begins would require very close clock 264cdf0c1d5Smjnelson # synchronization on network filesystems. 265cdf0c1d5Smjnelson # 266cdf0c1d5Smjnelson time.sleep(1) 267cdf0c1d5Smjnelson 268cdf0c1d5Smjnelson files = {} 269cdf0c1d5Smjnelson try: 270cdf0c1d5Smjnelson try: 271cdf0c1d5Smjnelson fuzz = patch.patch(diff, self.ws.ui, strip=1, 272cdf0c1d5Smjnelson cwd=self.ws.repo.root, files=files) 273cdf0c1d5Smjnelson if fuzz: 274cdf0c1d5Smjnelson raise util.Abort('working copy diff applied with fuzz') 275cdf0c1d5Smjnelson except Exception, e: 276cdf0c1d5Smjnelson raise util.Abort("couldn't apply working copy diff: %s\n" 277cdf0c1d5Smjnelson " %s" % (diff, e)) 278cdf0c1d5Smjnelson finally: 279cdf0c1d5Smjnelson patch.updatedir(self.ws.ui, self.ws.repo, files) 280cdf0c1d5Smjnelson 281cdf0c1d5Smjnelson if not os.path.exists(self.bu.backupfile('renames')): 282cdf0c1d5Smjnelson return 283cdf0c1d5Smjnelson 284cdf0c1d5Smjnelson # 285cdf0c1d5Smjnelson # We need to re-apply name changes where the new name 286cdf0c1d5Smjnelson # (rename/copy destination) is an already versioned file, as 287cdf0c1d5Smjnelson # Hg would otherwise ignore them. 288cdf0c1d5Smjnelson # 289cdf0c1d5Smjnelson try: 290cdf0c1d5Smjnelson fp = open(self.bu.backupfile('renames')) 291cdf0c1d5Smjnelson for line in fp: 292cdf0c1d5Smjnelson source, dest = line.strip().split() 293cdf0c1d5Smjnelson self.ws.repo.copy(source, dest) 294cdf0c1d5Smjnelson except EnvironmentError, e: 295cdf0c1d5Smjnelson raise util.Abort('unable to open renames file: %s' % e) 296cdf0c1d5Smjnelson except ValueError: 297cdf0c1d5Smjnelson raise util.Abort('corrupt renames file: %s' % 298cdf0c1d5Smjnelson self.bu.backupfile('renames')) 299cdf0c1d5Smjnelson 300cdf0c1d5Smjnelson def need_backup(self): 301cdf0c1d5Smjnelson '''Compare backup of uncommitted changes to workspace''' 302cdf0c1d5Smjnelson if self._dirstate() != node.hex(self.ws.repo.changectx().node()): 303cdf0c1d5Smjnelson return True 304cdf0c1d5Smjnelson 305cdf0c1d5Smjnelson curdiff = StringIO() 306cdf0c1d5Smjnelson diff = self.bu.backupfile('diff') 307cdf0c1d5Smjnelson fd = None 308cdf0c1d5Smjnelson 309cdf0c1d5Smjnelson patch.diff(self.ws.repo, fp=curdiff, 310cdf0c1d5Smjnelson opts=patch.diffopts(self.ws.ui, opts={'git': True})) 311cdf0c1d5Smjnelson 312cdf0c1d5Smjnelson if os.path.exists(diff): 313cdf0c1d5Smjnelson try: 314cdf0c1d5Smjnelson try: 315cdf0c1d5Smjnelson fd = open(diff) 316cdf0c1d5Smjnelson backdiff = fd.read() 317cdf0c1d5Smjnelson except EnvironmentError, e: 318cdf0c1d5Smjnelson raise util.Abort("couldn't open backup diff %s\n" 319cdf0c1d5Smjnelson " %s" % (diff, e)) 320cdf0c1d5Smjnelson finally: 321cdf0c1d5Smjnelson if fd and not fd.closed: 322cdf0c1d5Smjnelson fd.close() 323cdf0c1d5Smjnelson else: 324cdf0c1d5Smjnelson backdiff = '' 325cdf0c1d5Smjnelson 326cdf0c1d5Smjnelson if backdiff != curdiff.getvalue(): 327cdf0c1d5Smjnelson return True 328cdf0c1d5Smjnelson 329cdf0c1d5Smjnelson 330cdf0c1d5Smjnelson currrenamed = self._clobbering_renames() 331cdf0c1d5Smjnelson bakrenamed = None 332cdf0c1d5Smjnelson 333cdf0c1d5Smjnelson if os.path.exists(self.bu.backupfile('renames')): 334cdf0c1d5Smjnelson try: 335cdf0c1d5Smjnelson try: 336cdf0c1d5Smjnelson fd = open(self.bu.backupfile('renames')) 337cdf0c1d5Smjnelson bakrenamed = [line.strip().split(' ') for line in fd] 338cdf0c1d5Smjnelson except EnvironmentError, e: 339cdf0c1d5Smjnelson raise util.Abort("couldn't open renames file %s: %s\n" % 340cdf0c1d5Smjnelson (self.bu.backupfile('renames'), e)) 341cdf0c1d5Smjnelson finally: 342cdf0c1d5Smjnelson if fd and not fd.closed: 343cdf0c1d5Smjnelson fd.close() 344cdf0c1d5Smjnelson 345cdf0c1d5Smjnelson if currrenamed != bakrenamed: 346cdf0c1d5Smjnelson return True 347cdf0c1d5Smjnelson 348cdf0c1d5Smjnelson return False 349cdf0c1d5Smjnelson 350cdf0c1d5Smjnelson def cleanup(self): 351cdf0c1d5Smjnelson '''Remove backed up uncommitted changes''' 352cdf0c1d5Smjnelson for fname in ('dirstate', 'diff', 'renames'): 353cdf0c1d5Smjnelson if os.path.exists(self.bu.backupfile(fname)): 354cdf0c1d5Smjnelson os.unlink(self.bu.backupfile(fname)) 355cdf0c1d5Smjnelson 356cdf0c1d5Smjnelson 357cdf0c1d5Smjnelsonclass CdmMetadataBackup(object): 358cdf0c1d5Smjnelson '''Backup of workspace metadata''' 359cdf0c1d5Smjnelson 360cdf0c1d5Smjnelson def __init__(self, backup, ws): 361cdf0c1d5Smjnelson self.bu = backup 362cdf0c1d5Smjnelson self.ws = ws 363*9a70fc3bSMark J. Nelson self.files = ('hgrc', 'localtags', 'patches', 'cdm') 364cdf0c1d5Smjnelson 365cdf0c1d5Smjnelson def backup(self): 366cdf0c1d5Smjnelson '''Backup workspace metadata''' 367cdf0c1d5Smjnelson 368cdf0c1d5Smjnelson tar = None 369cdf0c1d5Smjnelson 370cdf0c1d5Smjnelson try: 371cdf0c1d5Smjnelson try: 372cdf0c1d5Smjnelson tar = tarfile.open(self.bu.backupfile('metadata.tar.gz'), 373cdf0c1d5Smjnelson 'w:gz') 374cdf0c1d5Smjnelson tar.errorlevel = 2 375cdf0c1d5Smjnelson except (EnvironmentError, tarfile.TarError), e: 376cdf0c1d5Smjnelson raise util.Abort("couldn't open %s for writing: %s" % 377cdf0c1d5Smjnelson (self.bu.backupfile('metadata.tar.gz'), e)) 378cdf0c1d5Smjnelson 379cdf0c1d5Smjnelson try: 380cdf0c1d5Smjnelson for elt in self.files: 381cdf0c1d5Smjnelson fpath = self.ws.repo.join(elt) 382cdf0c1d5Smjnelson if os.path.exists(fpath): 383cdf0c1d5Smjnelson tar.add(fpath, elt) 384cdf0c1d5Smjnelson except (EnvironmentError, tarfile.TarError), e: 385cdf0c1d5Smjnelson # 386cdf0c1d5Smjnelson # tarfile.TarError doesn't include the tar member or file 387cdf0c1d5Smjnelson # in question, so we have to do so ourselves. 388cdf0c1d5Smjnelson # 389cdf0c1d5Smjnelson if isinstance(e, tarfile.TarError): 390cdf0c1d5Smjnelson error = "%s: %s" % (elt, e) 391cdf0c1d5Smjnelson else: 392cdf0c1d5Smjnelson error = str(e) 393cdf0c1d5Smjnelson 394cdf0c1d5Smjnelson raise util.Abort("couldn't backup metadata to %s:\n" 395cdf0c1d5Smjnelson " %s" % 396cdf0c1d5Smjnelson (self.bu.backupfile('metadata.tar.gz'), 397cdf0c1d5Smjnelson error)) 398cdf0c1d5Smjnelson finally: 399cdf0c1d5Smjnelson if tar and not tar.closed: 400cdf0c1d5Smjnelson tar.close() 401cdf0c1d5Smjnelson 402cdf0c1d5Smjnelson def old_restore(self): 403cdf0c1d5Smjnelson '''Restore workspace metadata from an pre-tar backup''' 404cdf0c1d5Smjnelson 405cdf0c1d5Smjnelson for fname in self.files: 406cdf0c1d5Smjnelson bfile = self.bu.backupfile(fname) 407cdf0c1d5Smjnelson wfile = self.ws.repo.join(fname) 408cdf0c1d5Smjnelson 409cdf0c1d5Smjnelson if os.path.exists(bfile): 410cdf0c1d5Smjnelson try: 411cdf0c1d5Smjnelson shutil.copy2(bfile, wfile) 412cdf0c1d5Smjnelson except EnvironmentError, e: 413cdf0c1d5Smjnelson raise util.Abort("couldn't restore metadata from %s:\n" 414cdf0c1d5Smjnelson " %s" % (bfile, e)) 415cdf0c1d5Smjnelson 416cdf0c1d5Smjnelson def tar_restore(self): 417cdf0c1d5Smjnelson '''Restore workspace metadata (from a tar-style backup)''' 418cdf0c1d5Smjnelson 419cdf0c1d5Smjnelson if os.path.exists(self.bu.backupfile('metadata.tar.gz')): 420cdf0c1d5Smjnelson tar = None 421cdf0c1d5Smjnelson 422cdf0c1d5Smjnelson try: 423cdf0c1d5Smjnelson try: 424cdf0c1d5Smjnelson tar = tarfile.open(self.bu.backupfile('metadata.tar.gz')) 425cdf0c1d5Smjnelson tar.errorlevel = 2 426cdf0c1d5Smjnelson except (EnvironmentError, tarfile.TarError), e: 427cdf0c1d5Smjnelson raise util.Abort("couldn't open %s: %s" % 428cdf0c1d5Smjnelson (self.bu.backupfile('metadata.tar.gz'), e)) 429cdf0c1d5Smjnelson 430cdf0c1d5Smjnelson try: 431cdf0c1d5Smjnelson for elt in tar: 432cdf0c1d5Smjnelson tar.extract(elt, path=self.ws.repo.path) 433cdf0c1d5Smjnelson except (EnvironmentError, tarfile.TarError), e: 434cdf0c1d5Smjnelson # Make sure the member name is in the exception message. 435cdf0c1d5Smjnelson if isinstance(e, tarfile.TarError): 436cdf0c1d5Smjnelson error = "%s: %s" % (elt.name, e) 437cdf0c1d5Smjnelson else: 438cdf0c1d5Smjnelson error = str(e) 439cdf0c1d5Smjnelson 440cdf0c1d5Smjnelson raise util.Abort("couldn't restore metadata from %s:\n" 441cdf0c1d5Smjnelson " %s" % 442cdf0c1d5Smjnelson (self.bu.backupfile('metadata.tar.gz'), 443cdf0c1d5Smjnelson error)) 444cdf0c1d5Smjnelson finally: 445cdf0c1d5Smjnelson if tar and not tar.closed: 446cdf0c1d5Smjnelson tar.close() 447cdf0c1d5Smjnelson 448cdf0c1d5Smjnelson def restore(self): 449cdf0c1d5Smjnelson '''Restore workspace metadata''' 450cdf0c1d5Smjnelson 451cdf0c1d5Smjnelson if os.path.exists(self.bu.backupfile('hgrc')): 452cdf0c1d5Smjnelson self.old_restore() 453cdf0c1d5Smjnelson else: 454cdf0c1d5Smjnelson self.tar_restore() 455cdf0c1d5Smjnelson 456cdf0c1d5Smjnelson def need_backup(self): 457cdf0c1d5Smjnelson '''Compare backed up workspace metadata to workspace''' 458cdf0c1d5Smjnelson 459cdf0c1d5Smjnelson if os.path.exists(self.bu.backupfile('metadata.tar.gz')): 460cdf0c1d5Smjnelson try: 461cdf0c1d5Smjnelson tar = tarfile.open(self.bu.backupfile('metadata.tar.gz')) 462cdf0c1d5Smjnelson tar.errorlevel = 2 463cdf0c1d5Smjnelson except (EnvironmentError, tarfile.TarError), e: 464cdf0c1d5Smjnelson raise util.Abort("couldn't open metadata tarball: %s\n" 465cdf0c1d5Smjnelson " %s" % 466cdf0c1d5Smjnelson (self.bu.backupfile('metadata.tar.gz'), e)) 467cdf0c1d5Smjnelson 468cdf0c1d5Smjnelson for elt in tar: 469cdf0c1d5Smjnelson fpath = self.ws.repo.join(elt.name) 470cdf0c1d5Smjnelson if not os.path.exists(fpath): 471cdf0c1d5Smjnelson return True # File in tar, not workspace 472cdf0c1d5Smjnelson 473cdf0c1d5Smjnelson if elt.isdir(): # Don't care about directories 474cdf0c1d5Smjnelson continue 475cdf0c1d5Smjnelson 476cdf0c1d5Smjnelson if (elt.mtime != os.path.getmtime(fpath) or 477cdf0c1d5Smjnelson elt.size != os.path.getsize(fpath)): 478cdf0c1d5Smjnelson return True 479cdf0c1d5Smjnelson 480cdf0c1d5Smjnelson tarnames = tar.getnames() 481cdf0c1d5Smjnelson tar.close() 482cdf0c1d5Smjnelson else: 483cdf0c1d5Smjnelson tarnames = [] 484cdf0c1d5Smjnelson 485cdf0c1d5Smjnelson for mfile in self.files: 486cdf0c1d5Smjnelson fpath = self.ws.repo.join(mfile) 487cdf0c1d5Smjnelson 488cdf0c1d5Smjnelson if os.path.isdir(fpath): 489cdf0c1d5Smjnelson # Directories in tarfile always end with a '/' 490cdf0c1d5Smjnelson if not mfile.endswith('/'): 491cdf0c1d5Smjnelson mfile += '/' 492cdf0c1d5Smjnelson 493cdf0c1d5Smjnelson if mfile not in tarnames: 494cdf0c1d5Smjnelson return True 495cdf0c1d5Smjnelson 496cdf0c1d5Smjnelson for root, dirs, files in os.walk(fpath, topdown=True): 497cdf0c1d5Smjnelson for elt in files: 498cdf0c1d5Smjnelson path = os.path.join(root, elt) 499cdf0c1d5Smjnelson 500cdf0c1d5Smjnelson rpath = self.ws.repo.path 501cdf0c1d5Smjnelson if not rpath.endswith('/'): 502cdf0c1d5Smjnelson rpath += '/' 503cdf0c1d5Smjnelson 504cdf0c1d5Smjnelson path = path.replace(rpath, '', 1) 505cdf0c1d5Smjnelson if path not in tarnames: 506cdf0c1d5Smjnelson return True # In workspace not tar 507cdf0c1d5Smjnelson else: 508cdf0c1d5Smjnelson if os.path.exists(fpath) and mfile not in tarnames: 509cdf0c1d5Smjnelson return True 510cdf0c1d5Smjnelson 511cdf0c1d5Smjnelson return False 512cdf0c1d5Smjnelson 513cdf0c1d5Smjnelson def cleanup(self): 514cdf0c1d5Smjnelson '''Remove backed up workspace metadata''' 515cdf0c1d5Smjnelson if os.path.exists(self.bu.backupfile('metadata.tar.gz')): 516cdf0c1d5Smjnelson os.unlink(self.bu.backupfile('metadata.tar.gz')) 517cdf0c1d5Smjnelson 518cdf0c1d5Smjnelson 519cdf0c1d5Smjnelsonclass CdmBackup(object): 520cdf0c1d5Smjnelson '''A backup of a given workspace''' 521cdf0c1d5Smjnelson 522cdf0c1d5Smjnelson def __init__(self, ui, ws, name): 523cdf0c1d5Smjnelson self.ws = ws 524cdf0c1d5Smjnelson self.ui = ui 525cdf0c1d5Smjnelson self.backupdir = self._find_backup_dir(name) 526cdf0c1d5Smjnelson 527cdf0c1d5Smjnelson # 528cdf0c1d5Smjnelson # The order of instances here controls the order the various operations 529cdf0c1d5Smjnelson # are run. 530cdf0c1d5Smjnelson # 531cdf0c1d5Smjnelson # There's some inherent dependence, in that on restore we need 532cdf0c1d5Smjnelson # to restore committed changes prior to uncommitted changes 533cdf0c1d5Smjnelson # (as the parent revision of any uncommitted changes is quite 534cdf0c1d5Smjnelson # likely to not exist until committed changes are restored). 535cdf0c1d5Smjnelson # Metadata restore can happen at any point, but happens last 536cdf0c1d5Smjnelson # as a matter of convention. 537cdf0c1d5Smjnelson # 538cdf0c1d5Smjnelson self.modules = [x(self, ws) for x in [CdmCommittedBackup, 539cdf0c1d5Smjnelson CdmUncommittedBackup, 540cdf0c1d5Smjnelson CdmMetadataBackup]] 541cdf0c1d5Smjnelson 542cdf0c1d5Smjnelson 543cdf0c1d5Smjnelson if os.path.exists(os.path.join(self.backupdir, 'latest')): 544cdf0c1d5Smjnelson generation = os.readlink(os.path.join(self.backupdir, 'latest')) 545cdf0c1d5Smjnelson self.generation = int(os.path.split(generation)[1]) 546cdf0c1d5Smjnelson else: 547cdf0c1d5Smjnelson self.generation = 0 548cdf0c1d5Smjnelson 549cdf0c1d5Smjnelson def _find_backup_dir(self, name): 550cdf0c1d5Smjnelson '''Find the path to an appropriate backup directory based on NAME''' 551cdf0c1d5Smjnelson backupdir = None 552cdf0c1d5Smjnelson backupbase = None 553cdf0c1d5Smjnelson 554cdf0c1d5Smjnelson if os.path.isabs(name): 555cdf0c1d5Smjnelson return name 556cdf0c1d5Smjnelson 557cdf0c1d5Smjnelson if self.ui.config('cdm', 'backupdir'): 558cdf0c1d5Smjnelson backupbase = os.path.expanduser(self.ui.config('cdm', 'backupdir')) 559cdf0c1d5Smjnelson else: 560cdf0c1d5Smjnelson home = None 561cdf0c1d5Smjnelson 562cdf0c1d5Smjnelson try: 563cdf0c1d5Smjnelson home = os.getenv('HOME') or pwd.getpwuid(os.getuid()).pw_dir 564cdf0c1d5Smjnelson except KeyError: 565cdf0c1d5Smjnelson pass # Handled anyway 566cdf0c1d5Smjnelson 567cdf0c1d5Smjnelson if not home: 568cdf0c1d5Smjnelson raise util.Abort('Could not determine your HOME directory to ' 569cdf0c1d5Smjnelson 'find backup path') 570cdf0c1d5Smjnelson 571cdf0c1d5Smjnelson backupbase = os.path.join(home, 'cdm.backup') 572cdf0c1d5Smjnelson 573cdf0c1d5Smjnelson backupdir = os.path.join(backupbase, name) 574cdf0c1d5Smjnelson 575cdf0c1d5Smjnelson # If backupdir exists, it must be a directory. 576cdf0c1d5Smjnelson if (os.path.exists(backupdir) and not os.path.isdir(backupdir)): 577cdf0c1d5Smjnelson raise util.Abort('%s exists but is not a directory' % backupdir) 578cdf0c1d5Smjnelson 579cdf0c1d5Smjnelson return backupdir 580cdf0c1d5Smjnelson 581cdf0c1d5Smjnelson def backupfile(self, path): 582cdf0c1d5Smjnelson '''return full path to backup file FILE at GEN''' 583cdf0c1d5Smjnelson return os.path.join(self.backupdir, str(self.generation), path) 584cdf0c1d5Smjnelson 585cdf0c1d5Smjnelson def update_latest(self, gen): 586cdf0c1d5Smjnelson '''Update latest symlink to point to the current generation''' 587cdf0c1d5Smjnelson linkpath = os.path.join(self.backupdir, 'latest') 588cdf0c1d5Smjnelson 589cdf0c1d5Smjnelson if os.path.lexists(linkpath): 590cdf0c1d5Smjnelson os.unlink(linkpath) 591cdf0c1d5Smjnelson 592cdf0c1d5Smjnelson os.symlink(str(gen), linkpath) 593cdf0c1d5Smjnelson 594cdf0c1d5Smjnelson def create_gen(self, gen): 595cdf0c1d5Smjnelson '''Create a new backup generation''' 596cdf0c1d5Smjnelson try: 597cdf0c1d5Smjnelson os.makedirs(os.path.join(self.backupdir, str(gen))) 598cdf0c1d5Smjnelson self.update_latest(gen) 599cdf0c1d5Smjnelson except EnvironmentError, e: 600cdf0c1d5Smjnelson raise util.Abort("Couldn't create backup generation %s: %s" % 601cdf0c1d5Smjnelson (os.path.join(self.backupdir, str(gen)), e)) 602cdf0c1d5Smjnelson 603cdf0c1d5Smjnelson def need_backup(self): 604cdf0c1d5Smjnelson '''Compare backed up changes to workspace''' 605cdf0c1d5Smjnelson # 606cdf0c1d5Smjnelson # If there's no current backup generation, or the last backup was 607cdf0c1d5Smjnelson # invalid (lacking the dirstate file), we need a backup regardless 608cdf0c1d5Smjnelson # of anything else. 609cdf0c1d5Smjnelson # 610cdf0c1d5Smjnelson if (not self.generation or 611cdf0c1d5Smjnelson not os.path.exists(self.backupfile('dirstate'))): 612cdf0c1d5Smjnelson return True 613cdf0c1d5Smjnelson 614cdf0c1d5Smjnelson for x in self.modules: 615cdf0c1d5Smjnelson if x.need_backup(): 616cdf0c1d5Smjnelson return True 617cdf0c1d5Smjnelson 618cdf0c1d5Smjnelson return False 619cdf0c1d5Smjnelson 620cdf0c1d5Smjnelson def backup(self): 621cdf0c1d5Smjnelson '''Take a backup of the current workspace''' 622cdf0c1d5Smjnelson 623cdf0c1d5Smjnelson if not os.path.exists(self.backupdir): 624cdf0c1d5Smjnelson try: 625cdf0c1d5Smjnelson os.makedirs(self.backupdir) 626cdf0c1d5Smjnelson except EnvironmentError, e: 627cdf0c1d5Smjnelson raise util.Abort('Could not create backup directory %s: %s' % 628cdf0c1d5Smjnelson (self.backupdir, e)) 629cdf0c1d5Smjnelson 630cdf0c1d5Smjnelson self.generation += 1 631cdf0c1d5Smjnelson self.create_gen(self.generation) 632cdf0c1d5Smjnelson 633cdf0c1d5Smjnelson # 634cdf0c1d5Smjnelson # Lock the repo, so the backup can be consistent. We need the 635cdf0c1d5Smjnelson # wlock too to make sure the dirstate parent doesn't change 636cdf0c1d5Smjnelson # underneath us. 637cdf0c1d5Smjnelson # 638cdf0c1d5Smjnelson 639cdf0c1d5Smjnelson lock = self.ws.repo.lock() 640cdf0c1d5Smjnelson wlock = self.ws.repo.lock() 641cdf0c1d5Smjnelson 642cdf0c1d5Smjnelson try: 643cdf0c1d5Smjnelson for x in self.modules: 644cdf0c1d5Smjnelson x.backup() 645cdf0c1d5Smjnelson except Exception, e: 646cdf0c1d5Smjnelson if isinstance(e, KeyboardInterrupt): 647cdf0c1d5Smjnelson self.ws.ui.warn("Interrupted\n") 648cdf0c1d5Smjnelson else: 649cdf0c1d5Smjnelson self.ws.ui.warn("Error: %s\n" % e) 650cdf0c1d5Smjnelson 651cdf0c1d5Smjnelson # 652cdf0c1d5Smjnelson # If it's not a 'normal' error, we want to print a stack 653cdf0c1d5Smjnelson # trace now in case the attempt to remove the partial 654cdf0c1d5Smjnelson # backup also fails, and raises a second exception. 655cdf0c1d5Smjnelson # 656cdf0c1d5Smjnelson if (not isinstance(e, (EnvironmentError, util.Abort)) 657cdf0c1d5Smjnelson or self.ws.ui.traceback): 658cdf0c1d5Smjnelson traceback.print_exc() 659cdf0c1d5Smjnelson 660cdf0c1d5Smjnelson for x in self.modules: 661cdf0c1d5Smjnelson x.cleanup() 662cdf0c1d5Smjnelson 663cdf0c1d5Smjnelson os.rmdir(os.path.join(self.backupdir, str(self.generation))) 664cdf0c1d5Smjnelson self.generation -= 1 665cdf0c1d5Smjnelson 666cdf0c1d5Smjnelson if self.generation != 0: 667cdf0c1d5Smjnelson self.update_latest(self.generation) 668cdf0c1d5Smjnelson else: 669cdf0c1d5Smjnelson os.unlink(os.path.join(self.backupdir, 'latest')) 670cdf0c1d5Smjnelson 671cdf0c1d5Smjnelson raise util.Abort('Backup failed') 672cdf0c1d5Smjnelson 673cdf0c1d5Smjnelson def restore(self, gen=None): 674cdf0c1d5Smjnelson '''Restore workspace from backup 675cdf0c1d5Smjnelson 676cdf0c1d5Smjnelson Restores from backup generation GEN (defaulting to the latest) 677cdf0c1d5Smjnelson into workspace WS.''' 678cdf0c1d5Smjnelson 679cdf0c1d5Smjnelson wlock = self.ws.repo.wlock() 680cdf0c1d5Smjnelson lock = self.ws.repo.lock() 681cdf0c1d5Smjnelson 682cdf0c1d5Smjnelson if not os.path.exists(self.backupdir): 683cdf0c1d5Smjnelson raise util.Abort('Backup directory does not exist: %s' % 684cdf0c1d5Smjnelson (self.backupdir)) 685cdf0c1d5Smjnelson 686cdf0c1d5Smjnelson if gen: 687cdf0c1d5Smjnelson if not os.path.exists(os.path.join(self.backupdir, str(gen))): 688cdf0c1d5Smjnelson raise util.Abort('Backup generation does not exist: %s' % 689cdf0c1d5Smjnelson (os.path.join(self.backupdir, str(gen)))) 690cdf0c1d5Smjnelson self.generation = int(gen) 691cdf0c1d5Smjnelson 692cdf0c1d5Smjnelson if not self.generation: # This is ok, 0 is not a valid generation 693cdf0c1d5Smjnelson raise util.Abort('Backup has no generations: %s' % self.backupdir) 694cdf0c1d5Smjnelson 695cdf0c1d5Smjnelson if not os.path.exists(self.backupfile('dirstate')): 696cdf0c1d5Smjnelson raise util.Abort('Backup %s/%s is incomplete (dirstate missing)' % 697cdf0c1d5Smjnelson (self.backupdir, self.generation)) 698cdf0c1d5Smjnelson 699cdf0c1d5Smjnelson try: 700cdf0c1d5Smjnelson for x in self.modules: 701cdf0c1d5Smjnelson x.restore() 702cdf0c1d5Smjnelson except util.Abort, e: 703cdf0c1d5Smjnelson raise util.Abort('Error restoring workspace:\n' 704cdf0c1d5Smjnelson '%s\n' 705cdf0c1d5Smjnelson 'Workspace will be partially restored' % e) 706