Backup.py revision 12203c7149a0ba35c436436cd2400dc7aaba1e62
#
# it under the terms of the GNU General Public License version 2
# as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
#
#
# Copyright 2009 Sun Microsystems, Inc. All rights reserved.
# Use is subject to license terms.
#
'''
Workspace backup
Backup format is:
backupdir/
wsname/
generation#/
dirstate (handled by CdmUncommittedBackup)
File containing dirstate nodeid (the changeset we need
to update the workspace to after applying the bundle).
This is the node to which the working copy changes
(see 'diff', below) will be applied if applicable.
bundle (handled by CdmCommittedBackup)
An Hg bundle containing outgoing committed changes.
nodes (handled by CdmCommittedBackup)
A text file listing the full (hex) nodeid of all nodes in
bundle, used by need_backup.
diff (handled by CdmUncommittedBackup)
A Git-formatted diff containing uncommitted changes.
renames (handled by CdmUncommittedBackup)
A list of renames in the working copy that have to be
applied manually, rather than by the diff.
metadata.tar.gz (handled by CdmMetadataBackup)
latest -> generation#
Newest backup generation.
All files in a given backup generation, with the exception of
dirstate, are optional.
'''
'''a required node is not present in the destination workspace.
This may occur both in the case where the bundle contains a
changeset which is a child of a node not present in the
destination workspace (because the destination workspace is not as
up-to-date as the source), or because the source and destination
workspace are not related.
It may also happen in cases where the uncommitted changes need to
be applied onto a node that the workspace does not possess even
after application of the bundle (on a branch not present
in the bundle or destination workspace, for instance)'''
#
# If e.name is a string 20 characters long, it is
# assumed to be a node. (Mercurial makes this
# same assumption, when creating a LookupError)
#
else:
n = name
"Your workspace is either not "
"sufficiently up to date,\n"
"or is unrelated to the workspace from "
"which the backup was taken.\n" % (msg, n))
class CdmCommittedBackup(object):
'''Backup of committed changes'''
'''Return a list of all outgoing nodes in hex format'''
if parent:
else:
return []
'''Backup committed changes'''
if not parent:
'not be backed up\n')
return
if not out:
return
if outnodes:
fp = None
try:
try:
except EnvironmentError, e:
finally:
'''Restore committed changes from backup'''
f = None
try:
try:
'bundle:%s' % bfile)
except EnvironmentError, e:
" %s" % (bfile, e))
except revlog.LookupError, e:
raise CdmNodeMissing("couldn't restore committed changes",
e.name)
finally:
if f and not f.closed:
f.close()
def need_backup(self):
'''Compare backup of committed changes to workspace'''
f = None
try:
try:
f.close()
except EnvironmentError, e:
finally:
if f and not f.closed:
f.close()
else:
return True
return False
'''Remove backed up committed changes'''
class CdmUncommittedBackup(object):
'''Backup of uncommitted changes'''
def _clobbering_renames(self):
that clobber already versioned files. [(oldname newname)...]'''
#
# Note that this doesn't handle uncommitted merges
# as CdmUncommittedBackup itself doesn't.
#
ret = []
return ret
'''Backup uncommitted changes'''
"Please complete your merge and commit")
fp = None
try:
try:
except EnvironmentError, e:
finally:
try:
try:
except EnvironmentError, e:
finally:
try:
try:
except EnvironmentError, e:
finally:
'''Return the current working copy node'''
fp = None
try:
try:
return dirstate
except EnvironmentError, e:
finally:
'''Restore uncommitted changes'''
try:
except revlog.LookupError, e:
raise CdmNodeMissing("couldn't restore uncommitted changes",
e.name)
return
#
# There's a race here whereby if the patch (or part thereof)
# is applied within the same second as the clean above (such
# that mtime doesn't change) and if the size of that file
# does not change, Hg may not see the change.
#
# We sleep a full second to avoid this, as sleeping merely
# until the next second begins would require very close clock
# synchronization on network filesystems.
#
files = {}
try:
try:
if fuzz:
except Exception, e:
" %s" % (diff, e))
finally:
return
#
# We need to re-apply name changes where the new name
# Hg would otherwise ignore them.
#
try:
except EnvironmentError, e:
except ValueError:
def need_backup(self):
'''Compare backup of uncommitted changes to workspace'''
return True
fd = None
try:
try:
except EnvironmentError, e:
" %s" % (diff, e))
finally:
else:
backdiff = ''
return True
bakrenamed = None
try:
try:
except EnvironmentError, e:
finally:
if currrenamed != bakrenamed:
return True
return False
'''Remove backed up uncommitted changes'''
class CdmMetadataBackup(object):
'''Backup of workspace metadata'''
'''Backup workspace metadata'''
tar = None
try:
try:
'w:gz')
try:
#
# tarfile.TarError doesn't include the tar member or file
# in question, so we have to do so ourselves.
#
else:
" %s" %
error))
finally:
def old_restore(self):
'''Restore workspace metadata from an pre-tar backup'''
try:
except EnvironmentError, e:
" %s" % (bfile, e))
def tar_restore(self):
'''Restore workspace metadata (from a tar-style backup)'''
tar = None
try:
try:
try:
# Make sure the member name is in the exception message.
else:
" %s" %
error))
finally:
'''Restore workspace metadata'''
else:
def need_backup(self):
'''Compare backed up workspace metadata to workspace'''
try:
" %s" %
return True # File in tar, not workspace
continue
return True
else:
tarnames = []
# Directories in tarfile always end with a '/'
mfile += '/'
return True
rpath += '/'
return True # In workspace not tar
else:
return True
return False
'''Remove backed up workspace metadata'''
'''A backup of a given workspace'''
#
# The order of instances here controls the order the various operations
# are run.
#
# There's some inherent dependence, in that on restore we need
# to restore committed changes prior to uncommitted changes
# (as the parent revision of any uncommitted changes is quite
# likely to not exist until committed changes are restored).
# Metadata restore can happen at any point, but happens last
# as a matter of convention.
#
else:
'''Find the path to an appropriate backup directory based on NAME'''
backupdir = None
backupbase = None
return name
else:
home = None
try:
except KeyError:
pass # Handled anyway
if not home:
'find backup path')
# If backupdir exists, it must be a directory.
return backupdir
'''return full path to backup file FILE at GEN'''
'''Update latest symlink to point to the current generation'''
'''Create a new backup generation'''
try:
except EnvironmentError, e:
def need_backup(self):
'''Compare backed up changes to workspace'''
#
# If there's no current backup generation, or the last backup was
# invalid (lacking the dirstate file), we need a backup regardless
# of anything else.
#
if (not self.generation or
return True
if x.need_backup():
return True
return False
'''Take a backup of the current workspace'''
try:
except EnvironmentError, e:
#
# Lock the repo, so the backup can be consistent. We need the
# wlock too to make sure the dirstate parent doesn't change
# underneath us.
#
try:
x.backup()
except Exception, e:
if isinstance(e, KeyboardInterrupt):
else:
#
# If it's not a 'normal' error, we want to print a stack
# trace now in case the attempt to remove the partial
# backup also fails, and raises a second exception.
#
x.cleanup()
else:
'''Restore workspace from backup
Restores from backup generation GEN (defaulting to the latest)
into workspace WS.'''
if gen:
try:
x.restore()
'%s\n'
'Workspace may be partially restored' % e)