source: tailor/vcpx/project.py @ 638

Revision 638, 9.8 KB checked in by lele@…, 8 years ago (diff)

Import the ConfigurationError? exception and delimit the project name

Line 
1# -*- mode: python; coding: utf-8 -*-
2# :Progetto: vcpx -- Project details
3# :Creato:   gio 04 ago 2005 13:07:31 CEST
4# :Autore:   Lele Gaifax <lele@nautilus.homeip.net>
5# :Licenza:  GNU General Public License
6#
7
8"""
9This module implements a higher level of operations, with a Project
10class that knows how to drive the two main activities, bootstrap and
11update, layering on top of DualWorkingDir.
12"""
13
14__docformat__ = 'reStructuredText'
15
16from cPickle import load, dump
17from vcpx.config import ConfigurationError
18
19class StateFile(object):
20    """
21    State file that stores current revision and pending changesets.
22
23    It behaves as an iterator, and source backends loop over not yet
24    applied changesets, calling .applied() after each one: that writes
25    the applied changeset in a `journal` file, much more atomic than
26    rewriting the whole archive each time.
27
28    When the source backend finishes it's job, either because there
29    are no more pending changeset or stopped by an error, it calls
30    .finalize(), that in presence of a journal file adjust the
31    archive filtering out already applied changesets.
32
33    Should an hard error prevent .finalize() call, it will happen
34    automatically next time the state file is loaded.
35    """
36
37    def __init__(self, fname, config):
38        self.filename = fname
39        self.archive = None
40        self.last_applied = None
41        self.current = None
42        self.queuelen = 0
43
44    def _load(self):
45        """
46        Open the pickle file and load the first two items, respectively
47        the revision and the number of pending changesets.
48        """
49
50        # Take care of the journal file, if present.
51        self.finalize()
52
53        self.current = None
54        try:
55            self.archive = open(self.filename)
56            self.last_applied = load(self.archive)
57            self.queuelen = load(self.archive)
58        except IOError:
59            self.archive = None
60            self.last_applied = None
61            self.queuelen = 0
62
63    def _write(self, changesets):
64        """
65        Write the state file.
66        """
67
68        sf = open(self.filename, 'w')
69        dump(self.last_applied, sf)
70        dump(len(changesets), sf)
71        for cs in changesets:
72            dump(cs, sf)
73        sf.close()
74
75    def __str__(self):
76        return self.filename
77
78    def __iter__(self):
79        return self
80
81    def next(self):
82        if not self.archive:
83            raise StopIteration
84        try:
85            self.current = load(self.archive)
86        except EOFError:
87            self.archive.close()
88            self.archive = None
89            raise StopIteration
90        self.queuelen -= 1
91        return self.current
92
93    def __len__(self):
94        if self.archive is None:
95            self._load()
96        return self.queuelen
97
98    def applied(self, current=None):
99        """
100        Write the applied changeset to the journal file.
101        """
102
103        self.last_applied = current or self.current
104        journal = open(self.filename + '.journal', 'w')
105        dump(self.last_applied, journal)
106        journal.close()
107
108    def finalize(self):
109        """
110        If there is a journal file, adjust the archive accordingly,
111        dropping already applied changesets.
112        """
113
114        from os.path import exists
115        from os import unlink, rename
116
117        if self.archive is not None:
118            self.archive.close()
119            self.archive = None
120
121        if not exists(self.filename + '.journal'):
122            return
123
124        # Load last applied changeset from the journal
125        journal = open(self.filename + '.journal')
126        last_applied = load(journal)
127        journal.close()
128
129        # If there is an actual archive (ie, this is not bootstrap time)
130        # load the changesets from there, skipping the changesets until
131        # the last_applied one, then transfer the remaining to the new
132        # archive.
133        if exists(self.filename):
134            old = open(self.filename)
135            load(old) # last applied
136            queuelen = load(old)
137            cs = load(old)
138
139            # Skip already applied changesets
140            while cs <> last_applied:
141                queuelen -= 1
142                cs = load(old)
143
144            sf = open(self.filename + '.new', 'w')
145            dump(last_applied, sf)
146            dump(queuelen-1, sf)
147
148            while True:
149                try:
150                    cs = load(old)
151                except EOFError:
152                    break
153                dump(cs, sf)
154            sf.close()
155            old.close()
156            unlink(self.filename)
157            rename(sf.name, self.filename)
158        else:
159            sf = open(self.filename, 'w')
160            dump(last_applied, sf)
161            dump(0, sf)
162            sf.close()
163
164        unlink(journal.name)
165
166    def lastAppliedChangeset(self):
167        """
168        Return the last applied changeset, if any, None otherwise.
169        """
170
171        if self.archive is None:
172            self._load()
173        return self.last_applied
174
175    def setPendingChangesets(self, changesets):
176        """
177        Write pending changesets to the state file.
178        """
179
180        if self.archive is not None:
181            self.archive.close()
182            self.archive = None
183
184        self._write(changesets)
185        self._load()
186
187
188class UnknownProjectError(Exception):
189    "Project does not exist"
190
191
192class Project(object):
193    """
194    This class collects the information related to a single project, such
195    as its source and target repositories and state file.
196    """
197
198    def __init__(self, name, config):
199        if not config.has_section(name):
200            raise UnknownProjectError("'%s' is not a known project" % name)
201
202        self.config = config
203        self.name = name
204        self.dwd = None
205        self._load()
206
207    def __str__(self):
208        return "Project %s at %s:\n\t" % (self.name, self.rootdir) + \
209               "\n\t".join(['%s = %s' % (v, getattr(self, v))
210                            for v in ('source', 'target', 'state_file')])
211
212    def _load(self):
213        """
214        Load relevant information from the configuration.
215        """
216
217        from os import getcwd, makedirs
218        from os.path import join, exists, expanduser
219        import logging
220
221        self.rootdir = self.config.get(self.name, 'root-directory', getcwd())
222        if not exists(self.rootdir):
223            makedirs(self.rootdir)
224        self.subdir = self.config.get(self.name, 'subdir')
225        if not self.subdir:
226            self.subdir = '.'
227
228        self.source = self.__loadRepository('source')
229        self.target = self.__loadRepository('target')
230        sfpath = join(self.rootdir,
231                      expanduser(self.config.get(self.name, 'state-file')))
232        self.state_file = StateFile(sfpath, self.config)
233
234        before = self.config.getTuple(self.name, 'before-commit')
235        try:
236            self.before_commit = [self.config.namespace[f] for f in before]
237        except KeyError, e:
238            raise ConfigurationError('Project "%s" before-commit references '
239                                     'unknown function: %s' %
240                                     (self.name, str(e)))
241
242        after = self.config.getTuple(self.name, 'after-commit')
243        try:
244            self.after_commit = [self.config.namespace[f] for f in after]
245        except KeyError, e:
246            raise ConfigurationError('Project "%s" after-commit references '
247                                     'unknown function: %s' %
248                                     (self.name, str(e)))
249
250        self.verbose = self.config.get(self.name, 'verbose', False)
251        self.logger = logging.getLogger('tailor.%s' % self.name)
252        self.logfile = join(self.rootdir,
253                            expanduser(self.config.get(self.name, 'logfile',
254                                                       'tailor.log')))
255        hdlr = logging.FileHandler(self.logfile)
256        formatter = logging.Formatter('%(asctime)s [%(levelname)s] %(message)s')
257        hdlr.setFormatter(formatter)
258        self.logger.addHandler(hdlr)
259        self.logger.setLevel(logging.INFO)
260
261    def log_info(self, what):
262        """
263        Print some info on the log and, in verbose mode, to stdout as well.
264        """
265
266        if self.logger:
267            self.logger.info(what)
268
269        if self.verbose:
270            print what
271
272    def log_error(self, what, exc=False):
273        """
274        Print an error message, possibly with an exception traceback,
275        to the log and to stdout as well.
276        """
277
278        if self.logger:
279            if exc:
280                self.logger.exception(what)
281            else:
282                self.logger.error(what)
283
284        print "Error:", what,
285        if exc:
286            from sys import exc_info
287
288            ei = exc_info()
289            print ' -- Exception %s: %s' % ei[0:2]
290        else:
291            print
292
293    def __loadRepository(self, which):
294        """
295        Given a repository named 'somekind:somename', return a Repository
296        (or a subclass of it, if 'SomekindRepository' exists) instance
297        that wraps it.
298        """
299
300        import repository
301
302        repname = self.config.get(self.name, which)
303        kind = repname[:repname.index(':')]
304        klassname = kind.capitalize() + 'Repository'
305        try:
306            klass = getattr(repository, klassname)
307        except AttributeError:
308            klass = repository.Repository
309        return klass(repname, kind, self, which)
310
311    def exists(self):
312        """
313        Return True if the project exists, False otherwise.
314        """
315
316        return self.state_file.lastAppliedChangeset() is not None
317
318    def workingDir(self):
319        """
320        Return a DualWorkingDir instance, ready to work.
321        """
322
323        from dualwd import DualWorkingDir
324
325        if self.dwd is None:
326            self.dwd = DualWorkingDir(self.source, self.target)
327            self.dwd.setStateFile(self.state_file)
328            self.dwd.setLogfile(self.logfile)
329        return self.dwd
Note: See TracBrowser for help on using the repository browser.