Backup.py revision 605a716e6d38b3af09034c254382d0ae3b7d5f70
6185db853e024a486ff8837e6784dd290d866112dougm# This program is free software; you can redistribute it and/or modify
6185db853e024a486ff8837e6784dd290d866112dougm# it under the terms of the GNU General Public License version 2
6185db853e024a486ff8837e6784dd290d866112dougm# as published by the Free Software Foundation.
6185db853e024a486ff8837e6784dd290d866112dougm# This program is distributed in the hope that it will be useful,
6185db853e024a486ff8837e6784dd290d866112dougm# but WITHOUT ANY WARRANTY; without even the implied warranty of
6185db853e024a486ff8837e6784dd290d866112dougm# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
6185db853e024a486ff8837e6784dd290d866112dougm# GNU General Public License for more details.
6185db853e024a486ff8837e6784dd290d866112dougm# You should have received a copy of the GNU General Public License
6185db853e024a486ff8837e6784dd290d866112dougm# along with this program; if not, write to the Free Software
6185db853e024a486ff8837e6784dd290d866112dougm# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
6185db853e024a486ff8837e6784dd290d866112dougm# Copyright 2009 Sun Microsystems, Inc. All rights reserved.
6185db853e024a486ff8837e6784dd290d866112dougm# Use is subject to license terms.
6185db853e024a486ff8837e6784dd290d866112dougm# Copyright 2008, 2011, Richard Lowe
6185db853e024a486ff8837e6784dd290d866112dougmWorkspace backup
6185db853e024a486ff8837e6784dd290d866112dougmBackup format is:
6185db853e024a486ff8837e6784dd290d866112dougm generation#/
6185db853e024a486ff8837e6784dd290d866112dougm dirstate (handled by CdmUncommittedBackup)
6185db853e024a486ff8837e6784dd290d866112dougm File containing dirstate nodeid (the changeset we need
6185db853e024a486ff8837e6784dd290d866112dougm to update the workspace to after applying the bundle).
6185db853e024a486ff8837e6784dd290d866112dougm This is the node to which the working copy changes
6185db853e024a486ff8837e6784dd290d866112dougm (see 'diff', below) will be applied if applicable.
6185db853e024a486ff8837e6784dd290d866112dougm bundle (handled by CdmCommittedBackup)
6185db853e024a486ff8837e6784dd290d866112dougm An Hg bundle containing outgoing committed changes.
6185db853e024a486ff8837e6784dd290d866112dougm nodes (handled by CdmCommittedBackup)
6185db853e024a486ff8837e6784dd290d866112dougm A text file listing the full (hex) nodeid of all nodes in
6185db853e024a486ff8837e6784dd290d866112dougm bundle, used by need_backup.
6185db853e024a486ff8837e6784dd290d866112dougm diff (handled by CdmUncommittedBackup)
6185db853e024a486ff8837e6784dd290d866112dougm A Git-formatted diff containing uncommitted changes.
6185db853e024a486ff8837e6784dd290d866112dougm renames (handled by CdmUncommittedBackup)
6185db853e024a486ff8837e6784dd290d866112dougm A list of renames in the working copy that have to be
6185db853e024a486ff8837e6784dd290d866112dougm applied manually, rather than by the diff.
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw metadata.tar.gz (handled by CdmMetadataBackup)
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw $CODEMGR_WS/.hg/hgrc
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw $CODEMGR_WS/.hg/localtags
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw $CODEMGR_WS/.hg/patches (Mq data)
6185db853e024a486ff8837e6784dd290d866112dougm clear.tar.gz (handled by CdmClearBackup)
6185db853e024a486ff8837e6784dd290d866112dougm <short node>/
6185db853e024a486ff8837e6784dd290d866112dougm copies of each modified or added file, as it is in
6185db853e024a486ff8837e6784dd290d866112dougm ... for each outgoing head
6185db853e024a486ff8837e6784dd290d866112dougm copies of each modified or added file in the
6185db853e024a486ff8837e6784dd290d866112dougm working copy if any.
6185db853e024a486ff8837e6784dd290d866112dougm latest -> generation#
6185db853e024a486ff8837e6784dd290d866112dougm Newest backup generation.
6185db853e024a486ff8837e6784dd290d866112dougmAll files in a given backup generation, with the exception of
6185db853e024a486ff8837e6784dd290d866112dougmdirstate, are optional.
6185db853e024a486ff8837e6784dd290d866112dougmfrom mercurial import changegroup, error, node, patch, util
6185db853e024a486ff8837e6784dd290d866112dougm '''a required node is not present in the destination workspace.
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw This may occur both in the case where the bundle contains a
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw changeset which is a child of a node not present in the
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw destination workspace (because the destination workspace is not as
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw up-to-date as the source), or because the source and destination
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw workspace are not related.
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw It may also happen in cases where the uncommitted changes need to
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw be applied onto a node that the workspace does not possess even
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw after application of the bundle (on a branch not present
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw in the bundle or destination workspace, for instance)'''
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw # If e.name is a string 20 characters long, it is
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw # assumed to be a node. (Mercurial makes this
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw # same assumption, when creating a LookupError)
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw util.Abort.__init__(self, "%s: changeset '%s' is missing\n"
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw "Your workspace is either not "
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw "sufficiently up to date,\n"
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw "or is unrelated to the workspace from "
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw '''Tar file access + simple comparison to the filesystem, and
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw creation addition of files from Mercurial filectx objects.'''
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw '''Compare the contents of the tar archive to the directory
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw specified by rootpath. Return False if they differ.
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw Every file in the archive must match the equivalent file in
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw the filesystem.
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw The existence, modification time, and size of each file are
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw compared, content is not.'''
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw '''Compare a single member to its filesystem counterpart'''
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw # The filesystem may return a modification time with a
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw # fractional component (as a float), whereas the tar format
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw # only stores it to the whole second, perform the comparison
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw # using integers (truncated, not rounded)
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw elif not member.isdir() and member.size != os.path.getsize(fpath):
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw '''Add a filectx object to the archive.
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw Use the path specified by the filectx object or, if specified,
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw the PATH argument.
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw The size, modification time, type and permissions of the tar
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw member are taken from the filectx object, user and group id
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw are those of the invoking user, user and group name are those
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw of the invoking user if information is available, or "unknown"
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw if it is not.
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw # Mercurial versions symlinks by setting a flag and storing
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw # the destination path in place of the file content. The
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw # actual contents (in the tar), should be empty.
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw '''Backup of committed changes'''
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw '''Return a list of all outgoing nodes in hex format'''
573b0c00a1ee520c3f6938dda8d693236f37ae60dougm nodes = self.ws.repo.changelog.nodesbetween(outgoing)[0]
573b0c00a1ee520c3f6938dda8d693236f37ae60dougm '''Backup committed changes'''
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw self.ws.ui.warn('Workspace has no parent, committed changes will '
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw 'not be backed up\n')
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw changegroup.writebundle(cg, self.bu.backupfile('bundle'), 'HG10BZ')
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw raise util.Abort("couldn't store outgoing nodes: %s" % e)
573b0c00a1ee520c3f6938dda8d693236f37ae60dougm '''Restore committed changes from backup'''
573b0c00a1ee520c3f6938dda8d693236f37ae60dougm raise util.Abort("couldn't restore committed changes: %s\n"
573b0c00a1ee520c3f6938dda8d693236f37ae60dougm raise CdmNodeMissing("couldn't restore committed changes",
573b0c00a1ee520c3f6938dda8d693236f37ae60dougm if f and not f.closed:
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw '''Compare backup of committed changes to workspace'''
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw bnodes = set(line.rstrip('\r\n') for line in f.readlines())
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw raise util.Abort("couldn't open backup node list: %s" % e)
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw if f and not f.closed:
6185db853e024a486ff8837e6784dd290d866112dougm outnodes = set(self._outgoing_nodes(self.ws.parent()))
6185db853e024a486ff8837e6784dd290d866112dougm # If there are outgoing nodes not in the prior backup we need
6185db853e024a486ff8837e6784dd290d866112dougm # to take a new backup; it's fine if there are nodes in the
6185db853e024a486ff8837e6784dd290d866112dougm # old backup which are no longer outgoing, however.
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm '''Remove backed up committed changes'''
6185db853e024a486ff8837e6784dd290d866112dougm '''Backup of uncommitted changes'''
6185db853e024a486ff8837e6784dd290d866112dougm '''Return a list of pairs of files representing renames/copies
6185db853e024a486ff8837e6784dd290d866112dougm that clobber already versioned files. [(old-name new-name)...]
6185db853e024a486ff8837e6784dd290d866112dougm # Note that this doesn't handle uncommitted merges
6185db853e024a486ff8837e6784dd290d866112dougm # as CdmUncommittedBackup itself doesn't.
6185db853e024a486ff8837e6784dd290d866112dougm for fname in self.wctx.added() + self.wctx.modified():
6185db853e024a486ff8837e6784dd290d866112dougm '''Backup uncommitted changes'''
6185db853e024a486ff8837e6784dd290d866112dougm raise util.Abort("Unable to backup an uncommitted merge.\n"
6185db853e024a486ff8837e6784dd290d866112dougm "Please complete your merge and commit")
6185db853e024a486ff8837e6784dd290d866112dougm raise util.Abort("couldn't save working copy parent: %s" % e)
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm raise util.Abort("couldn't save clobbering copies: %s" % e)
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm fp.write(self.ws.diff(opts={'git': True}, match=match))
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm raise util.Abort("couldn't save working copy diff: %s" % e)
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm '''Return the desired working copy node from the backup'''
6185db853e024a486ff8837e6784dd290d866112dougm raise util.Abort("couldn't read saved parent: %s" % e)
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm '''Restore uncommitted changes'''
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm # Check that the patch's parent changeset exists.
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm raise CdmNodeMissing("couldn't restore uncommitted changes",
6185db853e024a486ff8837e6784dd290d866112dougm raise util.Abort("couldn't update to saved node: %s" % e)
6185db853e024a486ff8837e6784dd290d866112dougm # There's a race here whereby if the patch (or part thereof)
6185db853e024a486ff8837e6784dd290d866112dougm # is applied within the same second as the clean above (such
6185db853e024a486ff8837e6784dd290d866112dougm # that modification time doesn't change) and if the size of
6185db853e024a486ff8837e6784dd290d866112dougm # that file does not change, Hg may not see the change.
6185db853e024a486ff8837e6784dd290d866112dougm # We sleep a full second to avoid this, as sleeping merely
6185db853e024a486ff8837e6784dd290d866112dougm # until the next second begins would require very close clock
6185db853e024a486ff8837e6784dd290d866112dougm # synchronization on network filesystems.
6185db853e024a486ff8837e6784dd290d866112dougm raise util.Abort('working copy diff applied with fuzz')
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm raise util.Abort("couldn't apply working copy diff: %s\n"
6185db853e024a486ff8837e6784dd290d866112dougm # We need to re-apply name changes where the new name
6185db853e024a486ff8837e6784dd290d866112dougm # (rename/copy destination) is an already versioned file, as
6185db853e024a486ff8837e6784dd290d866112dougm # Hg would otherwise ignore them.
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw '''Compare backup of uncommitted changes to workspace'''
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw bakrenamed = [tuple(line.strip().split(' ')) for line in fd]
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw raise util.Abort("couldn't open renames file %s: %s\n" %
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw '''Remove backed up uncommitted changes'''
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw '''Backup of workspace metadata'''
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw '''Backup workspace metadata'''
7d968cb8b4b6274092771b93e94bf88d1ee31c6cdougm # Files is a list of tuples (name, path), where name is as in
7d968cb8b4b6274092771b93e94bf88d1ee31c6cdougm # self.files, and path is the absolute path.
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw files = filter(lambda (name, path): os.path.exists(path),
7d968cb8b4b6274092771b93e94bf88d1ee31c6cdougm # tarfile.TarError doesn't include the tar member or file
7d968cb8b4b6274092771b93e94bf88d1ee31c6cdougm # in question, so we have to do so ourselves.
330ef417fbd2286149a25e8033587edf7ae52ae5dougm '''Restore workspace metadata from an pre-tar backup'''
330ef417fbd2286149a25e8033587edf7ae52ae5dougm raise util.Abort("couldn't restore metadata from %s:\n"
7d968cb8b4b6274092771b93e94bf88d1ee31c6cdougm '''Restore workspace metadata (from a tar-style backup)'''
7d968cb8b4b6274092771b93e94bf88d1ee31c6cdougm raise util.Abort("couldn't open %s: %s" % (tarpath, e))
7d968cb8b4b6274092771b93e94bf88d1ee31c6cdougm # Make sure the member name is in the exception message.
330ef417fbd2286149a25e8033587edf7ae52ae5dougm raise util.Abort("couldn't restore metadata from %s:\n"
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm '''Restore workspace metadata'''
330ef417fbd2286149a25e8033587edf7ae52ae5dougm '''Yield the repo-relative path to each file we operate on,
330ef417fbd2286149a25e8033587edf7ae52ae5dougm including each file within any affected directory'''
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw '''Compare backed up workspace metadata to workspace'''
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw '''Remove a possible trailing path separator from PATHNAME'''
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw return pathname.endswith('/') and pathname[:-1] or pathname
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm raise util.Abort("couldn't open metadata tarball: %s\n"
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm tarnames = map(strip_trailing_pathsep, tar.getnames())
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm '''Remove backed up workspace metadata'''
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm '''A backup (in tar format) of complete source files from every
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm workspace head.
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm Paths in the tarball are prefixed by the revision and node of the
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm head, or "working" for the working directory.
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm This is done purely for the benefit of the user, and as such takes
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm no part in restore or need_backup checking, restore always
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm succeeds, need_backup always returns False
6185db853e024a486ff8837e6784dd290d866112dougm '''Return a list of tuples (parenttip, localtip) for each
6185db853e024a486ff8837e6784dd290d866112dougm outgoing head. If the working copy contains modified files,
6185db853e024a486ff8837e6784dd290d866112dougm it is a head, and neither of its parents are.
6185db853e024a486ff8837e6784dd290d866112dougm outnodes = set(self.ws.repo.changelog.nodesbetween(outgoing)[0])
6185db853e024a486ff8837e6784dd290d866112dougm heads = [self.ws.repo.changectx(n) for n in self.ws.repo.heads()
6185db853e024a486ff8837e6784dd290d866112dougm heads = filter(lambda x: x not in wctx.parents(), heads) + [wctx]
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm '''Save a clear copy of each source file modified between each
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm head and that head's parenttip (see WorkSpace.parenttip).
6185db853e024a486ff8837e6784dd290d866112dougm raise util.Abort("Could not open %s for writing: %s" %
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm tpath = child.node() and node.short(child.node()) or "working"
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm for fname, change in self.ws.status(parent, child).iteritems():
6185db853e024a486ff8837e6784dd290d866112dougm if crev is None:
6185db853e024a486ff8837e6784dd290d866112dougm '''Cleanup a failed Clear backup.
6185db853e024a486ff8837e6784dd290d866112dougm Remove the clear tarball from the backup directory.
6185db853e024a486ff8837e6784dd290d866112dougm '''Clear backups are never restored, do nothing'''
6185db853e024a486ff8837e6784dd290d866112dougm '''Clear backups are never compared, return False (no backup needed).
6185db853e024a486ff8837e6784dd290d866112dougm Should a backup actually be needed, one of the other
6185db853e024a486ff8837e6784dd290d866112dougm implementation classes would notice in any situation we would.
6185db853e024a486ff8837e6784dd290d866112dougm '''A backup of a given workspace'''
6185db853e024a486ff8837e6784dd290d866112dougm # The order of instances here controls the order the various operations
6185db853e024a486ff8837e6784dd290d866112dougm # There's some inherent dependence, in that on restore we need
6185db853e024a486ff8837e6784dd290d866112dougm # to restore committed changes prior to uncommitted changes
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm # (as the parent revision of any uncommitted changes is quite
6185db853e024a486ff8837e6784dd290d866112dougm # likely to not exist until committed changes are restored).
6185db853e024a486ff8837e6784dd290d866112dougm # Metadata restore can happen at any point, but happens last
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm # as a matter of convention.
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm self.modules = [x(self, ws) for x in [CdmCommittedBackup,
6185db853e024a486ff8837e6784dd290d866112dougm if os.path.exists(os.path.join(self.backupdir, 'latest')):
6185db853e024a486ff8837e6784dd290d866112dougm generation = os.readlink(os.path.join(self.backupdir, 'latest'))
6185db853e024a486ff8837e6784dd290d866112dougm '''Find the path to an appropriate backup directory based on NAME'''
6185db853e024a486ff8837e6784dd290d866112dougm backupbase = os.path.expanduser(self.ui.config('cdm', 'backupdir'))
6185db853e024a486ff8837e6784dd290d866112dougm home = os.getenv('HOME') or pwd.getpwuid(os.getuid()).pw_dir
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw pass # Handled anyway
6185db853e024a486ff8837e6784dd290d866112dougm raise util.Abort('Could not determine your HOME directory to '
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm 'find backup path')
6185db853e024a486ff8837e6784dd290d866112dougm # If backupdir exists, it must be a directory.
6185db853e024a486ff8837e6784dd290d866112dougm if (os.path.exists(backupdir) and not os.path.isdir(backupdir)):
6185db853e024a486ff8837e6784dd290d866112dougm raise util.Abort('%s exists but is not a directory' % backupdir)
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm '''Update latest symlink to point to the current generation'''
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm '''Create a new backup generation'''
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm raise util.Abort("Couldn't create backup generation %s: %s" %
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm '''return full path to backup file FILE at GEN'''
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm return os.path.join(self.backupdir, str(self.generation), path)
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm '''Unlink the specified path from the backup directory.
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm A no-op if the path does not exist.
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm '''Open the specified file in the backup directory'''
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm '''Return boolean indicating wether a given file exists in the
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw backup directory.'''
6185db853e024a486ff8837e6784dd290d866112dougm '''Compare backed up changes to workspace'''
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm # If there's no current backup generation, or the last backup was
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm # invalid (lacking the dirstate file), we need a backup regardless
6185db853e024a486ff8837e6784dd290d866112dougm # of anything else.
6185db853e024a486ff8837e6784dd290d866112dougm if not self.generation or not self.exists('dirstate'):
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw '''Take a backup of the current workspace
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw Calling code is expected to hold both the working copy lock
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw and repository lock.'''
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw raise util.Abort('Could not create backup directory %s: %s' %
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw show_traceback = self.ws.ui.configbool('ui', 'traceback',
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw # If it's not a 'normal' error, we want to print a stack
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw # trace now in case the attempt to remove the partial
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw # backup also fails, and raises a second exception.
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw os.rmdir(os.path.join(self.backupdir, str(self.generation)))
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw '''Restore workspace from backup
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw Restores from backup generation GEN (defaulting to the latest)
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw into workspace WS.
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw Calling code is expected to hold both the working copy lock
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw and repository lock of the destination workspace.'''
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw raise util.Abort('Backup directory does not exist: %s' %
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw if not os.path.exists(os.path.join(self.backupdir, str(gen))):
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw raise util.Abort('Backup generation does not exist: %s' %
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw if not self.generation: # This is OK, 0 is not a valid generation
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw raise util.Abort('Backup has no generations: %s' % self.backupdir)
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw raise util.Abort('Backup %s/%s is incomplete (dirstate missing)' %
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw 'Workspace may be partially restored' % e)