xref: /titanic_44/usr/src/tools/onbld/Scm/Backup.py (revision 036abaca93ddab92ba33036159c30112ab844810)
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#
20605a716eSRichard Lowe# Copyright 2008, 2011, Richard Lowe
2187039217SRichard Lowe#
22cdf0c1d5Smjnelson
23605a716eSRichard 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
56605a716eSRichard Lowe            clear.tar.gz (handled by CdmClearBackup)
57605a716eSRichard Lowe                <short node>/
58605a716eSRichard Lowe                    copies of each modified or added file, as it is in
59605a716eSRichard Lowe                    this head.
60605a716eSRichard Lowe
61605a716eSRichard Lowe                 ... for each outgoing head
62605a716eSRichard Lowe
63605a716eSRichard Lowe                working/
64605a716eSRichard Lowe                     copies of each modified or added file in the
65605a716eSRichard Lowe                     working copy if any.
66605a716eSRichard 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
74605a716eSRichard Loweimport grp, os, pwd, shutil, tarfile, time, traceback
75605a716eSRichard Lowefrom cStringIO import StringIO
76605a716eSRichard Lowe
77*036abacaSRichard Lowefrom mercurial import changegroup, cmdutil, error, node, patch, util
78*036abacaSRichard Lowefrom onbld.Scm import Version
79cdf0c1d5Smjnelson
80cdf0c1d5Smjnelson
8112203c71SRichard Loweclass CdmNodeMissing(util.Abort):
8212203c71SRichard Lowe    '''a required node is not present in the destination workspace.
8312203c71SRichard Lowe
8412203c71SRichard Lowe    This may occur both in the case where the bundle contains a
8512203c71SRichard Lowe    changeset which is a child of a node not present in the
8612203c71SRichard Lowe    destination workspace (because the destination workspace is not as
8712203c71SRichard Lowe    up-to-date as the source), or because the source and destination
8812203c71SRichard Lowe    workspace are not related.
8912203c71SRichard Lowe
9012203c71SRichard Lowe    It may also happen in cases where the uncommitted changes need to
9112203c71SRichard Lowe    be applied onto a node that the workspace does not possess even
9212203c71SRichard Lowe    after application of the bundle (on a branch not present
9312203c71SRichard Lowe    in the bundle or destination workspace, for instance)'''
9412203c71SRichard Lowe
9512203c71SRichard Lowe    def __init__(self, msg, name):
9612203c71SRichard Lowe        #
9712203c71SRichard Lowe        # If e.name is a string 20 characters long, it is
9812203c71SRichard Lowe        # assumed to be a node.  (Mercurial makes this
9912203c71SRichard Lowe        # same assumption, when creating a LookupError)
10012203c71SRichard Lowe        #
10112203c71SRichard Lowe        if isinstance(name, str) and len(name) == 20:
10212203c71SRichard Lowe            n = node.short(name)
10312203c71SRichard Lowe        else:
10412203c71SRichard Lowe            n = name
10512203c71SRichard Lowe
10612203c71SRichard Lowe        util.Abort.__init__(self, "%s: changeset '%s' is missing\n"
10712203c71SRichard Lowe                            "Your workspace is either not "
10812203c71SRichard Lowe                            "sufficiently up to date,\n"
10912203c71SRichard Lowe                            "or is unrelated to the workspace from "
11012203c71SRichard Lowe                            "which the backup was taken.\n" % (msg, n))
11112203c71SRichard Lowe
11212203c71SRichard Lowe
113605a716eSRichard Loweclass CdmTarFile(tarfile.TarFile):
114605a716eSRichard Lowe    '''Tar file access + simple comparison to the filesystem, and
115605a716eSRichard Lowe    creation addition of files from Mercurial filectx objects.'''
116605a716eSRichard Lowe
117605a716eSRichard Lowe    def __init__(self, *args, **kwargs):
118605a716eSRichard Lowe        tarfile.TarFile.__init__(self, *args, **kwargs)
119605a716eSRichard Lowe        self.errorlevel = 2
120605a716eSRichard Lowe
121605a716eSRichard Lowe    def members_match_fs(self, rootpath):
122605a716eSRichard Lowe        '''Compare the contents of the tar archive to the directory
123605a716eSRichard Lowe        specified by rootpath.  Return False if they differ.
124605a716eSRichard Lowe
125605a716eSRichard Lowe        Every file in the archive must match the equivalent file in
126605a716eSRichard Lowe        the filesystem.
127605a716eSRichard Lowe
128605a716eSRichard Lowe        The existence, modification time, and size of each file are
129605a716eSRichard Lowe        compared, content is not.'''
130605a716eSRichard Lowe
131605a716eSRichard Lowe        def _member_matches_fs(member, rootpath):
132605a716eSRichard Lowe            '''Compare a single member to its filesystem counterpart'''
133605a716eSRichard Lowe            fpath = os.path.join(rootpath, member.name)
134605a716eSRichard Lowe
135605a716eSRichard Lowe            if not os.path.exists(fpath):
136605a716eSRichard Lowe                return False
137605a716eSRichard Lowe            elif ((os.path.isfile(fpath) != member.isfile()) or
138605a716eSRichard Lowe                  (os.path.isdir(fpath) != member.isdir()) or
139605a716eSRichard Lowe                  (os.path.islink(fpath) != member.issym())):
140605a716eSRichard Lowe                return False
141605a716eSRichard Lowe
142605a716eSRichard Lowe            #
143605a716eSRichard Lowe            # The filesystem may return a modification time with a
144605a716eSRichard Lowe            # fractional component (as a float), whereas the tar format
145605a716eSRichard Lowe            # only stores it to the whole second, perform the comparison
146605a716eSRichard Lowe            # using integers (truncated, not rounded)
147605a716eSRichard Lowe            #
148605a716eSRichard Lowe            elif member.mtime != int(os.path.getmtime(fpath)):
149605a716eSRichard Lowe                return False
150605a716eSRichard Lowe            elif not member.isdir() and member.size != os.path.getsize(fpath):
151605a716eSRichard Lowe                return False
152605a716eSRichard Lowe            else:
153605a716eSRichard Lowe                return True
154605a716eSRichard Lowe
155605a716eSRichard Lowe        for elt in self:
156605a716eSRichard Lowe            if not _member_matches_fs(elt, rootpath):
157605a716eSRichard Lowe                return False
158605a716eSRichard Lowe
159605a716eSRichard Lowe        return True
160605a716eSRichard Lowe
161605a716eSRichard Lowe    def addfilectx(self, filectx, path=None):
162605a716eSRichard Lowe        '''Add a filectx object to the archive.
163605a716eSRichard Lowe
164605a716eSRichard Lowe        Use the path specified by the filectx object or, if specified,
165605a716eSRichard Lowe        the PATH argument.
166605a716eSRichard Lowe
167605a716eSRichard Lowe        The size, modification time, type and permissions of the tar
168605a716eSRichard Lowe        member are taken from the filectx object, user and group id
169605a716eSRichard Lowe        are those of the invoking user, user and group name are those
170605a716eSRichard Lowe        of the invoking user if information is available, or "unknown"
171605a716eSRichard Lowe        if it is not.
172605a716eSRichard Lowe        '''
173605a716eSRichard Lowe
174605a716eSRichard Lowe        t = tarfile.TarInfo(path or filectx.path())
175605a716eSRichard Lowe        t.size = filectx.size()
176605a716eSRichard Lowe        t.mtime = filectx.date()[0]
177605a716eSRichard Lowe        t.uid = os.getuid()
178605a716eSRichard Lowe        t.gid = os.getgid()
179605a716eSRichard Lowe
180605a716eSRichard Lowe        try:
181605a716eSRichard Lowe            t.uname = pwd.getpwuid(t.uid).pw_name
182605a716eSRichard Lowe        except KeyError:
183605a716eSRichard Lowe            t.uname = "unknown"
184605a716eSRichard Lowe
185605a716eSRichard Lowe        try:
186605a716eSRichard Lowe            t.gname = grp.getgrgid(t.gid).gr_name
187605a716eSRichard Lowe        except KeyError:
188605a716eSRichard Lowe            t.gname = "unknown"
189605a716eSRichard Lowe
190605a716eSRichard Lowe        #
191605a716eSRichard Lowe        # Mercurial versions symlinks by setting a flag and storing
192605a716eSRichard Lowe        # the destination path in place of the file content.  The
193605a716eSRichard Lowe        # actual contents (in the tar), should be empty.
194605a716eSRichard Lowe        #
195605a716eSRichard Lowe        if 'l' in filectx.flags():
196605a716eSRichard Lowe            t.type = tarfile.SYMTYPE
197605a716eSRichard Lowe            t.mode = 0777
198605a716eSRichard Lowe            t.linkname = filectx.data()
199605a716eSRichard Lowe            data = None
200605a716eSRichard Lowe        else:
201605a716eSRichard Lowe            t.type = tarfile.REGTYPE
202605a716eSRichard Lowe            t.mode = 'x' in filectx.flags() and 0755 or 0644
203605a716eSRichard Lowe            data = StringIO(filectx.data())
204605a716eSRichard Lowe
205605a716eSRichard Lowe        self.addfile(t, data)
206605a716eSRichard Lowe
207605a716eSRichard Lowe
208cdf0c1d5Smjnelsonclass CdmCommittedBackup(object):
209cdf0c1d5Smjnelson    '''Backup of committed changes'''
210cdf0c1d5Smjnelson
211cdf0c1d5Smjnelson    def __init__(self, backup, ws):
212cdf0c1d5Smjnelson        self.ws = ws
213cdf0c1d5Smjnelson        self.bu = backup
214cdf0c1d5Smjnelson        self.files = ('bundle', 'nodes')
215cdf0c1d5Smjnelson
216cdf0c1d5Smjnelson    def _outgoing_nodes(self, parent):
217cdf0c1d5Smjnelson        '''Return a list of all outgoing nodes in hex format'''
218cdf0c1d5Smjnelson
219cdf0c1d5Smjnelson        if parent:
220cdf0c1d5Smjnelson            outgoing = self.ws.findoutgoing(parent)
221cdf0c1d5Smjnelson            nodes = self.ws.repo.changelog.nodesbetween(outgoing)[0]
222cdf0c1d5Smjnelson            return map(node.hex, nodes)
223cdf0c1d5Smjnelson        else:
224cdf0c1d5Smjnelson            return []
225cdf0c1d5Smjnelson
226cdf0c1d5Smjnelson    def backup(self):
227cdf0c1d5Smjnelson        '''Backup committed changes'''
228cdf0c1d5Smjnelson        parent = self.ws.parent()
229cdf0c1d5Smjnelson
230cdf0c1d5Smjnelson        if not parent:
231cdf0c1d5Smjnelson            self.ws.ui.warn('Workspace has no parent, committed changes will '
232cdf0c1d5Smjnelson                            'not be backed up\n')
233cdf0c1d5Smjnelson            return
234cdf0c1d5Smjnelson
235cdf0c1d5Smjnelson        out = self.ws.findoutgoing(parent)
236cdf0c1d5Smjnelson        if not out:
237cdf0c1d5Smjnelson            return
238cdf0c1d5Smjnelson
239cdf0c1d5Smjnelson        cg = self.ws.repo.changegroup(out, 'bundle')
240cdf0c1d5Smjnelson        changegroup.writebundle(cg, self.bu.backupfile('bundle'), 'HG10BZ')
241cdf0c1d5Smjnelson
242cdf0c1d5Smjnelson        outnodes = self._outgoing_nodes(parent)
243605a716eSRichard Lowe        if not outnodes:
244605a716eSRichard Lowe            return
245605a716eSRichard Lowe
246cdf0c1d5Smjnelson        fp = None
247cdf0c1d5Smjnelson        try:
248cdf0c1d5Smjnelson            try:
249605a716eSRichard Lowe                fp = self.bu.open('nodes', 'w')
250cdf0c1d5Smjnelson                fp.write('%s\n' % '\n'.join(outnodes))
251cdf0c1d5Smjnelson            except EnvironmentError, e:
252cdf0c1d5Smjnelson                raise util.Abort("couldn't store outgoing nodes: %s" % e)
253cdf0c1d5Smjnelson        finally:
254cdf0c1d5Smjnelson            if fp and not fp.closed:
255cdf0c1d5Smjnelson                fp.close()
256cdf0c1d5Smjnelson
257cdf0c1d5Smjnelson    def restore(self):
258cdf0c1d5Smjnelson        '''Restore committed changes from backup'''
259cdf0c1d5Smjnelson
260605a716eSRichard Lowe        if not self.bu.exists('bundle'):
261605a716eSRichard Lowe            return
262605a716eSRichard Lowe
263605a716eSRichard Lowe        bpath = self.bu.backupfile('bundle')
264cdf0c1d5Smjnelson        f = None
265cdf0c1d5Smjnelson        try:
266cdf0c1d5Smjnelson            try:
267605a716eSRichard Lowe                f = self.bu.open('bundle')
268605a716eSRichard Lowe                bundle = changegroup.readbundle(f, bpath)
269cdf0c1d5Smjnelson                self.ws.repo.addchangegroup(bundle, 'strip',
270605a716eSRichard Lowe                                            'bundle:%s' % bpath)
271cdf0c1d5Smjnelson            except EnvironmentError, e:
272cdf0c1d5Smjnelson                raise util.Abort("couldn't restore committed changes: %s\n"
273605a716eSRichard Lowe                                 "   %s" % (bpath, e))
27487039217SRichard Lowe            except error.LookupError, e:
27512203c71SRichard Lowe                raise CdmNodeMissing("couldn't restore committed changes",
27612203c71SRichard Lowe                                                 e.name)
277cdf0c1d5Smjnelson        finally:
278cdf0c1d5Smjnelson            if f and not f.closed:
279cdf0c1d5Smjnelson                f.close()
280cdf0c1d5Smjnelson
281cdf0c1d5Smjnelson    def need_backup(self):
282cdf0c1d5Smjnelson        '''Compare backup of committed changes to workspace'''
283cdf0c1d5Smjnelson
284605a716eSRichard Lowe        if self.bu.exists('nodes'):
285cdf0c1d5Smjnelson            f = None
286cdf0c1d5Smjnelson            try:
287cdf0c1d5Smjnelson                try:
288605a716eSRichard Lowe                    f = self.bu.open('nodes')
289605a716eSRichard Lowe                    bnodes = set(line.rstrip('\r\n') for line in f.readlines())
290cdf0c1d5Smjnelson                    f.close()
291cdf0c1d5Smjnelson                except EnvironmentError, e:
292cdf0c1d5Smjnelson                    raise util.Abort("couldn't open backup node list: %s" % e)
293cdf0c1d5Smjnelson            finally:
294cdf0c1d5Smjnelson                if f and not f.closed:
295cdf0c1d5Smjnelson                    f.close()
296cdf0c1d5Smjnelson        else:
297cdf0c1d5Smjnelson            bnodes = set()
298cdf0c1d5Smjnelson
299cdf0c1d5Smjnelson        outnodes = set(self._outgoing_nodes(self.ws.parent()))
300605a716eSRichard Lowe
301605a716eSRichard Lowe        #
302605a716eSRichard Lowe        # If there are outgoing nodes not in the prior backup we need
303605a716eSRichard Lowe        # to take a new backup; it's fine if there are nodes in the
304605a716eSRichard Lowe        # old backup which are no longer outgoing, however.
305605a716eSRichard Lowe        #
306605a716eSRichard Lowe        if not outnodes <= bnodes:
307cdf0c1d5Smjnelson            return True
308cdf0c1d5Smjnelson
309cdf0c1d5Smjnelson        return False
310cdf0c1d5Smjnelson
311cdf0c1d5Smjnelson    def cleanup(self):
312cdf0c1d5Smjnelson        '''Remove backed up committed changes'''
313cdf0c1d5Smjnelson
314605a716eSRichard Lowe        for f in self.files:
315605a716eSRichard Lowe            self.bu.unlink(f)
316cdf0c1d5Smjnelson
317cdf0c1d5Smjnelson
318cdf0c1d5Smjnelsonclass CdmUncommittedBackup(object):
319cdf0c1d5Smjnelson    '''Backup of uncommitted changes'''
320cdf0c1d5Smjnelson
321cdf0c1d5Smjnelson    def __init__(self, backup, ws):
322cdf0c1d5Smjnelson        self.ws = ws
323cdf0c1d5Smjnelson        self.bu = backup
324605a716eSRichard Lowe        self.wctx = self.ws.workingctx(worklist=True)
325cdf0c1d5Smjnelson
326cdf0c1d5Smjnelson    def _clobbering_renames(self):
327cdf0c1d5Smjnelson        '''Return a list of pairs of files representing renames/copies
328605a716eSRichard Lowe        that clobber already versioned files.  [(old-name new-name)...]
329605a716eSRichard Lowe        '''
330cdf0c1d5Smjnelson
331cdf0c1d5Smjnelson        #
332cdf0c1d5Smjnelson        # Note that this doesn't handle uncommitted merges
333cdf0c1d5Smjnelson        # as CdmUncommittedBackup itself doesn't.
334cdf0c1d5Smjnelson        #
335605a716eSRichard Lowe        parent = self.wctx.parents()[0]
336cdf0c1d5Smjnelson
337cdf0c1d5Smjnelson        ret = []
338605a716eSRichard Lowe        for fname in self.wctx.added() + self.wctx.modified():
339605a716eSRichard Lowe            rn = self.wctx.filectx(fname).renamed()
340cdf0c1d5Smjnelson            if rn and fname in parent:
341cdf0c1d5Smjnelson                ret.append((rn[0], fname))
342cdf0c1d5Smjnelson        return ret
343cdf0c1d5Smjnelson
344cdf0c1d5Smjnelson    def backup(self):
345cdf0c1d5Smjnelson        '''Backup uncommitted changes'''
346cdf0c1d5Smjnelson
347cdf0c1d5Smjnelson        if self.ws.merged():
348cdf0c1d5Smjnelson            raise util.Abort("Unable to backup an uncommitted merge.\n"
349cdf0c1d5Smjnelson                             "Please complete your merge and commit")
350cdf0c1d5Smjnelson
351605a716eSRichard Lowe        dirstate = node.hex(self.wctx.parents()[0].node())
352cdf0c1d5Smjnelson
353cdf0c1d5Smjnelson        fp = None
354cdf0c1d5Smjnelson        try:
355cdf0c1d5Smjnelson            try:
356605a716eSRichard Lowe                fp = self.bu.open('dirstate', 'w')
357cdf0c1d5Smjnelson                fp.write(dirstate + '\n')
358605a716eSRichard Lowe                fp.close()
359cdf0c1d5Smjnelson            except EnvironmentError, e:
360cdf0c1d5Smjnelson                raise util.Abort("couldn't save working copy parent: %s" % e)
361cdf0c1d5Smjnelson
362cdf0c1d5Smjnelson            try:
363605a716eSRichard Lowe                fp = self.bu.open('renames', 'w')
364cdf0c1d5Smjnelson                for cons in self._clobbering_renames():
365cdf0c1d5Smjnelson                    fp.write("%s %s\n" % cons)
366605a716eSRichard Lowe                fp.close()
367cdf0c1d5Smjnelson            except EnvironmentError, e:
368cdf0c1d5Smjnelson                raise util.Abort("couldn't save clobbering copies: %s" % e)
369cdf0c1d5Smjnelson
370cdf0c1d5Smjnelson            try:
371605a716eSRichard Lowe                fp = self.bu.open('diff', 'w')
372605a716eSRichard Lowe                match = self.ws.matcher(files=self.wctx.files())
373605a716eSRichard Lowe                fp.write(self.ws.diff(opts={'git': True}, match=match))
374cdf0c1d5Smjnelson            except EnvironmentError, e:
375cdf0c1d5Smjnelson                raise util.Abort("couldn't save working copy diff: %s" % e)
376cdf0c1d5Smjnelson        finally:
377cdf0c1d5Smjnelson            if fp and not fp.closed:
378cdf0c1d5Smjnelson                fp.close()
379cdf0c1d5Smjnelson
380cdf0c1d5Smjnelson    def _dirstate(self):
3812b5878deSRich Lowe        '''Return the desired working copy node from the backup'''
382cdf0c1d5Smjnelson        fp = None
383cdf0c1d5Smjnelson        try:
384cdf0c1d5Smjnelson            try:
385605a716eSRichard Lowe                fp = self.bu.open('dirstate')
386cdf0c1d5Smjnelson                dirstate = fp.readline().strip()
387cdf0c1d5Smjnelson            except EnvironmentError, e:
388cdf0c1d5Smjnelson                raise util.Abort("couldn't read saved parent: %s" % e)
389cdf0c1d5Smjnelson        finally:
390cdf0c1d5Smjnelson            if fp and not fp.closed:
391cdf0c1d5Smjnelson                fp.close()
392cdf0c1d5Smjnelson
393605a716eSRichard Lowe        return dirstate
394605a716eSRichard Lowe
395cdf0c1d5Smjnelson    def restore(self):
396cdf0c1d5Smjnelson        '''Restore uncommitted changes'''
397cdf0c1d5Smjnelson        dirstate = self._dirstate()
398cdf0c1d5Smjnelson
3992b5878deSRich Lowe        #
4002b5878deSRich Lowe        # Check that the patch's parent changeset exists.
4012b5878deSRich Lowe        #
402cdf0c1d5Smjnelson        try:
4032b5878deSRich Lowe            n = node.bin(dirstate)
4042b5878deSRich Lowe            self.ws.repo.changelog.lookup(n)
40587039217SRichard Lowe        except error.LookupError, e:
40612203c71SRichard Lowe            raise CdmNodeMissing("couldn't restore uncommitted changes",
40712203c71SRichard Lowe                                 e.name)
4082b5878deSRich Lowe
4092b5878deSRich Lowe        try:
4102b5878deSRich Lowe            self.ws.clean(rev=dirstate)
411cdf0c1d5Smjnelson        except util.Abort, e:
412cdf0c1d5Smjnelson            raise util.Abort("couldn't update to saved node: %s" % e)
413cdf0c1d5Smjnelson
414605a716eSRichard Lowe        if not self.bu.exists('diff'):
415cdf0c1d5Smjnelson            return
416cdf0c1d5Smjnelson
417cdf0c1d5Smjnelson        #
418cdf0c1d5Smjnelson        # There's a race here whereby if the patch (or part thereof)
419cdf0c1d5Smjnelson        # is applied within the same second as the clean above (such
420605a716eSRichard Lowe        # that modification time doesn't change) and if the size of
421605a716eSRichard Lowe        # that file does not change, Hg may not see the change.
422cdf0c1d5Smjnelson        #
423cdf0c1d5Smjnelson        # We sleep a full second to avoid this, as sleeping merely
424cdf0c1d5Smjnelson        # until the next second begins would require very close clock
425cdf0c1d5Smjnelson        # synchronization on network filesystems.
426cdf0c1d5Smjnelson        #
427cdf0c1d5Smjnelson        time.sleep(1)
428cdf0c1d5Smjnelson
429cdf0c1d5Smjnelson        files = {}
430cdf0c1d5Smjnelson        try:
431605a716eSRichard Lowe            diff = self.bu.backupfile('diff')
432cdf0c1d5Smjnelson            try:
433cdf0c1d5Smjnelson                fuzz = patch.patch(diff, self.ws.ui, strip=1,
434cdf0c1d5Smjnelson                                   cwd=self.ws.repo.root, files=files)
435cdf0c1d5Smjnelson                if fuzz:
436cdf0c1d5Smjnelson                    raise util.Abort('working copy diff applied with fuzz')
437cdf0c1d5Smjnelson            except Exception, e:
438cdf0c1d5Smjnelson                raise util.Abort("couldn't apply working copy diff: %s\n"
439cdf0c1d5Smjnelson                                 "   %s" % (diff, e))
440cdf0c1d5Smjnelson        finally:
441*036abacaSRichard Lowe            if Version.at_least("1.7"):
442*036abacaSRichard Lowe                cmdutil.updatedir(self.ws.ui, self.ws.repo, files)
443*036abacaSRichard Lowe            else:
444cdf0c1d5Smjnelson                patch.updatedir(self.ws.ui, self.ws.repo, files)
445cdf0c1d5Smjnelson
446605a716eSRichard Lowe        if not self.bu.exists('renames'):
447cdf0c1d5Smjnelson            return
448cdf0c1d5Smjnelson
449cdf0c1d5Smjnelson        #
450cdf0c1d5Smjnelson        # We need to re-apply name changes where the new name
451cdf0c1d5Smjnelson        # (rename/copy destination) is an already versioned file, as
452cdf0c1d5Smjnelson        # Hg would otherwise ignore them.
453cdf0c1d5Smjnelson        #
454cdf0c1d5Smjnelson        try:
455605a716eSRichard Lowe            fp = self.bu.open('renames')
456cdf0c1d5Smjnelson            for line in fp:
457cdf0c1d5Smjnelson                source, dest = line.strip().split()
45887039217SRichard Lowe                self.ws.copy(source, dest)
459cdf0c1d5Smjnelson        except EnvironmentError, e:
460cdf0c1d5Smjnelson            raise util.Abort('unable to open renames file: %s' % e)
461cdf0c1d5Smjnelson        except ValueError:
462cdf0c1d5Smjnelson            raise util.Abort('corrupt renames file: %s' %
463cdf0c1d5Smjnelson                             self.bu.backupfile('renames'))
464cdf0c1d5Smjnelson
465cdf0c1d5Smjnelson    def need_backup(self):
466cdf0c1d5Smjnelson        '''Compare backup of uncommitted changes to workspace'''
467605a716eSRichard Lowe        cnode = self.wctx.parents()[0].node()
4682b5878deSRich Lowe        if self._dirstate() != node.hex(cnode):
469cdf0c1d5Smjnelson            return True
470cdf0c1d5Smjnelson
471605a716eSRichard Lowe        fd = None
472605a716eSRichard Lowe        match = self.ws.matcher(files=self.wctx.files())
473605a716eSRichard Lowe        curdiff = self.ws.diff(opts={'git': True}, match=match)
4742b5878deSRich Lowe
475cdf0c1d5Smjnelson        try:
476605a716eSRichard Lowe            if self.bu.exists('diff'):
477cdf0c1d5Smjnelson                try:
478605a716eSRichard Lowe                    fd = self.bu.open('diff')
479cdf0c1d5Smjnelson                    backdiff = fd.read()
480605a716eSRichard Lowe                    fd.close()
481cdf0c1d5Smjnelson                except EnvironmentError, e:
482cdf0c1d5Smjnelson                    raise util.Abort("couldn't open backup diff %s\n"
483605a716eSRichard Lowe                                     "   %s" % (self.bu.backupfile('diff'), e))
484cdf0c1d5Smjnelson            else:
485cdf0c1d5Smjnelson                backdiff = ''
486cdf0c1d5Smjnelson
4872b5878deSRich Lowe            if backdiff != curdiff:
488cdf0c1d5Smjnelson                return True
489cdf0c1d5Smjnelson
490cdf0c1d5Smjnelson            currrenamed = self._clobbering_renames()
491cdf0c1d5Smjnelson            bakrenamed = None
492cdf0c1d5Smjnelson
493605a716eSRichard Lowe            if self.bu.exists('renames'):
494cdf0c1d5Smjnelson                try:
495605a716eSRichard Lowe                    fd = self.bu.open('renames')
496605a716eSRichard Lowe                    bakrenamed = [tuple(line.strip().split(' ')) for line in fd]
497605a716eSRichard Lowe                    fd.close()
498cdf0c1d5Smjnelson                except EnvironmentError, e:
499cdf0c1d5Smjnelson                    raise util.Abort("couldn't open renames file %s: %s\n" %
500cdf0c1d5Smjnelson                                     (self.bu.backupfile('renames'), e))
501cdf0c1d5Smjnelson
502cdf0c1d5Smjnelson            if currrenamed != bakrenamed:
503cdf0c1d5Smjnelson                return True
504605a716eSRichard Lowe        finally:
505605a716eSRichard Lowe            if fd and not fd.closed:
506605a716eSRichard Lowe                fd.close()
507cdf0c1d5Smjnelson
508cdf0c1d5Smjnelson        return False
509cdf0c1d5Smjnelson
510cdf0c1d5Smjnelson    def cleanup(self):
511cdf0c1d5Smjnelson        '''Remove backed up uncommitted changes'''
512605a716eSRichard Lowe
513605a716eSRichard Lowe        for f in ('dirstate', 'diff', 'renames'):
514605a716eSRichard Lowe            self.bu.unlink(f)
515cdf0c1d5Smjnelson
516cdf0c1d5Smjnelson
517cdf0c1d5Smjnelsonclass CdmMetadataBackup(object):
518cdf0c1d5Smjnelson    '''Backup of workspace metadata'''
519cdf0c1d5Smjnelson
520cdf0c1d5Smjnelson    def __init__(self, backup, ws):
521cdf0c1d5Smjnelson        self.bu = backup
522cdf0c1d5Smjnelson        self.ws = ws
5239a70fc3bSMark J. Nelson        self.files = ('hgrc', 'localtags', 'patches', 'cdm')
524cdf0c1d5Smjnelson
525cdf0c1d5Smjnelson    def backup(self):
526cdf0c1d5Smjnelson        '''Backup workspace metadata'''
527cdf0c1d5Smjnelson
528605a716eSRichard Lowe        tarpath = self.bu.backupfile('metadata.tar.gz')
529605a716eSRichard Lowe
530605a716eSRichard Lowe        #
531605a716eSRichard Lowe        # Files is a list of tuples (name, path), where name is as in
532605a716eSRichard Lowe        # self.files, and path is the absolute path.
533605a716eSRichard Lowe        #
534605a716eSRichard Lowe        files = filter(lambda (name, path): os.path.exists(path),
535605a716eSRichard Lowe                       zip(self.files, map(self.ws.repo.join, self.files)))
536605a716eSRichard Lowe
537605a716eSRichard Lowe        if not files:
538605a716eSRichard Lowe            return
539cdf0c1d5Smjnelson
540cdf0c1d5Smjnelson        try:
541605a716eSRichard Lowe            tar = CdmTarFile.gzopen(tarpath, 'w')
542cdf0c1d5Smjnelson        except (EnvironmentError, tarfile.TarError), e:
543cdf0c1d5Smjnelson            raise util.Abort("couldn't open %s for writing: %s" %
544605a716eSRichard Lowe                             (tarpath, e))
545cdf0c1d5Smjnelson
546cdf0c1d5Smjnelson        try:
547605a716eSRichard Lowe            for name, path in files:
548605a716eSRichard Lowe                try:
549605a716eSRichard Lowe                    tar.add(path, name)
550cdf0c1d5Smjnelson                except (EnvironmentError, tarfile.TarError), e:
551cdf0c1d5Smjnelson                    #
552cdf0c1d5Smjnelson                    # tarfile.TarError doesn't include the tar member or file
553cdf0c1d5Smjnelson                    # in question, so we have to do so ourselves.
554cdf0c1d5Smjnelson                    #
555cdf0c1d5Smjnelson                    if isinstance(e, tarfile.TarError):
556605a716eSRichard Lowe                        errstr = "%s: %s" % (name, e)
557cdf0c1d5Smjnelson                    else:
55887039217SRichard Lowe                        errstr = str(e)
559cdf0c1d5Smjnelson
560cdf0c1d5Smjnelson                    raise util.Abort("couldn't backup metadata to %s:\n"
561605a716eSRichard Lowe                                     "  %s" % (tarpath, errstr))
562cdf0c1d5Smjnelson        finally:
563cdf0c1d5Smjnelson            tar.close()
564cdf0c1d5Smjnelson
565cdf0c1d5Smjnelson    def old_restore(self):
566cdf0c1d5Smjnelson        '''Restore workspace metadata from an pre-tar backup'''
567cdf0c1d5Smjnelson
568cdf0c1d5Smjnelson        for fname in self.files:
569605a716eSRichard Lowe            if self.bu.exists(fname):
570cdf0c1d5Smjnelson                bfile = self.bu.backupfile(fname)
571cdf0c1d5Smjnelson                wfile = self.ws.repo.join(fname)
572cdf0c1d5Smjnelson
573cdf0c1d5Smjnelson                try:
574cdf0c1d5Smjnelson                    shutil.copy2(bfile, wfile)
575cdf0c1d5Smjnelson                except EnvironmentError, e:
576cdf0c1d5Smjnelson                    raise util.Abort("couldn't restore metadata from %s:\n"
577cdf0c1d5Smjnelson                                     "   %s" % (bfile, e))
578cdf0c1d5Smjnelson
579cdf0c1d5Smjnelson    def tar_restore(self):
580cdf0c1d5Smjnelson        '''Restore workspace metadata (from a tar-style backup)'''
581cdf0c1d5Smjnelson
582605a716eSRichard Lowe        if not self.bu.exists('metadata.tar.gz'):
583605a716eSRichard Lowe            return
584605a716eSRichard Lowe
585605a716eSRichard Lowe        tarpath = self.bu.backupfile('metadata.tar.gz')
586cdf0c1d5Smjnelson
587cdf0c1d5Smjnelson        try:
588605a716eSRichard Lowe            tar = CdmTarFile.gzopen(tarpath)
589cdf0c1d5Smjnelson        except (EnvironmentError, tarfile.TarError), e:
590605a716eSRichard Lowe            raise util.Abort("couldn't open %s: %s" % (tarpath, e))
591cdf0c1d5Smjnelson
592cdf0c1d5Smjnelson        try:
593cdf0c1d5Smjnelson            for elt in tar:
594605a716eSRichard Lowe                try:
595cdf0c1d5Smjnelson                    tar.extract(elt, path=self.ws.repo.path)
596cdf0c1d5Smjnelson                except (EnvironmentError, tarfile.TarError), e:
597cdf0c1d5Smjnelson                    # Make sure the member name is in the exception message.
598cdf0c1d5Smjnelson                    if isinstance(e, tarfile.TarError):
59987039217SRichard Lowe                        errstr = "%s: %s" % (elt.name, e)
600cdf0c1d5Smjnelson                    else:
60187039217SRichard Lowe                        errstr = str(e)
602cdf0c1d5Smjnelson
603cdf0c1d5Smjnelson                    raise util.Abort("couldn't restore metadata from %s:\n"
604cdf0c1d5Smjnelson                                     "   %s" %
605605a716eSRichard Lowe                                     (tarpath, errstr))
606cdf0c1d5Smjnelson        finally:
607cdf0c1d5Smjnelson            if tar and not tar.closed:
608cdf0c1d5Smjnelson                tar.close()
609cdf0c1d5Smjnelson
610cdf0c1d5Smjnelson    def restore(self):
611cdf0c1d5Smjnelson        '''Restore workspace metadata'''
612cdf0c1d5Smjnelson
613605a716eSRichard Lowe        if self.bu.exists('hgrc'):
614cdf0c1d5Smjnelson            self.old_restore()
615cdf0c1d5Smjnelson        else:
616cdf0c1d5Smjnelson            self.tar_restore()
617cdf0c1d5Smjnelson
618605a716eSRichard Lowe    def _walk(self):
619605a716eSRichard Lowe        '''Yield the repo-relative path to each file we operate on,
620605a716eSRichard Lowe        including each file within any affected directory'''
621605a716eSRichard Lowe
622605a716eSRichard Lowe        for elt in self.files:
623605a716eSRichard Lowe            path = self.ws.repo.join(elt)
624605a716eSRichard Lowe
625605a716eSRichard Lowe            if not os.path.exists(path):
626605a716eSRichard Lowe                continue
627605a716eSRichard Lowe
628605a716eSRichard Lowe            if os.path.isdir(path):
629605a716eSRichard Lowe                for root, dirs, files in os.walk(path, topdown=True):
630605a716eSRichard Lowe                    yield root
631605a716eSRichard Lowe
632605a716eSRichard Lowe                    for f in files:
633605a716eSRichard Lowe                        yield os.path.join(root, f)
634605a716eSRichard Lowe            else:
635605a716eSRichard Lowe                yield path
636605a716eSRichard Lowe
637cdf0c1d5Smjnelson    def need_backup(self):
638cdf0c1d5Smjnelson        '''Compare backed up workspace metadata to workspace'''
639cdf0c1d5Smjnelson
640605a716eSRichard Lowe        def strip_trailing_pathsep(pathname):
641605a716eSRichard Lowe            '''Remove a possible trailing path separator from PATHNAME'''
642605a716eSRichard Lowe            return pathname.endswith('/') and pathname[:-1] or pathname
643605a716eSRichard Lowe
644605a716eSRichard Lowe        if self.bu.exists('metadata.tar.gz'):
645605a716eSRichard Lowe            tarpath = self.bu.backupfile('metadata.tar.gz')
646cdf0c1d5Smjnelson            try:
647605a716eSRichard Lowe                tar = CdmTarFile.gzopen(tarpath)
648cdf0c1d5Smjnelson            except (EnvironmentError, tarfile.TarError), e:
649cdf0c1d5Smjnelson                raise util.Abort("couldn't open metadata tarball: %s\n"
650605a716eSRichard Lowe                                 "   %s" % (tarpath, e))
651cdf0c1d5Smjnelson
652605a716eSRichard Lowe            if not tar.members_match_fs(self.ws.repo.path):
653605a716eSRichard Lowe                tar.close()
654cdf0c1d5Smjnelson                return True
655cdf0c1d5Smjnelson
656605a716eSRichard Lowe            tarnames = map(strip_trailing_pathsep, tar.getnames())
657cdf0c1d5Smjnelson            tar.close()
658cdf0c1d5Smjnelson        else:
659cdf0c1d5Smjnelson            tarnames = []
660cdf0c1d5Smjnelson
661605a716eSRichard Lowe        repopath = self.ws.repo.path
662605a716eSRichard Lowe        if not repopath.endswith('/'):
663605a716eSRichard Lowe            repopath += '/'
664cdf0c1d5Smjnelson
665605a716eSRichard Lowe        for path in self._walk():
666605a716eSRichard Lowe            if path.replace(repopath, '', 1) not in tarnames:
667cdf0c1d5Smjnelson                return True
668cdf0c1d5Smjnelson
669cdf0c1d5Smjnelson        return False
670cdf0c1d5Smjnelson
671cdf0c1d5Smjnelson    def cleanup(self):
672cdf0c1d5Smjnelson        '''Remove backed up workspace metadata'''
673605a716eSRichard Lowe        self.bu.unlink('metadata.tar.gz')
674605a716eSRichard Lowe
675605a716eSRichard Lowe
676605a716eSRichard Loweclass CdmClearBackup(object):
677605a716eSRichard Lowe    '''A backup (in tar format) of complete source files from every
678605a716eSRichard Lowe    workspace head.
679605a716eSRichard Lowe
680605a716eSRichard Lowe    Paths in the tarball are prefixed by the revision and node of the
681605a716eSRichard Lowe    head, or "working" for the working directory.
682605a716eSRichard Lowe
683605a716eSRichard Lowe    This is done purely for the benefit of the user, and as such takes
684605a716eSRichard Lowe    no part in restore or need_backup checking, restore always
685605a716eSRichard Lowe    succeeds, need_backup always returns False
686605a716eSRichard Lowe    '''
687605a716eSRichard Lowe
688605a716eSRichard Lowe    def __init__(self, backup, ws):
689605a716eSRichard Lowe        self.bu = backup
690605a716eSRichard Lowe        self.ws = ws
691605a716eSRichard Lowe
692605a716eSRichard Lowe    def _branch_pairs(self):
693605a716eSRichard Lowe        '''Return a list of tuples (parenttip, localtip) for each
694605a716eSRichard Lowe        outgoing head.  If the working copy contains modified files,
695605a716eSRichard Lowe        it is a head, and neither of its parents are.
696605a716eSRichard Lowe        '''
697605a716eSRichard Lowe
698605a716eSRichard Lowe        parent = self.ws.parent()
699605a716eSRichard Lowe
700605a716eSRichard Lowe        if parent:
701605a716eSRichard Lowe            outgoing = self.ws.findoutgoing(parent)
702605a716eSRichard Lowe            outnodes = set(self.ws.repo.changelog.nodesbetween(outgoing)[0])
703605a716eSRichard Lowe
704605a716eSRichard Lowe            heads = [self.ws.repo.changectx(n) for n in self.ws.repo.heads()
705605a716eSRichard Lowe                     if n in outnodes]
706605a716eSRichard Lowe        else:
707605a716eSRichard Lowe            heads = []
708605a716eSRichard Lowe            outnodes = []
709605a716eSRichard Lowe
710605a716eSRichard Lowe        wctx = self.ws.workingctx()
711605a716eSRichard Lowe        if wctx.files():        # We only care about file changes.
712605a716eSRichard Lowe            heads = filter(lambda x: x not in wctx.parents(), heads) + [wctx]
713605a716eSRichard Lowe
714605a716eSRichard Lowe        pairs = []
715605a716eSRichard Lowe        for head in heads:
716605a716eSRichard Lowe            if head.rev() is None:
717605a716eSRichard Lowe                c = head.parents()
718605a716eSRichard Lowe            else:
719605a716eSRichard Lowe                c = [head]
720605a716eSRichard Lowe
721605a716eSRichard Lowe            pairs.append((self.ws.parenttip(c, outnodes), head))
722605a716eSRichard Lowe        return pairs
723605a716eSRichard Lowe
724605a716eSRichard Lowe    def backup(self):
725605a716eSRichard Lowe        '''Save a clear copy of each source file modified between each
726605a716eSRichard Lowe        head and that head's parenttip (see WorkSpace.parenttip).
727605a716eSRichard Lowe        '''
728605a716eSRichard Lowe
729605a716eSRichard Lowe        tarpath = self.bu.backupfile('clear.tar.gz')
730605a716eSRichard Lowe        branches = self._branch_pairs()
731605a716eSRichard Lowe
732605a716eSRichard Lowe        if not branches:
733605a716eSRichard Lowe            return
734605a716eSRichard Lowe
735605a716eSRichard Lowe        try:
736605a716eSRichard Lowe            tar = CdmTarFile.gzopen(tarpath, 'w')
737605a716eSRichard Lowe        except (EnvironmentError, tarfile.TarError), e:
738605a716eSRichard Lowe            raise util.Abort("Could not open %s for writing: %s" %
739605a716eSRichard Lowe                             (tarpath, e))
740605a716eSRichard Lowe
741605a716eSRichard Lowe        try:
742605a716eSRichard Lowe            for parent, child in branches:
743605a716eSRichard Lowe                tpath = child.node() and node.short(child.node()) or "working"
744605a716eSRichard Lowe
745605a716eSRichard Lowe                for fname, change in self.ws.status(parent, child).iteritems():
746605a716eSRichard Lowe                    if change not in ('added', 'modified'):
747605a716eSRichard Lowe                        continue
748605a716eSRichard Lowe
749605a716eSRichard Lowe                    try:
750605a716eSRichard Lowe                        tar.addfilectx(child.filectx(fname),
751605a716eSRichard Lowe                                       os.path.join(tpath, fname))
752605a716eSRichard Lowe                    except ValueError, e:
753605a716eSRichard Lowe                        crev = child.rev()
754605a716eSRichard Lowe                        if crev is None:
755605a716eSRichard Lowe                            crev = "working copy"
756605a716eSRichard Lowe                        raise util.Abort("Could not backup clear file %s "
757605a716eSRichard Lowe                                         "from %s: %s\n" % (fname, crev, e))
758605a716eSRichard Lowe        finally:
759605a716eSRichard Lowe            tar.close()
760605a716eSRichard Lowe
761605a716eSRichard Lowe    def cleanup(self):
762605a716eSRichard Lowe        '''Cleanup a failed Clear backup.
763605a716eSRichard Lowe
764605a716eSRichard Lowe        Remove the clear tarball from the backup directory.
765605a716eSRichard Lowe        '''
766605a716eSRichard Lowe
767605a716eSRichard Lowe        self.bu.unlink('clear.tar.gz')
768605a716eSRichard Lowe
769605a716eSRichard Lowe    def restore(self):
770605a716eSRichard Lowe        '''Clear backups are never restored, do nothing'''
771605a716eSRichard Lowe        pass
772605a716eSRichard Lowe
773605a716eSRichard Lowe    def need_backup(self):
774605a716eSRichard Lowe        '''Clear backups are never compared, return False (no backup needed).
775605a716eSRichard Lowe
776605a716eSRichard Lowe        Should a backup actually be needed, one of the other
777605a716eSRichard Lowe        implementation classes would notice in any situation we would.
778605a716eSRichard Lowe        '''
779605a716eSRichard Lowe
780605a716eSRichard Lowe        return False
781cdf0c1d5Smjnelson
782cdf0c1d5Smjnelson
783cdf0c1d5Smjnelsonclass CdmBackup(object):
784cdf0c1d5Smjnelson    '''A backup of a given workspace'''
785cdf0c1d5Smjnelson
786cdf0c1d5Smjnelson    def __init__(self, ui, ws, name):
787cdf0c1d5Smjnelson        self.ws = ws
788cdf0c1d5Smjnelson        self.ui = ui
789cdf0c1d5Smjnelson        self.backupdir = self._find_backup_dir(name)
790cdf0c1d5Smjnelson
791cdf0c1d5Smjnelson        #
792cdf0c1d5Smjnelson        # The order of instances here controls the order the various operations
793cdf0c1d5Smjnelson        # are run.
794cdf0c1d5Smjnelson        #
795cdf0c1d5Smjnelson        # There's some inherent dependence, in that on restore we need
796cdf0c1d5Smjnelson        # to restore committed changes prior to uncommitted changes
797cdf0c1d5Smjnelson        # (as the parent revision of any uncommitted changes is quite
798cdf0c1d5Smjnelson        # likely to not exist until committed changes are restored).
799cdf0c1d5Smjnelson        # Metadata restore can happen at any point, but happens last
800cdf0c1d5Smjnelson        # as a matter of convention.
801cdf0c1d5Smjnelson        #
802cdf0c1d5Smjnelson        self.modules = [x(self, ws) for x in [CdmCommittedBackup,
803cdf0c1d5Smjnelson                                              CdmUncommittedBackup,
804605a716eSRichard Lowe                                              CdmClearBackup,
805cdf0c1d5Smjnelson                                              CdmMetadataBackup]]
806cdf0c1d5Smjnelson
807cdf0c1d5Smjnelson        if os.path.exists(os.path.join(self.backupdir, 'latest')):
808cdf0c1d5Smjnelson            generation = os.readlink(os.path.join(self.backupdir, 'latest'))
809cdf0c1d5Smjnelson            self.generation = int(os.path.split(generation)[1])
810cdf0c1d5Smjnelson        else:
811cdf0c1d5Smjnelson            self.generation = 0
812cdf0c1d5Smjnelson
813cdf0c1d5Smjnelson    def _find_backup_dir(self, name):
814cdf0c1d5Smjnelson        '''Find the path to an appropriate backup directory based on NAME'''
815cdf0c1d5Smjnelson
816cdf0c1d5Smjnelson        if os.path.isabs(name):
817cdf0c1d5Smjnelson            return name
818cdf0c1d5Smjnelson
819cdf0c1d5Smjnelson        if self.ui.config('cdm', 'backupdir'):
820cdf0c1d5Smjnelson            backupbase = os.path.expanduser(self.ui.config('cdm', 'backupdir'))
821cdf0c1d5Smjnelson        else:
822cdf0c1d5Smjnelson            home = None
823cdf0c1d5Smjnelson
824cdf0c1d5Smjnelson            try:
825cdf0c1d5Smjnelson                home = os.getenv('HOME') or pwd.getpwuid(os.getuid()).pw_dir
826cdf0c1d5Smjnelson            except KeyError:
827cdf0c1d5Smjnelson                pass                    # Handled anyway
828cdf0c1d5Smjnelson
829cdf0c1d5Smjnelson            if not home:
830cdf0c1d5Smjnelson                raise util.Abort('Could not determine your HOME directory to '
831cdf0c1d5Smjnelson                                 'find backup path')
832cdf0c1d5Smjnelson
833cdf0c1d5Smjnelson            backupbase = os.path.join(home, 'cdm.backup')
834cdf0c1d5Smjnelson
835cdf0c1d5Smjnelson        backupdir = os.path.join(backupbase, name)
836cdf0c1d5Smjnelson
837cdf0c1d5Smjnelson        # If backupdir exists, it must be a directory.
838cdf0c1d5Smjnelson        if (os.path.exists(backupdir) and not os.path.isdir(backupdir)):
839cdf0c1d5Smjnelson            raise util.Abort('%s exists but is not a directory' % backupdir)
840cdf0c1d5Smjnelson
841cdf0c1d5Smjnelson        return backupdir
842cdf0c1d5Smjnelson
843605a716eSRichard Lowe    def _update_latest(self, gen):
844cdf0c1d5Smjnelson        '''Update latest symlink to point to the current generation'''
845cdf0c1d5Smjnelson        linkpath = os.path.join(self.backupdir, 'latest')
846cdf0c1d5Smjnelson
847cdf0c1d5Smjnelson        if os.path.lexists(linkpath):
848cdf0c1d5Smjnelson            os.unlink(linkpath)
849cdf0c1d5Smjnelson
850cdf0c1d5Smjnelson        os.symlink(str(gen), linkpath)
851cdf0c1d5Smjnelson
852605a716eSRichard Lowe    def _create_gen(self, gen):
853cdf0c1d5Smjnelson        '''Create a new backup generation'''
854cdf0c1d5Smjnelson        try:
855cdf0c1d5Smjnelson            os.makedirs(os.path.join(self.backupdir, str(gen)))
856605a716eSRichard Lowe            self._update_latest(gen)
857cdf0c1d5Smjnelson        except EnvironmentError, e:
858cdf0c1d5Smjnelson            raise util.Abort("Couldn't create backup generation %s: %s" %
859cdf0c1d5Smjnelson                             (os.path.join(self.backupdir, str(gen)), e))
860cdf0c1d5Smjnelson
861605a716eSRichard Lowe    def backupfile(self, path):
862605a716eSRichard Lowe        '''return full path to backup file FILE at GEN'''
863605a716eSRichard Lowe        return os.path.join(self.backupdir, str(self.generation), path)
864605a716eSRichard Lowe
865605a716eSRichard Lowe    def unlink(self, name):
866605a716eSRichard Lowe        '''Unlink the specified path from the backup directory.
867605a716eSRichard Lowe        A no-op if the path does not exist.
868605a716eSRichard Lowe        '''
869605a716eSRichard Lowe
870605a716eSRichard Lowe        fpath = self.backupfile(name)
871605a716eSRichard Lowe        if os.path.exists(fpath):
872605a716eSRichard Lowe            os.unlink(fpath)
873605a716eSRichard Lowe
874605a716eSRichard Lowe    def open(self, name, mode='r'):
875605a716eSRichard Lowe        '''Open the specified file in the backup directory'''
876605a716eSRichard Lowe        return open(self.backupfile(name), mode)
877605a716eSRichard Lowe
878605a716eSRichard Lowe    def exists(self, name):
879605a716eSRichard Lowe        '''Return boolean indicating wether a given file exists in the
880605a716eSRichard Lowe        backup directory.'''
881605a716eSRichard Lowe        return os.path.exists(self.backupfile(name))
882605a716eSRichard Lowe
883cdf0c1d5Smjnelson    def need_backup(self):
884cdf0c1d5Smjnelson        '''Compare backed up changes to workspace'''
885cdf0c1d5Smjnelson        #
886cdf0c1d5Smjnelson        # If there's no current backup generation, or the last backup was
887cdf0c1d5Smjnelson        # invalid (lacking the dirstate file), we need a backup regardless
888cdf0c1d5Smjnelson        # of anything else.
889cdf0c1d5Smjnelson        #
890605a716eSRichard Lowe        if not self.generation or not self.exists('dirstate'):
891cdf0c1d5Smjnelson            return True
892cdf0c1d5Smjnelson
893cdf0c1d5Smjnelson        for x in self.modules:
894cdf0c1d5Smjnelson            if x.need_backup():
895cdf0c1d5Smjnelson                return True
896cdf0c1d5Smjnelson
897cdf0c1d5Smjnelson        return False
898cdf0c1d5Smjnelson
899cdf0c1d5Smjnelson    def backup(self):
900c959a081SRichard Lowe        '''Take a backup of the current workspace
901c959a081SRichard Lowe
902c959a081SRichard Lowe        Calling code is expected to hold both the working copy lock
903c959a081SRichard Lowe        and repository lock.'''
904cdf0c1d5Smjnelson
905cdf0c1d5Smjnelson        if not os.path.exists(self.backupdir):
906cdf0c1d5Smjnelson            try:
907cdf0c1d5Smjnelson                os.makedirs(self.backupdir)
908cdf0c1d5Smjnelson            except EnvironmentError, e:
909cdf0c1d5Smjnelson                raise util.Abort('Could not create backup directory %s: %s' %
910cdf0c1d5Smjnelson                                 (self.backupdir, e))
911cdf0c1d5Smjnelson
912cdf0c1d5Smjnelson        self.generation += 1
913605a716eSRichard Lowe        self._create_gen(self.generation)
914cdf0c1d5Smjnelson
915cdf0c1d5Smjnelson        try:
916cdf0c1d5Smjnelson            for x in self.modules:
917cdf0c1d5Smjnelson                x.backup()
918cdf0c1d5Smjnelson        except Exception, e:
919cdf0c1d5Smjnelson            if isinstance(e, KeyboardInterrupt):
920cdf0c1d5Smjnelson                self.ws.ui.warn("Interrupted\n")
921cdf0c1d5Smjnelson            else:
922cdf0c1d5Smjnelson                self.ws.ui.warn("Error: %s\n" % e)
923c959a081SRichard Lowe                show_traceback = self.ws.ui.configbool('ui', 'traceback',
924c959a081SRichard Lowe                                                       False)
925cdf0c1d5Smjnelson
926cdf0c1d5Smjnelson                #
927cdf0c1d5Smjnelson                # If it's not a 'normal' error, we want to print a stack
928cdf0c1d5Smjnelson                # trace now in case the attempt to remove the partial
929cdf0c1d5Smjnelson                # backup also fails, and raises a second exception.
930cdf0c1d5Smjnelson                #
931cdf0c1d5Smjnelson                if (not isinstance(e, (EnvironmentError, util.Abort))
932c959a081SRichard Lowe                    or show_traceback):
933cdf0c1d5Smjnelson                    traceback.print_exc()
934cdf0c1d5Smjnelson
935cdf0c1d5Smjnelson            for x in self.modules:
936cdf0c1d5Smjnelson                x.cleanup()
937cdf0c1d5Smjnelson
938cdf0c1d5Smjnelson            os.rmdir(os.path.join(self.backupdir, str(self.generation)))
939cdf0c1d5Smjnelson            self.generation -= 1
940cdf0c1d5Smjnelson
941cdf0c1d5Smjnelson            if self.generation != 0:
942605a716eSRichard Lowe                self._update_latest(self.generation)
943cdf0c1d5Smjnelson            else:
944cdf0c1d5Smjnelson                os.unlink(os.path.join(self.backupdir, 'latest'))
945cdf0c1d5Smjnelson
946cdf0c1d5Smjnelson            raise util.Abort('Backup failed')
947cdf0c1d5Smjnelson
948cdf0c1d5Smjnelson    def restore(self, gen=None):
949cdf0c1d5Smjnelson        '''Restore workspace from backup
950cdf0c1d5Smjnelson
951cdf0c1d5Smjnelson        Restores from backup generation GEN (defaulting to the latest)
952c959a081SRichard Lowe        into workspace WS.
953cdf0c1d5Smjnelson
954c959a081SRichard Lowe        Calling code is expected to hold both the working copy lock
955c959a081SRichard Lowe        and repository lock of the destination workspace.'''
956cdf0c1d5Smjnelson
957cdf0c1d5Smjnelson        if not os.path.exists(self.backupdir):
958cdf0c1d5Smjnelson            raise util.Abort('Backup directory does not exist: %s' %
959cdf0c1d5Smjnelson                             (self.backupdir))
960cdf0c1d5Smjnelson
961cdf0c1d5Smjnelson        if gen:
962cdf0c1d5Smjnelson            if not os.path.exists(os.path.join(self.backupdir, str(gen))):
963cdf0c1d5Smjnelson                raise util.Abort('Backup generation does not exist: %s' %
964cdf0c1d5Smjnelson                                 (os.path.join(self.backupdir, str(gen))))
965cdf0c1d5Smjnelson            self.generation = int(gen)
966cdf0c1d5Smjnelson
967605a716eSRichard Lowe        if not self.generation: # This is OK, 0 is not a valid generation
968cdf0c1d5Smjnelson            raise util.Abort('Backup has no generations: %s' % self.backupdir)
969cdf0c1d5Smjnelson
970605a716eSRichard Lowe        if not self.exists('dirstate'):
971cdf0c1d5Smjnelson            raise util.Abort('Backup %s/%s is incomplete (dirstate missing)' %
972cdf0c1d5Smjnelson                             (self.backupdir, self.generation))
973cdf0c1d5Smjnelson
974cdf0c1d5Smjnelson        try:
975cdf0c1d5Smjnelson            for x in self.modules:
976cdf0c1d5Smjnelson                x.restore()
977cdf0c1d5Smjnelson        except util.Abort, e:
978cdf0c1d5Smjnelson            raise util.Abort('Error restoring workspace:\n'
979cdf0c1d5Smjnelson                             '%s\n'
98012203c71SRichard Lowe                             'Workspace may be partially restored' % e)
981