Backup.py revision 2b5878de2735cb61d008168e1f27e390d2edf915
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson# This program is free software; you can redistribute it and/or modify
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson# it under the terms of the GNU General Public License version 2
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson# as published by the Free Software Foundation.
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson# This program is distributed in the hope that it will be useful,
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson# but WITHOUT ANY WARRANTY; without even the implied warranty of
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson# GNU General Public License for more details.
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson# You should have received a copy of the GNU General Public License
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson# along with this program; if not, write to the Free Software
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
12203c7149a0ba35c436436cd2400dc7aaba1e62Richard Lowe# Copyright 2009 Sun Microsystems, Inc. All rights reserved.
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson# Use is subject to license terms.
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelsonWorkspace backup
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelsonBackup format is:
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson generation#/
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson dirstate (handled by CdmUncommittedBackup)
12203c7149a0ba35c436436cd2400dc7aaba1e62Richard Lowe File containing dirstate nodeid (the changeset we need
12203c7149a0ba35c436436cd2400dc7aaba1e62Richard Lowe to update the workspace to after applying the bundle).
12203c7149a0ba35c436436cd2400dc7aaba1e62Richard Lowe This is the node to which the working copy changes
12203c7149a0ba35c436436cd2400dc7aaba1e62Richard Lowe (see 'diff', below) will be applied if applicable.
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson bundle (handled by CdmCommittedBackup)
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson An Hg bundle containing outgoing committed changes.
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson nodes (handled by CdmCommittedBackup)
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson A text file listing the full (hex) nodeid of all nodes in
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson bundle, used by need_backup.
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson diff (handled by CdmUncommittedBackup)
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson A Git-formatted diff containing uncommitted changes.
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson renames (handled by CdmUncommittedBackup)
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson A list of renames in the working copy that have to be
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson applied manually, rather than by the diff.
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson metadata.tar.gz (handled by CdmMetadataBackup)
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson $CODEMGR_WS/.hg/hgrc
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson $CODEMGR_WS/.hg/localtags
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson $CODEMGR_WS/.hg/patches (Mq data)
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson latest -> generation#
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson Newest backup generation.
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelsonAll files in a given backup generation, with the exception of
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelsondirstate, are optional.
12203c7149a0ba35c436436cd2400dc7aaba1e62Richard Lowefrom mercurial import changegroup, patch, node, util, revlog
12203c7149a0ba35c436436cd2400dc7aaba1e62Richard Lowe '''a required node is not present in the destination workspace.
12203c7149a0ba35c436436cd2400dc7aaba1e62Richard Lowe This may occur both in the case where the bundle contains a
12203c7149a0ba35c436436cd2400dc7aaba1e62Richard Lowe changeset which is a child of a node not present in the
12203c7149a0ba35c436436cd2400dc7aaba1e62Richard Lowe destination workspace (because the destination workspace is not as
12203c7149a0ba35c436436cd2400dc7aaba1e62Richard Lowe up-to-date as the source), or because the source and destination
12203c7149a0ba35c436436cd2400dc7aaba1e62Richard Lowe workspace are not related.
12203c7149a0ba35c436436cd2400dc7aaba1e62Richard Lowe It may also happen in cases where the uncommitted changes need to
12203c7149a0ba35c436436cd2400dc7aaba1e62Richard Lowe be applied onto a node that the workspace does not possess even
12203c7149a0ba35c436436cd2400dc7aaba1e62Richard Lowe after application of the bundle (on a branch not present
12203c7149a0ba35c436436cd2400dc7aaba1e62Richard Lowe in the bundle or destination workspace, for instance)'''
12203c7149a0ba35c436436cd2400dc7aaba1e62Richard Lowe # If e.name is a string 20 characters long, it is
12203c7149a0ba35c436436cd2400dc7aaba1e62Richard Lowe # assumed to be a node. (Mercurial makes this
12203c7149a0ba35c436436cd2400dc7aaba1e62Richard Lowe # same assumption, when creating a LookupError)
12203c7149a0ba35c436436cd2400dc7aaba1e62Richard Lowe util.Abort.__init__(self, "%s: changeset '%s' is missing\n"
12203c7149a0ba35c436436cd2400dc7aaba1e62Richard Lowe "Your workspace is either not "
12203c7149a0ba35c436436cd2400dc7aaba1e62Richard Lowe "sufficiently up to date,\n"
12203c7149a0ba35c436436cd2400dc7aaba1e62Richard Lowe "or is unrelated to the workspace from "
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson '''Backup of committed changes'''
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson '''Return a list of all outgoing nodes in hex format'''
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson nodes = self.ws.repo.changelog.nodesbetween(outgoing)[0]
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson '''Backup committed changes'''
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson self.ws.ui.warn('Workspace has no parent, committed changes will '
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson 'not be backed up\n')
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson changegroup.writebundle(cg, self.bu.backupfile('bundle'), 'HG10BZ')
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson raise util.Abort("couldn't store outgoing nodes: %s" % e)
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson '''Restore committed changes from backup'''
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson raise util.Abort("couldn't restore committed changes: %s\n"
12203c7149a0ba35c436436cd2400dc7aaba1e62Richard Lowe raise CdmNodeMissing("couldn't restore committed changes",
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson if f and not f.closed:
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson '''Compare backup of committed changes to workspace'''
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson raise util.Abort("couldn't open backup node list: %s" % e)
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson if f and not f.closed:
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson outnodes = set(self._outgoing_nodes(self.ws.parent()))
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson '''Remove backed up committed changes'''
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson '''Backup of uncommitted changes'''
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson '''Return a list of pairs of files representing renames/copies
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson that clobber already versioned files. [(oldname newname)...]'''
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson # Note that this doesn't handle uncommitted merges
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson # as CdmUncommittedBackup itself doesn't.
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson '''Backup uncommitted changes'''
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson raise util.Abort("Unable to backup an uncommitted merge.\n"
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson "Please complete your merge and commit")
2b5878de2735cb61d008168e1f27e390d2edf915Rich Lowe dirstate = node.hex(self.ws.workingctx().parents()[0].node())
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson raise util.Abort("couldn't save working copy parent: %s" % e)
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson raise util.Abort("couldn't save clobbering copies: %s" % e)
2b5878de2735cb61d008168e1f27e390d2edf915Rich Lowe opts = patch.diffopts(self.ws.ui, opts={'git': True})
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson raise util.Abort("couldn't save working copy diff: %s" % e)
2b5878de2735cb61d008168e1f27e390d2edf915Rich Lowe '''Return the desired working copy node from the backup'''
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson raise util.Abort("couldn't read saved parent: %s" % e)
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson '''Restore uncommitted changes'''
2b5878de2735cb61d008168e1f27e390d2edf915Rich Lowe # Check that the patch's parent changeset exists.
12203c7149a0ba35c436436cd2400dc7aaba1e62Richard Lowe raise CdmNodeMissing("couldn't restore uncommitted changes",
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson raise util.Abort("couldn't update to saved node: %s" % e)
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson # There's a race here whereby if the patch (or part thereof)
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson # is applied within the same second as the clean above (such
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson # that mtime doesn't change) and if the size of that file
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson # does not change, Hg may not see the change.
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson # We sleep a full second to avoid this, as sleeping merely
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson # until the next second begins would require very close clock
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson # synchronization on network filesystems.
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson raise util.Abort('working copy diff applied with fuzz')
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson raise util.Abort("couldn't apply working copy diff: %s\n"
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson if not os.path.exists(self.bu.backupfile('renames')):
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson # We need to re-apply name changes where the new name
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson # (rename/copy destination) is an already versioned file, as
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson # Hg would otherwise ignore them.
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson raise util.Abort('unable to open renames file: %s' % e)
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson '''Compare backup of uncommitted changes to workspace'''
2b5878de2735cb61d008168e1f27e390d2edf915Rich Lowe opts = patch.diffopts(self.ws.ui, opts={'git': True})
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson bakrenamed = [line.strip().split(' ') for line in fd]
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson raise util.Abort("couldn't open renames file %s: %s\n" %
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson '''Remove backed up uncommitted changes'''
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson '''Backup of workspace metadata'''
9a70fc3be3b1e966bf78825cdb8d509963a6f0a1Mark J. Nelson self.files = ('hgrc', 'localtags', 'patches', 'cdm')
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson '''Backup workspace metadata'''
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson tar = tarfile.open(self.bu.backupfile('metadata.tar.gz'),
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson raise util.Abort("couldn't open %s for writing: %s" %
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson # tarfile.TarError doesn't include the tar member or file
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson # in question, so we have to do so ourselves.
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson raise util.Abort("couldn't backup metadata to %s:\n"
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson '''Restore workspace metadata from an pre-tar backup'''
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson raise util.Abort("couldn't restore metadata from %s:\n"
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson '''Restore workspace metadata (from a tar-style backup)'''
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson if os.path.exists(self.bu.backupfile('metadata.tar.gz')):
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson tar = tarfile.open(self.bu.backupfile('metadata.tar.gz'))
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson # Make sure the member name is in the exception message.
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson raise util.Abort("couldn't restore metadata from %s:\n"
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson '''Restore workspace metadata'''
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson '''Compare backed up workspace metadata to workspace'''
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson if os.path.exists(self.bu.backupfile('metadata.tar.gz')):
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson tar = tarfile.open(self.bu.backupfile('metadata.tar.gz'))
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson raise util.Abort("couldn't open metadata tarball: %s\n"
2b5878de2735cb61d008168e1f27e390d2edf915Rich Lowe # The filesystem can give us mtime with fractional seconds
2b5878de2735cb61d008168e1f27e390d2edf915Rich Lowe # (as a float), whereas tar files only keep it to the second.
2b5878de2735cb61d008168e1f27e390d2edf915Rich Lowe # Always compare to the integer (second-granularity) mtime.
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson # Directories in tarfile always end with a '/'
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson for root, dirs, files in os.walk(fpath, topdown=True):
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson if os.path.exists(fpath) and mfile not in tarnames:
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson '''Remove backed up workspace metadata'''
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson if os.path.exists(self.bu.backupfile('metadata.tar.gz')):
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson '''A backup of a given workspace'''
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson # The order of instances here controls the order the various operations
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson # There's some inherent dependence, in that on restore we need
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson # to restore committed changes prior to uncommitted changes
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson # (as the parent revision of any uncommitted changes is quite
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson # likely to not exist until committed changes are restored).
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson # Metadata restore can happen at any point, but happens last
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson # as a matter of convention.
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson self.modules = [x(self, ws) for x in [CdmCommittedBackup,
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson if os.path.exists(os.path.join(self.backupdir, 'latest')):
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson generation = os.readlink(os.path.join(self.backupdir, 'latest'))
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson self.generation = int(os.path.split(generation)[1])
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson '''Find the path to an appropriate backup directory based on NAME'''
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson backupbase = os.path.expanduser(self.ui.config('cdm', 'backupdir'))
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson home = os.getenv('HOME') or pwd.getpwuid(os.getuid()).pw_dir
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson pass # Handled anyway
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson raise util.Abort('Could not determine your HOME directory to '
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson 'find backup path')
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson # If backupdir exists, it must be a directory.
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson if (os.path.exists(backupdir) and not os.path.isdir(backupdir)):
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson raise util.Abort('%s exists but is not a directory' % backupdir)
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson '''return full path to backup file FILE at GEN'''
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson return os.path.join(self.backupdir, str(self.generation), path)
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson '''Update latest symlink to point to the current generation'''
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson '''Create a new backup generation'''
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson os.makedirs(os.path.join(self.backupdir, str(gen)))
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson raise util.Abort("Couldn't create backup generation %s: %s" %
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson '''Compare backed up changes to workspace'''
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson # If there's no current backup generation, or the last backup was
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson # invalid (lacking the dirstate file), we need a backup regardless
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson # of anything else.
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson '''Take a backup of the current workspace'''
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson raise util.Abort('Could not create backup directory %s: %s' %
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson # Lock the repo, so the backup can be consistent. We need the
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson # wlock too to make sure the dirstate parent doesn't change
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson # underneath us.
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson # If it's not a 'normal' error, we want to print a stack
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson # trace now in case the attempt to remove the partial
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson # backup also fails, and raises a second exception.
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson if (not isinstance(e, (EnvironmentError, util.Abort))
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson os.rmdir(os.path.join(self.backupdir, str(self.generation)))
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson '''Restore workspace from backup
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson Restores from backup generation GEN (defaulting to the latest)
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson into workspace WS.'''
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson raise util.Abort('Backup directory does not exist: %s' %
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson if not os.path.exists(os.path.join(self.backupdir, str(gen))):
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson raise util.Abort('Backup generation does not exist: %s' %
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson if not self.generation: # This is ok, 0 is not a valid generation
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson raise util.Abort('Backup has no generations: %s' % self.backupdir)
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson if not os.path.exists(self.backupfile('dirstate')):
cdf0c1d55d9b3b6beaf994835440dfb01aef5cf0mjnelson raise util.Abort('Backup %s/%s is incomplete (dirstate missing)' %
12203c7149a0ba35c436436cd2400dc7aaba1e62Richard Lowe 'Workspace may be partially restored' % e)