| 1 | # -*- mode: python; coding: utf-8 -*- |
|---|
| 2 | # :Progetto: vcpx -- Configuration bits |
|---|
| 3 | # :Creato: sab 30 lug 2005 20:51:28 CEST |
|---|
| 4 | # :Autore: Lele Gaifax <lele@nautilus.homeip.net> |
|---|
| 5 | # :Licenza: GNU General Public License |
|---|
| 6 | # |
|---|
| 7 | |
|---|
| 8 | """ |
|---|
| 9 | Handle the configuration details. |
|---|
| 10 | """ |
|---|
| 11 | |
|---|
| 12 | __docformat__ = 'reStructuredText' |
|---|
| 13 | |
|---|
| 14 | from ConfigParser import SafeConfigParser |
|---|
| 15 | |
|---|
| 16 | class ConfigurationError(Exception): |
|---|
| 17 | """ |
|---|
| 18 | Raised on invalid configuration. |
|---|
| 19 | """ |
|---|
| 20 | |
|---|
| 21 | class StateFile(object): |
|---|
| 22 | """ |
|---|
| 23 | State file that stores current revision and pending changesets. |
|---|
| 24 | """ |
|---|
| 25 | def __init__(self, fname, config): |
|---|
| 26 | self.filename = fname |
|---|
| 27 | |
|---|
| 28 | def __str__(self): |
|---|
| 29 | return self.filename |
|---|
| 30 | |
|---|
| 31 | def load(self): |
|---|
| 32 | """ |
|---|
| 33 | Read the source revision and pending changesets from the state file. |
|---|
| 34 | """ |
|---|
| 35 | |
|---|
| 36 | from cPickle import load |
|---|
| 37 | |
|---|
| 38 | try: |
|---|
| 39 | sf = open(self.filename) |
|---|
| 40 | revision, changesets = load(sf) |
|---|
| 41 | sf.close() |
|---|
| 42 | except IOError: |
|---|
| 43 | revision = None |
|---|
| 44 | changesets = None |
|---|
| 45 | |
|---|
| 46 | return revision, changesets |
|---|
| 47 | |
|---|
| 48 | def write(self, revision, changesets): |
|---|
| 49 | """ |
|---|
| 50 | Write current source revision and pending changesets in the state file. |
|---|
| 51 | """ |
|---|
| 52 | |
|---|
| 53 | from cPickle import dump |
|---|
| 54 | |
|---|
| 55 | sf = open(self.filename, 'w') |
|---|
| 56 | dump((revision, changesets), sf) |
|---|
| 57 | sf.close() |
|---|
| 58 | |
|---|
| 59 | |
|---|
| 60 | class Project(object): |
|---|
| 61 | """ |
|---|
| 62 | This class collects the information related to a single project, such |
|---|
| 63 | as its source and target repositories and state file. |
|---|
| 64 | """ |
|---|
| 65 | |
|---|
| 66 | def __init__(self, name, config): |
|---|
| 67 | self.config = config |
|---|
| 68 | self.name = name |
|---|
| 69 | self._load() |
|---|
| 70 | |
|---|
| 71 | def __str__(self): |
|---|
| 72 | return "Project %s at %s:\n\t" % (self.name, self.root) + \ |
|---|
| 73 | "\n\t".join(['%s = %s' % (v, getattr(self, v)) |
|---|
| 74 | for v in ('source', 'target', 'state_file')]) |
|---|
| 75 | |
|---|
| 76 | def _load(self): |
|---|
| 77 | """ |
|---|
| 78 | Load relevant information from the configuration. |
|---|
| 79 | """ |
|---|
| 80 | |
|---|
| 81 | from os import getcwd |
|---|
| 82 | |
|---|
| 83 | self.root = self.config.get(self.name, 'root', getcwd()) |
|---|
| 84 | self.source = self.__loadRepository('source') |
|---|
| 85 | self.target = self.__loadRepository('target') |
|---|
| 86 | self.state_file = StateFile(self.config.get(self.name, 'state-file'), |
|---|
| 87 | self.config) |
|---|
| 88 | before = self.config.getTuple(self.name, 'before-commit') |
|---|
| 89 | try: |
|---|
| 90 | self.before_commit = [self.config.namespace[f] for f in before] |
|---|
| 91 | except KeyError, e: |
|---|
| 92 | raise ConfigurationError('Project %s before-commit references ' |
|---|
| 93 | 'unknown function: '%self.name + str(e)) |
|---|
| 94 | after = self.config.getTuple(self.name, 'after-commit') |
|---|
| 95 | try: |
|---|
| 96 | self.after_commit = [self.config.namespace[f] for f in after] |
|---|
| 97 | except KeyError, e: |
|---|
| 98 | raise ConfigurationError('Project %s after-commit references ' |
|---|
| 99 | 'unknown function: '%self.name + str(e)) |
|---|
| 100 | |
|---|
| 101 | def __loadRepository(self, which): |
|---|
| 102 | """ |
|---|
| 103 | Given a repository named 'somekind:somename', return a Repository |
|---|
| 104 | (or a subclass of it, if 'SomekindRepository' exists) instance |
|---|
| 105 | that wraps it. |
|---|
| 106 | """ |
|---|
| 107 | |
|---|
| 108 | repname = self.config.get(self.name, which) |
|---|
| 109 | kind = repname[:repname.index(':')] |
|---|
| 110 | klassname = kind.capitalize() + 'Repository' |
|---|
| 111 | try: |
|---|
| 112 | klass = globals()[klassname] |
|---|
| 113 | except KeyError: |
|---|
| 114 | klass = Repository |
|---|
| 115 | return klass(repname, kind, self.config, which) |
|---|
| 116 | |
|---|
| 117 | def workingDir(self): |
|---|
| 118 | """ |
|---|
| 119 | Return a DualWorkingDir instance, ready to work. |
|---|
| 120 | """ |
|---|
| 121 | |
|---|
| 122 | dwd = DualWorkingDir(self.source, self.target) |
|---|
| 123 | dwd.setStateFile(self.state_file) |
|---|
| 124 | |
|---|
| 125 | |
|---|
| 126 | class Repository(object): |
|---|
| 127 | """ |
|---|
| 128 | Collector for the configuration of a single repository. |
|---|
| 129 | """ |
|---|
| 130 | |
|---|
| 131 | def __init__(self, name, kind, config, which): |
|---|
| 132 | self.name = name |
|---|
| 133 | self.kind = kind |
|---|
| 134 | self._load(config, which) |
|---|
| 135 | |
|---|
| 136 | def __str__(self): |
|---|
| 137 | return "%s repository at %s" % (self.kind, self.repository) |
|---|
| 138 | |
|---|
| 139 | def _load(self, config, which): |
|---|
| 140 | """ |
|---|
| 141 | Load the configuration for this repository. |
|---|
| 142 | |
|---|
| 143 | The two main and mandatory attributes, ``repository`` and ``module`` |
|---|
| 144 | can be specified either on the specific slot in the config file, or |
|---|
| 145 | as ``source-repository`` (or ``target-repository``) in its [DEFAULT] |
|---|
| 146 | section. |
|---|
| 147 | """ |
|---|
| 148 | |
|---|
| 149 | self.repository = config.get(self.name, 'repository') or \ |
|---|
| 150 | config.get(self.name, '%s-repository' % which) |
|---|
| 151 | self.module = config.get(self.name, 'module') or \ |
|---|
| 152 | config.get(self.name, '%s-module' % which) |
|---|
| 153 | |
|---|
| 154 | def workingDir(self): |
|---|
| 155 | """ |
|---|
| 156 | Return an instance of the specific WorkingDir for this kind of |
|---|
| 157 | repository. |
|---|
| 158 | """ |
|---|
| 159 | |
|---|
| 160 | wdname = self.kind.capitalize() + 'WorkingDir' |
|---|
| 161 | modname = 'vcpx.' + self.kind |
|---|
| 162 | try: |
|---|
| 163 | wdmod = __import__(modname, globals(), locals(), [wdname]) |
|---|
| 164 | workingdir = getattr(wdmod, wdname) |
|---|
| 165 | except (AttributeError, ImportError): |
|---|
| 166 | raise InvocationError("Unhandled source VCS kind: " + self.kind) |
|---|
| 167 | return workingdir() |
|---|
| 168 | |
|---|
| 169 | class BzrRepository(Repository): |
|---|
| 170 | METADIR = '.bzr' |
|---|
| 171 | |
|---|
| 172 | class CdvRepository(Repository): |
|---|
| 173 | METADIR = '.cdv' |
|---|
| 174 | |
|---|
| 175 | class CvsRepository(Repository): |
|---|
| 176 | METADIR = 'CVS' |
|---|
| 177 | |
|---|
| 178 | class CvspsRepository(CvsRepository): |
|---|
| 179 | pass |
|---|
| 180 | |
|---|
| 181 | class DarcsRepository(Repository): |
|---|
| 182 | METADIR = '_darcs' |
|---|
| 183 | |
|---|
| 184 | class HgRepository(Repository): |
|---|
| 185 | METADIR = '.hg' |
|---|
| 186 | |
|---|
| 187 | class MonotoneRepository(Repository): |
|---|
| 188 | METADIR = 'MT' |
|---|
| 189 | |
|---|
| 190 | def _load(self, config, which): |
|---|
| 191 | Repository._load(self, config, which) |
|---|
| 192 | self.passphrase = config.get(self.name, 'passphrase') |
|---|
| 193 | |
|---|
| 194 | class SvnRepository(Repository): |
|---|
| 195 | METADIR = '.svn' |
|---|
| 196 | |
|---|
| 197 | def _load(self, config, which): |
|---|
| 198 | Repository._load(self, config, which) |
|---|
| 199 | self.use_propset = config.get(self.name, 'use-propset', False) |
|---|
| 200 | |
|---|
| 201 | def workingDir(self): |
|---|
| 202 | wd = Repository.workingDir(self) |
|---|
| 203 | wd.USE_PROPSET = self.use_propset |
|---|
| 204 | return wd |
|---|
| 205 | |
|---|
| 206 | |
|---|
| 207 | class Config(SafeConfigParser): |
|---|
| 208 | """ |
|---|
| 209 | Syntactic sugar around standard ConfigParser, for easier access to |
|---|
| 210 | the configuration. To access any single project use the configuration |
|---|
| 211 | as a dictionary. |
|---|
| 212 | |
|---|
| 213 | The file may be a full fledged Python script, starting |
|---|
| 214 | with the usual "#!..." notation: in this case, it gets evaluated and |
|---|
| 215 | its documentation becomes the actual configuration, while the functions |
|---|
| 216 | it defines may be referenced by the 'before-commit' and 'after-commit' |
|---|
| 217 | slots. |
|---|
| 218 | """ |
|---|
| 219 | |
|---|
| 220 | def __init__(self, fp, defaults): |
|---|
| 221 | from cStringIO import StringIO |
|---|
| 222 | |
|---|
| 223 | SafeConfigParser.__init__(self, defaults) |
|---|
| 224 | self.namespace = {} |
|---|
| 225 | if fp.read(2) == '#!': |
|---|
| 226 | fp.seek(0) |
|---|
| 227 | exec fp.read() in globals(), self.namespace |
|---|
| 228 | config = StringIO(self.namespace['__doc__']) |
|---|
| 229 | self.readfp(config) |
|---|
| 230 | else: |
|---|
| 231 | fp.seek(0) |
|---|
| 232 | self.readfp(fp) |
|---|
| 233 | |
|---|
| 234 | def projects(self): |
|---|
| 235 | """ |
|---|
| 236 | Return either the default projects or all the projects in the |
|---|
| 237 | in the configuration. |
|---|
| 238 | """ |
|---|
| 239 | |
|---|
| 240 | defaultp = self.getTuple('DEFAULT', 'projects') |
|---|
| 241 | return defaultp or [s for s in self.sections() if not ':' in s] |
|---|
| 242 | |
|---|
| 243 | def get(self, section, option, default=None): |
|---|
| 244 | """ |
|---|
| 245 | Return the requested option value if present, otherwise the default. |
|---|
| 246 | """ |
|---|
| 247 | if self.has_option(section, option): |
|---|
| 248 | return SafeConfigParser.get(self, section, option) |
|---|
| 249 | else: |
|---|
| 250 | return default |
|---|
| 251 | |
|---|
| 252 | def getTuple(self, section, option, default=None): |
|---|
| 253 | """ |
|---|
| 254 | Parse the requested option as a tuple, if its value starts with |
|---|
| 255 | an open bracket, otherwise consider the value a single item |
|---|
| 256 | tuple. |
|---|
| 257 | """ |
|---|
| 258 | |
|---|
| 259 | value = self.get(section, option, default) |
|---|
| 260 | if value: |
|---|
| 261 | if value.startswith('('): |
|---|
| 262 | items = value.strip()[1:-1] |
|---|
| 263 | else: |
|---|
| 264 | items = value |
|---|
| 265 | return [i.strip() for i in items.split(',')] |
|---|
| 266 | else: |
|---|
| 267 | return [] |
|---|
| 268 | |
|---|
| 269 | def __getitem__(self, project): |
|---|
| 270 | return Project(project, self) |
|---|