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