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