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"""
13from __future__ import absolute_import
14
15from future import standard_library
16standard_library.install_aliases()
17from builtins import str
18from builtins import object
19__docformat__ = 'reStructuredText'
20
21from vcpx import TailorException
22from vcpx.config import ConfigurationError
23from vcpx.statefile import StateFile
24
25
26class UnknownProjectError(TailorException):
27    "Project does not exist"
28
29
30class Project(object):
31    """
32    This class collects the information related to a single project, such
33    as its source and target repositories and state file. All the setup
34    comes from a section in the configuration file (.ini-like format) with
35    the same name as the project.
36
37    Mandatory options are:
38
39    root-directory
40      This is where all the fun will happen: this directory will contain
41      the source and the target working copy, and usually the state and
42      the log file. It support the conventional "~user" to indicate user's
43      home directory.
44
45    subdir
46      This is the subdirectory, relative to the root-directory, where
47      tailor will extract the source working copy. It may be '.' for some
48      backend kinds.
49
50    state-file
51      Name of the state file needed to store tailor last activity.
52
53    source
54      The source repository: a repository name is something like
55      "darcs:somename", that will be loaded from the homonymous section
56      in the configuration.
57
58    target
59      The counterpart of `source`, the repository that will receive the
60      changes coming from there.
61
62    Non mandatory options:
63
64    before-commit
65      This is a function name, or a sequence of function names enclosed
66      by brackets, that will be executed on each changeset just before
67      it get replayed on the target system: this may be used to perform
68      any kind of alteration on the content of the changeset, or to skip
69      some of them.
70
71    after-commit
72      This is a function name, or a sequence of function names enclosed
73      by brackets, that will be executed on each changeset just after
74      the commit on the target system: this may be used for example to
75      create a tag.
76
77    start-revision
78      This identifies from when tailor should start the migration. It can
79      be either ``INITIAL``, to indicate the start of the history, or
80      ``HEAD`` to indicate the current latest changeset, or a backend
81      specific way of indicate a particular revision/tag in the history.
82    """
83
84    def __init__(self, name, config):
85        """
86        Initialize a new instance representing the project `name`.
87        """
88
89        from configparser import Error
90
91        self.loghandler = None
92
93        if not config.has_section(name):
94            raise UnknownProjectError("'%s' is not a known project" % name)
95
96        self.config = config
97        self.name = name
98        self.dwd = None
99        try:
100            self._load()
101        except Error as e:
102            raise ConfigurationError('Invalid configuration in section %s: %s'
103                                     % (self.name, str(e)))
104
105    def __str__(self):
106        return "Project %s at %s:\n\t" % (self.name, self.rootdir) + \
107               "\n\t".join(['%s = %s' % (v, getattr(self, v))
108                            for v in ('source', 'target', 'state_file')])
109
110    def _load(self):
111        """
112        Load relevant information from the configuration.
113        """
114
115        from os import makedirs
116        from os.path import join, exists, expanduser, abspath
117        from logging import getLogger, CRITICAL, DEBUG, FileHandler, \
118             StreamHandler, Formatter
119
120        self.verbose = self.config.get(self.name, 'verbose', False)
121        rootdir = self.config.get(self.name, 'root-directory', '.')
122        self.rootdir = abspath(expanduser(rootdir))
123        if not exists(self.rootdir):
124            makedirs(self.rootdir)
125        self.subdir = self.config.get(self.name, 'subdir')
126        if not self.subdir:
127            self.subdir = '.'
128
129        self.logfile = join(self.rootdir, self.name + '.log')
130        self.log = getLogger('tailor.project.%s' % self.name)
131        if self.config.get(self.name, 'debug'):
132            self.log.setLevel(DEBUG)
133        tailorlog = getLogger('tailor')
134        formatter = Formatter(self.config.get(
135            self.name, 'log-format',
136            '%(asctime)s %(levelname)8s: %(message)s', raw=True),
137                              self.config.get(
138            self.name, 'log-datefmt', '%Y-%m-%d %H:%M:%S', raw=True))
139        self.loghandler = FileHandler(self.logfile)
140        self.loghandler.setFormatter(formatter)
141        self.loghandler.setLevel(DEBUG)
142        tailorlog.addHandler(self.loghandler)
143
144        self.source = self.__loadRepository('source')
145        self.target = self.__loadRepository('target')
146
147        sfpath = self.config.get(self.name, 'state-file', self.name + '.state')
148        sfpath = join(self.rootdir, self.target.stateFilePath(sfpath))
149        self.state_file = StateFile(sfpath, self.config)
150
151        before = self.config.getTuple(self.name, 'before-commit')
152        try:
153            self.before_commit = [self.config.namespace[f] for f in before]
154        except KeyError as e:
155            raise ConfigurationError('Project "%s" before-commit references '
156                                     'unknown function: %s' %
157                                     (self.name, str(e)))
158
159        after = self.config.getTuple(self.name, 'after-commit')
160        try:
161            self.after_commit = [self.config.namespace[f] for f in after]
162        except KeyError as e:
163            raise ConfigurationError('Project "%s" after-commit references '
164                                     'unknown function: %s' %
165                                     (self.name, str(e)))
166
167        if not self.config.get(self.name, 'verbose', False):
168            # Disable console output
169            rootlog = getLogger()
170            rootlog.disabled = True
171            for h in rootlog.handlers:
172                if isinstance(h, StreamHandler):
173                    h.setLevel(CRITICAL)
174
175    def __del__(self):
176        if self.loghandler is not None:
177            from logging import getLogger
178            getLogger('tailor').removeHandler(self.loghandler)
179
180    def __loadRepository(self, which):
181        """
182        Given a repository named 'somekind:somename', return a Repository
183        (or a subclass of it, if 'SomekindRepository' exists) instance
184        that wraps it.
185        """
186
187        from .repository import Repository
188
189        repname = self.config.get(self.name, which)
190        if repname.endswith(':'):
191            repname += self.name
192        return Repository(repname, self, which)
193
194    def exists(self):
195        """
196        Return True if the project exists, False otherwise.
197        """
198
199        return self.state_file.lastAppliedChangeset() is not None
200
201    def workingDir(self):
202        """
203        Return a DualWorkingDir instance, ready to work.
204        """
205
206        from .dualwd import DualWorkingDir
207
208        if self.dwd is None:
209            self.dwd = DualWorkingDir(self.source, self.target)
210            self.dwd.setStateFile(self.state_file)
211            self.dwd.setLogfile(self.logfile)
212        return self.dwd
213