Backup.py revision 605a716e6d38b3af09034c254382d0ae3b7d5f70
6185db853e024a486ff8837e6784dd290d866112dougm#
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#
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#
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#
6185db853e024a486ff8837e6784dd290d866112dougm
6185db853e024a486ff8837e6784dd290d866112dougm#
6185db853e024a486ff8837e6784dd290d866112dougm# Copyright 2009 Sun Microsystems, Inc. All rights reserved.
6185db853e024a486ff8837e6784dd290d866112dougm# Use is subject to license terms.
6185db853e024a486ff8837e6784dd290d866112dougm#
6185db853e024a486ff8837e6784dd290d866112dougm# Copyright 2008, 2011, Richard Lowe
6185db853e024a486ff8837e6784dd290d866112dougm#
6185db853e024a486ff8837e6784dd290d866112dougm
dc20a3024900c47dd2ee44b9707e6df38f7d62a5as
6185db853e024a486ff8837e6784dd290d866112dougm'''
6185db853e024a486ff8837e6784dd290d866112dougmWorkspace backup
6185db853e024a486ff8837e6784dd290d866112dougm
6185db853e024a486ff8837e6784dd290d866112dougmBackup format is:
6185db853e024a486ff8837e6784dd290d866112dougm backupdir/
6185db853e024a486ff8837e6784dd290d866112dougm wsname/
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
6185db853e024a486ff8837e6784dd290d866112dougm bundle (handled by CdmCommittedBackup)
6185db853e024a486ff8837e6784dd290d866112dougm An Hg bundle containing outgoing committed changes.
6185db853e024a486ff8837e6784dd290d866112dougm
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
6185db853e024a486ff8837e6784dd290d866112dougm diff (handled by CdmUncommittedBackup)
6185db853e024a486ff8837e6784dd290d866112dougm A Git-formatted diff containing uncommitted changes.
6185db853e024a486ff8837e6784dd290d866112dougm
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.
6185db853e024a486ff8837e6784dd290d866112dougm
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
6185db853e024a486ff8837e6784dd290d866112dougm clear.tar.gz (handled by CdmClearBackup)
6185db853e024a486ff8837e6784dd290d866112dougm <short node>/
6185db853e024a486ff8837e6784dd290d866112dougm copies of each modified or added file, as it is in
6185db853e024a486ff8837e6784dd290d866112dougm this head.
6185db853e024a486ff8837e6784dd290d866112dougm
6185db853e024a486ff8837e6784dd290d866112dougm ... for each outgoing head
6185db853e024a486ff8837e6784dd290d866112dougm
6185db853e024a486ff8837e6784dd290d866112dougm working/
6185db853e024a486ff8837e6784dd290d866112dougm copies of each modified or added file in the
6185db853e024a486ff8837e6784dd290d866112dougm working copy if any.
6185db853e024a486ff8837e6784dd290d866112dougm
6185db853e024a486ff8837e6784dd290d866112dougm latest -> generation#
6185db853e024a486ff8837e6784dd290d866112dougm Newest backup generation.
6185db853e024a486ff8837e6784dd290d866112dougm
6185db853e024a486ff8837e6784dd290d866112dougmAll files in a given backup generation, with the exception of
6185db853e024a486ff8837e6784dd290d866112dougmdirstate, are optional.
6185db853e024a486ff8837e6784dd290d866112dougm'''
6185db853e024a486ff8837e6784dd290d866112dougm
6185db853e024a486ff8837e6784dd290d866112dougmimport grp, os, pwd, shutil, tarfile, time, traceback
6185db853e024a486ff8837e6784dd290d866112dougmfrom cStringIO import StringIO
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm
6185db853e024a486ff8837e6784dd290d866112dougmfrom mercurial import changegroup, error, node, patch, util
6185db853e024a486ff8837e6784dd290d866112dougm
6185db853e024a486ff8837e6784dd290d866112dougm
6185db853e024a486ff8837e6784dd290d866112dougmclass CdmNodeMissing(util.Abort):
6185db853e024a486ff8837e6784dd290d866112dougm '''a required node is not present in the destination workspace.
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
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
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
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw def __init__(self, msg, name):
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw #
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 #
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw if isinstance(name, str) and len(name) == 20:
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw n = node.short(name)
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw else:
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw n = name
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
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 "which the backup was taken.\n" % (msg, n))
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amwclass CdmTarFile(tarfile.TarFile):
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw '''Tar file access + simple comparison to the filesystem, and
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw creation addition of files from Mercurial filectx objects.'''
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw def __init__(self, *args, **kwargs):
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw tarfile.TarFile.__init__(self, *args, **kwargs)
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw self.errorlevel = 2
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw def members_match_fs(self, rootpath):
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw '''Compare the contents of the tar archive to the directory
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw specified by rootpath. Return False if they differ.
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw Every file in the archive must match the equivalent file in
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw the filesystem.
55bf511df53aad0fdb7eb3fa349f0308cc05234cas
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw The existence, modification time, and size of each file are
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw compared, content is not.'''
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw def _member_matches_fs(member, rootpath):
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw '''Compare a single member to its filesystem counterpart'''
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw fpath = os.path.join(rootpath, member.name)
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw if not os.path.exists(fpath):
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw return False
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw elif ((os.path.isfile(fpath) != member.isfile()) or
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw (os.path.isdir(fpath) != member.isdir()) or
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw (os.path.islink(fpath) != member.issym())):
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw return False
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw #
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)
55bf511df53aad0fdb7eb3fa349f0308cc05234cas #
55bf511df53aad0fdb7eb3fa349f0308cc05234cas elif member.mtime != int(os.path.getmtime(fpath)):
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw return False
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw elif not member.isdir() and member.size != os.path.getsize(fpath):
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw return False
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw else:
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw return True
55bf511df53aad0fdb7eb3fa349f0308cc05234cas
55bf511df53aad0fdb7eb3fa349f0308cc05234cas for elt in self:
55bf511df53aad0fdb7eb3fa349f0308cc05234cas if not _member_matches_fs(elt, rootpath):
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw return False
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw return True
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw def addfilectx(self, filectx, path=None):
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw '''Add a filectx object to the archive.
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw Use the path specified by the filectx object or, if specified,
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw the PATH argument.
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
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 '''
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw t = tarfile.TarInfo(path or filectx.path())
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw t.size = filectx.size()
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw t.mtime = filectx.date()[0]
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw t.uid = os.getuid()
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw t.gid = os.getgid()
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw try:
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw t.uname = pwd.getpwuid(t.uid).pw_name
55bf511df53aad0fdb7eb3fa349f0308cc05234cas except KeyError:
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw t.uname = "unknown"
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw try:
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw t.gname = grp.getgrgid(t.gid).gr_name
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw except KeyError:
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw t.gname = "unknown"
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw #
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 #
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw if 'l' in filectx.flags():
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw t.type = tarfile.SYMTYPE
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw t.mode = 0777
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw t.linkname = filectx.data()
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw data = None
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw else:
55bf511df53aad0fdb7eb3fa349f0308cc05234cas t.type = tarfile.REGTYPE
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw t.mode = 'x' in filectx.flags() and 0755 or 0644
55bf511df53aad0fdb7eb3fa349f0308cc05234cas data = StringIO(filectx.data())
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
55bf511df53aad0fdb7eb3fa349f0308cc05234cas self.addfile(t, data)
55bf511df53aad0fdb7eb3fa349f0308cc05234cas
55bf511df53aad0fdb7eb3fa349f0308cc05234cas
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amwclass CdmCommittedBackup(object):
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw '''Backup of committed changes'''
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw def __init__(self, backup, ws):
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw self.ws = ws
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw self.bu = backup
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw self.files = ('bundle', 'nodes')
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw def _outgoing_nodes(self, parent):
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw '''Return a list of all outgoing nodes in hex format'''
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw if parent:
573b0c00a1ee520c3f6938dda8d693236f37ae60dougm outgoing = self.ws.findoutgoing(parent)
573b0c00a1ee520c3f6938dda8d693236f37ae60dougm nodes = self.ws.repo.changelog.nodesbetween(outgoing)[0]
573b0c00a1ee520c3f6938dda8d693236f37ae60dougm return map(node.hex, nodes)
573b0c00a1ee520c3f6938dda8d693236f37ae60dougm else:
573b0c00a1ee520c3f6938dda8d693236f37ae60dougm return []
573b0c00a1ee520c3f6938dda8d693236f37ae60dougm
573b0c00a1ee520c3f6938dda8d693236f37ae60dougm def backup(self):
573b0c00a1ee520c3f6938dda8d693236f37ae60dougm '''Backup committed changes'''
573b0c00a1ee520c3f6938dda8d693236f37ae60dougm parent = self.ws.parent()
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
573b0c00a1ee520c3f6938dda8d693236f37ae60dougm if not parent:
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw self.ws.ui.warn('Workspace has no parent, committed changes will '
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw 'not be backed up\n')
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw return
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
573b0c00a1ee520c3f6938dda8d693236f37ae60dougm out = self.ws.findoutgoing(parent)
573b0c00a1ee520c3f6938dda8d693236f37ae60dougm if not out:
573b0c00a1ee520c3f6938dda8d693236f37ae60dougm return
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw cg = self.ws.repo.changegroup(out, 'bundle')
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw changegroup.writebundle(cg, self.bu.backupfile('bundle'), 'HG10BZ')
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw outnodes = self._outgoing_nodes(parent)
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw if not outnodes:
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw return
573b0c00a1ee520c3f6938dda8d693236f37ae60dougm
573b0c00a1ee520c3f6938dda8d693236f37ae60dougm fp = None
573b0c00a1ee520c3f6938dda8d693236f37ae60dougm try:
573b0c00a1ee520c3f6938dda8d693236f37ae60dougm try:
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw fp = self.bu.open('nodes', 'w')
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw fp.write('%s\n' % '\n'.join(outnodes))
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw except EnvironmentError, e:
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw raise util.Abort("couldn't store outgoing nodes: %s" % e)
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw finally:
573b0c00a1ee520c3f6938dda8d693236f37ae60dougm if fp and not fp.closed:
573b0c00a1ee520c3f6938dda8d693236f37ae60dougm fp.close()
573b0c00a1ee520c3f6938dda8d693236f37ae60dougm
573b0c00a1ee520c3f6938dda8d693236f37ae60dougm def restore(self):
573b0c00a1ee520c3f6938dda8d693236f37ae60dougm '''Restore committed changes from backup'''
573b0c00a1ee520c3f6938dda8d693236f37ae60dougm
573b0c00a1ee520c3f6938dda8d693236f37ae60dougm if not self.bu.exists('bundle'):
573b0c00a1ee520c3f6938dda8d693236f37ae60dougm return
573b0c00a1ee520c3f6938dda8d693236f37ae60dougm
573b0c00a1ee520c3f6938dda8d693236f37ae60dougm bpath = self.bu.backupfile('bundle')
573b0c00a1ee520c3f6938dda8d693236f37ae60dougm f = None
573b0c00a1ee520c3f6938dda8d693236f37ae60dougm try:
573b0c00a1ee520c3f6938dda8d693236f37ae60dougm try:
573b0c00a1ee520c3f6938dda8d693236f37ae60dougm f = self.bu.open('bundle')
573b0c00a1ee520c3f6938dda8d693236f37ae60dougm bundle = changegroup.readbundle(f, bpath)
573b0c00a1ee520c3f6938dda8d693236f37ae60dougm self.ws.repo.addchangegroup(bundle, 'strip',
573b0c00a1ee520c3f6938dda8d693236f37ae60dougm 'bundle:%s' % bpath)
573b0c00a1ee520c3f6938dda8d693236f37ae60dougm except EnvironmentError, e:
573b0c00a1ee520c3f6938dda8d693236f37ae60dougm raise util.Abort("couldn't restore committed changes: %s\n"
573b0c00a1ee520c3f6938dda8d693236f37ae60dougm " %s" % (bpath, e))
573b0c00a1ee520c3f6938dda8d693236f37ae60dougm except error.LookupError, e:
573b0c00a1ee520c3f6938dda8d693236f37ae60dougm raise CdmNodeMissing("couldn't restore committed changes",
573b0c00a1ee520c3f6938dda8d693236f37ae60dougm e.name)
573b0c00a1ee520c3f6938dda8d693236f37ae60dougm finally:
573b0c00a1ee520c3f6938dda8d693236f37ae60dougm if f and not f.closed:
573b0c00a1ee520c3f6938dda8d693236f37ae60dougm f.close()
573b0c00a1ee520c3f6938dda8d693236f37ae60dougm
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw def need_backup(self):
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw '''Compare backup of committed changes to workspace'''
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw if self.bu.exists('nodes'):
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw f = None
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw try:
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw try:
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw f = self.bu.open('nodes')
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw bnodes = set(line.rstrip('\r\n') for line in f.readlines())
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw f.close()
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw except EnvironmentError, e:
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw raise util.Abort("couldn't open backup node list: %s" % e)
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw finally:
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw if f and not f.closed:
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw f.close()
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw else:
6185db853e024a486ff8837e6784dd290d866112dougm bnodes = set()
6185db853e024a486ff8837e6784dd290d866112dougm
6185db853e024a486ff8837e6784dd290d866112dougm outnodes = set(self._outgoing_nodes(self.ws.parent()))
6185db853e024a486ff8837e6784dd290d866112dougm
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw #
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.
6185db853e024a486ff8837e6784dd290d866112dougm #
6185db853e024a486ff8837e6784dd290d866112dougm if not outnodes <= bnodes:
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm return True
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm return False
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
6185db853e024a486ff8837e6784dd290d866112dougm def cleanup(self):
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm '''Remove backed up committed changes'''
6185db853e024a486ff8837e6784dd290d866112dougm
6185db853e024a486ff8837e6784dd290d866112dougm for f in self.files:
6185db853e024a486ff8837e6784dd290d866112dougm self.bu.unlink(f)
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm
6185db853e024a486ff8837e6784dd290d866112dougm
6185db853e024a486ff8837e6784dd290d866112dougmclass CdmUncommittedBackup(object):
6185db853e024a486ff8837e6784dd290d866112dougm '''Backup of uncommitted changes'''
6185db853e024a486ff8837e6784dd290d866112dougm
6185db853e024a486ff8837e6784dd290d866112dougm def __init__(self, backup, ws):
6185db853e024a486ff8837e6784dd290d866112dougm self.ws = ws
6185db853e024a486ff8837e6784dd290d866112dougm self.bu = backup
6185db853e024a486ff8837e6784dd290d866112dougm self.wctx = self.ws.workingctx(worklist=True)
6185db853e024a486ff8837e6784dd290d866112dougm
6185db853e024a486ff8837e6784dd290d866112dougm def _clobbering_renames(self):
6185db853e024a486ff8837e6784dd290d866112dougm '''Return a list of pairs of files representing renames/copies
6185db853e024a486ff8837e6784dd290d866112dougm that clobber already versioned files. [(old-name new-name)...]
6185db853e024a486ff8837e6784dd290d866112dougm '''
6185db853e024a486ff8837e6784dd290d866112dougm
6185db853e024a486ff8837e6784dd290d866112dougm #
6185db853e024a486ff8837e6784dd290d866112dougm # Note that this doesn't handle uncommitted merges
6185db853e024a486ff8837e6784dd290d866112dougm # as CdmUncommittedBackup itself doesn't.
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm #
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm parent = self.wctx.parents()[0]
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm
6185db853e024a486ff8837e6784dd290d866112dougm ret = []
6185db853e024a486ff8837e6784dd290d866112dougm for fname in self.wctx.added() + self.wctx.modified():
6185db853e024a486ff8837e6784dd290d866112dougm rn = self.wctx.filectx(fname).renamed()
6185db853e024a486ff8837e6784dd290d866112dougm if rn and fname in parent:
6185db853e024a486ff8837e6784dd290d866112dougm ret.append((rn[0], fname))
6185db853e024a486ff8837e6784dd290d866112dougm return ret
6185db853e024a486ff8837e6784dd290d866112dougm
6185db853e024a486ff8837e6784dd290d866112dougm def backup(self):
6185db853e024a486ff8837e6784dd290d866112dougm '''Backup uncommitted changes'''
6185db853e024a486ff8837e6784dd290d866112dougm
6185db853e024a486ff8837e6784dd290d866112dougm if self.ws.merged():
6185db853e024a486ff8837e6784dd290d866112dougm raise util.Abort("Unable to backup an uncommitted merge.\n"
6185db853e024a486ff8837e6784dd290d866112dougm "Please complete your merge and commit")
6185db853e024a486ff8837e6784dd290d866112dougm
6185db853e024a486ff8837e6784dd290d866112dougm dirstate = node.hex(self.wctx.parents()[0].node())
6185db853e024a486ff8837e6784dd290d866112dougm
6185db853e024a486ff8837e6784dd290d866112dougm fp = None
6185db853e024a486ff8837e6784dd290d866112dougm try:
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm try:
6185db853e024a486ff8837e6784dd290d866112dougm fp = self.bu.open('dirstate', 'w')
6185db853e024a486ff8837e6784dd290d866112dougm fp.write(dirstate + '\n')
6185db853e024a486ff8837e6784dd290d866112dougm fp.close()
6185db853e024a486ff8837e6784dd290d866112dougm except EnvironmentError, e:
6185db853e024a486ff8837e6784dd290d866112dougm raise util.Abort("couldn't save working copy parent: %s" % e)
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm try:
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm fp = self.bu.open('renames', 'w')
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm for cons in self._clobbering_renames():
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm fp.write("%s %s\n" % cons)
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm fp.close()
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm except EnvironmentError, e:
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm raise util.Abort("couldn't save clobbering copies: %s" % e)
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm try:
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm fp = self.bu.open('diff', 'w')
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm match = self.ws.matcher(files=self.wctx.files())
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm fp.write(self.ws.diff(opts={'git': True}, match=match))
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm except EnvironmentError, e:
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm raise util.Abort("couldn't save working copy diff: %s" % e)
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm finally:
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm if fp and not fp.closed:
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm fp.close()
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm def _dirstate(self):
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm '''Return the desired working copy node from the backup'''
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm fp = None
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm try:
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm try:
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm fp = self.bu.open('dirstate')
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm dirstate = fp.readline().strip()
6185db853e024a486ff8837e6784dd290d866112dougm except EnvironmentError, e:
6185db853e024a486ff8837e6784dd290d866112dougm raise util.Abort("couldn't read saved parent: %s" % e)
6185db853e024a486ff8837e6784dd290d866112dougm finally:
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm if fp and not fp.closed:
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm fp.close()
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm return dirstate
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm def restore(self):
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm '''Restore uncommitted changes'''
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm dirstate = self._dirstate()
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm #
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm # Check that the patch's parent changeset exists.
6185db853e024a486ff8837e6784dd290d866112dougm #
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm try:
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm n = node.bin(dirstate)
6185db853e024a486ff8837e6784dd290d866112dougm self.ws.repo.changelog.lookup(n)
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm except error.LookupError, e:
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm raise CdmNodeMissing("couldn't restore uncommitted changes",
6185db853e024a486ff8837e6784dd290d866112dougm e.name)
6185db853e024a486ff8837e6784dd290d866112dougm
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm try:
6185db853e024a486ff8837e6784dd290d866112dougm self.ws.clean(rev=dirstate)
6185db853e024a486ff8837e6784dd290d866112dougm except util.Abort, e:
6185db853e024a486ff8837e6784dd290d866112dougm raise util.Abort("couldn't update to saved node: %s" % e)
6185db853e024a486ff8837e6784dd290d866112dougm
6185db853e024a486ff8837e6784dd290d866112dougm if not self.bu.exists('diff'):
6185db853e024a486ff8837e6784dd290d866112dougm return
6185db853e024a486ff8837e6784dd290d866112dougm
6185db853e024a486ff8837e6784dd290d866112dougm #
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 #
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 #
6185db853e024a486ff8837e6784dd290d866112dougm time.sleep(1)
6185db853e024a486ff8837e6784dd290d866112dougm
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm files = {}
6185db853e024a486ff8837e6784dd290d866112dougm try:
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm diff = self.bu.backupfile('diff')
6185db853e024a486ff8837e6784dd290d866112dougm try:
6185db853e024a486ff8837e6784dd290d866112dougm fuzz = patch.patch(diff, self.ws.ui, strip=1,
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm cwd=self.ws.repo.root, files=files)
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm if fuzz:
6185db853e024a486ff8837e6784dd290d866112dougm raise util.Abort('working copy diff applied with fuzz')
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm except Exception, e:
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm raise util.Abort("couldn't apply working copy diff: %s\n"
6185db853e024a486ff8837e6784dd290d866112dougm " %s" % (diff, e))
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm finally:
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm patch.updatedir(self.ws.ui, self.ws.repo, files)
6185db853e024a486ff8837e6784dd290d866112dougm
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm if not self.bu.exists('renames'):
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm return
6185db853e024a486ff8837e6784dd290d866112dougm
6185db853e024a486ff8837e6784dd290d866112dougm #
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 #
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw try:
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw fp = self.bu.open('renames')
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw for line in fp:
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw source, dest = line.strip().split()
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw self.ws.copy(source, dest)
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw except EnvironmentError, e:
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw raise util.Abort('unable to open renames file: %s' % e)
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw except ValueError:
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw raise util.Abort('corrupt renames file: %s' %
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw self.bu.backupfile('renames'))
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw def need_backup(self):
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw '''Compare backup of uncommitted changes to workspace'''
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw cnode = self.wctx.parents()[0].node()
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw if self._dirstate() != node.hex(cnode):
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw return True
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw fd = None
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw match = self.ws.matcher(files=self.wctx.files())
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw curdiff = self.ws.diff(opts={'git': True}, match=match)
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw try:
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw if self.bu.exists('diff'):
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw try:
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw fd = self.bu.open('diff')
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw backdiff = fd.read()
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw fd.close()
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw except EnvironmentError, e:
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw raise util.Abort("couldn't open backup diff %s\n"
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw " %s" % (self.bu.backupfile('diff'), e))
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw else:
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw backdiff = ''
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw if backdiff != curdiff:
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw return True
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw currrenamed = self._clobbering_renames()
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw bakrenamed = None
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw if self.bu.exists('renames'):
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw try:
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw fd = self.bu.open('renames')
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw bakrenamed = [tuple(line.strip().split(' ')) for line in fd]
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw fd.close()
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw except EnvironmentError, e:
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw raise util.Abort("couldn't open renames file %s: %s\n" %
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw (self.bu.backupfile('renames'), e))
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw if currrenamed != bakrenamed:
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw return True
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw finally:
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw if fd and not fd.closed:
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw fd.close()
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw return False
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw def cleanup(self):
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw '''Remove backed up uncommitted changes'''
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw for f in ('dirstate', 'diff', 'renames'):
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw self.bu.unlink(f)
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amwclass CdmMetadataBackup(object):
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw '''Backup of workspace metadata'''
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw def __init__(self, backup, ws):
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw self.bu = backup
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw self.ws = ws
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw self.files = ('hgrc', 'localtags', 'patches', 'cdm')
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw def backup(self):
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw '''Backup workspace metadata'''
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw tarpath = self.bu.backupfile('metadata.tar.gz')
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
7d968cb8b4b6274092771b93e94bf88d1ee31c6cdougm #
7d968cb8b4b6274092771b93e94bf88d1ee31c6cdougm # Files is a list of tuples (name, path), where name is as in
7d968cb8b4b6274092771b93e94bf88d1ee31c6cdougm # self.files, and path is the absolute path.
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw #
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw files = filter(lambda (name, path): os.path.exists(path),
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw zip(self.files, map(self.ws.repo.join, self.files)))
6185db853e024a486ff8837e6784dd290d866112dougm
7d968cb8b4b6274092771b93e94bf88d1ee31c6cdougm if not files:
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw return
6185db853e024a486ff8837e6784dd290d866112dougm
6185db853e024a486ff8837e6784dd290d866112dougm try:
7d968cb8b4b6274092771b93e94bf88d1ee31c6cdougm tar = CdmTarFile.gzopen(tarpath, 'w')
7d968cb8b4b6274092771b93e94bf88d1ee31c6cdougm except (EnvironmentError, tarfile.TarError), e:
7d968cb8b4b6274092771b93e94bf88d1ee31c6cdougm raise util.Abort("couldn't open %s for writing: %s" %
7d968cb8b4b6274092771b93e94bf88d1ee31c6cdougm (tarpath, e))
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm try:
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw for name, path in files:
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw try:
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw tar.add(path, name)
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw except (EnvironmentError, tarfile.TarError), e:
7d968cb8b4b6274092771b93e94bf88d1ee31c6cdougm #
7d968cb8b4b6274092771b93e94bf88d1ee31c6cdougm # tarfile.TarError doesn't include the tar member or file
7d968cb8b4b6274092771b93e94bf88d1ee31c6cdougm # in question, so we have to do so ourselves.
330ef417fbd2286149a25e8033587edf7ae52ae5dougm #
330ef417fbd2286149a25e8033587edf7ae52ae5dougm if isinstance(e, tarfile.TarError):
330ef417fbd2286149a25e8033587edf7ae52ae5dougm errstr = "%s: %s" % (name, e)
330ef417fbd2286149a25e8033587edf7ae52ae5dougm else:
330ef417fbd2286149a25e8033587edf7ae52ae5dougm errstr = str(e)
330ef417fbd2286149a25e8033587edf7ae52ae5dougm
330ef417fbd2286149a25e8033587edf7ae52ae5dougm raise util.Abort("couldn't backup metadata to %s:\n"
330ef417fbd2286149a25e8033587edf7ae52ae5dougm " %s" % (tarpath, errstr))
330ef417fbd2286149a25e8033587edf7ae52ae5dougm finally:
330ef417fbd2286149a25e8033587edf7ae52ae5dougm tar.close()
330ef417fbd2286149a25e8033587edf7ae52ae5dougm
330ef417fbd2286149a25e8033587edf7ae52ae5dougm def old_restore(self):
330ef417fbd2286149a25e8033587edf7ae52ae5dougm '''Restore workspace metadata from an pre-tar backup'''
330ef417fbd2286149a25e8033587edf7ae52ae5dougm
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm for fname in self.files:
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm if self.bu.exists(fname):
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw bfile = self.bu.backupfile(fname)
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm wfile = self.ws.repo.join(fname)
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm try:
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm shutil.copy2(bfile, wfile)
330ef417fbd2286149a25e8033587edf7ae52ae5dougm except EnvironmentError, e:
330ef417fbd2286149a25e8033587edf7ae52ae5dougm raise util.Abort("couldn't restore metadata from %s:\n"
330ef417fbd2286149a25e8033587edf7ae52ae5dougm " %s" % (bfile, e))
330ef417fbd2286149a25e8033587edf7ae52ae5dougm
7d968cb8b4b6274092771b93e94bf88d1ee31c6cdougm def tar_restore(self):
7d968cb8b4b6274092771b93e94bf88d1ee31c6cdougm '''Restore workspace metadata (from a tar-style backup)'''
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw if not self.bu.exists('metadata.tar.gz'):
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw return
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw tarpath = self.bu.backupfile('metadata.tar.gz')
7d968cb8b4b6274092771b93e94bf88d1ee31c6cdougm
7d968cb8b4b6274092771b93e94bf88d1ee31c6cdougm try:
549ec3fff108310966327d1dc9004551b63210b7dougm tar = CdmTarFile.gzopen(tarpath)
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw except (EnvironmentError, tarfile.TarError), e:
7d968cb8b4b6274092771b93e94bf88d1ee31c6cdougm raise util.Abort("couldn't open %s: %s" % (tarpath, e))
330ef417fbd2286149a25e8033587edf7ae52ae5dougm
6185db853e024a486ff8837e6784dd290d866112dougm try:
6185db853e024a486ff8837e6784dd290d866112dougm for elt in tar:
6185db853e024a486ff8837e6784dd290d866112dougm try:
6185db853e024a486ff8837e6784dd290d866112dougm tar.extract(elt, path=self.ws.repo.path)
6185db853e024a486ff8837e6784dd290d866112dougm except (EnvironmentError, tarfile.TarError), e:
7d968cb8b4b6274092771b93e94bf88d1ee31c6cdougm # Make sure the member name is in the exception message.
6185db853e024a486ff8837e6784dd290d866112dougm if isinstance(e, tarfile.TarError):
330ef417fbd2286149a25e8033587edf7ae52ae5dougm errstr = "%s: %s" % (elt.name, e)
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm else:
330ef417fbd2286149a25e8033587edf7ae52ae5dougm errstr = str(e)
330ef417fbd2286149a25e8033587edf7ae52ae5dougm
330ef417fbd2286149a25e8033587edf7ae52ae5dougm raise util.Abort("couldn't restore metadata from %s:\n"
330ef417fbd2286149a25e8033587edf7ae52ae5dougm " %s" %
330ef417fbd2286149a25e8033587edf7ae52ae5dougm (tarpath, errstr))
330ef417fbd2286149a25e8033587edf7ae52ae5dougm finally:
330ef417fbd2286149a25e8033587edf7ae52ae5dougm if tar and not tar.closed:
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm tar.close()
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm def restore(self):
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm '''Restore workspace metadata'''
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm
330ef417fbd2286149a25e8033587edf7ae52ae5dougm if self.bu.exists('hgrc'):
330ef417fbd2286149a25e8033587edf7ae52ae5dougm self.old_restore()
330ef417fbd2286149a25e8033587edf7ae52ae5dougm else:
330ef417fbd2286149a25e8033587edf7ae52ae5dougm self.tar_restore()
330ef417fbd2286149a25e8033587edf7ae52ae5dougm
330ef417fbd2286149a25e8033587edf7ae52ae5dougm def _walk(self):
330ef417fbd2286149a25e8033587edf7ae52ae5dougm '''Yield the repo-relative path to each file we operate on,
330ef417fbd2286149a25e8033587edf7ae52ae5dougm including each file within any affected directory'''
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm for elt in self.files:
330ef417fbd2286149a25e8033587edf7ae52ae5dougm path = self.ws.repo.join(elt)
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm if not os.path.exists(path):
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw continue
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw if os.path.isdir(path):
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw for root, dirs, files in os.walk(path, topdown=True):
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw yield root
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw for f in files:
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw yield os.path.join(root, f)
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw else:
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw yield path
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw def need_backup(self):
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw '''Compare backed up workspace metadata to workspace'''
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw def strip_trailing_pathsep(pathname):
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw '''Remove a possible trailing path separator from PATHNAME'''
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw return pathname.endswith('/') and pathname[:-1] or pathname
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm if self.bu.exists('metadata.tar.gz'):
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm tarpath = self.bu.backupfile('metadata.tar.gz')
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm try:
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm tar = CdmTarFile.gzopen(tarpath)
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm except (EnvironmentError, tarfile.TarError), e:
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm raise util.Abort("couldn't open metadata tarball: %s\n"
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm " %s" % (tarpath, e))
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm if not tar.members_match_fs(self.ws.repo.path):
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw tar.close()
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm return True
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm tarnames = map(strip_trailing_pathsep, tar.getnames())
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw tar.close()
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw else:
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm tarnames = []
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm repopath = self.ws.repo.path
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm if not repopath.endswith('/'):
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw repopath += '/'
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm for path in self._walk():
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm if path.replace(repopath, '', 1) not in tarnames:
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm return True
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm return False
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm def cleanup(self):
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm '''Remove backed up workspace metadata'''
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm self.bu.unlink('metadata.tar.gz')
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm
25a68471b9ababbc21cfdbbb2866014f34f419ecdougmclass CdmClearBackup(object):
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm '''A backup (in tar format) of complete source files from every
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm workspace head.
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm Paths in the tarball are prefixed by the revision and node of the
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm head, or "working" for the working directory.
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm
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
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm '''
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm
6185db853e024a486ff8837e6784dd290d866112dougm def __init__(self, backup, ws):
6185db853e024a486ff8837e6784dd290d866112dougm self.bu = backup
6185db853e024a486ff8837e6784dd290d866112dougm self.ws = ws
6185db853e024a486ff8837e6784dd290d866112dougm
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm def _branch_pairs(self):
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 '''
6185db853e024a486ff8837e6784dd290d866112dougm
6185db853e024a486ff8837e6784dd290d866112dougm parent = self.ws.parent()
6185db853e024a486ff8837e6784dd290d866112dougm
6185db853e024a486ff8837e6784dd290d866112dougm if parent:
6185db853e024a486ff8837e6784dd290d866112dougm outgoing = self.ws.findoutgoing(parent)
6185db853e024a486ff8837e6784dd290d866112dougm outnodes = set(self.ws.repo.changelog.nodesbetween(outgoing)[0])
6185db853e024a486ff8837e6784dd290d866112dougm
6185db853e024a486ff8837e6784dd290d866112dougm heads = [self.ws.repo.changectx(n) for n in self.ws.repo.heads()
6185db853e024a486ff8837e6784dd290d866112dougm if n in outnodes]
6185db853e024a486ff8837e6784dd290d866112dougm else:
6185db853e024a486ff8837e6784dd290d866112dougm heads = []
6185db853e024a486ff8837e6784dd290d866112dougm outnodes = []
6185db853e024a486ff8837e6784dd290d866112dougm
6185db853e024a486ff8837e6784dd290d866112dougm wctx = self.ws.workingctx()
6185db853e024a486ff8837e6784dd290d866112dougm if wctx.files(): # We only care about file changes.
6185db853e024a486ff8837e6784dd290d866112dougm heads = filter(lambda x: x not in wctx.parents(), heads) + [wctx]
6185db853e024a486ff8837e6784dd290d866112dougm
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm pairs = []
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm for head in heads:
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm if head.rev() is None:
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm c = head.parents()
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm else:
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm c = [head]
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm pairs.append((self.ws.parenttip(c, outnodes), head))
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm return pairs
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm def backup(self):
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm '''Save a clear copy of each source file modified between each
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm head and that head's parenttip (see WorkSpace.parenttip).
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm '''
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm tarpath = self.bu.backupfile('clear.tar.gz')
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm branches = self._branch_pairs()
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm if not branches:
6185db853e024a486ff8837e6784dd290d866112dougm return
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm try:
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm tar = CdmTarFile.gzopen(tarpath, 'w')
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm except (EnvironmentError, tarfile.TarError), e:
6185db853e024a486ff8837e6784dd290d866112dougm raise util.Abort("Could not open %s for writing: %s" %
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm (tarpath, e))
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm try:
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm for parent, child in branches:
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm tpath = child.node() and node.short(child.node()) or "working"
6185db853e024a486ff8837e6784dd290d866112dougm
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm for fname, change in self.ws.status(parent, child).iteritems():
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm if change not in ('added', 'modified'):
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm continue
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm
6185db853e024a486ff8837e6784dd290d866112dougm try:
6185db853e024a486ff8837e6784dd290d866112dougm tar.addfilectx(child.filectx(fname),
6185db853e024a486ff8837e6784dd290d866112dougm os.path.join(tpath, fname))
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm except ValueError, e:
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm crev = child.rev()
6185db853e024a486ff8837e6784dd290d866112dougm if crev is None:
6185db853e024a486ff8837e6784dd290d866112dougm crev = "working copy"
6185db853e024a486ff8837e6784dd290d866112dougm raise util.Abort("Could not backup clear file %s "
6185db853e024a486ff8837e6784dd290d866112dougm "from %s: %s\n" % (fname, crev, e))
6185db853e024a486ff8837e6784dd290d866112dougm finally:
6185db853e024a486ff8837e6784dd290d866112dougm tar.close()
6185db853e024a486ff8837e6784dd290d866112dougm
6185db853e024a486ff8837e6784dd290d866112dougm def cleanup(self):
6185db853e024a486ff8837e6784dd290d866112dougm '''Cleanup a failed Clear backup.
6185db853e024a486ff8837e6784dd290d866112dougm
6185db853e024a486ff8837e6784dd290d866112dougm Remove the clear tarball from the backup directory.
6185db853e024a486ff8837e6784dd290d866112dougm '''
6185db853e024a486ff8837e6784dd290d866112dougm
6185db853e024a486ff8837e6784dd290d866112dougm self.bu.unlink('clear.tar.gz')
6185db853e024a486ff8837e6784dd290d866112dougm
6185db853e024a486ff8837e6784dd290d866112dougm def restore(self):
6185db853e024a486ff8837e6784dd290d866112dougm '''Clear backups are never restored, do nothing'''
6185db853e024a486ff8837e6784dd290d866112dougm pass
6185db853e024a486ff8837e6784dd290d866112dougm
6185db853e024a486ff8837e6784dd290d866112dougm def need_backup(self):
6185db853e024a486ff8837e6784dd290d866112dougm '''Clear backups are never compared, return False (no backup needed).
6185db853e024a486ff8837e6784dd290d866112dougm
6185db853e024a486ff8837e6784dd290d866112dougm Should a backup actually be needed, one of the other
6185db853e024a486ff8837e6784dd290d866112dougm implementation classes would notice in any situation we would.
6185db853e024a486ff8837e6784dd290d866112dougm '''
6185db853e024a486ff8837e6784dd290d866112dougm
6185db853e024a486ff8837e6784dd290d866112dougm return False
6185db853e024a486ff8837e6784dd290d866112dougm
6185db853e024a486ff8837e6784dd290d866112dougm
6185db853e024a486ff8837e6784dd290d866112dougmclass CdmBackup(object):
6185db853e024a486ff8837e6784dd290d866112dougm '''A backup of a given workspace'''
6185db853e024a486ff8837e6784dd290d866112dougm
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm def __init__(self, ui, ws, name):
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm self.ws = ws
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm self.ui = ui
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm self.backupdir = self._find_backup_dir(name)
6185db853e024a486ff8837e6784dd290d866112dougm
6185db853e024a486ff8837e6784dd290d866112dougm #
6185db853e024a486ff8837e6784dd290d866112dougm # The order of instances here controls the order the various operations
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm # are run.
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm #
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 #
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm self.modules = [x(self, ws) for x in [CdmCommittedBackup,
6185db853e024a486ff8837e6784dd290d866112dougm CdmUncommittedBackup,
6185db853e024a486ff8837e6784dd290d866112dougm CdmClearBackup,
6185db853e024a486ff8837e6784dd290d866112dougm CdmMetadataBackup]]
6185db853e024a486ff8837e6784dd290d866112dougm
6185db853e024a486ff8837e6784dd290d866112dougm if os.path.exists(os.path.join(self.backupdir, 'latest')):
6185db853e024a486ff8837e6784dd290d866112dougm generation = os.readlink(os.path.join(self.backupdir, 'latest'))
6185db853e024a486ff8837e6784dd290d866112dougm self.generation = int(os.path.split(generation)[1])
6185db853e024a486ff8837e6784dd290d866112dougm else:
6185db853e024a486ff8837e6784dd290d866112dougm self.generation = 0
6185db853e024a486ff8837e6784dd290d866112dougm
6185db853e024a486ff8837e6784dd290d866112dougm def _find_backup_dir(self, name):
6185db853e024a486ff8837e6784dd290d866112dougm '''Find the path to an appropriate backup directory based on NAME'''
6185db853e024a486ff8837e6784dd290d866112dougm
6185db853e024a486ff8837e6784dd290d866112dougm if os.path.isabs(name):
6185db853e024a486ff8837e6784dd290d866112dougm return name
6185db853e024a486ff8837e6784dd290d866112dougm
6185db853e024a486ff8837e6784dd290d866112dougm if self.ui.config('cdm', 'backupdir'):
6185db853e024a486ff8837e6784dd290d866112dougm backupbase = os.path.expanduser(self.ui.config('cdm', 'backupdir'))
6185db853e024a486ff8837e6784dd290d866112dougm else:
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw home = None
6185db853e024a486ff8837e6784dd290d866112dougm
6185db853e024a486ff8837e6784dd290d866112dougm try:
6185db853e024a486ff8837e6784dd290d866112dougm home = os.getenv('HOME') or pwd.getpwuid(os.getuid()).pw_dir
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm except KeyError:
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw pass # Handled anyway
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw if not home:
6185db853e024a486ff8837e6784dd290d866112dougm raise util.Abort('Could not determine your HOME directory to '
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm 'find backup path')
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm backupbase = os.path.join(home, 'cdm.backup')
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm backupdir = os.path.join(backupbase, name)
6185db853e024a486ff8837e6784dd290d866112dougm
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)
6185db853e024a486ff8837e6784dd290d866112dougm
6185db853e024a486ff8837e6784dd290d866112dougm return backupdir
6185db853e024a486ff8837e6784dd290d866112dougm
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm def _update_latest(self, gen):
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm '''Update latest symlink to point to the current generation'''
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm linkpath = os.path.join(self.backupdir, 'latest')
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm if os.path.lexists(linkpath):
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm os.unlink(linkpath)
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm os.symlink(str(gen), linkpath)
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm def _create_gen(self, gen):
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm '''Create a new backup generation'''
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm try:
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm os.makedirs(os.path.join(self.backupdir, str(gen)))
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm self._update_latest(gen)
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm except EnvironmentError, e:
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm raise util.Abort("Couldn't create backup generation %s: %s" %
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm (os.path.join(self.backupdir, str(gen)), e))
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm def backupfile(self, path):
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm '''return full path to backup file FILE at GEN'''
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm return os.path.join(self.backupdir, str(self.generation), path)
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm def unlink(self, name):
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm '''Unlink the specified path from the backup directory.
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm A no-op if the path does not exist.
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw '''
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm fpath = self.backupfile(name)
6185db853e024a486ff8837e6784dd290d866112dougm if os.path.exists(fpath):
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm os.unlink(fpath)
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm def open(self, name, mode='r'):
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm '''Open the specified file in the backup directory'''
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm return open(self.backupfile(name), mode)
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm def exists(self, name):
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm '''Return boolean indicating wether a given file exists in the
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw backup directory.'''
6185db853e024a486ff8837e6784dd290d866112dougm return os.path.exists(self.backupfile(name))
6185db853e024a486ff8837e6784dd290d866112dougm
6185db853e024a486ff8837e6784dd290d866112dougm def need_backup(self):
6185db853e024a486ff8837e6784dd290d866112dougm '''Compare backed up changes to workspace'''
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm #
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.
25a68471b9ababbc21cfdbbb2866014f34f419ecdougm #
6185db853e024a486ff8837e6784dd290d866112dougm if not self.generation or not self.exists('dirstate'):
6185db853e024a486ff8837e6784dd290d866112dougm return True
6185db853e024a486ff8837e6784dd290d866112dougm
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw for x in self.modules:
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw if x.need_backup():
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw return True
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw return False
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw def backup(self):
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw '''Take a backup of the current workspace
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw Calling code is expected to hold both the working copy lock
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw and repository lock.'''
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw if not os.path.exists(self.backupdir):
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw try:
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw os.makedirs(self.backupdir)
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw except EnvironmentError, e:
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw raise util.Abort('Could not create backup directory %s: %s' %
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw (self.backupdir, e))
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw self.generation += 1
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw self._create_gen(self.generation)
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw try:
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw for x in self.modules:
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw x.backup()
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw except Exception, e:
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw if isinstance(e, KeyboardInterrupt):
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw self.ws.ui.warn("Interrupted\n")
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw else:
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw self.ws.ui.warn("Error: %s\n" % e)
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw show_traceback = self.ws.ui.configbool('ui', 'traceback',
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw False)
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw #
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 #
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw if (not isinstance(e, (EnvironmentError, util.Abort))
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw or show_traceback):
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw traceback.print_exc()
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw for x in self.modules:
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw x.cleanup()
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw os.rmdir(os.path.join(self.backupdir, str(self.generation)))
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw self.generation -= 1
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw if self.generation != 0:
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw self._update_latest(self.generation)
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw else:
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw os.unlink(os.path.join(self.backupdir, 'latest'))
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw raise util.Abort('Backup failed')
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw def restore(self, gen=None):
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw '''Restore workspace from backup
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw Restores from backup generation GEN (defaulting to the latest)
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw into workspace WS.
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw Calling code is expected to hold both the working copy lock
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw and repository lock of the destination workspace.'''
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw if not os.path.exists(self.backupdir):
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw raise util.Abort('Backup directory does not exist: %s' %
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw (self.backupdir))
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw if gen:
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw if not os.path.exists(os.path.join(self.backupdir, str(gen))):
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw raise util.Abort('Backup generation does not exist: %s' %
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw (os.path.join(self.backupdir, str(gen))))
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw self.generation = int(gen)
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
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
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw if not self.exists('dirstate'):
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw raise util.Abort('Backup %s/%s is incomplete (dirstate missing)' %
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw (self.backupdir, self.generation))
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw try:
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw for x in self.modules:
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw x.restore()
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw except util.Abort, e:
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw raise util.Abort('Error restoring workspace:\n'
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw '%s\n'
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw 'Workspace may be partially restored' % e)
da6c28aaf62fa55f0fdb8004aa40f88f23bf53f0amw