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