Backup.py revision 036abaca93ddab92ba33036159c30112ab844810
#
# 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.
#
# Copyright 2008, 2011, Richard Lowe
#
'''
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)
clear.tar.gz (handled by CdmClearBackup)
<short node>/
copies of each modified or added file, as it is in
this head.
... for each outgoing head
working/
copies of each modified or added file in the
working copy if any.
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))
'''Tar file access + simple comparison to the filesystem, and
creation addition of files from Mercurial filectx objects.'''
'''Compare the contents of the tar archive to the directory
specified by rootpath. Return False if they differ.
Every file in the archive must match the equivalent file in
the filesystem.
The existence, modification time, and size of each file are
compared, content is not.'''
'''Compare a single member to its filesystem counterpart'''
return False
return False
#
# The filesystem may return a modification time with a
# fractional component (as a float), whereas the tar format
# only stores it to the whole second, perform the comparison
# using integers (truncated, not rounded)
#
return False
return False
else:
return True
return False
return True
'''Add a filectx object to the archive.
Use the path specified by the filectx object or, if specified,
the PATH argument.
The size, modification time, type and permissions of the tar
member are taken from the filectx object, user and group id
are those of the invoking user, user and group name are those
of the invoking user if information is available, or "unknown"
if it is not.
'''
try:
except KeyError:
t.uname = "unknown"
try:
except KeyError:
t.gname = "unknown"
#
# Mercurial versions symlinks by setting a flag and storing
# the destination path in place of the file content. The
# actual contents (in the tar), should be empty.
#
t.mode = 0777
data = None
else:
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 not outnodes:
return
fp = None
try:
try:
except EnvironmentError, e:
finally:
'''Restore committed changes from backup'''
return
f = None
try:
try:
'bundle:%s' % bpath)
except EnvironmentError, e:
" %s" % (bpath, e))
except error.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:
#
# If there are outgoing nodes not in the prior backup we need
# to take a new backup; it's fine if there are nodes in the
# old backup which are no longer outgoing, however.
#
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. [(old-name new-name)...]
'''
#
# 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:
try:
except EnvironmentError, e:
try:
except EnvironmentError, e:
finally:
'''Return the desired working copy node from the backup'''
fp = None
try:
try:
except EnvironmentError, e:
finally:
return dirstate
'''Restore uncommitted changes'''
#
# Check that the patch's parent changeset exists.
#
try:
except error.LookupError, e:
raise CdmNodeMissing("couldn't restore uncommitted changes",
e.name)
try:
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 modification time 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:
else:
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:
else:
backdiff = ''
return True
bakrenamed = None
try:
except EnvironmentError, e:
if currrenamed != bakrenamed:
return True
finally:
return False
'''Remove backed up uncommitted changes'''
for f in ('dirstate', 'diff', 'renames'):
class CdmMetadataBackup(object):
'''Backup of workspace metadata'''
'''Backup workspace metadata'''
#
# Files is a list of tuples (name, path), where name is as in
# self.files, and path is the absolute path.
#
if not files:
return
try:
(tarpath, e))
try:
try:
#
# tarfile.TarError doesn't include the tar member or file
# in question, so we have to do so ourselves.
#
else:
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)'''
return
try:
try:
try:
# Make sure the member name is in the exception message.
else:
" %s" %
finally:
'''Restore workspace metadata'''
else:
'''Yield the repo-relative path to each file we operate on,
including each file within any affected directory'''
continue
yield root
for f in files:
else:
yield path
def need_backup(self):
'''Compare backed up workspace metadata to workspace'''
'''Remove a possible trailing path separator from PATHNAME'''
try:
" %s" % (tarpath, e))
return True
else:
tarnames = []
repopath += '/'
return True
return False
'''Remove backed up workspace metadata'''
class CdmClearBackup(object):
'''A backup (in tar format) of complete source files from every
workspace head.
Paths in the tarball are prefixed by the revision and node of the
head, or "working" for the working directory.
This is done purely for the benefit of the user, and as such takes
no part in restore or need_backup checking, restore always
succeeds, need_backup always returns False
'''
def _branch_pairs(self):
'''Return a list of tuples (parenttip, localtip) for each
outgoing head. If the working copy contains modified files,
it is a head, and neither of its parents are.
'''
if parent:
if n in outnodes]
else:
heads = []
outnodes = []
pairs = []
else:
c = [head]
return pairs
'''Save a clear copy of each source file modified between each
head and that head's parenttip (see WorkSpace.parenttip).
'''
if not branches:
return
try:
(tarpath, e))
try:
continue
try:
except ValueError, e:
if crev is None:
crev = "working copy"
finally:
'''Cleanup a failed Clear backup.
Remove the clear tarball from the backup directory.
'''
'''Clear backups are never restored, do nothing'''
pass
def need_backup(self):
'''Clear backups are never compared, return False (no backup needed).
Should a backup actually be needed, one of the other
implementation classes would notice in any situation we would.
'''
return False
'''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'''
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
'''Update latest symlink to point to the current generation'''
'''Create a new backup generation'''
try:
except EnvironmentError, e:
'''return full path to backup file FILE at GEN'''
'''Unlink the specified path from the backup directory.
A no-op if the path does not exist.
'''
'''Open the specified file in the backup directory'''
'''Return boolean indicating wether a given file exists in the
backup directory.'''
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.
#
return True
if x.need_backup():
return True
return False
'''Take a backup of the current workspace
Calling code is expected to hold both the working copy lock
and repository lock.'''
try:
except EnvironmentError, e:
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.
#
or show_traceback):
x.cleanup()
else:
'''Restore workspace from backup
Restores from backup generation GEN (defaulting to the latest)
into workspace WS.
Calling code is expected to hold both the working copy lock
and repository lock of the destination workspace.'''
if gen:
try:
x.restore()
'%s\n'
'Workspace may be partially restored' % e)