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