xref: /titanic_50/usr/src/tools/onbld/Scm/Backup.py (revision 9a70fc3be3b1e966bf78825cdb8d509963a6f0a1)
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#
17cdf0c1d5Smjnelson# Copyright 2008 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)
29cdf0c1d5Smjnelson                File containing dirstate nodeid (the tip we expect to be at
30cdf0c1d5Smjnelson                after applying the bundle).
31cdf0c1d5Smjnelson
32cdf0c1d5Smjnelson            bundle (handled by CdmCommittedBackup)
33cdf0c1d5Smjnelson                An Hg bundle containing outgoing committed changes.
34cdf0c1d5Smjnelson
35cdf0c1d5Smjnelson            nodes (handled by CdmCommittedBackup)
36cdf0c1d5Smjnelson                A text file listing the full (hex) nodeid of all nodes in
37cdf0c1d5Smjnelson                bundle, used by need_backup.
38cdf0c1d5Smjnelson
39cdf0c1d5Smjnelson            diff (handled by CdmUncommittedBackup)
40cdf0c1d5Smjnelson                A Git-formatted diff containing uncommitted changes.
41cdf0c1d5Smjnelson
42cdf0c1d5Smjnelson            renames (handled by CdmUncommittedBackup)
43cdf0c1d5Smjnelson                A list of renames in the working copy that have to be
44cdf0c1d5Smjnelson                applied manually, rather than by the diff.
45cdf0c1d5Smjnelson
46cdf0c1d5Smjnelson            metadata.tar.gz (handled by CdmMetadataBackup)
47cdf0c1d5Smjnelson                $CODEMGR_WS/.hg/hgrc
48cdf0c1d5Smjnelson                $CODEMGR_WS/.hg/localtags
49cdf0c1d5Smjnelson                $CODEMGR_WS/.hg/patches (Mq data)
50cdf0c1d5Smjnelson
51cdf0c1d5Smjnelson         latest -> generation#
52cdf0c1d5Smjnelson            Newest backup generation.
53cdf0c1d5Smjnelson
54cdf0c1d5SmjnelsonAll files in a given backup generation, with the exception of
55cdf0c1d5Smjnelsondirstate, are optional.
56cdf0c1d5Smjnelson'''
57cdf0c1d5Smjnelson
58cdf0c1d5Smjnelsonimport os, pwd, shutil, traceback, tarfile, time
59cdf0c1d5Smjnelsonfrom mercurial import changegroup, patch, node, util
60cdf0c1d5Smjnelsonfrom cStringIO import StringIO
61cdf0c1d5Smjnelson
62cdf0c1d5Smjnelson
63cdf0c1d5Smjnelsonclass CdmCommittedBackup(object):
64cdf0c1d5Smjnelson    '''Backup of committed changes'''
65cdf0c1d5Smjnelson
66cdf0c1d5Smjnelson    def __init__(self, backup, ws):
67cdf0c1d5Smjnelson        self.ws = ws
68cdf0c1d5Smjnelson        self.bu = backup
69cdf0c1d5Smjnelson        self.files = ('bundle', 'nodes')
70cdf0c1d5Smjnelson
71cdf0c1d5Smjnelson    def _outgoing_nodes(self, parent):
72cdf0c1d5Smjnelson        '''Return a list of all outgoing nodes in hex format'''
73cdf0c1d5Smjnelson
74cdf0c1d5Smjnelson        if parent:
75cdf0c1d5Smjnelson            outgoing = self.ws.findoutgoing(parent)
76cdf0c1d5Smjnelson            nodes = self.ws.repo.changelog.nodesbetween(outgoing)[0]
77cdf0c1d5Smjnelson            return map(node.hex, nodes)
78cdf0c1d5Smjnelson        else:
79cdf0c1d5Smjnelson            return []
80cdf0c1d5Smjnelson
81cdf0c1d5Smjnelson    def backup(self):
82cdf0c1d5Smjnelson        '''Backup committed changes'''
83cdf0c1d5Smjnelson        parent = self.ws.parent()
84cdf0c1d5Smjnelson
85cdf0c1d5Smjnelson        if not parent:
86cdf0c1d5Smjnelson            self.ws.ui.warn('Workspace has no parent, committed changes will '
87cdf0c1d5Smjnelson                            'not be backed up\n')
88cdf0c1d5Smjnelson            return
89cdf0c1d5Smjnelson
90cdf0c1d5Smjnelson        out = self.ws.findoutgoing(parent)
91cdf0c1d5Smjnelson        if not out:
92cdf0c1d5Smjnelson            return
93cdf0c1d5Smjnelson
94cdf0c1d5Smjnelson        cg = self.ws.repo.changegroup(out, 'bundle')
95cdf0c1d5Smjnelson        changegroup.writebundle(cg, self.bu.backupfile('bundle'), 'HG10BZ')
96cdf0c1d5Smjnelson
97cdf0c1d5Smjnelson        outnodes = self._outgoing_nodes(parent)
98cdf0c1d5Smjnelson        if outnodes:
99cdf0c1d5Smjnelson            fp = None
100cdf0c1d5Smjnelson            try:
101cdf0c1d5Smjnelson                try:
102cdf0c1d5Smjnelson                    fp = open(self.bu.backupfile('nodes'), 'w')
103cdf0c1d5Smjnelson                    fp.write('%s\n' % '\n'.join(outnodes))
104cdf0c1d5Smjnelson                except EnvironmentError, e:
105cdf0c1d5Smjnelson                    raise util.Abort("couldn't store outgoing nodes: %s" % e)
106cdf0c1d5Smjnelson            finally:
107cdf0c1d5Smjnelson                if fp and not fp.closed:
108cdf0c1d5Smjnelson                    fp.close()
109cdf0c1d5Smjnelson
110cdf0c1d5Smjnelson    def restore(self):
111cdf0c1d5Smjnelson        '''Restore committed changes from backup'''
112cdf0c1d5Smjnelson        bfile = self.bu.backupfile('bundle')
113cdf0c1d5Smjnelson
114cdf0c1d5Smjnelson        if os.path.exists(bfile):
115cdf0c1d5Smjnelson            f = None
116cdf0c1d5Smjnelson            try:
117cdf0c1d5Smjnelson                try:
118cdf0c1d5Smjnelson                    f = open(bfile, 'r')
119cdf0c1d5Smjnelson                    bundle = changegroup.readbundle(f, bfile)
120cdf0c1d5Smjnelson                    self.ws.repo.addchangegroup(bundle, 'strip',
121cdf0c1d5Smjnelson                                                'bundle:%s' % bfile)
122cdf0c1d5Smjnelson                except EnvironmentError, e:
123cdf0c1d5Smjnelson                    raise util.Abort("couldn't restore committed changes: %s\n"
124cdf0c1d5Smjnelson                                     "   %s" % (bfile, e))
125cdf0c1d5Smjnelson            finally:
126cdf0c1d5Smjnelson                if f and not f.closed:
127cdf0c1d5Smjnelson                    f.close()
128cdf0c1d5Smjnelson
129cdf0c1d5Smjnelson    def need_backup(self):
130cdf0c1d5Smjnelson        '''Compare backup of committed changes to workspace'''
131cdf0c1d5Smjnelson
132cdf0c1d5Smjnelson        if os.path.exists(self.bu.backupfile('nodes')):
133cdf0c1d5Smjnelson            f = None
134cdf0c1d5Smjnelson            try:
135cdf0c1d5Smjnelson                try:
136cdf0c1d5Smjnelson                    f = open(self.bu.backupfile('nodes'))
137cdf0c1d5Smjnelson                    bnodes = set([line.rstrip('\r\n')
138cdf0c1d5Smjnelson                                  for line in f.readlines()])
139cdf0c1d5Smjnelson                    f.close()
140cdf0c1d5Smjnelson                except EnvironmentError, e:
141cdf0c1d5Smjnelson                    raise util.Abort("couldn't open backup node list: %s" % e)
142cdf0c1d5Smjnelson            finally:
143cdf0c1d5Smjnelson                if f and not f.closed:
144cdf0c1d5Smjnelson                    f.close()
145cdf0c1d5Smjnelson        else:
146cdf0c1d5Smjnelson            bnodes = set()
147cdf0c1d5Smjnelson
148cdf0c1d5Smjnelson        outnodes = set(self._outgoing_nodes(self.ws.parent()))
149cdf0c1d5Smjnelson        if outnodes != bnodes:
150cdf0c1d5Smjnelson            return True
151cdf0c1d5Smjnelson
152cdf0c1d5Smjnelson        return False
153cdf0c1d5Smjnelson
154cdf0c1d5Smjnelson    def cleanup(self):
155cdf0c1d5Smjnelson        '''Remove backed up committed changes'''
156cdf0c1d5Smjnelson
157cdf0c1d5Smjnelson        for fname in self.files:
158cdf0c1d5Smjnelson            if os.path.exists(self.bu.backupfile(fname)):
159cdf0c1d5Smjnelson                os.unlink(self.bu.backupfile(fname))
160cdf0c1d5Smjnelson
161cdf0c1d5Smjnelson
162cdf0c1d5Smjnelsonclass CdmUncommittedBackup(object):
163cdf0c1d5Smjnelson    '''Backup of uncommitted changes'''
164cdf0c1d5Smjnelson
165cdf0c1d5Smjnelson    def __init__(self, backup, ws):
166cdf0c1d5Smjnelson        self.ws = ws
167cdf0c1d5Smjnelson        self.bu = backup
168cdf0c1d5Smjnelson
169cdf0c1d5Smjnelson    def _clobbering_renames(self):
170cdf0c1d5Smjnelson        '''Return a list of pairs of files representing renames/copies
171cdf0c1d5Smjnelson        that clobber already versioned files.  [(oldname newname)...]'''
172cdf0c1d5Smjnelson
173cdf0c1d5Smjnelson        #
174cdf0c1d5Smjnelson        # Note that this doesn't handle uncommitted merges
175cdf0c1d5Smjnelson        # as CdmUncommittedBackup itself doesn't.
176cdf0c1d5Smjnelson        #
177cdf0c1d5Smjnelson        wctx = self.ws.repo.workingctx()
178cdf0c1d5Smjnelson        parent = wctx.parents()[0]
179cdf0c1d5Smjnelson
180cdf0c1d5Smjnelson        ret = []
181cdf0c1d5Smjnelson        for fname in wctx.added() + wctx.modified():
182cdf0c1d5Smjnelson            rn = wctx.filectx(fname).renamed()
183cdf0c1d5Smjnelson            if rn and fname in parent:
184cdf0c1d5Smjnelson                ret.append((rn[0], fname))
185cdf0c1d5Smjnelson        return ret
186cdf0c1d5Smjnelson
187cdf0c1d5Smjnelson    def backup(self):
188cdf0c1d5Smjnelson        '''Backup uncommitted changes'''
189cdf0c1d5Smjnelson
190cdf0c1d5Smjnelson        if self.ws.merged():
191cdf0c1d5Smjnelson            raise util.Abort("Unable to backup an uncommitted merge.\n"
192cdf0c1d5Smjnelson                             "Please complete your merge and commit")
193cdf0c1d5Smjnelson
194cdf0c1d5Smjnelson        dirstate = node.hex(self.ws.repo.changectx().node())
195cdf0c1d5Smjnelson
196cdf0c1d5Smjnelson        fp = None
197cdf0c1d5Smjnelson        try:
198cdf0c1d5Smjnelson            try:
199cdf0c1d5Smjnelson                fp = open(self.bu.backupfile('dirstate'), 'w')
200cdf0c1d5Smjnelson                fp.write(dirstate + '\n')
201cdf0c1d5Smjnelson            except EnvironmentError, e:
202cdf0c1d5Smjnelson                raise util.Abort("couldn't save working copy parent: %s" % e)
203cdf0c1d5Smjnelson        finally:
204cdf0c1d5Smjnelson            if fp and not fp.closed:
205cdf0c1d5Smjnelson                fp.close()
206cdf0c1d5Smjnelson
207cdf0c1d5Smjnelson        try:
208cdf0c1d5Smjnelson            try:
209cdf0c1d5Smjnelson                fp = open(self.bu.backupfile('renames'), 'w')
210cdf0c1d5Smjnelson                for cons in self._clobbering_renames():
211cdf0c1d5Smjnelson                    fp.write("%s %s\n" % cons)
212cdf0c1d5Smjnelson            except EnvironmentError, e:
213cdf0c1d5Smjnelson                raise util.Abort("couldn't save clobbering copies: %s" % e)
214cdf0c1d5Smjnelson        finally:
215cdf0c1d5Smjnelson            if fp and not fp.closed:
216cdf0c1d5Smjnelson                fp.close()
217cdf0c1d5Smjnelson
218cdf0c1d5Smjnelson        try:
219cdf0c1d5Smjnelson            try:
220cdf0c1d5Smjnelson                fp = open(self.bu.backupfile('diff'), 'w')
221cdf0c1d5Smjnelson                patch.diff(self.ws.repo, fp=fp,
222cdf0c1d5Smjnelson                           opts=patch.diffopts(self.ws.ui, opts={'git': True}))
223cdf0c1d5Smjnelson            except EnvironmentError, e:
224cdf0c1d5Smjnelson                raise util.Abort("couldn't save working copy diff: %s" % e)
225cdf0c1d5Smjnelson        finally:
226cdf0c1d5Smjnelson            if fp and not fp.closed:
227cdf0c1d5Smjnelson                fp.close()
228cdf0c1d5Smjnelson
229cdf0c1d5Smjnelson    def _dirstate(self):
230cdf0c1d5Smjnelson        '''Return the current working copy node'''
231cdf0c1d5Smjnelson        fp = None
232cdf0c1d5Smjnelson        try:
233cdf0c1d5Smjnelson            try:
234cdf0c1d5Smjnelson                fp = open(self.bu.backupfile('dirstate'))
235cdf0c1d5Smjnelson                dirstate = fp.readline().strip()
236cdf0c1d5Smjnelson                return dirstate
237cdf0c1d5Smjnelson            except EnvironmentError, e:
238cdf0c1d5Smjnelson                raise util.Abort("couldn't read saved parent: %s" % e)
239cdf0c1d5Smjnelson        finally:
240cdf0c1d5Smjnelson            if fp and not fp.closed:
241cdf0c1d5Smjnelson                fp.close()
242cdf0c1d5Smjnelson
243cdf0c1d5Smjnelson    def restore(self):
244cdf0c1d5Smjnelson        '''Restore uncommitted changes'''
245cdf0c1d5Smjnelson        diff = self.bu.backupfile('diff')
246cdf0c1d5Smjnelson        dirstate = self._dirstate()
247cdf0c1d5Smjnelson
248cdf0c1d5Smjnelson        try:
249cdf0c1d5Smjnelson            self.ws.clean(rev=dirstate)
250cdf0c1d5Smjnelson        except util.Abort, e:
251cdf0c1d5Smjnelson            raise util.Abort("couldn't update to saved node: %s" % e)
252cdf0c1d5Smjnelson
253cdf0c1d5Smjnelson        if not os.path.exists(diff):
254cdf0c1d5Smjnelson            return
255cdf0c1d5Smjnelson
256cdf0c1d5Smjnelson        #
257cdf0c1d5Smjnelson        # There's a race here whereby if the patch (or part thereof)
258cdf0c1d5Smjnelson        # is applied within the same second as the clean above (such
259cdf0c1d5Smjnelson        # that mtime doesn't change) and if the size of that file
260cdf0c1d5Smjnelson        # does not change, Hg may not see the change.
261cdf0c1d5Smjnelson        #
262cdf0c1d5Smjnelson        # We sleep a full second to avoid this, as sleeping merely
263cdf0c1d5Smjnelson        # until the next second begins would require very close clock
264cdf0c1d5Smjnelson        # synchronization on network filesystems.
265cdf0c1d5Smjnelson        #
266cdf0c1d5Smjnelson        time.sleep(1)
267cdf0c1d5Smjnelson
268cdf0c1d5Smjnelson        files = {}
269cdf0c1d5Smjnelson        try:
270cdf0c1d5Smjnelson            try:
271cdf0c1d5Smjnelson                fuzz = patch.patch(diff, self.ws.ui, strip=1,
272cdf0c1d5Smjnelson                                   cwd=self.ws.repo.root, files=files)
273cdf0c1d5Smjnelson                if fuzz:
274cdf0c1d5Smjnelson                    raise util.Abort('working copy diff applied with fuzz')
275cdf0c1d5Smjnelson            except Exception, e:
276cdf0c1d5Smjnelson                raise util.Abort("couldn't apply working copy diff: %s\n"
277cdf0c1d5Smjnelson                                 "   %s" % (diff, e))
278cdf0c1d5Smjnelson        finally:
279cdf0c1d5Smjnelson            patch.updatedir(self.ws.ui, self.ws.repo, files)
280cdf0c1d5Smjnelson
281cdf0c1d5Smjnelson        if not os.path.exists(self.bu.backupfile('renames')):
282cdf0c1d5Smjnelson            return
283cdf0c1d5Smjnelson
284cdf0c1d5Smjnelson        #
285cdf0c1d5Smjnelson        # We need to re-apply name changes where the new name
286cdf0c1d5Smjnelson        # (rename/copy destination) is an already versioned file, as
287cdf0c1d5Smjnelson        # Hg would otherwise ignore them.
288cdf0c1d5Smjnelson        #
289cdf0c1d5Smjnelson        try:
290cdf0c1d5Smjnelson            fp = open(self.bu.backupfile('renames'))
291cdf0c1d5Smjnelson            for line in fp:
292cdf0c1d5Smjnelson                source, dest = line.strip().split()
293cdf0c1d5Smjnelson                self.ws.repo.copy(source, dest)
294cdf0c1d5Smjnelson        except EnvironmentError, e:
295cdf0c1d5Smjnelson            raise util.Abort('unable to open renames file: %s' % e)
296cdf0c1d5Smjnelson        except ValueError:
297cdf0c1d5Smjnelson            raise util.Abort('corrupt renames file: %s' %
298cdf0c1d5Smjnelson                             self.bu.backupfile('renames'))
299cdf0c1d5Smjnelson
300cdf0c1d5Smjnelson    def need_backup(self):
301cdf0c1d5Smjnelson        '''Compare backup of uncommitted changes to workspace'''
302cdf0c1d5Smjnelson        if self._dirstate() != node.hex(self.ws.repo.changectx().node()):
303cdf0c1d5Smjnelson            return True
304cdf0c1d5Smjnelson
305cdf0c1d5Smjnelson        curdiff = StringIO()
306cdf0c1d5Smjnelson        diff = self.bu.backupfile('diff')
307cdf0c1d5Smjnelson        fd = None
308cdf0c1d5Smjnelson
309cdf0c1d5Smjnelson        patch.diff(self.ws.repo, fp=curdiff,
310cdf0c1d5Smjnelson                   opts=patch.diffopts(self.ws.ui, opts={'git': True}))
311cdf0c1d5Smjnelson
312cdf0c1d5Smjnelson        if os.path.exists(diff):
313cdf0c1d5Smjnelson            try:
314cdf0c1d5Smjnelson                try:
315cdf0c1d5Smjnelson                    fd = open(diff)
316cdf0c1d5Smjnelson                    backdiff = fd.read()
317cdf0c1d5Smjnelson                except EnvironmentError, e:
318cdf0c1d5Smjnelson                    raise util.Abort("couldn't open backup diff %s\n"
319cdf0c1d5Smjnelson                                     "   %s" % (diff, e))
320cdf0c1d5Smjnelson            finally:
321cdf0c1d5Smjnelson                if fd and not fd.closed:
322cdf0c1d5Smjnelson                    fd.close()
323cdf0c1d5Smjnelson        else:
324cdf0c1d5Smjnelson            backdiff = ''
325cdf0c1d5Smjnelson
326cdf0c1d5Smjnelson        if backdiff != curdiff.getvalue():
327cdf0c1d5Smjnelson            return True
328cdf0c1d5Smjnelson
329cdf0c1d5Smjnelson
330cdf0c1d5Smjnelson        currrenamed = self._clobbering_renames()
331cdf0c1d5Smjnelson        bakrenamed = None
332cdf0c1d5Smjnelson
333cdf0c1d5Smjnelson        if os.path.exists(self.bu.backupfile('renames')):
334cdf0c1d5Smjnelson            try:
335cdf0c1d5Smjnelson                try:
336cdf0c1d5Smjnelson                    fd = open(self.bu.backupfile('renames'))
337cdf0c1d5Smjnelson                    bakrenamed = [line.strip().split(' ') for line in fd]
338cdf0c1d5Smjnelson                except EnvironmentError, e:
339cdf0c1d5Smjnelson                    raise util.Abort("couldn't open renames file %s: %s\n" %
340cdf0c1d5Smjnelson                                     (self.bu.backupfile('renames'), e))
341cdf0c1d5Smjnelson            finally:
342cdf0c1d5Smjnelson                if fd and not fd.closed:
343cdf0c1d5Smjnelson                    fd.close()
344cdf0c1d5Smjnelson
345cdf0c1d5Smjnelson            if currrenamed != bakrenamed:
346cdf0c1d5Smjnelson                return True
347cdf0c1d5Smjnelson
348cdf0c1d5Smjnelson        return False
349cdf0c1d5Smjnelson
350cdf0c1d5Smjnelson    def cleanup(self):
351cdf0c1d5Smjnelson        '''Remove backed up uncommitted changes'''
352cdf0c1d5Smjnelson        for fname in ('dirstate', 'diff', 'renames'):
353cdf0c1d5Smjnelson            if os.path.exists(self.bu.backupfile(fname)):
354cdf0c1d5Smjnelson                os.unlink(self.bu.backupfile(fname))
355cdf0c1d5Smjnelson
356cdf0c1d5Smjnelson
357cdf0c1d5Smjnelsonclass CdmMetadataBackup(object):
358cdf0c1d5Smjnelson    '''Backup of workspace metadata'''
359cdf0c1d5Smjnelson
360cdf0c1d5Smjnelson    def __init__(self, backup, ws):
361cdf0c1d5Smjnelson        self.bu = backup
362cdf0c1d5Smjnelson        self.ws = ws
363*9a70fc3bSMark J. Nelson        self.files = ('hgrc', 'localtags', 'patches', 'cdm')
364cdf0c1d5Smjnelson
365cdf0c1d5Smjnelson    def backup(self):
366cdf0c1d5Smjnelson        '''Backup workspace metadata'''
367cdf0c1d5Smjnelson
368cdf0c1d5Smjnelson        tar = None
369cdf0c1d5Smjnelson
370cdf0c1d5Smjnelson        try:
371cdf0c1d5Smjnelson            try:
372cdf0c1d5Smjnelson                tar = tarfile.open(self.bu.backupfile('metadata.tar.gz'),
373cdf0c1d5Smjnelson                                   'w:gz')
374cdf0c1d5Smjnelson                tar.errorlevel = 2
375cdf0c1d5Smjnelson            except (EnvironmentError, tarfile.TarError), e:
376cdf0c1d5Smjnelson                raise util.Abort("couldn't open %s for writing: %s" %
377cdf0c1d5Smjnelson                                 (self.bu.backupfile('metadata.tar.gz'), e))
378cdf0c1d5Smjnelson
379cdf0c1d5Smjnelson            try:
380cdf0c1d5Smjnelson                for elt in self.files:
381cdf0c1d5Smjnelson                    fpath = self.ws.repo.join(elt)
382cdf0c1d5Smjnelson                    if os.path.exists(fpath):
383cdf0c1d5Smjnelson                        tar.add(fpath, elt)
384cdf0c1d5Smjnelson            except (EnvironmentError, tarfile.TarError), e:
385cdf0c1d5Smjnelson                #
386cdf0c1d5Smjnelson                # tarfile.TarError doesn't include the tar member or file
387cdf0c1d5Smjnelson                # in question, so we have to do so ourselves.
388cdf0c1d5Smjnelson                #
389cdf0c1d5Smjnelson                if isinstance(e, tarfile.TarError):
390cdf0c1d5Smjnelson                    error = "%s: %s" % (elt, e)
391cdf0c1d5Smjnelson                else:
392cdf0c1d5Smjnelson                    error = str(e)
393cdf0c1d5Smjnelson
394cdf0c1d5Smjnelson                raise util.Abort("couldn't backup metadata to %s:\n"
395cdf0c1d5Smjnelson                                 "  %s" %
396cdf0c1d5Smjnelson                                 (self.bu.backupfile('metadata.tar.gz'),
397cdf0c1d5Smjnelson                                  error))
398cdf0c1d5Smjnelson        finally:
399cdf0c1d5Smjnelson            if tar and not tar.closed:
400cdf0c1d5Smjnelson                tar.close()
401cdf0c1d5Smjnelson
402cdf0c1d5Smjnelson    def old_restore(self):
403cdf0c1d5Smjnelson        '''Restore workspace metadata from an pre-tar backup'''
404cdf0c1d5Smjnelson
405cdf0c1d5Smjnelson        for fname in self.files:
406cdf0c1d5Smjnelson            bfile = self.bu.backupfile(fname)
407cdf0c1d5Smjnelson            wfile = self.ws.repo.join(fname)
408cdf0c1d5Smjnelson
409cdf0c1d5Smjnelson            if os.path.exists(bfile):
410cdf0c1d5Smjnelson                try:
411cdf0c1d5Smjnelson                    shutil.copy2(bfile, wfile)
412cdf0c1d5Smjnelson                except EnvironmentError, e:
413cdf0c1d5Smjnelson                    raise util.Abort("couldn't restore metadata from %s:\n"
414cdf0c1d5Smjnelson                                     "   %s" % (bfile, e))
415cdf0c1d5Smjnelson
416cdf0c1d5Smjnelson    def tar_restore(self):
417cdf0c1d5Smjnelson        '''Restore workspace metadata (from a tar-style backup)'''
418cdf0c1d5Smjnelson
419cdf0c1d5Smjnelson        if os.path.exists(self.bu.backupfile('metadata.tar.gz')):
420cdf0c1d5Smjnelson            tar = None
421cdf0c1d5Smjnelson
422cdf0c1d5Smjnelson            try:
423cdf0c1d5Smjnelson                try:
424cdf0c1d5Smjnelson                    tar = tarfile.open(self.bu.backupfile('metadata.tar.gz'))
425cdf0c1d5Smjnelson                    tar.errorlevel = 2
426cdf0c1d5Smjnelson                except (EnvironmentError, tarfile.TarError), e:
427cdf0c1d5Smjnelson                    raise util.Abort("couldn't open %s: %s" %
428cdf0c1d5Smjnelson                                 (self.bu.backupfile('metadata.tar.gz'), e))
429cdf0c1d5Smjnelson
430cdf0c1d5Smjnelson                try:
431cdf0c1d5Smjnelson                    for elt in tar:
432cdf0c1d5Smjnelson                        tar.extract(elt, path=self.ws.repo.path)
433cdf0c1d5Smjnelson                except (EnvironmentError, tarfile.TarError), e:
434cdf0c1d5Smjnelson                    # Make sure the member name is in the exception message.
435cdf0c1d5Smjnelson                    if isinstance(e, tarfile.TarError):
436cdf0c1d5Smjnelson                        error = "%s: %s" % (elt.name, e)
437cdf0c1d5Smjnelson                    else:
438cdf0c1d5Smjnelson                        error = str(e)
439cdf0c1d5Smjnelson
440cdf0c1d5Smjnelson                    raise util.Abort("couldn't restore metadata from %s:\n"
441cdf0c1d5Smjnelson                                     "   %s" %
442cdf0c1d5Smjnelson                                     (self.bu.backupfile('metadata.tar.gz'),
443cdf0c1d5Smjnelson                                      error))
444cdf0c1d5Smjnelson            finally:
445cdf0c1d5Smjnelson                if tar and not tar.closed:
446cdf0c1d5Smjnelson                    tar.close()
447cdf0c1d5Smjnelson
448cdf0c1d5Smjnelson    def restore(self):
449cdf0c1d5Smjnelson        '''Restore workspace metadata'''
450cdf0c1d5Smjnelson
451cdf0c1d5Smjnelson        if os.path.exists(self.bu.backupfile('hgrc')):
452cdf0c1d5Smjnelson            self.old_restore()
453cdf0c1d5Smjnelson        else:
454cdf0c1d5Smjnelson            self.tar_restore()
455cdf0c1d5Smjnelson
456cdf0c1d5Smjnelson    def need_backup(self):
457cdf0c1d5Smjnelson        '''Compare backed up workspace metadata to workspace'''
458cdf0c1d5Smjnelson
459cdf0c1d5Smjnelson        if os.path.exists(self.bu.backupfile('metadata.tar.gz')):
460cdf0c1d5Smjnelson            try:
461cdf0c1d5Smjnelson                tar = tarfile.open(self.bu.backupfile('metadata.tar.gz'))
462cdf0c1d5Smjnelson                tar.errorlevel = 2
463cdf0c1d5Smjnelson            except (EnvironmentError, tarfile.TarError), e:
464cdf0c1d5Smjnelson                raise util.Abort("couldn't open metadata tarball: %s\n"
465cdf0c1d5Smjnelson                                 "   %s" %
466cdf0c1d5Smjnelson                                 (self.bu.backupfile('metadata.tar.gz'), e))
467cdf0c1d5Smjnelson
468cdf0c1d5Smjnelson            for elt in tar:
469cdf0c1d5Smjnelson                fpath = self.ws.repo.join(elt.name)
470cdf0c1d5Smjnelson                if not os.path.exists(fpath):
471cdf0c1d5Smjnelson                    return True     # File in tar, not workspace
472cdf0c1d5Smjnelson
473cdf0c1d5Smjnelson                if elt.isdir():     # Don't care about directories
474cdf0c1d5Smjnelson                    continue
475cdf0c1d5Smjnelson
476cdf0c1d5Smjnelson                if (elt.mtime != os.path.getmtime(fpath) or
477cdf0c1d5Smjnelson                    elt.size != os.path.getsize(fpath)):
478cdf0c1d5Smjnelson                    return True
479cdf0c1d5Smjnelson
480cdf0c1d5Smjnelson            tarnames = tar.getnames()
481cdf0c1d5Smjnelson            tar.close()
482cdf0c1d5Smjnelson        else:
483cdf0c1d5Smjnelson            tarnames = []
484cdf0c1d5Smjnelson
485cdf0c1d5Smjnelson        for mfile in self.files:
486cdf0c1d5Smjnelson            fpath = self.ws.repo.join(mfile)
487cdf0c1d5Smjnelson
488cdf0c1d5Smjnelson            if os.path.isdir(fpath):
489cdf0c1d5Smjnelson                # Directories in tarfile always end with a '/'
490cdf0c1d5Smjnelson                if not mfile.endswith('/'):
491cdf0c1d5Smjnelson                    mfile += '/'
492cdf0c1d5Smjnelson
493cdf0c1d5Smjnelson                if mfile not in tarnames:
494cdf0c1d5Smjnelson                    return True
495cdf0c1d5Smjnelson
496cdf0c1d5Smjnelson                for root, dirs, files in os.walk(fpath, topdown=True):
497cdf0c1d5Smjnelson                    for elt in files:
498cdf0c1d5Smjnelson                        path = os.path.join(root, elt)
499cdf0c1d5Smjnelson
500cdf0c1d5Smjnelson                        rpath = self.ws.repo.path
501cdf0c1d5Smjnelson                        if not rpath.endswith('/'):
502cdf0c1d5Smjnelson                            rpath += '/'
503cdf0c1d5Smjnelson
504cdf0c1d5Smjnelson                        path = path.replace(rpath, '', 1)
505cdf0c1d5Smjnelson                        if path not in tarnames:
506cdf0c1d5Smjnelson                            return True # In workspace not tar
507cdf0c1d5Smjnelson            else:
508cdf0c1d5Smjnelson                if os.path.exists(fpath) and mfile not in tarnames:
509cdf0c1d5Smjnelson                    return True
510cdf0c1d5Smjnelson
511cdf0c1d5Smjnelson        return False
512cdf0c1d5Smjnelson
513cdf0c1d5Smjnelson    def cleanup(self):
514cdf0c1d5Smjnelson        '''Remove backed up workspace metadata'''
515cdf0c1d5Smjnelson        if os.path.exists(self.bu.backupfile('metadata.tar.gz')):
516cdf0c1d5Smjnelson            os.unlink(self.bu.backupfile('metadata.tar.gz'))
517cdf0c1d5Smjnelson
518cdf0c1d5Smjnelson
519cdf0c1d5Smjnelsonclass CdmBackup(object):
520cdf0c1d5Smjnelson    '''A backup of a given workspace'''
521cdf0c1d5Smjnelson
522cdf0c1d5Smjnelson    def __init__(self, ui, ws, name):
523cdf0c1d5Smjnelson        self.ws = ws
524cdf0c1d5Smjnelson        self.ui = ui
525cdf0c1d5Smjnelson        self.backupdir = self._find_backup_dir(name)
526cdf0c1d5Smjnelson
527cdf0c1d5Smjnelson        #
528cdf0c1d5Smjnelson        # The order of instances here controls the order the various operations
529cdf0c1d5Smjnelson        # are run.
530cdf0c1d5Smjnelson        #
531cdf0c1d5Smjnelson        # There's some inherent dependence, in that on restore we need
532cdf0c1d5Smjnelson        # to restore committed changes prior to uncommitted changes
533cdf0c1d5Smjnelson        # (as the parent revision of any uncommitted changes is quite
534cdf0c1d5Smjnelson        # likely to not exist until committed changes are restored).
535cdf0c1d5Smjnelson        # Metadata restore can happen at any point, but happens last
536cdf0c1d5Smjnelson        # as a matter of convention.
537cdf0c1d5Smjnelson        #
538cdf0c1d5Smjnelson        self.modules = [x(self, ws) for x in [CdmCommittedBackup,
539cdf0c1d5Smjnelson                                              CdmUncommittedBackup,
540cdf0c1d5Smjnelson                                              CdmMetadataBackup]]
541cdf0c1d5Smjnelson
542cdf0c1d5Smjnelson
543cdf0c1d5Smjnelson        if os.path.exists(os.path.join(self.backupdir, 'latest')):
544cdf0c1d5Smjnelson            generation = os.readlink(os.path.join(self.backupdir, 'latest'))
545cdf0c1d5Smjnelson            self.generation = int(os.path.split(generation)[1])
546cdf0c1d5Smjnelson        else:
547cdf0c1d5Smjnelson            self.generation = 0
548cdf0c1d5Smjnelson
549cdf0c1d5Smjnelson    def _find_backup_dir(self, name):
550cdf0c1d5Smjnelson        '''Find the path to an appropriate backup directory based on NAME'''
551cdf0c1d5Smjnelson        backupdir = None
552cdf0c1d5Smjnelson        backupbase = None
553cdf0c1d5Smjnelson
554cdf0c1d5Smjnelson        if os.path.isabs(name):
555cdf0c1d5Smjnelson            return name
556cdf0c1d5Smjnelson
557cdf0c1d5Smjnelson        if self.ui.config('cdm', 'backupdir'):
558cdf0c1d5Smjnelson            backupbase = os.path.expanduser(self.ui.config('cdm', 'backupdir'))
559cdf0c1d5Smjnelson        else:
560cdf0c1d5Smjnelson            home = None
561cdf0c1d5Smjnelson
562cdf0c1d5Smjnelson            try:
563cdf0c1d5Smjnelson                home = os.getenv('HOME') or pwd.getpwuid(os.getuid()).pw_dir
564cdf0c1d5Smjnelson            except KeyError:
565cdf0c1d5Smjnelson                pass                    # Handled anyway
566cdf0c1d5Smjnelson
567cdf0c1d5Smjnelson            if not home:
568cdf0c1d5Smjnelson                raise util.Abort('Could not determine your HOME directory to '
569cdf0c1d5Smjnelson                                 'find backup path')
570cdf0c1d5Smjnelson
571cdf0c1d5Smjnelson            backupbase = os.path.join(home, 'cdm.backup')
572cdf0c1d5Smjnelson
573cdf0c1d5Smjnelson        backupdir = os.path.join(backupbase, name)
574cdf0c1d5Smjnelson
575cdf0c1d5Smjnelson        # If backupdir exists, it must be a directory.
576cdf0c1d5Smjnelson        if (os.path.exists(backupdir) and not os.path.isdir(backupdir)):
577cdf0c1d5Smjnelson            raise util.Abort('%s exists but is not a directory' % backupdir)
578cdf0c1d5Smjnelson
579cdf0c1d5Smjnelson        return backupdir
580cdf0c1d5Smjnelson
581cdf0c1d5Smjnelson    def backupfile(self, path):
582cdf0c1d5Smjnelson        '''return full path to backup file FILE at GEN'''
583cdf0c1d5Smjnelson        return os.path.join(self.backupdir, str(self.generation), path)
584cdf0c1d5Smjnelson
585cdf0c1d5Smjnelson    def update_latest(self, gen):
586cdf0c1d5Smjnelson        '''Update latest symlink to point to the current generation'''
587cdf0c1d5Smjnelson        linkpath = os.path.join(self.backupdir, 'latest')
588cdf0c1d5Smjnelson
589cdf0c1d5Smjnelson        if os.path.lexists(linkpath):
590cdf0c1d5Smjnelson            os.unlink(linkpath)
591cdf0c1d5Smjnelson
592cdf0c1d5Smjnelson        os.symlink(str(gen), linkpath)
593cdf0c1d5Smjnelson
594cdf0c1d5Smjnelson    def create_gen(self, gen):
595cdf0c1d5Smjnelson        '''Create a new backup generation'''
596cdf0c1d5Smjnelson        try:
597cdf0c1d5Smjnelson            os.makedirs(os.path.join(self.backupdir, str(gen)))
598cdf0c1d5Smjnelson            self.update_latest(gen)
599cdf0c1d5Smjnelson        except EnvironmentError, e:
600cdf0c1d5Smjnelson            raise util.Abort("Couldn't create backup generation %s: %s" %
601cdf0c1d5Smjnelson                             (os.path.join(self.backupdir, str(gen)), e))
602cdf0c1d5Smjnelson
603cdf0c1d5Smjnelson    def need_backup(self):
604cdf0c1d5Smjnelson        '''Compare backed up changes to workspace'''
605cdf0c1d5Smjnelson        #
606cdf0c1d5Smjnelson        # If there's no current backup generation, or the last backup was
607cdf0c1d5Smjnelson        # invalid (lacking the dirstate file), we need a backup regardless
608cdf0c1d5Smjnelson        # of anything else.
609cdf0c1d5Smjnelson        #
610cdf0c1d5Smjnelson        if (not self.generation or
611cdf0c1d5Smjnelson            not os.path.exists(self.backupfile('dirstate'))):
612cdf0c1d5Smjnelson            return True
613cdf0c1d5Smjnelson
614cdf0c1d5Smjnelson        for x in self.modules:
615cdf0c1d5Smjnelson            if x.need_backup():
616cdf0c1d5Smjnelson                return True
617cdf0c1d5Smjnelson
618cdf0c1d5Smjnelson        return False
619cdf0c1d5Smjnelson
620cdf0c1d5Smjnelson    def backup(self):
621cdf0c1d5Smjnelson        '''Take a backup of the current workspace'''
622cdf0c1d5Smjnelson
623cdf0c1d5Smjnelson        if not os.path.exists(self.backupdir):
624cdf0c1d5Smjnelson            try:
625cdf0c1d5Smjnelson                os.makedirs(self.backupdir)
626cdf0c1d5Smjnelson            except EnvironmentError, e:
627cdf0c1d5Smjnelson                raise util.Abort('Could not create backup directory %s: %s' %
628cdf0c1d5Smjnelson                                 (self.backupdir, e))
629cdf0c1d5Smjnelson
630cdf0c1d5Smjnelson        self.generation += 1
631cdf0c1d5Smjnelson        self.create_gen(self.generation)
632cdf0c1d5Smjnelson
633cdf0c1d5Smjnelson        #
634cdf0c1d5Smjnelson        # Lock the repo, so the backup can be consistent.  We need the
635cdf0c1d5Smjnelson        # wlock too to make sure the dirstate parent doesn't change
636cdf0c1d5Smjnelson        # underneath us.
637cdf0c1d5Smjnelson        #
638cdf0c1d5Smjnelson
639cdf0c1d5Smjnelson        lock = self.ws.repo.lock()
640cdf0c1d5Smjnelson        wlock = self.ws.repo.lock()
641cdf0c1d5Smjnelson
642cdf0c1d5Smjnelson        try:
643cdf0c1d5Smjnelson            for x in self.modules:
644cdf0c1d5Smjnelson                x.backup()
645cdf0c1d5Smjnelson        except Exception, e:
646cdf0c1d5Smjnelson            if isinstance(e, KeyboardInterrupt):
647cdf0c1d5Smjnelson                self.ws.ui.warn("Interrupted\n")
648cdf0c1d5Smjnelson            else:
649cdf0c1d5Smjnelson                self.ws.ui.warn("Error: %s\n" % e)
650cdf0c1d5Smjnelson
651cdf0c1d5Smjnelson                #
652cdf0c1d5Smjnelson                # If it's not a 'normal' error, we want to print a stack
653cdf0c1d5Smjnelson                # trace now in case the attempt to remove the partial
654cdf0c1d5Smjnelson                # backup also fails, and raises a second exception.
655cdf0c1d5Smjnelson                #
656cdf0c1d5Smjnelson                if (not isinstance(e, (EnvironmentError, util.Abort))
657cdf0c1d5Smjnelson                    or self.ws.ui.traceback):
658cdf0c1d5Smjnelson                    traceback.print_exc()
659cdf0c1d5Smjnelson
660cdf0c1d5Smjnelson            for x in self.modules:
661cdf0c1d5Smjnelson                x.cleanup()
662cdf0c1d5Smjnelson
663cdf0c1d5Smjnelson            os.rmdir(os.path.join(self.backupdir, str(self.generation)))
664cdf0c1d5Smjnelson            self.generation -= 1
665cdf0c1d5Smjnelson
666cdf0c1d5Smjnelson            if self.generation != 0:
667cdf0c1d5Smjnelson                self.update_latest(self.generation)
668cdf0c1d5Smjnelson            else:
669cdf0c1d5Smjnelson                os.unlink(os.path.join(self.backupdir, 'latest'))
670cdf0c1d5Smjnelson
671cdf0c1d5Smjnelson            raise util.Abort('Backup failed')
672cdf0c1d5Smjnelson
673cdf0c1d5Smjnelson    def restore(self, gen=None):
674cdf0c1d5Smjnelson        '''Restore workspace from backup
675cdf0c1d5Smjnelson
676cdf0c1d5Smjnelson        Restores from backup generation GEN (defaulting to the latest)
677cdf0c1d5Smjnelson        into workspace WS.'''
678cdf0c1d5Smjnelson
679cdf0c1d5Smjnelson        wlock = self.ws.repo.wlock()
680cdf0c1d5Smjnelson        lock = self.ws.repo.lock()
681cdf0c1d5Smjnelson
682cdf0c1d5Smjnelson        if not os.path.exists(self.backupdir):
683cdf0c1d5Smjnelson            raise util.Abort('Backup directory does not exist: %s' %
684cdf0c1d5Smjnelson                             (self.backupdir))
685cdf0c1d5Smjnelson
686cdf0c1d5Smjnelson        if gen:
687cdf0c1d5Smjnelson            if not os.path.exists(os.path.join(self.backupdir, str(gen))):
688cdf0c1d5Smjnelson                raise util.Abort('Backup generation does not exist: %s' %
689cdf0c1d5Smjnelson                                 (os.path.join(self.backupdir, str(gen))))
690cdf0c1d5Smjnelson            self.generation = int(gen)
691cdf0c1d5Smjnelson
692cdf0c1d5Smjnelson        if not self.generation: # This is ok, 0 is not a valid generation
693cdf0c1d5Smjnelson            raise util.Abort('Backup has no generations: %s' % self.backupdir)
694cdf0c1d5Smjnelson
695cdf0c1d5Smjnelson        if not os.path.exists(self.backupfile('dirstate')):
696cdf0c1d5Smjnelson            raise util.Abort('Backup %s/%s is incomplete (dirstate missing)' %
697cdf0c1d5Smjnelson                             (self.backupdir, self.generation))
698cdf0c1d5Smjnelson
699cdf0c1d5Smjnelson        try:
700cdf0c1d5Smjnelson            for x in self.modules:
701cdf0c1d5Smjnelson                x.restore()
702cdf0c1d5Smjnelson        except util.Abort, e:
703cdf0c1d5Smjnelson            raise util.Abort('Error restoring workspace:\n'
704cdf0c1d5Smjnelson                             '%s\n'
705cdf0c1d5Smjnelson                             'Workspace will be partially restored' % e)
706