xref: /titanic_50/usr/src/tools/onbld/Scm/Backup.py (revision c959a081a8aebb76386b6d8ea3afa850e328f6c7)
1cdf0c1d5Smjnelson#
2cdf0c1d5Smjnelson#  This program is free software; you can redistribute it and/or modify
3cdf0c1d5Smjnelson#  it under the terms of the GNU General Public License version 2
4cdf0c1d5Smjnelson#  as published by the Free Software Foundation.
5cdf0c1d5Smjnelson#
6cdf0c1d5Smjnelson#  This program is distributed in the hope that it will be useful,
7cdf0c1d5Smjnelson#  but WITHOUT ANY WARRANTY; without even the implied warranty of
8cdf0c1d5Smjnelson#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
9cdf0c1d5Smjnelson#  GNU General Public License for more details.
10cdf0c1d5Smjnelson#
11cdf0c1d5Smjnelson#  You should have received a copy of the GNU General Public License
12cdf0c1d5Smjnelson#  along with this program; if not, write to the Free Software
13cdf0c1d5Smjnelson#  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
14cdf0c1d5Smjnelson#
15cdf0c1d5Smjnelson
16cdf0c1d5Smjnelson#
1712203c71SRichard Lowe# Copyright 2009 Sun Microsystems, Inc.  All rights reserved.
18cdf0c1d5Smjnelson# Use is subject to license terms.
19cdf0c1d5Smjnelson#
20cdf0c1d5Smjnelson
21cdf0c1d5Smjnelson'''
22cdf0c1d5SmjnelsonWorkspace backup
23cdf0c1d5Smjnelson
24cdf0c1d5SmjnelsonBackup format is:
25cdf0c1d5Smjnelson   backupdir/
26cdf0c1d5Smjnelson      wsname/
27cdf0c1d5Smjnelson         generation#/
28cdf0c1d5Smjnelson            dirstate (handled by CdmUncommittedBackup)
2912203c71SRichard Lowe                File containing dirstate nodeid (the changeset we need
3012203c71SRichard Lowe                to update the workspace to after applying the bundle).
3112203c71SRichard Lowe                This is the node to which the working copy changes
3212203c71SRichard Lowe                (see 'diff', below) will be applied if applicable.
33cdf0c1d5Smjnelson
34cdf0c1d5Smjnelson            bundle (handled by CdmCommittedBackup)
35cdf0c1d5Smjnelson                An Hg bundle containing outgoing committed changes.
36cdf0c1d5Smjnelson
37cdf0c1d5Smjnelson            nodes (handled by CdmCommittedBackup)
38cdf0c1d5Smjnelson                A text file listing the full (hex) nodeid of all nodes in
39cdf0c1d5Smjnelson                bundle, used by need_backup.
40cdf0c1d5Smjnelson
41cdf0c1d5Smjnelson            diff (handled by CdmUncommittedBackup)
42cdf0c1d5Smjnelson                A Git-formatted diff containing uncommitted changes.
43cdf0c1d5Smjnelson
44cdf0c1d5Smjnelson            renames (handled by CdmUncommittedBackup)
45cdf0c1d5Smjnelson                A list of renames in the working copy that have to be
46cdf0c1d5Smjnelson                applied manually, rather than by the diff.
47cdf0c1d5Smjnelson
48cdf0c1d5Smjnelson            metadata.tar.gz (handled by CdmMetadataBackup)
49cdf0c1d5Smjnelson                $CODEMGR_WS/.hg/hgrc
50cdf0c1d5Smjnelson                $CODEMGR_WS/.hg/localtags
51cdf0c1d5Smjnelson                $CODEMGR_WS/.hg/patches (Mq data)
52cdf0c1d5Smjnelson
53cdf0c1d5Smjnelson         latest -> generation#
54cdf0c1d5Smjnelson            Newest backup generation.
55cdf0c1d5Smjnelson
56cdf0c1d5SmjnelsonAll files in a given backup generation, with the exception of
57cdf0c1d5Smjnelsondirstate, are optional.
58cdf0c1d5Smjnelson'''
59cdf0c1d5Smjnelson
60*c959a081SRichard Loweimport os, pwd, shutil, tarfile, time, traceback
61*c959a081SRichard Lowefrom mercurial import changegroup, node, patch, util
62*c959a081SRichard Lowe
63*c959a081SRichard Lowefrom onbld.Scm.WorkSpace import HgLookupError
64*c959a081SRichard Loweimport onbld.Scm.Version as Version
65cdf0c1d5Smjnelson
66cdf0c1d5Smjnelson
6712203c71SRichard Loweclass CdmNodeMissing(util.Abort):
6812203c71SRichard Lowe    '''a required node is not present in the destination workspace.
6912203c71SRichard Lowe
7012203c71SRichard Lowe    This may occur both in the case where the bundle contains a
7112203c71SRichard Lowe    changeset which is a child of a node not present in the
7212203c71SRichard Lowe    destination workspace (because the destination workspace is not as
7312203c71SRichard Lowe    up-to-date as the source), or because the source and destination
7412203c71SRichard Lowe    workspace are not related.
7512203c71SRichard Lowe
7612203c71SRichard Lowe    It may also happen in cases where the uncommitted changes need to
7712203c71SRichard Lowe    be applied onto a node that the workspace does not possess even
7812203c71SRichard Lowe    after application of the bundle (on a branch not present
7912203c71SRichard Lowe    in the bundle or destination workspace, for instance)'''
8012203c71SRichard Lowe
8112203c71SRichard Lowe    def __init__(self, msg, name):
8212203c71SRichard Lowe        #
8312203c71SRichard Lowe        # If e.name is a string 20 characters long, it is
8412203c71SRichard Lowe        # assumed to be a node.  (Mercurial makes this
8512203c71SRichard Lowe        # same assumption, when creating a LookupError)
8612203c71SRichard Lowe        #
8712203c71SRichard Lowe        if isinstance(name, str) and len(name) == 20:
8812203c71SRichard Lowe            n = node.short(name)
8912203c71SRichard Lowe        else:
9012203c71SRichard Lowe            n = name
9112203c71SRichard Lowe
9212203c71SRichard Lowe        util.Abort.__init__(self, "%s: changeset '%s' is missing\n"
9312203c71SRichard Lowe                            "Your workspace is either not "
9412203c71SRichard Lowe                            "sufficiently up to date,\n"
9512203c71SRichard Lowe                            "or is unrelated to the workspace from "
9612203c71SRichard Lowe                            "which the backup was taken.\n" % (msg, n))
9712203c71SRichard Lowe
9812203c71SRichard Lowe
99cdf0c1d5Smjnelsonclass CdmCommittedBackup(object):
100cdf0c1d5Smjnelson    '''Backup of committed changes'''
101cdf0c1d5Smjnelson
102cdf0c1d5Smjnelson    def __init__(self, backup, ws):
103cdf0c1d5Smjnelson        self.ws = ws
104cdf0c1d5Smjnelson        self.bu = backup
105cdf0c1d5Smjnelson        self.files = ('bundle', 'nodes')
106cdf0c1d5Smjnelson
107cdf0c1d5Smjnelson    def _outgoing_nodes(self, parent):
108cdf0c1d5Smjnelson        '''Return a list of all outgoing nodes in hex format'''
109cdf0c1d5Smjnelson
110cdf0c1d5Smjnelson        if parent:
111cdf0c1d5Smjnelson            outgoing = self.ws.findoutgoing(parent)
112cdf0c1d5Smjnelson            nodes = self.ws.repo.changelog.nodesbetween(outgoing)[0]
113cdf0c1d5Smjnelson            return map(node.hex, nodes)
114cdf0c1d5Smjnelson        else:
115cdf0c1d5Smjnelson            return []
116cdf0c1d5Smjnelson
117cdf0c1d5Smjnelson    def backup(self):
118cdf0c1d5Smjnelson        '''Backup committed changes'''
119cdf0c1d5Smjnelson        parent = self.ws.parent()
120cdf0c1d5Smjnelson
121cdf0c1d5Smjnelson        if not parent:
122cdf0c1d5Smjnelson            self.ws.ui.warn('Workspace has no parent, committed changes will '
123cdf0c1d5Smjnelson                            'not be backed up\n')
124cdf0c1d5Smjnelson            return
125cdf0c1d5Smjnelson
126cdf0c1d5Smjnelson        out = self.ws.findoutgoing(parent)
127cdf0c1d5Smjnelson        if not out:
128cdf0c1d5Smjnelson            return
129cdf0c1d5Smjnelson
130cdf0c1d5Smjnelson        cg = self.ws.repo.changegroup(out, 'bundle')
131cdf0c1d5Smjnelson        changegroup.writebundle(cg, self.bu.backupfile('bundle'), 'HG10BZ')
132cdf0c1d5Smjnelson
133cdf0c1d5Smjnelson        outnodes = self._outgoing_nodes(parent)
134cdf0c1d5Smjnelson        if outnodes:
135cdf0c1d5Smjnelson            fp = None
136cdf0c1d5Smjnelson            try:
137cdf0c1d5Smjnelson                try:
138cdf0c1d5Smjnelson                    fp = open(self.bu.backupfile('nodes'), 'w')
139cdf0c1d5Smjnelson                    fp.write('%s\n' % '\n'.join(outnodes))
140cdf0c1d5Smjnelson                except EnvironmentError, e:
141cdf0c1d5Smjnelson                    raise util.Abort("couldn't store outgoing nodes: %s" % e)
142cdf0c1d5Smjnelson            finally:
143cdf0c1d5Smjnelson                if fp and not fp.closed:
144cdf0c1d5Smjnelson                    fp.close()
145cdf0c1d5Smjnelson
146cdf0c1d5Smjnelson    def restore(self):
147cdf0c1d5Smjnelson        '''Restore committed changes from backup'''
148cdf0c1d5Smjnelson        bfile = self.bu.backupfile('bundle')
149cdf0c1d5Smjnelson
150cdf0c1d5Smjnelson        if os.path.exists(bfile):
151cdf0c1d5Smjnelson            f = None
152cdf0c1d5Smjnelson            try:
153cdf0c1d5Smjnelson                try:
154cdf0c1d5Smjnelson                    f = open(bfile, 'r')
155cdf0c1d5Smjnelson                    bundle = changegroup.readbundle(f, bfile)
156cdf0c1d5Smjnelson                    self.ws.repo.addchangegroup(bundle, 'strip',
157cdf0c1d5Smjnelson                                                'bundle:%s' % bfile)
158cdf0c1d5Smjnelson                except EnvironmentError, e:
159cdf0c1d5Smjnelson                    raise util.Abort("couldn't restore committed changes: %s\n"
160cdf0c1d5Smjnelson                                     "   %s" % (bfile, e))
161*c959a081SRichard Lowe                except HgLookupError, e:
16212203c71SRichard Lowe                    raise CdmNodeMissing("couldn't restore committed changes",
16312203c71SRichard Lowe                                                     e.name)
164cdf0c1d5Smjnelson            finally:
165cdf0c1d5Smjnelson                if f and not f.closed:
166cdf0c1d5Smjnelson                    f.close()
167cdf0c1d5Smjnelson
168cdf0c1d5Smjnelson    def need_backup(self):
169cdf0c1d5Smjnelson        '''Compare backup of committed changes to workspace'''
170cdf0c1d5Smjnelson
171cdf0c1d5Smjnelson        if os.path.exists(self.bu.backupfile('nodes')):
172cdf0c1d5Smjnelson            f = None
173cdf0c1d5Smjnelson            try:
174cdf0c1d5Smjnelson                try:
175cdf0c1d5Smjnelson                    f = open(self.bu.backupfile('nodes'))
176cdf0c1d5Smjnelson                    bnodes = set([line.rstrip('\r\n')
177cdf0c1d5Smjnelson                                  for line in f.readlines()])
178cdf0c1d5Smjnelson                    f.close()
179cdf0c1d5Smjnelson                except EnvironmentError, e:
180cdf0c1d5Smjnelson                    raise util.Abort("couldn't open backup node list: %s" % e)
181cdf0c1d5Smjnelson            finally:
182cdf0c1d5Smjnelson                if f and not f.closed:
183cdf0c1d5Smjnelson                    f.close()
184cdf0c1d5Smjnelson        else:
185cdf0c1d5Smjnelson            bnodes = set()
186cdf0c1d5Smjnelson
187cdf0c1d5Smjnelson        outnodes = set(self._outgoing_nodes(self.ws.parent()))
188cdf0c1d5Smjnelson        if outnodes != bnodes:
189cdf0c1d5Smjnelson            return True
190cdf0c1d5Smjnelson
191cdf0c1d5Smjnelson        return False
192cdf0c1d5Smjnelson
193cdf0c1d5Smjnelson    def cleanup(self):
194cdf0c1d5Smjnelson        '''Remove backed up committed changes'''
195cdf0c1d5Smjnelson
196cdf0c1d5Smjnelson        for fname in self.files:
197cdf0c1d5Smjnelson            if os.path.exists(self.bu.backupfile(fname)):
198cdf0c1d5Smjnelson                os.unlink(self.bu.backupfile(fname))
199cdf0c1d5Smjnelson
200cdf0c1d5Smjnelson
201cdf0c1d5Smjnelsonclass CdmUncommittedBackup(object):
202cdf0c1d5Smjnelson    '''Backup of uncommitted changes'''
203cdf0c1d5Smjnelson
204cdf0c1d5Smjnelson    def __init__(self, backup, ws):
205cdf0c1d5Smjnelson        self.ws = ws
206cdf0c1d5Smjnelson        self.bu = backup
207cdf0c1d5Smjnelson
208cdf0c1d5Smjnelson    def _clobbering_renames(self):
209cdf0c1d5Smjnelson        '''Return a list of pairs of files representing renames/copies
210cdf0c1d5Smjnelson        that clobber already versioned files.  [(oldname newname)...]'''
211cdf0c1d5Smjnelson
212cdf0c1d5Smjnelson        #
213cdf0c1d5Smjnelson        # Note that this doesn't handle uncommitted merges
214cdf0c1d5Smjnelson        # as CdmUncommittedBackup itself doesn't.
215cdf0c1d5Smjnelson        #
2162b5878deSRich Lowe        wctx = self.ws.workingctx()
217cdf0c1d5Smjnelson        parent = wctx.parents()[0]
218cdf0c1d5Smjnelson
219cdf0c1d5Smjnelson        ret = []
220cdf0c1d5Smjnelson        for fname in wctx.added() + wctx.modified():
221cdf0c1d5Smjnelson            rn = wctx.filectx(fname).renamed()
222cdf0c1d5Smjnelson            if rn and fname in parent:
223cdf0c1d5Smjnelson                ret.append((rn[0], fname))
224cdf0c1d5Smjnelson        return ret
225cdf0c1d5Smjnelson
226cdf0c1d5Smjnelson    def backup(self):
227cdf0c1d5Smjnelson        '''Backup uncommitted changes'''
228cdf0c1d5Smjnelson
229cdf0c1d5Smjnelson        if self.ws.merged():
230cdf0c1d5Smjnelson            raise util.Abort("Unable to backup an uncommitted merge.\n"
231cdf0c1d5Smjnelson                             "Please complete your merge and commit")
232cdf0c1d5Smjnelson
2332b5878deSRich Lowe        dirstate = node.hex(self.ws.workingctx().parents()[0].node())
234cdf0c1d5Smjnelson
235cdf0c1d5Smjnelson        fp = None
236cdf0c1d5Smjnelson        try:
237cdf0c1d5Smjnelson            try:
238cdf0c1d5Smjnelson                fp = open(self.bu.backupfile('dirstate'), 'w')
239cdf0c1d5Smjnelson                fp.write(dirstate + '\n')
240cdf0c1d5Smjnelson            except EnvironmentError, e:
241cdf0c1d5Smjnelson                raise util.Abort("couldn't save working copy parent: %s" % e)
242cdf0c1d5Smjnelson        finally:
243cdf0c1d5Smjnelson            if fp and not fp.closed:
244cdf0c1d5Smjnelson                fp.close()
245cdf0c1d5Smjnelson
246cdf0c1d5Smjnelson        try:
247cdf0c1d5Smjnelson            try:
248cdf0c1d5Smjnelson                fp = open(self.bu.backupfile('renames'), 'w')
249cdf0c1d5Smjnelson                for cons in self._clobbering_renames():
250cdf0c1d5Smjnelson                    fp.write("%s %s\n" % cons)
251cdf0c1d5Smjnelson            except EnvironmentError, e:
252cdf0c1d5Smjnelson                raise util.Abort("couldn't save clobbering copies: %s" % e)
253cdf0c1d5Smjnelson        finally:
254cdf0c1d5Smjnelson            if fp and not fp.closed:
255cdf0c1d5Smjnelson                fp.close()
256cdf0c1d5Smjnelson
257cdf0c1d5Smjnelson        try:
258cdf0c1d5Smjnelson            try:
259cdf0c1d5Smjnelson                fp = open(self.bu.backupfile('diff'), 'w')
2602b5878deSRich Lowe                opts = patch.diffopts(self.ws.ui, opts={'git': True})
2612b5878deSRich Lowe                fp.write(self.ws.diff(opts=opts))
262cdf0c1d5Smjnelson            except EnvironmentError, e:
263cdf0c1d5Smjnelson                raise util.Abort("couldn't save working copy diff: %s" % e)
264cdf0c1d5Smjnelson        finally:
265cdf0c1d5Smjnelson            if fp and not fp.closed:
266cdf0c1d5Smjnelson                fp.close()
267cdf0c1d5Smjnelson
268cdf0c1d5Smjnelson    def _dirstate(self):
2692b5878deSRich Lowe        '''Return the desired working copy node from the backup'''
270cdf0c1d5Smjnelson        fp = None
271cdf0c1d5Smjnelson        try:
272cdf0c1d5Smjnelson            try:
273cdf0c1d5Smjnelson                fp = open(self.bu.backupfile('dirstate'))
274cdf0c1d5Smjnelson                dirstate = fp.readline().strip()
275cdf0c1d5Smjnelson                return dirstate
276cdf0c1d5Smjnelson            except EnvironmentError, e:
277cdf0c1d5Smjnelson                raise util.Abort("couldn't read saved parent: %s" % e)
278cdf0c1d5Smjnelson        finally:
279cdf0c1d5Smjnelson            if fp and not fp.closed:
280cdf0c1d5Smjnelson                fp.close()
281cdf0c1d5Smjnelson
282cdf0c1d5Smjnelson    def restore(self):
283cdf0c1d5Smjnelson        '''Restore uncommitted changes'''
284cdf0c1d5Smjnelson        diff = self.bu.backupfile('diff')
285cdf0c1d5Smjnelson        dirstate = self._dirstate()
286cdf0c1d5Smjnelson
2872b5878deSRich Lowe        #
2882b5878deSRich Lowe        # Check that the patch's parent changeset exists.
2892b5878deSRich Lowe        #
290cdf0c1d5Smjnelson        try:
2912b5878deSRich Lowe            n = node.bin(dirstate)
2922b5878deSRich Lowe            self.ws.repo.changelog.lookup(n)
293*c959a081SRichard Lowe        except HgLookupError, e:
29412203c71SRichard Lowe            raise CdmNodeMissing("couldn't restore uncommitted changes",
29512203c71SRichard Lowe                                 e.name)
2962b5878deSRich Lowe
2972b5878deSRich Lowe        try:
2982b5878deSRich Lowe            self.ws.clean(rev=dirstate)
299cdf0c1d5Smjnelson        except util.Abort, e:
300cdf0c1d5Smjnelson            raise util.Abort("couldn't update to saved node: %s" % e)
301cdf0c1d5Smjnelson
302cdf0c1d5Smjnelson        if not os.path.exists(diff):
303cdf0c1d5Smjnelson            return
304cdf0c1d5Smjnelson
305cdf0c1d5Smjnelson        #
306cdf0c1d5Smjnelson        # There's a race here whereby if the patch (or part thereof)
307cdf0c1d5Smjnelson        # is applied within the same second as the clean above (such
308cdf0c1d5Smjnelson        # that mtime doesn't change) and if the size of that file
309cdf0c1d5Smjnelson        # does not change, Hg may not see the change.
310cdf0c1d5Smjnelson        #
311cdf0c1d5Smjnelson        # We sleep a full second to avoid this, as sleeping merely
312cdf0c1d5Smjnelson        # until the next second begins would require very close clock
313cdf0c1d5Smjnelson        # synchronization on network filesystems.
314cdf0c1d5Smjnelson        #
315cdf0c1d5Smjnelson        time.sleep(1)
316cdf0c1d5Smjnelson
317cdf0c1d5Smjnelson        files = {}
318cdf0c1d5Smjnelson        try:
319cdf0c1d5Smjnelson            try:
320cdf0c1d5Smjnelson                fuzz = patch.patch(diff, self.ws.ui, strip=1,
321cdf0c1d5Smjnelson                                   cwd=self.ws.repo.root, files=files)
322cdf0c1d5Smjnelson                if fuzz:
323cdf0c1d5Smjnelson                    raise util.Abort('working copy diff applied with fuzz')
324cdf0c1d5Smjnelson            except Exception, e:
325cdf0c1d5Smjnelson                raise util.Abort("couldn't apply working copy diff: %s\n"
326cdf0c1d5Smjnelson                                 "   %s" % (diff, e))
327cdf0c1d5Smjnelson        finally:
328cdf0c1d5Smjnelson            patch.updatedir(self.ws.ui, self.ws.repo, files)
329cdf0c1d5Smjnelson
330cdf0c1d5Smjnelson        if not os.path.exists(self.bu.backupfile('renames')):
331cdf0c1d5Smjnelson            return
332cdf0c1d5Smjnelson
333cdf0c1d5Smjnelson        #
334cdf0c1d5Smjnelson        # We need to re-apply name changes where the new name
335cdf0c1d5Smjnelson        # (rename/copy destination) is an already versioned file, as
336cdf0c1d5Smjnelson        # Hg would otherwise ignore them.
337cdf0c1d5Smjnelson        #
338cdf0c1d5Smjnelson        try:
339cdf0c1d5Smjnelson            fp = open(self.bu.backupfile('renames'))
340cdf0c1d5Smjnelson            for line in fp:
341cdf0c1d5Smjnelson                source, dest = line.strip().split()
342cdf0c1d5Smjnelson                self.ws.repo.copy(source, dest)
343cdf0c1d5Smjnelson        except EnvironmentError, e:
344cdf0c1d5Smjnelson            raise util.Abort('unable to open renames file: %s' % e)
345cdf0c1d5Smjnelson        except ValueError:
346cdf0c1d5Smjnelson            raise util.Abort('corrupt renames file: %s' %
347cdf0c1d5Smjnelson                             self.bu.backupfile('renames'))
348cdf0c1d5Smjnelson
349cdf0c1d5Smjnelson    def need_backup(self):
350cdf0c1d5Smjnelson        '''Compare backup of uncommitted changes to workspace'''
3512b5878deSRich Lowe        cnode = self.ws.workingctx().parents()[0].node()
3522b5878deSRich Lowe        if self._dirstate() != node.hex(cnode):
353cdf0c1d5Smjnelson            return True
354cdf0c1d5Smjnelson
3552b5878deSRich Lowe        opts = patch.diffopts(self.ws.ui, opts={'git': True})
3562b5878deSRich Lowe        curdiff = self.ws.diff(opts=opts)
3572b5878deSRich Lowe
358cdf0c1d5Smjnelson        diff = self.bu.backupfile('diff')
359cdf0c1d5Smjnelson        if os.path.exists(diff):
360cdf0c1d5Smjnelson            try:
361cdf0c1d5Smjnelson                try:
362cdf0c1d5Smjnelson                    fd = open(diff)
363cdf0c1d5Smjnelson                    backdiff = fd.read()
364cdf0c1d5Smjnelson                except EnvironmentError, e:
365cdf0c1d5Smjnelson                    raise util.Abort("couldn't open backup diff %s\n"
366cdf0c1d5Smjnelson                                     "   %s" % (diff, e))
367cdf0c1d5Smjnelson            finally:
368cdf0c1d5Smjnelson                if fd and not fd.closed:
369cdf0c1d5Smjnelson                    fd.close()
370cdf0c1d5Smjnelson        else:
371cdf0c1d5Smjnelson            backdiff = ''
372cdf0c1d5Smjnelson
3732b5878deSRich Lowe        if backdiff != curdiff:
374cdf0c1d5Smjnelson            return True
375cdf0c1d5Smjnelson
376cdf0c1d5Smjnelson
377cdf0c1d5Smjnelson        currrenamed = self._clobbering_renames()
378cdf0c1d5Smjnelson        bakrenamed = None
379cdf0c1d5Smjnelson
380cdf0c1d5Smjnelson        if os.path.exists(self.bu.backupfile('renames')):
381cdf0c1d5Smjnelson            try:
382cdf0c1d5Smjnelson                try:
383cdf0c1d5Smjnelson                    fd = open(self.bu.backupfile('renames'))
384cdf0c1d5Smjnelson                    bakrenamed = [line.strip().split(' ') for line in fd]
385cdf0c1d5Smjnelson                except EnvironmentError, e:
386cdf0c1d5Smjnelson                    raise util.Abort("couldn't open renames file %s: %s\n" %
387cdf0c1d5Smjnelson                                     (self.bu.backupfile('renames'), e))
388cdf0c1d5Smjnelson            finally:
389cdf0c1d5Smjnelson                if fd and not fd.closed:
390cdf0c1d5Smjnelson                    fd.close()
391cdf0c1d5Smjnelson
392cdf0c1d5Smjnelson            if currrenamed != bakrenamed:
393cdf0c1d5Smjnelson                return True
394cdf0c1d5Smjnelson
395cdf0c1d5Smjnelson        return False
396cdf0c1d5Smjnelson
397cdf0c1d5Smjnelson    def cleanup(self):
398cdf0c1d5Smjnelson        '''Remove backed up uncommitted changes'''
399cdf0c1d5Smjnelson        for fname in ('dirstate', 'diff', 'renames'):
400cdf0c1d5Smjnelson            if os.path.exists(self.bu.backupfile(fname)):
401cdf0c1d5Smjnelson                os.unlink(self.bu.backupfile(fname))
402cdf0c1d5Smjnelson
403cdf0c1d5Smjnelson
404cdf0c1d5Smjnelsonclass CdmMetadataBackup(object):
405cdf0c1d5Smjnelson    '''Backup of workspace metadata'''
406cdf0c1d5Smjnelson
407cdf0c1d5Smjnelson    def __init__(self, backup, ws):
408cdf0c1d5Smjnelson        self.bu = backup
409cdf0c1d5Smjnelson        self.ws = ws
4109a70fc3bSMark J. Nelson        self.files = ('hgrc', 'localtags', 'patches', 'cdm')
411cdf0c1d5Smjnelson
412cdf0c1d5Smjnelson    def backup(self):
413cdf0c1d5Smjnelson        '''Backup workspace metadata'''
414cdf0c1d5Smjnelson
415cdf0c1d5Smjnelson        tar = None
416cdf0c1d5Smjnelson
417cdf0c1d5Smjnelson        try:
418cdf0c1d5Smjnelson            try:
419cdf0c1d5Smjnelson                tar = tarfile.open(self.bu.backupfile('metadata.tar.gz'),
420cdf0c1d5Smjnelson                                   'w:gz')
421cdf0c1d5Smjnelson                tar.errorlevel = 2
422cdf0c1d5Smjnelson            except (EnvironmentError, tarfile.TarError), e:
423cdf0c1d5Smjnelson                raise util.Abort("couldn't open %s for writing: %s" %
424cdf0c1d5Smjnelson                                 (self.bu.backupfile('metadata.tar.gz'), e))
425cdf0c1d5Smjnelson
426cdf0c1d5Smjnelson            try:
427cdf0c1d5Smjnelson                for elt in self.files:
428cdf0c1d5Smjnelson                    fpath = self.ws.repo.join(elt)
429cdf0c1d5Smjnelson                    if os.path.exists(fpath):
430cdf0c1d5Smjnelson                        tar.add(fpath, elt)
431cdf0c1d5Smjnelson            except (EnvironmentError, tarfile.TarError), e:
432cdf0c1d5Smjnelson                #
433cdf0c1d5Smjnelson                # tarfile.TarError doesn't include the tar member or file
434cdf0c1d5Smjnelson                # in question, so we have to do so ourselves.
435cdf0c1d5Smjnelson                #
436cdf0c1d5Smjnelson                if isinstance(e, tarfile.TarError):
437cdf0c1d5Smjnelson                    error = "%s: %s" % (elt, e)
438cdf0c1d5Smjnelson                else:
439cdf0c1d5Smjnelson                    error = str(e)
440cdf0c1d5Smjnelson
441cdf0c1d5Smjnelson                raise util.Abort("couldn't backup metadata to %s:\n"
442cdf0c1d5Smjnelson                                 "  %s" %
443cdf0c1d5Smjnelson                                 (self.bu.backupfile('metadata.tar.gz'),
444cdf0c1d5Smjnelson                                  error))
445cdf0c1d5Smjnelson        finally:
446cdf0c1d5Smjnelson            if tar and not tar.closed:
447cdf0c1d5Smjnelson                tar.close()
448cdf0c1d5Smjnelson
449cdf0c1d5Smjnelson    def old_restore(self):
450cdf0c1d5Smjnelson        '''Restore workspace metadata from an pre-tar backup'''
451cdf0c1d5Smjnelson
452cdf0c1d5Smjnelson        for fname in self.files:
453cdf0c1d5Smjnelson            bfile = self.bu.backupfile(fname)
454cdf0c1d5Smjnelson            wfile = self.ws.repo.join(fname)
455cdf0c1d5Smjnelson
456cdf0c1d5Smjnelson            if os.path.exists(bfile):
457cdf0c1d5Smjnelson                try:
458cdf0c1d5Smjnelson                    shutil.copy2(bfile, wfile)
459cdf0c1d5Smjnelson                except EnvironmentError, e:
460cdf0c1d5Smjnelson                    raise util.Abort("couldn't restore metadata from %s:\n"
461cdf0c1d5Smjnelson                                     "   %s" % (bfile, e))
462cdf0c1d5Smjnelson
463cdf0c1d5Smjnelson    def tar_restore(self):
464cdf0c1d5Smjnelson        '''Restore workspace metadata (from a tar-style backup)'''
465cdf0c1d5Smjnelson
466cdf0c1d5Smjnelson        if os.path.exists(self.bu.backupfile('metadata.tar.gz')):
467cdf0c1d5Smjnelson            tar = None
468cdf0c1d5Smjnelson
469cdf0c1d5Smjnelson            try:
470cdf0c1d5Smjnelson                try:
471cdf0c1d5Smjnelson                    tar = tarfile.open(self.bu.backupfile('metadata.tar.gz'))
472cdf0c1d5Smjnelson                    tar.errorlevel = 2
473cdf0c1d5Smjnelson                except (EnvironmentError, tarfile.TarError), e:
474cdf0c1d5Smjnelson                    raise util.Abort("couldn't open %s: %s" %
475cdf0c1d5Smjnelson                                 (self.bu.backupfile('metadata.tar.gz'), e))
476cdf0c1d5Smjnelson
477cdf0c1d5Smjnelson                try:
478cdf0c1d5Smjnelson                    for elt in tar:
479cdf0c1d5Smjnelson                        tar.extract(elt, path=self.ws.repo.path)
480cdf0c1d5Smjnelson                except (EnvironmentError, tarfile.TarError), e:
481cdf0c1d5Smjnelson                    # Make sure the member name is in the exception message.
482cdf0c1d5Smjnelson                    if isinstance(e, tarfile.TarError):
483cdf0c1d5Smjnelson                        error = "%s: %s" % (elt.name, e)
484cdf0c1d5Smjnelson                    else:
485cdf0c1d5Smjnelson                        error = str(e)
486cdf0c1d5Smjnelson
487cdf0c1d5Smjnelson                    raise util.Abort("couldn't restore metadata from %s:\n"
488cdf0c1d5Smjnelson                                     "   %s" %
489cdf0c1d5Smjnelson                                     (self.bu.backupfile('metadata.tar.gz'),
490cdf0c1d5Smjnelson                                      error))
491cdf0c1d5Smjnelson            finally:
492cdf0c1d5Smjnelson                if tar and not tar.closed:
493cdf0c1d5Smjnelson                    tar.close()
494cdf0c1d5Smjnelson
495cdf0c1d5Smjnelson    def restore(self):
496cdf0c1d5Smjnelson        '''Restore workspace metadata'''
497cdf0c1d5Smjnelson
498cdf0c1d5Smjnelson        if os.path.exists(self.bu.backupfile('hgrc')):
499cdf0c1d5Smjnelson            self.old_restore()
500cdf0c1d5Smjnelson        else:
501cdf0c1d5Smjnelson            self.tar_restore()
502cdf0c1d5Smjnelson
503cdf0c1d5Smjnelson    def need_backup(self):
504cdf0c1d5Smjnelson        '''Compare backed up workspace metadata to workspace'''
505cdf0c1d5Smjnelson
506cdf0c1d5Smjnelson        if os.path.exists(self.bu.backupfile('metadata.tar.gz')):
507cdf0c1d5Smjnelson            try:
508cdf0c1d5Smjnelson                tar = tarfile.open(self.bu.backupfile('metadata.tar.gz'))
509cdf0c1d5Smjnelson                tar.errorlevel = 2
510cdf0c1d5Smjnelson            except (EnvironmentError, tarfile.TarError), e:
511cdf0c1d5Smjnelson                raise util.Abort("couldn't open metadata tarball: %s\n"
512cdf0c1d5Smjnelson                                 "   %s" %
513cdf0c1d5Smjnelson                                 (self.bu.backupfile('metadata.tar.gz'), e))
514cdf0c1d5Smjnelson
515cdf0c1d5Smjnelson            for elt in tar:
516cdf0c1d5Smjnelson                fpath = self.ws.repo.join(elt.name)
517cdf0c1d5Smjnelson                if not os.path.exists(fpath):
518cdf0c1d5Smjnelson                    return True     # File in tar, not workspace
519cdf0c1d5Smjnelson
520cdf0c1d5Smjnelson                if elt.isdir():     # Don't care about directories
521cdf0c1d5Smjnelson                    continue
522cdf0c1d5Smjnelson
5232b5878deSRich Lowe                #
5242b5878deSRich Lowe                # The filesystem can give us mtime with fractional seconds
5252b5878deSRich Lowe                # (as a float), whereas tar files only keep it to the second.
5262b5878deSRich Lowe                #
5272b5878deSRich Lowe                # Always compare to the integer (second-granularity) mtime.
5282b5878deSRich Lowe                #
5292b5878deSRich Lowe                if (elt.mtime != int(os.path.getmtime(fpath)) or
530cdf0c1d5Smjnelson                    elt.size != os.path.getsize(fpath)):
531cdf0c1d5Smjnelson                    return True
532cdf0c1d5Smjnelson
533cdf0c1d5Smjnelson            tarnames = tar.getnames()
534cdf0c1d5Smjnelson            tar.close()
535cdf0c1d5Smjnelson        else:
536cdf0c1d5Smjnelson            tarnames = []
537cdf0c1d5Smjnelson
538cdf0c1d5Smjnelson        for mfile in self.files:
539cdf0c1d5Smjnelson            fpath = self.ws.repo.join(mfile)
540cdf0c1d5Smjnelson
541cdf0c1d5Smjnelson            if os.path.isdir(fpath):
542cdf0c1d5Smjnelson                # Directories in tarfile always end with a '/'
543cdf0c1d5Smjnelson                if not mfile.endswith('/'):
544cdf0c1d5Smjnelson                    mfile += '/'
545cdf0c1d5Smjnelson
546cdf0c1d5Smjnelson                if mfile not in tarnames:
547cdf0c1d5Smjnelson                    return True
548cdf0c1d5Smjnelson
549cdf0c1d5Smjnelson                for root, dirs, files in os.walk(fpath, topdown=True):
550cdf0c1d5Smjnelson                    for elt in files:
551cdf0c1d5Smjnelson                        path = os.path.join(root, elt)
552cdf0c1d5Smjnelson
553cdf0c1d5Smjnelson                        rpath = self.ws.repo.path
554cdf0c1d5Smjnelson                        if not rpath.endswith('/'):
555cdf0c1d5Smjnelson                            rpath += '/'
556cdf0c1d5Smjnelson
557cdf0c1d5Smjnelson                        path = path.replace(rpath, '', 1)
558cdf0c1d5Smjnelson                        if path not in tarnames:
559cdf0c1d5Smjnelson                            return True # In workspace not tar
560cdf0c1d5Smjnelson            else:
561cdf0c1d5Smjnelson                if os.path.exists(fpath) and mfile not in tarnames:
562cdf0c1d5Smjnelson                    return True
563cdf0c1d5Smjnelson
564cdf0c1d5Smjnelson        return False
565cdf0c1d5Smjnelson
566cdf0c1d5Smjnelson    def cleanup(self):
567cdf0c1d5Smjnelson        '''Remove backed up workspace metadata'''
568cdf0c1d5Smjnelson        if os.path.exists(self.bu.backupfile('metadata.tar.gz')):
569cdf0c1d5Smjnelson            os.unlink(self.bu.backupfile('metadata.tar.gz'))
570cdf0c1d5Smjnelson
571cdf0c1d5Smjnelson
572cdf0c1d5Smjnelsonclass CdmBackup(object):
573cdf0c1d5Smjnelson    '''A backup of a given workspace'''
574cdf0c1d5Smjnelson
575cdf0c1d5Smjnelson    def __init__(self, ui, ws, name):
576cdf0c1d5Smjnelson        self.ws = ws
577cdf0c1d5Smjnelson        self.ui = ui
578cdf0c1d5Smjnelson        self.backupdir = self._find_backup_dir(name)
579cdf0c1d5Smjnelson
580cdf0c1d5Smjnelson        #
581cdf0c1d5Smjnelson        # The order of instances here controls the order the various operations
582cdf0c1d5Smjnelson        # are run.
583cdf0c1d5Smjnelson        #
584cdf0c1d5Smjnelson        # There's some inherent dependence, in that on restore we need
585cdf0c1d5Smjnelson        # to restore committed changes prior to uncommitted changes
586cdf0c1d5Smjnelson        # (as the parent revision of any uncommitted changes is quite
587cdf0c1d5Smjnelson        # likely to not exist until committed changes are restored).
588cdf0c1d5Smjnelson        # Metadata restore can happen at any point, but happens last
589cdf0c1d5Smjnelson        # as a matter of convention.
590cdf0c1d5Smjnelson        #
591cdf0c1d5Smjnelson        self.modules = [x(self, ws) for x in [CdmCommittedBackup,
592cdf0c1d5Smjnelson                                              CdmUncommittedBackup,
593cdf0c1d5Smjnelson                                              CdmMetadataBackup]]
594cdf0c1d5Smjnelson
595cdf0c1d5Smjnelson
596cdf0c1d5Smjnelson        if os.path.exists(os.path.join(self.backupdir, 'latest')):
597cdf0c1d5Smjnelson            generation = os.readlink(os.path.join(self.backupdir, 'latest'))
598cdf0c1d5Smjnelson            self.generation = int(os.path.split(generation)[1])
599cdf0c1d5Smjnelson        else:
600cdf0c1d5Smjnelson            self.generation = 0
601cdf0c1d5Smjnelson
602cdf0c1d5Smjnelson    def _find_backup_dir(self, name):
603cdf0c1d5Smjnelson        '''Find the path to an appropriate backup directory based on NAME'''
604cdf0c1d5Smjnelson        backupdir = None
605cdf0c1d5Smjnelson        backupbase = None
606cdf0c1d5Smjnelson
607cdf0c1d5Smjnelson        if os.path.isabs(name):
608cdf0c1d5Smjnelson            return name
609cdf0c1d5Smjnelson
610cdf0c1d5Smjnelson        if self.ui.config('cdm', 'backupdir'):
611cdf0c1d5Smjnelson            backupbase = os.path.expanduser(self.ui.config('cdm', 'backupdir'))
612cdf0c1d5Smjnelson        else:
613cdf0c1d5Smjnelson            home = None
614cdf0c1d5Smjnelson
615cdf0c1d5Smjnelson            try:
616cdf0c1d5Smjnelson                home = os.getenv('HOME') or pwd.getpwuid(os.getuid()).pw_dir
617cdf0c1d5Smjnelson            except KeyError:
618cdf0c1d5Smjnelson                pass                    # Handled anyway
619cdf0c1d5Smjnelson
620cdf0c1d5Smjnelson            if not home:
621cdf0c1d5Smjnelson                raise util.Abort('Could not determine your HOME directory to '
622cdf0c1d5Smjnelson                                 'find backup path')
623cdf0c1d5Smjnelson
624cdf0c1d5Smjnelson            backupbase = os.path.join(home, 'cdm.backup')
625cdf0c1d5Smjnelson
626cdf0c1d5Smjnelson        backupdir = os.path.join(backupbase, name)
627cdf0c1d5Smjnelson
628cdf0c1d5Smjnelson        # If backupdir exists, it must be a directory.
629cdf0c1d5Smjnelson        if (os.path.exists(backupdir) and not os.path.isdir(backupdir)):
630cdf0c1d5Smjnelson            raise util.Abort('%s exists but is not a directory' % backupdir)
631cdf0c1d5Smjnelson
632cdf0c1d5Smjnelson        return backupdir
633cdf0c1d5Smjnelson
634cdf0c1d5Smjnelson    def backupfile(self, path):
635cdf0c1d5Smjnelson        '''return full path to backup file FILE at GEN'''
636cdf0c1d5Smjnelson        return os.path.join(self.backupdir, str(self.generation), path)
637cdf0c1d5Smjnelson
638cdf0c1d5Smjnelson    def update_latest(self, gen):
639cdf0c1d5Smjnelson        '''Update latest symlink to point to the current generation'''
640cdf0c1d5Smjnelson        linkpath = os.path.join(self.backupdir, 'latest')
641cdf0c1d5Smjnelson
642cdf0c1d5Smjnelson        if os.path.lexists(linkpath):
643cdf0c1d5Smjnelson            os.unlink(linkpath)
644cdf0c1d5Smjnelson
645cdf0c1d5Smjnelson        os.symlink(str(gen), linkpath)
646cdf0c1d5Smjnelson
647cdf0c1d5Smjnelson    def create_gen(self, gen):
648cdf0c1d5Smjnelson        '''Create a new backup generation'''
649cdf0c1d5Smjnelson        try:
650cdf0c1d5Smjnelson            os.makedirs(os.path.join(self.backupdir, str(gen)))
651cdf0c1d5Smjnelson            self.update_latest(gen)
652cdf0c1d5Smjnelson        except EnvironmentError, e:
653cdf0c1d5Smjnelson            raise util.Abort("Couldn't create backup generation %s: %s" %
654cdf0c1d5Smjnelson                             (os.path.join(self.backupdir, str(gen)), e))
655cdf0c1d5Smjnelson
656cdf0c1d5Smjnelson    def need_backup(self):
657cdf0c1d5Smjnelson        '''Compare backed up changes to workspace'''
658cdf0c1d5Smjnelson        #
659cdf0c1d5Smjnelson        # If there's no current backup generation, or the last backup was
660cdf0c1d5Smjnelson        # invalid (lacking the dirstate file), we need a backup regardless
661cdf0c1d5Smjnelson        # of anything else.
662cdf0c1d5Smjnelson        #
663cdf0c1d5Smjnelson        if (not self.generation or
664cdf0c1d5Smjnelson            not os.path.exists(self.backupfile('dirstate'))):
665cdf0c1d5Smjnelson            return True
666cdf0c1d5Smjnelson
667cdf0c1d5Smjnelson        for x in self.modules:
668cdf0c1d5Smjnelson            if x.need_backup():
669cdf0c1d5Smjnelson                return True
670cdf0c1d5Smjnelson
671cdf0c1d5Smjnelson        return False
672cdf0c1d5Smjnelson
673cdf0c1d5Smjnelson    def backup(self):
674*c959a081SRichard Lowe        '''Take a backup of the current workspace
675*c959a081SRichard Lowe
676*c959a081SRichard Lowe        Calling code is expected to hold both the working copy lock
677*c959a081SRichard Lowe        and repository lock.'''
678cdf0c1d5Smjnelson
679cdf0c1d5Smjnelson        if not os.path.exists(self.backupdir):
680cdf0c1d5Smjnelson            try:
681cdf0c1d5Smjnelson                os.makedirs(self.backupdir)
682cdf0c1d5Smjnelson            except EnvironmentError, e:
683cdf0c1d5Smjnelson                raise util.Abort('Could not create backup directory %s: %s' %
684cdf0c1d5Smjnelson                                 (self.backupdir, e))
685cdf0c1d5Smjnelson
686cdf0c1d5Smjnelson        self.generation += 1
687cdf0c1d5Smjnelson        self.create_gen(self.generation)
688cdf0c1d5Smjnelson
689cdf0c1d5Smjnelson        try:
690cdf0c1d5Smjnelson            for x in self.modules:
691cdf0c1d5Smjnelson                x.backup()
692cdf0c1d5Smjnelson        except Exception, e:
693cdf0c1d5Smjnelson            if isinstance(e, KeyboardInterrupt):
694cdf0c1d5Smjnelson                self.ws.ui.warn("Interrupted\n")
695cdf0c1d5Smjnelson            else:
696cdf0c1d5Smjnelson                self.ws.ui.warn("Error: %s\n" % e)
697*c959a081SRichard Lowe                if Version.at_least("1.3.0"):
698*c959a081SRichard Lowe                    show_traceback = self.ws.ui.configbool('ui', 'traceback',
699*c959a081SRichard Lowe                                                   False)
700*c959a081SRichard Lowe                else:
701*c959a081SRichard Lowe                    show_traceback = self.ws.ui.traceback
702cdf0c1d5Smjnelson
703cdf0c1d5Smjnelson                #
704cdf0c1d5Smjnelson                # If it's not a 'normal' error, we want to print a stack
705cdf0c1d5Smjnelson                # trace now in case the attempt to remove the partial
706cdf0c1d5Smjnelson                # backup also fails, and raises a second exception.
707cdf0c1d5Smjnelson                #
708cdf0c1d5Smjnelson                if (not isinstance(e, (EnvironmentError, util.Abort))
709*c959a081SRichard Lowe                    or show_traceback):
710cdf0c1d5Smjnelson                    traceback.print_exc()
711cdf0c1d5Smjnelson
712cdf0c1d5Smjnelson            for x in self.modules:
713cdf0c1d5Smjnelson                x.cleanup()
714cdf0c1d5Smjnelson
715cdf0c1d5Smjnelson            os.rmdir(os.path.join(self.backupdir, str(self.generation)))
716cdf0c1d5Smjnelson            self.generation -= 1
717cdf0c1d5Smjnelson
718cdf0c1d5Smjnelson            if self.generation != 0:
719cdf0c1d5Smjnelson                self.update_latest(self.generation)
720cdf0c1d5Smjnelson            else:
721cdf0c1d5Smjnelson                os.unlink(os.path.join(self.backupdir, 'latest'))
722cdf0c1d5Smjnelson
723cdf0c1d5Smjnelson            raise util.Abort('Backup failed')
724cdf0c1d5Smjnelson
725cdf0c1d5Smjnelson    def restore(self, gen=None):
726cdf0c1d5Smjnelson        '''Restore workspace from backup
727cdf0c1d5Smjnelson
728cdf0c1d5Smjnelson        Restores from backup generation GEN (defaulting to the latest)
729*c959a081SRichard Lowe        into workspace WS.
730cdf0c1d5Smjnelson
731*c959a081SRichard Lowe        Calling code is expected to hold both the working copy lock
732*c959a081SRichard Lowe        and repository lock of the destination workspace.'''
733cdf0c1d5Smjnelson
734cdf0c1d5Smjnelson        if not os.path.exists(self.backupdir):
735cdf0c1d5Smjnelson            raise util.Abort('Backup directory does not exist: %s' %
736cdf0c1d5Smjnelson                             (self.backupdir))
737cdf0c1d5Smjnelson
738cdf0c1d5Smjnelson        if gen:
739cdf0c1d5Smjnelson            if not os.path.exists(os.path.join(self.backupdir, str(gen))):
740cdf0c1d5Smjnelson                raise util.Abort('Backup generation does not exist: %s' %
741cdf0c1d5Smjnelson                                 (os.path.join(self.backupdir, str(gen))))
742cdf0c1d5Smjnelson            self.generation = int(gen)
743cdf0c1d5Smjnelson
744cdf0c1d5Smjnelson        if not self.generation: # This is ok, 0 is not a valid generation
745cdf0c1d5Smjnelson            raise util.Abort('Backup has no generations: %s' % self.backupdir)
746cdf0c1d5Smjnelson
747cdf0c1d5Smjnelson        if not os.path.exists(self.backupfile('dirstate')):
748cdf0c1d5Smjnelson            raise util.Abort('Backup %s/%s is incomplete (dirstate missing)' %
749cdf0c1d5Smjnelson                             (self.backupdir, self.generation))
750cdf0c1d5Smjnelson
751cdf0c1d5Smjnelson        try:
752cdf0c1d5Smjnelson            for x in self.modules:
753cdf0c1d5Smjnelson                x.restore()
754cdf0c1d5Smjnelson        except util.Abort, e:
755cdf0c1d5Smjnelson            raise util.Abort('Error restoring workspace:\n'
756cdf0c1d5Smjnelson                             '%s\n'
75712203c71SRichard Lowe                             'Workspace may be partially restored' % e)
758