xref: /titanic_50/usr/src/tools/onbld/Scm/Backup.py (revision 605a716e6d38b3af09034c254382d0ae3b7d5f70)
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*605a716eSRichard Lowe# Copyright 2008, 2011, Richard Lowe
2187039217SRichard Lowe#
22cdf0c1d5Smjnelson
23*605a716eSRichard Lowe
24cdf0c1d5Smjnelson'''
25cdf0c1d5SmjnelsonWorkspace backup
26cdf0c1d5Smjnelson
27cdf0c1d5SmjnelsonBackup format is:
28cdf0c1d5Smjnelson   backupdir/
29cdf0c1d5Smjnelson      wsname/
30cdf0c1d5Smjnelson         generation#/
31cdf0c1d5Smjnelson            dirstate (handled by CdmUncommittedBackup)
3212203c71SRichard Lowe                File containing dirstate nodeid (the changeset we need
3312203c71SRichard Lowe                to update the workspace to after applying the bundle).
3412203c71SRichard Lowe                This is the node to which the working copy changes
3512203c71SRichard Lowe                (see 'diff', below) will be applied if applicable.
36cdf0c1d5Smjnelson
37cdf0c1d5Smjnelson            bundle (handled by CdmCommittedBackup)
38cdf0c1d5Smjnelson                An Hg bundle containing outgoing committed changes.
39cdf0c1d5Smjnelson
40cdf0c1d5Smjnelson            nodes (handled by CdmCommittedBackup)
41cdf0c1d5Smjnelson                A text file listing the full (hex) nodeid of all nodes in
42cdf0c1d5Smjnelson                bundle, used by need_backup.
43cdf0c1d5Smjnelson
44cdf0c1d5Smjnelson            diff (handled by CdmUncommittedBackup)
45cdf0c1d5Smjnelson                A Git-formatted diff containing uncommitted changes.
46cdf0c1d5Smjnelson
47cdf0c1d5Smjnelson            renames (handled by CdmUncommittedBackup)
48cdf0c1d5Smjnelson                A list of renames in the working copy that have to be
49cdf0c1d5Smjnelson                applied manually, rather than by the diff.
50cdf0c1d5Smjnelson
51cdf0c1d5Smjnelson            metadata.tar.gz (handled by CdmMetadataBackup)
52cdf0c1d5Smjnelson                $CODEMGR_WS/.hg/hgrc
53cdf0c1d5Smjnelson                $CODEMGR_WS/.hg/localtags
54cdf0c1d5Smjnelson                $CODEMGR_WS/.hg/patches (Mq data)
55cdf0c1d5Smjnelson
56*605a716eSRichard Lowe            clear.tar.gz (handled by CdmClearBackup)
57*605a716eSRichard Lowe                <short node>/
58*605a716eSRichard Lowe                    copies of each modified or added file, as it is in
59*605a716eSRichard Lowe                    this head.
60*605a716eSRichard Lowe
61*605a716eSRichard Lowe                 ... for each outgoing head
62*605a716eSRichard Lowe
63*605a716eSRichard Lowe                working/
64*605a716eSRichard Lowe                     copies of each modified or added file in the
65*605a716eSRichard Lowe                     working copy if any.
66*605a716eSRichard Lowe
67cdf0c1d5Smjnelson         latest -> generation#
68cdf0c1d5Smjnelson            Newest backup generation.
69cdf0c1d5Smjnelson
70cdf0c1d5SmjnelsonAll files in a given backup generation, with the exception of
71cdf0c1d5Smjnelsondirstate, are optional.
72cdf0c1d5Smjnelson'''
73cdf0c1d5Smjnelson
74*605a716eSRichard Loweimport grp, os, pwd, shutil, tarfile, time, traceback
75*605a716eSRichard Lowefrom cStringIO import StringIO
76*605a716eSRichard Lowe
7787039217SRichard Lowefrom mercurial import changegroup, error, node, patch, util
78cdf0c1d5Smjnelson
79cdf0c1d5Smjnelson
8012203c71SRichard Loweclass CdmNodeMissing(util.Abort):
8112203c71SRichard Lowe    '''a required node is not present in the destination workspace.
8212203c71SRichard Lowe
8312203c71SRichard Lowe    This may occur both in the case where the bundle contains a
8412203c71SRichard Lowe    changeset which is a child of a node not present in the
8512203c71SRichard Lowe    destination workspace (because the destination workspace is not as
8612203c71SRichard Lowe    up-to-date as the source), or because the source and destination
8712203c71SRichard Lowe    workspace are not related.
8812203c71SRichard Lowe
8912203c71SRichard Lowe    It may also happen in cases where the uncommitted changes need to
9012203c71SRichard Lowe    be applied onto a node that the workspace does not possess even
9112203c71SRichard Lowe    after application of the bundle (on a branch not present
9212203c71SRichard Lowe    in the bundle or destination workspace, for instance)'''
9312203c71SRichard Lowe
9412203c71SRichard Lowe    def __init__(self, msg, name):
9512203c71SRichard Lowe        #
9612203c71SRichard Lowe        # If e.name is a string 20 characters long, it is
9712203c71SRichard Lowe        # assumed to be a node.  (Mercurial makes this
9812203c71SRichard Lowe        # same assumption, when creating a LookupError)
9912203c71SRichard Lowe        #
10012203c71SRichard Lowe        if isinstance(name, str) and len(name) == 20:
10112203c71SRichard Lowe            n = node.short(name)
10212203c71SRichard Lowe        else:
10312203c71SRichard Lowe            n = name
10412203c71SRichard Lowe
10512203c71SRichard Lowe        util.Abort.__init__(self, "%s: changeset '%s' is missing\n"
10612203c71SRichard Lowe                            "Your workspace is either not "
10712203c71SRichard Lowe                            "sufficiently up to date,\n"
10812203c71SRichard Lowe                            "or is unrelated to the workspace from "
10912203c71SRichard Lowe                            "which the backup was taken.\n" % (msg, n))
11012203c71SRichard Lowe
11112203c71SRichard Lowe
112*605a716eSRichard Loweclass CdmTarFile(tarfile.TarFile):
113*605a716eSRichard Lowe    '''Tar file access + simple comparison to the filesystem, and
114*605a716eSRichard Lowe    creation addition of files from Mercurial filectx objects.'''
115*605a716eSRichard Lowe
116*605a716eSRichard Lowe    def __init__(self, *args, **kwargs):
117*605a716eSRichard Lowe        tarfile.TarFile.__init__(self, *args, **kwargs)
118*605a716eSRichard Lowe        self.errorlevel = 2
119*605a716eSRichard Lowe
120*605a716eSRichard Lowe    def members_match_fs(self, rootpath):
121*605a716eSRichard Lowe        '''Compare the contents of the tar archive to the directory
122*605a716eSRichard Lowe        specified by rootpath.  Return False if they differ.
123*605a716eSRichard Lowe
124*605a716eSRichard Lowe        Every file in the archive must match the equivalent file in
125*605a716eSRichard Lowe        the filesystem.
126*605a716eSRichard Lowe
127*605a716eSRichard Lowe        The existence, modification time, and size of each file are
128*605a716eSRichard Lowe        compared, content is not.'''
129*605a716eSRichard Lowe
130*605a716eSRichard Lowe        def _member_matches_fs(member, rootpath):
131*605a716eSRichard Lowe            '''Compare a single member to its filesystem counterpart'''
132*605a716eSRichard Lowe            fpath = os.path.join(rootpath, member.name)
133*605a716eSRichard Lowe
134*605a716eSRichard Lowe            if not os.path.exists(fpath):
135*605a716eSRichard Lowe                return False
136*605a716eSRichard Lowe            elif ((os.path.isfile(fpath) != member.isfile()) or
137*605a716eSRichard Lowe                  (os.path.isdir(fpath) != member.isdir()) or
138*605a716eSRichard Lowe                  (os.path.islink(fpath) != member.issym())):
139*605a716eSRichard Lowe                return False
140*605a716eSRichard Lowe
141*605a716eSRichard Lowe            #
142*605a716eSRichard Lowe            # The filesystem may return a modification time with a
143*605a716eSRichard Lowe            # fractional component (as a float), whereas the tar format
144*605a716eSRichard Lowe            # only stores it to the whole second, perform the comparison
145*605a716eSRichard Lowe            # using integers (truncated, not rounded)
146*605a716eSRichard Lowe            #
147*605a716eSRichard Lowe            elif member.mtime != int(os.path.getmtime(fpath)):
148*605a716eSRichard Lowe                return False
149*605a716eSRichard Lowe            elif not member.isdir() and member.size != os.path.getsize(fpath):
150*605a716eSRichard Lowe                return False
151*605a716eSRichard Lowe            else:
152*605a716eSRichard Lowe                return True
153*605a716eSRichard Lowe
154*605a716eSRichard Lowe        for elt in self:
155*605a716eSRichard Lowe            if not _member_matches_fs(elt, rootpath):
156*605a716eSRichard Lowe                return False
157*605a716eSRichard Lowe
158*605a716eSRichard Lowe        return True
159*605a716eSRichard Lowe
160*605a716eSRichard Lowe    def addfilectx(self, filectx, path=None):
161*605a716eSRichard Lowe        '''Add a filectx object to the archive.
162*605a716eSRichard Lowe
163*605a716eSRichard Lowe        Use the path specified by the filectx object or, if specified,
164*605a716eSRichard Lowe        the PATH argument.
165*605a716eSRichard Lowe
166*605a716eSRichard Lowe        The size, modification time, type and permissions of the tar
167*605a716eSRichard Lowe        member are taken from the filectx object, user and group id
168*605a716eSRichard Lowe        are those of the invoking user, user and group name are those
169*605a716eSRichard Lowe        of the invoking user if information is available, or "unknown"
170*605a716eSRichard Lowe        if it is not.
171*605a716eSRichard Lowe        '''
172*605a716eSRichard Lowe
173*605a716eSRichard Lowe        t = tarfile.TarInfo(path or filectx.path())
174*605a716eSRichard Lowe        t.size = filectx.size()
175*605a716eSRichard Lowe        t.mtime = filectx.date()[0]
176*605a716eSRichard Lowe        t.uid = os.getuid()
177*605a716eSRichard Lowe        t.gid = os.getgid()
178*605a716eSRichard Lowe
179*605a716eSRichard Lowe        try:
180*605a716eSRichard Lowe            t.uname = pwd.getpwuid(t.uid).pw_name
181*605a716eSRichard Lowe        except KeyError:
182*605a716eSRichard Lowe            t.uname = "unknown"
183*605a716eSRichard Lowe
184*605a716eSRichard Lowe        try:
185*605a716eSRichard Lowe            t.gname = grp.getgrgid(t.gid).gr_name
186*605a716eSRichard Lowe        except KeyError:
187*605a716eSRichard Lowe            t.gname = "unknown"
188*605a716eSRichard Lowe
189*605a716eSRichard Lowe        #
190*605a716eSRichard Lowe        # Mercurial versions symlinks by setting a flag and storing
191*605a716eSRichard Lowe        # the destination path in place of the file content.  The
192*605a716eSRichard Lowe        # actual contents (in the tar), should be empty.
193*605a716eSRichard Lowe        #
194*605a716eSRichard Lowe        if 'l' in filectx.flags():
195*605a716eSRichard Lowe            t.type = tarfile.SYMTYPE
196*605a716eSRichard Lowe            t.mode = 0777
197*605a716eSRichard Lowe            t.linkname = filectx.data()
198*605a716eSRichard Lowe            data = None
199*605a716eSRichard Lowe        else:
200*605a716eSRichard Lowe            t.type = tarfile.REGTYPE
201*605a716eSRichard Lowe            t.mode = 'x' in filectx.flags() and 0755 or 0644
202*605a716eSRichard Lowe            data = StringIO(filectx.data())
203*605a716eSRichard Lowe
204*605a716eSRichard Lowe        self.addfile(t, data)
205*605a716eSRichard Lowe
206*605a716eSRichard Lowe
207cdf0c1d5Smjnelsonclass CdmCommittedBackup(object):
208cdf0c1d5Smjnelson    '''Backup of committed changes'''
209cdf0c1d5Smjnelson
210cdf0c1d5Smjnelson    def __init__(self, backup, ws):
211cdf0c1d5Smjnelson        self.ws = ws
212cdf0c1d5Smjnelson        self.bu = backup
213cdf0c1d5Smjnelson        self.files = ('bundle', 'nodes')
214cdf0c1d5Smjnelson
215cdf0c1d5Smjnelson    def _outgoing_nodes(self, parent):
216cdf0c1d5Smjnelson        '''Return a list of all outgoing nodes in hex format'''
217cdf0c1d5Smjnelson
218cdf0c1d5Smjnelson        if parent:
219cdf0c1d5Smjnelson            outgoing = self.ws.findoutgoing(parent)
220cdf0c1d5Smjnelson            nodes = self.ws.repo.changelog.nodesbetween(outgoing)[0]
221cdf0c1d5Smjnelson            return map(node.hex, nodes)
222cdf0c1d5Smjnelson        else:
223cdf0c1d5Smjnelson            return []
224cdf0c1d5Smjnelson
225cdf0c1d5Smjnelson    def backup(self):
226cdf0c1d5Smjnelson        '''Backup committed changes'''
227cdf0c1d5Smjnelson        parent = self.ws.parent()
228cdf0c1d5Smjnelson
229cdf0c1d5Smjnelson        if not parent:
230cdf0c1d5Smjnelson            self.ws.ui.warn('Workspace has no parent, committed changes will '
231cdf0c1d5Smjnelson                            'not be backed up\n')
232cdf0c1d5Smjnelson            return
233cdf0c1d5Smjnelson
234cdf0c1d5Smjnelson        out = self.ws.findoutgoing(parent)
235cdf0c1d5Smjnelson        if not out:
236cdf0c1d5Smjnelson            return
237cdf0c1d5Smjnelson
238cdf0c1d5Smjnelson        cg = self.ws.repo.changegroup(out, 'bundle')
239cdf0c1d5Smjnelson        changegroup.writebundle(cg, self.bu.backupfile('bundle'), 'HG10BZ')
240cdf0c1d5Smjnelson
241cdf0c1d5Smjnelson        outnodes = self._outgoing_nodes(parent)
242*605a716eSRichard Lowe        if not outnodes:
243*605a716eSRichard Lowe            return
244*605a716eSRichard Lowe
245cdf0c1d5Smjnelson        fp = None
246cdf0c1d5Smjnelson        try:
247cdf0c1d5Smjnelson            try:
248*605a716eSRichard Lowe                fp = self.bu.open('nodes', 'w')
249cdf0c1d5Smjnelson                fp.write('%s\n' % '\n'.join(outnodes))
250cdf0c1d5Smjnelson            except EnvironmentError, e:
251cdf0c1d5Smjnelson                raise util.Abort("couldn't store outgoing nodes: %s" % e)
252cdf0c1d5Smjnelson        finally:
253cdf0c1d5Smjnelson            if fp and not fp.closed:
254cdf0c1d5Smjnelson                fp.close()
255cdf0c1d5Smjnelson
256cdf0c1d5Smjnelson    def restore(self):
257cdf0c1d5Smjnelson        '''Restore committed changes from backup'''
258cdf0c1d5Smjnelson
259*605a716eSRichard Lowe        if not self.bu.exists('bundle'):
260*605a716eSRichard Lowe            return
261*605a716eSRichard Lowe
262*605a716eSRichard Lowe        bpath = self.bu.backupfile('bundle')
263cdf0c1d5Smjnelson        f = None
264cdf0c1d5Smjnelson        try:
265cdf0c1d5Smjnelson            try:
266*605a716eSRichard Lowe                f = self.bu.open('bundle')
267*605a716eSRichard Lowe                bundle = changegroup.readbundle(f, bpath)
268cdf0c1d5Smjnelson                self.ws.repo.addchangegroup(bundle, 'strip',
269*605a716eSRichard Lowe                                            'bundle:%s' % bpath)
270cdf0c1d5Smjnelson            except EnvironmentError, e:
271cdf0c1d5Smjnelson                raise util.Abort("couldn't restore committed changes: %s\n"
272*605a716eSRichard Lowe                                 "   %s" % (bpath, e))
27387039217SRichard Lowe            except error.LookupError, e:
27412203c71SRichard Lowe                raise CdmNodeMissing("couldn't restore committed changes",
27512203c71SRichard Lowe                                                 e.name)
276cdf0c1d5Smjnelson        finally:
277cdf0c1d5Smjnelson            if f and not f.closed:
278cdf0c1d5Smjnelson                f.close()
279cdf0c1d5Smjnelson
280cdf0c1d5Smjnelson    def need_backup(self):
281cdf0c1d5Smjnelson        '''Compare backup of committed changes to workspace'''
282cdf0c1d5Smjnelson
283*605a716eSRichard Lowe        if self.bu.exists('nodes'):
284cdf0c1d5Smjnelson            f = None
285cdf0c1d5Smjnelson            try:
286cdf0c1d5Smjnelson                try:
287*605a716eSRichard Lowe                    f = self.bu.open('nodes')
288*605a716eSRichard Lowe                    bnodes = set(line.rstrip('\r\n') for line in f.readlines())
289cdf0c1d5Smjnelson                    f.close()
290cdf0c1d5Smjnelson                except EnvironmentError, e:
291cdf0c1d5Smjnelson                    raise util.Abort("couldn't open backup node list: %s" % e)
292cdf0c1d5Smjnelson            finally:
293cdf0c1d5Smjnelson                if f and not f.closed:
294cdf0c1d5Smjnelson                    f.close()
295cdf0c1d5Smjnelson        else:
296cdf0c1d5Smjnelson            bnodes = set()
297cdf0c1d5Smjnelson
298cdf0c1d5Smjnelson        outnodes = set(self._outgoing_nodes(self.ws.parent()))
299*605a716eSRichard Lowe
300*605a716eSRichard Lowe        #
301*605a716eSRichard Lowe        # If there are outgoing nodes not in the prior backup we need
302*605a716eSRichard Lowe        # to take a new backup; it's fine if there are nodes in the
303*605a716eSRichard Lowe        # old backup which are no longer outgoing, however.
304*605a716eSRichard Lowe        #
305*605a716eSRichard Lowe        if not outnodes <= bnodes:
306cdf0c1d5Smjnelson            return True
307cdf0c1d5Smjnelson
308cdf0c1d5Smjnelson        return False
309cdf0c1d5Smjnelson
310cdf0c1d5Smjnelson    def cleanup(self):
311cdf0c1d5Smjnelson        '''Remove backed up committed changes'''
312cdf0c1d5Smjnelson
313*605a716eSRichard Lowe        for f in self.files:
314*605a716eSRichard Lowe            self.bu.unlink(f)
315cdf0c1d5Smjnelson
316cdf0c1d5Smjnelson
317cdf0c1d5Smjnelsonclass CdmUncommittedBackup(object):
318cdf0c1d5Smjnelson    '''Backup of uncommitted changes'''
319cdf0c1d5Smjnelson
320cdf0c1d5Smjnelson    def __init__(self, backup, ws):
321cdf0c1d5Smjnelson        self.ws = ws
322cdf0c1d5Smjnelson        self.bu = backup
323*605a716eSRichard Lowe        self.wctx = self.ws.workingctx(worklist=True)
324cdf0c1d5Smjnelson
325cdf0c1d5Smjnelson    def _clobbering_renames(self):
326cdf0c1d5Smjnelson        '''Return a list of pairs of files representing renames/copies
327*605a716eSRichard Lowe        that clobber already versioned files.  [(old-name new-name)...]
328*605a716eSRichard Lowe        '''
329cdf0c1d5Smjnelson
330cdf0c1d5Smjnelson        #
331cdf0c1d5Smjnelson        # Note that this doesn't handle uncommitted merges
332cdf0c1d5Smjnelson        # as CdmUncommittedBackup itself doesn't.
333cdf0c1d5Smjnelson        #
334*605a716eSRichard Lowe        parent = self.wctx.parents()[0]
335cdf0c1d5Smjnelson
336cdf0c1d5Smjnelson        ret = []
337*605a716eSRichard Lowe        for fname in self.wctx.added() + self.wctx.modified():
338*605a716eSRichard Lowe            rn = self.wctx.filectx(fname).renamed()
339cdf0c1d5Smjnelson            if rn and fname in parent:
340cdf0c1d5Smjnelson                ret.append((rn[0], fname))
341cdf0c1d5Smjnelson        return ret
342cdf0c1d5Smjnelson
343cdf0c1d5Smjnelson    def backup(self):
344cdf0c1d5Smjnelson        '''Backup uncommitted changes'''
345cdf0c1d5Smjnelson
346cdf0c1d5Smjnelson        if self.ws.merged():
347cdf0c1d5Smjnelson            raise util.Abort("Unable to backup an uncommitted merge.\n"
348cdf0c1d5Smjnelson                             "Please complete your merge and commit")
349cdf0c1d5Smjnelson
350*605a716eSRichard Lowe        dirstate = node.hex(self.wctx.parents()[0].node())
351cdf0c1d5Smjnelson
352cdf0c1d5Smjnelson        fp = None
353cdf0c1d5Smjnelson        try:
354cdf0c1d5Smjnelson            try:
355*605a716eSRichard Lowe                fp = self.bu.open('dirstate', 'w')
356cdf0c1d5Smjnelson                fp.write(dirstate + '\n')
357*605a716eSRichard Lowe                fp.close()
358cdf0c1d5Smjnelson            except EnvironmentError, e:
359cdf0c1d5Smjnelson                raise util.Abort("couldn't save working copy parent: %s" % e)
360cdf0c1d5Smjnelson
361cdf0c1d5Smjnelson            try:
362*605a716eSRichard Lowe                fp = self.bu.open('renames', 'w')
363cdf0c1d5Smjnelson                for cons in self._clobbering_renames():
364cdf0c1d5Smjnelson                    fp.write("%s %s\n" % cons)
365*605a716eSRichard Lowe                fp.close()
366cdf0c1d5Smjnelson            except EnvironmentError, e:
367cdf0c1d5Smjnelson                raise util.Abort("couldn't save clobbering copies: %s" % e)
368cdf0c1d5Smjnelson
369cdf0c1d5Smjnelson            try:
370*605a716eSRichard Lowe                fp = self.bu.open('diff', 'w')
371*605a716eSRichard Lowe                match = self.ws.matcher(files=self.wctx.files())
372*605a716eSRichard Lowe                fp.write(self.ws.diff(opts={'git': True}, match=match))
373cdf0c1d5Smjnelson            except EnvironmentError, e:
374cdf0c1d5Smjnelson                raise util.Abort("couldn't save working copy diff: %s" % e)
375cdf0c1d5Smjnelson        finally:
376cdf0c1d5Smjnelson            if fp and not fp.closed:
377cdf0c1d5Smjnelson                fp.close()
378cdf0c1d5Smjnelson
379cdf0c1d5Smjnelson    def _dirstate(self):
3802b5878deSRich Lowe        '''Return the desired working copy node from the backup'''
381cdf0c1d5Smjnelson        fp = None
382cdf0c1d5Smjnelson        try:
383cdf0c1d5Smjnelson            try:
384*605a716eSRichard Lowe                fp = self.bu.open('dirstate')
385cdf0c1d5Smjnelson                dirstate = fp.readline().strip()
386cdf0c1d5Smjnelson            except EnvironmentError, e:
387cdf0c1d5Smjnelson                raise util.Abort("couldn't read saved parent: %s" % e)
388cdf0c1d5Smjnelson        finally:
389cdf0c1d5Smjnelson            if fp and not fp.closed:
390cdf0c1d5Smjnelson                fp.close()
391cdf0c1d5Smjnelson
392*605a716eSRichard Lowe        return dirstate
393*605a716eSRichard Lowe
394cdf0c1d5Smjnelson    def restore(self):
395cdf0c1d5Smjnelson        '''Restore uncommitted changes'''
396cdf0c1d5Smjnelson        dirstate = self._dirstate()
397cdf0c1d5Smjnelson
3982b5878deSRich Lowe        #
3992b5878deSRich Lowe        # Check that the patch's parent changeset exists.
4002b5878deSRich Lowe        #
401cdf0c1d5Smjnelson        try:
4022b5878deSRich Lowe            n = node.bin(dirstate)
4032b5878deSRich Lowe            self.ws.repo.changelog.lookup(n)
40487039217SRichard Lowe        except error.LookupError, e:
40512203c71SRichard Lowe            raise CdmNodeMissing("couldn't restore uncommitted changes",
40612203c71SRichard Lowe                                 e.name)
4072b5878deSRich Lowe
4082b5878deSRich Lowe        try:
4092b5878deSRich Lowe            self.ws.clean(rev=dirstate)
410cdf0c1d5Smjnelson        except util.Abort, e:
411cdf0c1d5Smjnelson            raise util.Abort("couldn't update to saved node: %s" % e)
412cdf0c1d5Smjnelson
413*605a716eSRichard Lowe        if not self.bu.exists('diff'):
414cdf0c1d5Smjnelson            return
415cdf0c1d5Smjnelson
416cdf0c1d5Smjnelson        #
417cdf0c1d5Smjnelson        # There's a race here whereby if the patch (or part thereof)
418cdf0c1d5Smjnelson        # is applied within the same second as the clean above (such
419*605a716eSRichard Lowe        # that modification time doesn't change) and if the size of
420*605a716eSRichard Lowe        # that file does not change, Hg may not see the change.
421cdf0c1d5Smjnelson        #
422cdf0c1d5Smjnelson        # We sleep a full second to avoid this, as sleeping merely
423cdf0c1d5Smjnelson        # until the next second begins would require very close clock
424cdf0c1d5Smjnelson        # synchronization on network filesystems.
425cdf0c1d5Smjnelson        #
426cdf0c1d5Smjnelson        time.sleep(1)
427cdf0c1d5Smjnelson
428cdf0c1d5Smjnelson        files = {}
429cdf0c1d5Smjnelson        try:
430*605a716eSRichard Lowe            diff = self.bu.backupfile('diff')
431cdf0c1d5Smjnelson            try:
432cdf0c1d5Smjnelson                fuzz = patch.patch(diff, self.ws.ui, strip=1,
433cdf0c1d5Smjnelson                                   cwd=self.ws.repo.root, files=files)
434cdf0c1d5Smjnelson                if fuzz:
435cdf0c1d5Smjnelson                    raise util.Abort('working copy diff applied with fuzz')
436cdf0c1d5Smjnelson            except Exception, e:
437cdf0c1d5Smjnelson                raise util.Abort("couldn't apply working copy diff: %s\n"
438cdf0c1d5Smjnelson                                 "   %s" % (diff, e))
439cdf0c1d5Smjnelson        finally:
440cdf0c1d5Smjnelson            patch.updatedir(self.ws.ui, self.ws.repo, files)
441cdf0c1d5Smjnelson
442*605a716eSRichard Lowe        if not self.bu.exists('renames'):
443cdf0c1d5Smjnelson            return
444cdf0c1d5Smjnelson
445cdf0c1d5Smjnelson        #
446cdf0c1d5Smjnelson        # We need to re-apply name changes where the new name
447cdf0c1d5Smjnelson        # (rename/copy destination) is an already versioned file, as
448cdf0c1d5Smjnelson        # Hg would otherwise ignore them.
449cdf0c1d5Smjnelson        #
450cdf0c1d5Smjnelson        try:
451*605a716eSRichard Lowe            fp = self.bu.open('renames')
452cdf0c1d5Smjnelson            for line in fp:
453cdf0c1d5Smjnelson                source, dest = line.strip().split()
45487039217SRichard Lowe                self.ws.copy(source, dest)
455cdf0c1d5Smjnelson        except EnvironmentError, e:
456cdf0c1d5Smjnelson            raise util.Abort('unable to open renames file: %s' % e)
457cdf0c1d5Smjnelson        except ValueError:
458cdf0c1d5Smjnelson            raise util.Abort('corrupt renames file: %s' %
459cdf0c1d5Smjnelson                             self.bu.backupfile('renames'))
460cdf0c1d5Smjnelson
461cdf0c1d5Smjnelson    def need_backup(self):
462cdf0c1d5Smjnelson        '''Compare backup of uncommitted changes to workspace'''
463*605a716eSRichard Lowe        cnode = self.wctx.parents()[0].node()
4642b5878deSRich Lowe        if self._dirstate() != node.hex(cnode):
465cdf0c1d5Smjnelson            return True
466cdf0c1d5Smjnelson
467*605a716eSRichard Lowe        fd = None
468*605a716eSRichard Lowe        match = self.ws.matcher(files=self.wctx.files())
469*605a716eSRichard Lowe        curdiff = self.ws.diff(opts={'git': True}, match=match)
4702b5878deSRich Lowe
471cdf0c1d5Smjnelson        try:
472*605a716eSRichard Lowe            if self.bu.exists('diff'):
473cdf0c1d5Smjnelson                try:
474*605a716eSRichard Lowe                    fd = self.bu.open('diff')
475cdf0c1d5Smjnelson                    backdiff = fd.read()
476*605a716eSRichard Lowe                    fd.close()
477cdf0c1d5Smjnelson                except EnvironmentError, e:
478cdf0c1d5Smjnelson                    raise util.Abort("couldn't open backup diff %s\n"
479*605a716eSRichard Lowe                                     "   %s" % (self.bu.backupfile('diff'), e))
480cdf0c1d5Smjnelson            else:
481cdf0c1d5Smjnelson                backdiff = ''
482cdf0c1d5Smjnelson
4832b5878deSRich Lowe            if backdiff != curdiff:
484cdf0c1d5Smjnelson                return True
485cdf0c1d5Smjnelson
486cdf0c1d5Smjnelson            currrenamed = self._clobbering_renames()
487cdf0c1d5Smjnelson            bakrenamed = None
488cdf0c1d5Smjnelson
489*605a716eSRichard Lowe            if self.bu.exists('renames'):
490cdf0c1d5Smjnelson                try:
491*605a716eSRichard Lowe                    fd = self.bu.open('renames')
492*605a716eSRichard Lowe                    bakrenamed = [tuple(line.strip().split(' ')) for line in fd]
493*605a716eSRichard Lowe                    fd.close()
494cdf0c1d5Smjnelson                except EnvironmentError, e:
495cdf0c1d5Smjnelson                    raise util.Abort("couldn't open renames file %s: %s\n" %
496cdf0c1d5Smjnelson                                     (self.bu.backupfile('renames'), e))
497cdf0c1d5Smjnelson
498cdf0c1d5Smjnelson            if currrenamed != bakrenamed:
499cdf0c1d5Smjnelson                return True
500*605a716eSRichard Lowe        finally:
501*605a716eSRichard Lowe            if fd and not fd.closed:
502*605a716eSRichard Lowe                fd.close()
503cdf0c1d5Smjnelson
504cdf0c1d5Smjnelson        return False
505cdf0c1d5Smjnelson
506cdf0c1d5Smjnelson    def cleanup(self):
507cdf0c1d5Smjnelson        '''Remove backed up uncommitted changes'''
508*605a716eSRichard Lowe
509*605a716eSRichard Lowe        for f in ('dirstate', 'diff', 'renames'):
510*605a716eSRichard Lowe            self.bu.unlink(f)
511cdf0c1d5Smjnelson
512cdf0c1d5Smjnelson
513cdf0c1d5Smjnelsonclass CdmMetadataBackup(object):
514cdf0c1d5Smjnelson    '''Backup of workspace metadata'''
515cdf0c1d5Smjnelson
516cdf0c1d5Smjnelson    def __init__(self, backup, ws):
517cdf0c1d5Smjnelson        self.bu = backup
518cdf0c1d5Smjnelson        self.ws = ws
5199a70fc3bSMark J. Nelson        self.files = ('hgrc', 'localtags', 'patches', 'cdm')
520cdf0c1d5Smjnelson
521cdf0c1d5Smjnelson    def backup(self):
522cdf0c1d5Smjnelson        '''Backup workspace metadata'''
523cdf0c1d5Smjnelson
524*605a716eSRichard Lowe        tarpath = self.bu.backupfile('metadata.tar.gz')
525*605a716eSRichard Lowe
526*605a716eSRichard Lowe        #
527*605a716eSRichard Lowe        # Files is a list of tuples (name, path), where name is as in
528*605a716eSRichard Lowe        # self.files, and path is the absolute path.
529*605a716eSRichard Lowe        #
530*605a716eSRichard Lowe        files = filter(lambda (name, path): os.path.exists(path),
531*605a716eSRichard Lowe                       zip(self.files, map(self.ws.repo.join, self.files)))
532*605a716eSRichard Lowe
533*605a716eSRichard Lowe        if not files:
534*605a716eSRichard Lowe            return
535cdf0c1d5Smjnelson
536cdf0c1d5Smjnelson        try:
537*605a716eSRichard Lowe            tar = CdmTarFile.gzopen(tarpath, 'w')
538cdf0c1d5Smjnelson        except (EnvironmentError, tarfile.TarError), e:
539cdf0c1d5Smjnelson            raise util.Abort("couldn't open %s for writing: %s" %
540*605a716eSRichard Lowe                             (tarpath, e))
541cdf0c1d5Smjnelson
542cdf0c1d5Smjnelson        try:
543*605a716eSRichard Lowe            for name, path in files:
544*605a716eSRichard Lowe                try:
545*605a716eSRichard Lowe                    tar.add(path, name)
546cdf0c1d5Smjnelson                except (EnvironmentError, tarfile.TarError), e:
547cdf0c1d5Smjnelson                    #
548cdf0c1d5Smjnelson                    # tarfile.TarError doesn't include the tar member or file
549cdf0c1d5Smjnelson                    # in question, so we have to do so ourselves.
550cdf0c1d5Smjnelson                    #
551cdf0c1d5Smjnelson                    if isinstance(e, tarfile.TarError):
552*605a716eSRichard Lowe                        errstr = "%s: %s" % (name, e)
553cdf0c1d5Smjnelson                    else:
55487039217SRichard Lowe                        errstr = str(e)
555cdf0c1d5Smjnelson
556cdf0c1d5Smjnelson                    raise util.Abort("couldn't backup metadata to %s:\n"
557*605a716eSRichard Lowe                                     "  %s" % (tarpath, errstr))
558cdf0c1d5Smjnelson        finally:
559cdf0c1d5Smjnelson            tar.close()
560cdf0c1d5Smjnelson
561cdf0c1d5Smjnelson    def old_restore(self):
562cdf0c1d5Smjnelson        '''Restore workspace metadata from an pre-tar backup'''
563cdf0c1d5Smjnelson
564cdf0c1d5Smjnelson        for fname in self.files:
565*605a716eSRichard Lowe            if self.bu.exists(fname):
566cdf0c1d5Smjnelson                bfile = self.bu.backupfile(fname)
567cdf0c1d5Smjnelson                wfile = self.ws.repo.join(fname)
568cdf0c1d5Smjnelson
569cdf0c1d5Smjnelson                try:
570cdf0c1d5Smjnelson                    shutil.copy2(bfile, wfile)
571cdf0c1d5Smjnelson                except EnvironmentError, e:
572cdf0c1d5Smjnelson                    raise util.Abort("couldn't restore metadata from %s:\n"
573cdf0c1d5Smjnelson                                     "   %s" % (bfile, e))
574cdf0c1d5Smjnelson
575cdf0c1d5Smjnelson    def tar_restore(self):
576cdf0c1d5Smjnelson        '''Restore workspace metadata (from a tar-style backup)'''
577cdf0c1d5Smjnelson
578*605a716eSRichard Lowe        if not self.bu.exists('metadata.tar.gz'):
579*605a716eSRichard Lowe            return
580*605a716eSRichard Lowe
581*605a716eSRichard Lowe        tarpath = self.bu.backupfile('metadata.tar.gz')
582cdf0c1d5Smjnelson
583cdf0c1d5Smjnelson        try:
584*605a716eSRichard Lowe            tar = CdmTarFile.gzopen(tarpath)
585cdf0c1d5Smjnelson        except (EnvironmentError, tarfile.TarError), e:
586*605a716eSRichard Lowe            raise util.Abort("couldn't open %s: %s" % (tarpath, e))
587cdf0c1d5Smjnelson
588cdf0c1d5Smjnelson        try:
589cdf0c1d5Smjnelson            for elt in tar:
590*605a716eSRichard Lowe                try:
591cdf0c1d5Smjnelson                    tar.extract(elt, path=self.ws.repo.path)
592cdf0c1d5Smjnelson                except (EnvironmentError, tarfile.TarError), e:
593cdf0c1d5Smjnelson                    # Make sure the member name is in the exception message.
594cdf0c1d5Smjnelson                    if isinstance(e, tarfile.TarError):
59587039217SRichard Lowe                        errstr = "%s: %s" % (elt.name, e)
596cdf0c1d5Smjnelson                    else:
59787039217SRichard Lowe                        errstr = str(e)
598cdf0c1d5Smjnelson
599cdf0c1d5Smjnelson                    raise util.Abort("couldn't restore metadata from %s:\n"
600cdf0c1d5Smjnelson                                     "   %s" %
601*605a716eSRichard Lowe                                     (tarpath, errstr))
602cdf0c1d5Smjnelson        finally:
603cdf0c1d5Smjnelson            if tar and not tar.closed:
604cdf0c1d5Smjnelson                tar.close()
605cdf0c1d5Smjnelson
606cdf0c1d5Smjnelson    def restore(self):
607cdf0c1d5Smjnelson        '''Restore workspace metadata'''
608cdf0c1d5Smjnelson
609*605a716eSRichard Lowe        if self.bu.exists('hgrc'):
610cdf0c1d5Smjnelson            self.old_restore()
611cdf0c1d5Smjnelson        else:
612cdf0c1d5Smjnelson            self.tar_restore()
613cdf0c1d5Smjnelson
614*605a716eSRichard Lowe    def _walk(self):
615*605a716eSRichard Lowe        '''Yield the repo-relative path to each file we operate on,
616*605a716eSRichard Lowe        including each file within any affected directory'''
617*605a716eSRichard Lowe
618*605a716eSRichard Lowe        for elt in self.files:
619*605a716eSRichard Lowe            path = self.ws.repo.join(elt)
620*605a716eSRichard Lowe
621*605a716eSRichard Lowe            if not os.path.exists(path):
622*605a716eSRichard Lowe                continue
623*605a716eSRichard Lowe
624*605a716eSRichard Lowe            if os.path.isdir(path):
625*605a716eSRichard Lowe                for root, dirs, files in os.walk(path, topdown=True):
626*605a716eSRichard Lowe                    yield root
627*605a716eSRichard Lowe
628*605a716eSRichard Lowe                    for f in files:
629*605a716eSRichard Lowe                        yield os.path.join(root, f)
630*605a716eSRichard Lowe            else:
631*605a716eSRichard Lowe                yield path
632*605a716eSRichard Lowe
633cdf0c1d5Smjnelson    def need_backup(self):
634cdf0c1d5Smjnelson        '''Compare backed up workspace metadata to workspace'''
635cdf0c1d5Smjnelson
636*605a716eSRichard Lowe        def strip_trailing_pathsep(pathname):
637*605a716eSRichard Lowe            '''Remove a possible trailing path separator from PATHNAME'''
638*605a716eSRichard Lowe            return pathname.endswith('/') and pathname[:-1] or pathname
639*605a716eSRichard Lowe
640*605a716eSRichard Lowe        if self.bu.exists('metadata.tar.gz'):
641*605a716eSRichard Lowe            tarpath = self.bu.backupfile('metadata.tar.gz')
642cdf0c1d5Smjnelson            try:
643*605a716eSRichard Lowe                tar = CdmTarFile.gzopen(tarpath)
644cdf0c1d5Smjnelson            except (EnvironmentError, tarfile.TarError), e:
645cdf0c1d5Smjnelson                raise util.Abort("couldn't open metadata tarball: %s\n"
646*605a716eSRichard Lowe                                 "   %s" % (tarpath, e))
647cdf0c1d5Smjnelson
648*605a716eSRichard Lowe            if not tar.members_match_fs(self.ws.repo.path):
649*605a716eSRichard Lowe                tar.close()
650cdf0c1d5Smjnelson                return True
651cdf0c1d5Smjnelson
652*605a716eSRichard Lowe            tarnames = map(strip_trailing_pathsep, tar.getnames())
653cdf0c1d5Smjnelson            tar.close()
654cdf0c1d5Smjnelson        else:
655cdf0c1d5Smjnelson            tarnames = []
656cdf0c1d5Smjnelson
657*605a716eSRichard Lowe        repopath = self.ws.repo.path
658*605a716eSRichard Lowe        if not repopath.endswith('/'):
659*605a716eSRichard Lowe            repopath += '/'
660cdf0c1d5Smjnelson
661*605a716eSRichard Lowe        for path in self._walk():
662*605a716eSRichard Lowe            if path.replace(repopath, '', 1) not in tarnames:
663cdf0c1d5Smjnelson                return True
664cdf0c1d5Smjnelson
665cdf0c1d5Smjnelson        return False
666cdf0c1d5Smjnelson
667cdf0c1d5Smjnelson    def cleanup(self):
668cdf0c1d5Smjnelson        '''Remove backed up workspace metadata'''
669*605a716eSRichard Lowe        self.bu.unlink('metadata.tar.gz')
670*605a716eSRichard Lowe
671*605a716eSRichard Lowe
672*605a716eSRichard Loweclass CdmClearBackup(object):
673*605a716eSRichard Lowe    '''A backup (in tar format) of complete source files from every
674*605a716eSRichard Lowe    workspace head.
675*605a716eSRichard Lowe
676*605a716eSRichard Lowe    Paths in the tarball are prefixed by the revision and node of the
677*605a716eSRichard Lowe    head, or "working" for the working directory.
678*605a716eSRichard Lowe
679*605a716eSRichard Lowe    This is done purely for the benefit of the user, and as such takes
680*605a716eSRichard Lowe    no part in restore or need_backup checking, restore always
681*605a716eSRichard Lowe    succeeds, need_backup always returns False
682*605a716eSRichard Lowe    '''
683*605a716eSRichard Lowe
684*605a716eSRichard Lowe    def __init__(self, backup, ws):
685*605a716eSRichard Lowe        self.bu = backup
686*605a716eSRichard Lowe        self.ws = ws
687*605a716eSRichard Lowe
688*605a716eSRichard Lowe    def _branch_pairs(self):
689*605a716eSRichard Lowe        '''Return a list of tuples (parenttip, localtip) for each
690*605a716eSRichard Lowe        outgoing head.  If the working copy contains modified files,
691*605a716eSRichard Lowe        it is a head, and neither of its parents are.
692*605a716eSRichard Lowe        '''
693*605a716eSRichard Lowe
694*605a716eSRichard Lowe        parent = self.ws.parent()
695*605a716eSRichard Lowe
696*605a716eSRichard Lowe        if parent:
697*605a716eSRichard Lowe            outgoing = self.ws.findoutgoing(parent)
698*605a716eSRichard Lowe            outnodes = set(self.ws.repo.changelog.nodesbetween(outgoing)[0])
699*605a716eSRichard Lowe
700*605a716eSRichard Lowe            heads = [self.ws.repo.changectx(n) for n in self.ws.repo.heads()
701*605a716eSRichard Lowe                     if n in outnodes]
702*605a716eSRichard Lowe        else:
703*605a716eSRichard Lowe            heads = []
704*605a716eSRichard Lowe            outnodes = []
705*605a716eSRichard Lowe
706*605a716eSRichard Lowe        wctx = self.ws.workingctx()
707*605a716eSRichard Lowe        if wctx.files():        # We only care about file changes.
708*605a716eSRichard Lowe            heads = filter(lambda x: x not in wctx.parents(), heads) + [wctx]
709*605a716eSRichard Lowe
710*605a716eSRichard Lowe        pairs = []
711*605a716eSRichard Lowe        for head in heads:
712*605a716eSRichard Lowe            if head.rev() is None:
713*605a716eSRichard Lowe                c = head.parents()
714*605a716eSRichard Lowe            else:
715*605a716eSRichard Lowe                c = [head]
716*605a716eSRichard Lowe
717*605a716eSRichard Lowe            pairs.append((self.ws.parenttip(c, outnodes), head))
718*605a716eSRichard Lowe        return pairs
719*605a716eSRichard Lowe
720*605a716eSRichard Lowe    def backup(self):
721*605a716eSRichard Lowe        '''Save a clear copy of each source file modified between each
722*605a716eSRichard Lowe        head and that head's parenttip (see WorkSpace.parenttip).
723*605a716eSRichard Lowe        '''
724*605a716eSRichard Lowe
725*605a716eSRichard Lowe        tarpath = self.bu.backupfile('clear.tar.gz')
726*605a716eSRichard Lowe        branches = self._branch_pairs()
727*605a716eSRichard Lowe
728*605a716eSRichard Lowe        if not branches:
729*605a716eSRichard Lowe            return
730*605a716eSRichard Lowe
731*605a716eSRichard Lowe        try:
732*605a716eSRichard Lowe            tar = CdmTarFile.gzopen(tarpath, 'w')
733*605a716eSRichard Lowe        except (EnvironmentError, tarfile.TarError), e:
734*605a716eSRichard Lowe            raise util.Abort("Could not open %s for writing: %s" %
735*605a716eSRichard Lowe                             (tarpath, e))
736*605a716eSRichard Lowe
737*605a716eSRichard Lowe        try:
738*605a716eSRichard Lowe            for parent, child in branches:
739*605a716eSRichard Lowe                tpath = child.node() and node.short(child.node()) or "working"
740*605a716eSRichard Lowe
741*605a716eSRichard Lowe                for fname, change in self.ws.status(parent, child).iteritems():
742*605a716eSRichard Lowe                    if change not in ('added', 'modified'):
743*605a716eSRichard Lowe                        continue
744*605a716eSRichard Lowe
745*605a716eSRichard Lowe                    try:
746*605a716eSRichard Lowe                        tar.addfilectx(child.filectx(fname),
747*605a716eSRichard Lowe                                       os.path.join(tpath, fname))
748*605a716eSRichard Lowe                    except ValueError, e:
749*605a716eSRichard Lowe                        crev = child.rev()
750*605a716eSRichard Lowe                        if crev is None:
751*605a716eSRichard Lowe                            crev = "working copy"
752*605a716eSRichard Lowe                        raise util.Abort("Could not backup clear file %s "
753*605a716eSRichard Lowe                                         "from %s: %s\n" % (fname, crev, e))
754*605a716eSRichard Lowe        finally:
755*605a716eSRichard Lowe            tar.close()
756*605a716eSRichard Lowe
757*605a716eSRichard Lowe    def cleanup(self):
758*605a716eSRichard Lowe        '''Cleanup a failed Clear backup.
759*605a716eSRichard Lowe
760*605a716eSRichard Lowe        Remove the clear tarball from the backup directory.
761*605a716eSRichard Lowe        '''
762*605a716eSRichard Lowe
763*605a716eSRichard Lowe        self.bu.unlink('clear.tar.gz')
764*605a716eSRichard Lowe
765*605a716eSRichard Lowe    def restore(self):
766*605a716eSRichard Lowe        '''Clear backups are never restored, do nothing'''
767*605a716eSRichard Lowe        pass
768*605a716eSRichard Lowe
769*605a716eSRichard Lowe    def need_backup(self):
770*605a716eSRichard Lowe        '''Clear backups are never compared, return False (no backup needed).
771*605a716eSRichard Lowe
772*605a716eSRichard Lowe        Should a backup actually be needed, one of the other
773*605a716eSRichard Lowe        implementation classes would notice in any situation we would.
774*605a716eSRichard Lowe        '''
775*605a716eSRichard Lowe
776*605a716eSRichard Lowe        return False
777cdf0c1d5Smjnelson
778cdf0c1d5Smjnelson
779cdf0c1d5Smjnelsonclass CdmBackup(object):
780cdf0c1d5Smjnelson    '''A backup of a given workspace'''
781cdf0c1d5Smjnelson
782cdf0c1d5Smjnelson    def __init__(self, ui, ws, name):
783cdf0c1d5Smjnelson        self.ws = ws
784cdf0c1d5Smjnelson        self.ui = ui
785cdf0c1d5Smjnelson        self.backupdir = self._find_backup_dir(name)
786cdf0c1d5Smjnelson
787cdf0c1d5Smjnelson        #
788cdf0c1d5Smjnelson        # The order of instances here controls the order the various operations
789cdf0c1d5Smjnelson        # are run.
790cdf0c1d5Smjnelson        #
791cdf0c1d5Smjnelson        # There's some inherent dependence, in that on restore we need
792cdf0c1d5Smjnelson        # to restore committed changes prior to uncommitted changes
793cdf0c1d5Smjnelson        # (as the parent revision of any uncommitted changes is quite
794cdf0c1d5Smjnelson        # likely to not exist until committed changes are restored).
795cdf0c1d5Smjnelson        # Metadata restore can happen at any point, but happens last
796cdf0c1d5Smjnelson        # as a matter of convention.
797cdf0c1d5Smjnelson        #
798cdf0c1d5Smjnelson        self.modules = [x(self, ws) for x in [CdmCommittedBackup,
799cdf0c1d5Smjnelson                                              CdmUncommittedBackup,
800*605a716eSRichard Lowe                                              CdmClearBackup,
801cdf0c1d5Smjnelson                                              CdmMetadataBackup]]
802cdf0c1d5Smjnelson
803cdf0c1d5Smjnelson        if os.path.exists(os.path.join(self.backupdir, 'latest')):
804cdf0c1d5Smjnelson            generation = os.readlink(os.path.join(self.backupdir, 'latest'))
805cdf0c1d5Smjnelson            self.generation = int(os.path.split(generation)[1])
806cdf0c1d5Smjnelson        else:
807cdf0c1d5Smjnelson            self.generation = 0
808cdf0c1d5Smjnelson
809cdf0c1d5Smjnelson    def _find_backup_dir(self, name):
810cdf0c1d5Smjnelson        '''Find the path to an appropriate backup directory based on NAME'''
811cdf0c1d5Smjnelson
812cdf0c1d5Smjnelson        if os.path.isabs(name):
813cdf0c1d5Smjnelson            return name
814cdf0c1d5Smjnelson
815cdf0c1d5Smjnelson        if self.ui.config('cdm', 'backupdir'):
816cdf0c1d5Smjnelson            backupbase = os.path.expanduser(self.ui.config('cdm', 'backupdir'))
817cdf0c1d5Smjnelson        else:
818cdf0c1d5Smjnelson            home = None
819cdf0c1d5Smjnelson
820cdf0c1d5Smjnelson            try:
821cdf0c1d5Smjnelson                home = os.getenv('HOME') or pwd.getpwuid(os.getuid()).pw_dir
822cdf0c1d5Smjnelson            except KeyError:
823cdf0c1d5Smjnelson                pass                    # Handled anyway
824cdf0c1d5Smjnelson
825cdf0c1d5Smjnelson            if not home:
826cdf0c1d5Smjnelson                raise util.Abort('Could not determine your HOME directory to '
827cdf0c1d5Smjnelson                                 'find backup path')
828cdf0c1d5Smjnelson
829cdf0c1d5Smjnelson            backupbase = os.path.join(home, 'cdm.backup')
830cdf0c1d5Smjnelson
831cdf0c1d5Smjnelson        backupdir = os.path.join(backupbase, name)
832cdf0c1d5Smjnelson
833cdf0c1d5Smjnelson        # If backupdir exists, it must be a directory.
834cdf0c1d5Smjnelson        if (os.path.exists(backupdir) and not os.path.isdir(backupdir)):
835cdf0c1d5Smjnelson            raise util.Abort('%s exists but is not a directory' % backupdir)
836cdf0c1d5Smjnelson
837cdf0c1d5Smjnelson        return backupdir
838cdf0c1d5Smjnelson
839*605a716eSRichard Lowe    def _update_latest(self, gen):
840cdf0c1d5Smjnelson        '''Update latest symlink to point to the current generation'''
841cdf0c1d5Smjnelson        linkpath = os.path.join(self.backupdir, 'latest')
842cdf0c1d5Smjnelson
843cdf0c1d5Smjnelson        if os.path.lexists(linkpath):
844cdf0c1d5Smjnelson            os.unlink(linkpath)
845cdf0c1d5Smjnelson
846cdf0c1d5Smjnelson        os.symlink(str(gen), linkpath)
847cdf0c1d5Smjnelson
848*605a716eSRichard Lowe    def _create_gen(self, gen):
849cdf0c1d5Smjnelson        '''Create a new backup generation'''
850cdf0c1d5Smjnelson        try:
851cdf0c1d5Smjnelson            os.makedirs(os.path.join(self.backupdir, str(gen)))
852*605a716eSRichard Lowe            self._update_latest(gen)
853cdf0c1d5Smjnelson        except EnvironmentError, e:
854cdf0c1d5Smjnelson            raise util.Abort("Couldn't create backup generation %s: %s" %
855cdf0c1d5Smjnelson                             (os.path.join(self.backupdir, str(gen)), e))
856cdf0c1d5Smjnelson
857*605a716eSRichard Lowe    def backupfile(self, path):
858*605a716eSRichard Lowe        '''return full path to backup file FILE at GEN'''
859*605a716eSRichard Lowe        return os.path.join(self.backupdir, str(self.generation), path)
860*605a716eSRichard Lowe
861*605a716eSRichard Lowe    def unlink(self, name):
862*605a716eSRichard Lowe        '''Unlink the specified path from the backup directory.
863*605a716eSRichard Lowe        A no-op if the path does not exist.
864*605a716eSRichard Lowe        '''
865*605a716eSRichard Lowe
866*605a716eSRichard Lowe        fpath = self.backupfile(name)
867*605a716eSRichard Lowe        if os.path.exists(fpath):
868*605a716eSRichard Lowe            os.unlink(fpath)
869*605a716eSRichard Lowe
870*605a716eSRichard Lowe    def open(self, name, mode='r'):
871*605a716eSRichard Lowe        '''Open the specified file in the backup directory'''
872*605a716eSRichard Lowe        return open(self.backupfile(name), mode)
873*605a716eSRichard Lowe
874*605a716eSRichard Lowe    def exists(self, name):
875*605a716eSRichard Lowe        '''Return boolean indicating wether a given file exists in the
876*605a716eSRichard Lowe        backup directory.'''
877*605a716eSRichard Lowe        return os.path.exists(self.backupfile(name))
878*605a716eSRichard Lowe
879cdf0c1d5Smjnelson    def need_backup(self):
880cdf0c1d5Smjnelson        '''Compare backed up changes to workspace'''
881cdf0c1d5Smjnelson        #
882cdf0c1d5Smjnelson        # If there's no current backup generation, or the last backup was
883cdf0c1d5Smjnelson        # invalid (lacking the dirstate file), we need a backup regardless
884cdf0c1d5Smjnelson        # of anything else.
885cdf0c1d5Smjnelson        #
886*605a716eSRichard Lowe        if not self.generation or not self.exists('dirstate'):
887cdf0c1d5Smjnelson            return True
888cdf0c1d5Smjnelson
889cdf0c1d5Smjnelson        for x in self.modules:
890cdf0c1d5Smjnelson            if x.need_backup():
891cdf0c1d5Smjnelson                return True
892cdf0c1d5Smjnelson
893cdf0c1d5Smjnelson        return False
894cdf0c1d5Smjnelson
895cdf0c1d5Smjnelson    def backup(self):
896c959a081SRichard Lowe        '''Take a backup of the current workspace
897c959a081SRichard Lowe
898c959a081SRichard Lowe        Calling code is expected to hold both the working copy lock
899c959a081SRichard Lowe        and repository lock.'''
900cdf0c1d5Smjnelson
901cdf0c1d5Smjnelson        if not os.path.exists(self.backupdir):
902cdf0c1d5Smjnelson            try:
903cdf0c1d5Smjnelson                os.makedirs(self.backupdir)
904cdf0c1d5Smjnelson            except EnvironmentError, e:
905cdf0c1d5Smjnelson                raise util.Abort('Could not create backup directory %s: %s' %
906cdf0c1d5Smjnelson                                 (self.backupdir, e))
907cdf0c1d5Smjnelson
908cdf0c1d5Smjnelson        self.generation += 1
909*605a716eSRichard Lowe        self._create_gen(self.generation)
910cdf0c1d5Smjnelson
911cdf0c1d5Smjnelson        try:
912cdf0c1d5Smjnelson            for x in self.modules:
913cdf0c1d5Smjnelson                x.backup()
914cdf0c1d5Smjnelson        except Exception, e:
915cdf0c1d5Smjnelson            if isinstance(e, KeyboardInterrupt):
916cdf0c1d5Smjnelson                self.ws.ui.warn("Interrupted\n")
917cdf0c1d5Smjnelson            else:
918cdf0c1d5Smjnelson                self.ws.ui.warn("Error: %s\n" % e)
919c959a081SRichard Lowe                show_traceback = self.ws.ui.configbool('ui', 'traceback',
920c959a081SRichard Lowe                                                       False)
921cdf0c1d5Smjnelson
922cdf0c1d5Smjnelson                #
923cdf0c1d5Smjnelson                # If it's not a 'normal' error, we want to print a stack
924cdf0c1d5Smjnelson                # trace now in case the attempt to remove the partial
925cdf0c1d5Smjnelson                # backup also fails, and raises a second exception.
926cdf0c1d5Smjnelson                #
927cdf0c1d5Smjnelson                if (not isinstance(e, (EnvironmentError, util.Abort))
928c959a081SRichard Lowe                    or show_traceback):
929cdf0c1d5Smjnelson                    traceback.print_exc()
930cdf0c1d5Smjnelson
931cdf0c1d5Smjnelson            for x in self.modules:
932cdf0c1d5Smjnelson                x.cleanup()
933cdf0c1d5Smjnelson
934cdf0c1d5Smjnelson            os.rmdir(os.path.join(self.backupdir, str(self.generation)))
935cdf0c1d5Smjnelson            self.generation -= 1
936cdf0c1d5Smjnelson
937cdf0c1d5Smjnelson            if self.generation != 0:
938*605a716eSRichard Lowe                self._update_latest(self.generation)
939cdf0c1d5Smjnelson            else:
940cdf0c1d5Smjnelson                os.unlink(os.path.join(self.backupdir, 'latest'))
941cdf0c1d5Smjnelson
942cdf0c1d5Smjnelson            raise util.Abort('Backup failed')
943cdf0c1d5Smjnelson
944cdf0c1d5Smjnelson    def restore(self, gen=None):
945cdf0c1d5Smjnelson        '''Restore workspace from backup
946cdf0c1d5Smjnelson
947cdf0c1d5Smjnelson        Restores from backup generation GEN (defaulting to the latest)
948c959a081SRichard Lowe        into workspace WS.
949cdf0c1d5Smjnelson
950c959a081SRichard Lowe        Calling code is expected to hold both the working copy lock
951c959a081SRichard Lowe        and repository lock of the destination workspace.'''
952cdf0c1d5Smjnelson
953cdf0c1d5Smjnelson        if not os.path.exists(self.backupdir):
954cdf0c1d5Smjnelson            raise util.Abort('Backup directory does not exist: %s' %
955cdf0c1d5Smjnelson                             (self.backupdir))
956cdf0c1d5Smjnelson
957cdf0c1d5Smjnelson        if gen:
958cdf0c1d5Smjnelson            if not os.path.exists(os.path.join(self.backupdir, str(gen))):
959cdf0c1d5Smjnelson                raise util.Abort('Backup generation does not exist: %s' %
960cdf0c1d5Smjnelson                                 (os.path.join(self.backupdir, str(gen))))
961cdf0c1d5Smjnelson            self.generation = int(gen)
962cdf0c1d5Smjnelson
963*605a716eSRichard Lowe        if not self.generation: # This is OK, 0 is not a valid generation
964cdf0c1d5Smjnelson            raise util.Abort('Backup has no generations: %s' % self.backupdir)
965cdf0c1d5Smjnelson
966*605a716eSRichard Lowe        if not self.exists('dirstate'):
967cdf0c1d5Smjnelson            raise util.Abort('Backup %s/%s is incomplete (dirstate missing)' %
968cdf0c1d5Smjnelson                             (self.backupdir, self.generation))
969cdf0c1d5Smjnelson
970cdf0c1d5Smjnelson        try:
971cdf0c1d5Smjnelson            for x in self.modules:
972cdf0c1d5Smjnelson                x.restore()
973cdf0c1d5Smjnelson        except util.Abort, e:
974cdf0c1d5Smjnelson            raise util.Abort('Error restoring workspace:\n'
975cdf0c1d5Smjnelson                             '%s\n'
97612203c71SRichard Lowe                             'Workspace may be partially restored' % e)
977