Changeset 485 in tailor
- Timestamp:
- 08/05/05 00:27:29 (8 years ago)
- Hash name:
- 20050804222729-97f81-0e9a149c0c69768a5abb729e7f2aa54e5e06eeea
- File:
-
- 1 edited
-
vcpx/tailor.py (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
vcpx/tailor.py
r469 r485 19 19 20 20 from optparse import OptionParser, OptionGroup, make_option 21 from dualwd import DualWorkingDir21 from config import Config 22 22 from source import InvocationError 23 23 from session import interactive 24 from svn import SvnWorkingDir 25 26 STATUS_FILENAME = 'tailor.info' 27 LOG_FILENAME = 'tailor.log' 28 29 def relpathto(source, dest): 24 25 class Tailorizer(object): 30 26 """ 31 Compute the relative path needed to point ``source`` from ``dest``. 32 33 Warning: ``dest`` is assumed to be a directory. 34 """ 35 36 from os.path import abspath, split, commonprefix 37 38 source = abspath(source) 39 dest = abspath(dest) 40 41 if source.startswith(dest): 42 return source[len(dest)+1:] 43 44 prefix = commonprefix([source, dest]) 45 46 source = source[len(prefix):] 47 dest = dest[len(prefix):] 48 49 return '../' * len(dest.split('/')) + source 50 51 52 class TailorConfig(object): 53 """ 54 Configuration of a set of tailorized projects. 55 56 The configuration is stored in a persistent dictionary keyed on the 57 relative path of each project. The information about a single project 58 is another dictionary. 59 """ 60 61 def __init__(self, options): 62 from os.path import abspath, split 63 64 self.options = options 65 self.configfile = abspath(options.configfile) 66 self.basedir = split(self.configfile)[0] 67 68 def __call__(self, args): 69 from os.path import join, exists, split 70 from source import ChangesetApplicationFailure 71 72 self.__load() 73 74 if len(args) == 0: 75 fromconfig = True 76 if self.options.bootstrap: 77 f = lambda x: not exists(x) 78 else: 79 f = exists 80 81 args = [p for p in [join(self.basedir, r) 82 for r in self.config.keys()] if f(p)] 83 args.sort() 84 else: 85 fromconfig = False 86 87 try: 88 for root in args: 89 if self.options.bootstrap: 90 if not (fromconfig or self.options.source_repository): 91 raise InvocationError('Need a repository to bootstrap ' 92 '%r' % root, '--bootstrap') 93 else: 94 if not self.config.has_key(relpathto(root, self.basedir)): 95 raise UnknownProjectError("Project %r does not exist" % 96 root) 97 98 tailored = TailorizedProject(root, self.options.verbose, self) 99 100 if self.options.bootstrap: 101 if fromconfig: 102 info = self.loadProject(root=root) 103 self.options.source_kind = info['source_kind'] 104 self.options.target_kind = info['target_kind'] 105 self.options.source_repository = info['upstream_repos'] 106 self.options.source_module = info['module'] 107 self.options.subdir = info.get('subdir', 108 split(info['module'])[1]) 109 self.options.revision = info['upstream_revision'] 110 111 tailored.bootstrap(self.options.source_kind, 112 self.options.target_kind, 113 self.options.source_repository, 114 self.options.source_module, 115 self.options.revision, 116 self.options.target_repository, 117 self.options.target_module, 118 self.options.subdir) 119 elif self.options.migrate: 120 tailored.migrateConfiguration() 121 elif self.options.update: 122 try: 123 tailored.update(self.options.single_commit, 124 self.options.concatenate_logs) 125 except ChangesetApplicationFailure, e: 126 print "Skipping '%s' because of errors:" % root, e 127 finally: 128 self.__save() 129 130 def __save(self): 131 from pprint import pprint 132 133 configfile = open(self.configfile, 'w') 134 pprint(self.config, configfile) 135 configfile.close() 136 137 def __load(self): 138 from os.path import exists 139 140 if exists(self.options.configfile): 141 configfile = open(self.configfile) 142 self.config = eval(configfile.read()) 143 configfile.close() 144 else: 145 self.config = {} 146 147 def loadProject(self, project=None, root=None): 148 from os.path import split 149 150 relpath = relpathto(project and project.root or root, self.basedir) 151 152 info = self.config.get(relpath) 153 if info and project: 154 project.source_kind = info['source_kind'] 155 project.target_kind = info['target_kind'] 156 project.upstream_module = info['module'] 157 project.subdir = info.get('subdir', 158 split(project.upstream_module)[1]) 159 project.upstream_repos = info['upstream_repos'] 160 project.upstream_revision = info['upstream_revision'] 161 162 return info 163 164 def saveProject(self, project): 165 relpath = relpathto(project.root, self.basedir) 166 167 self.config[relpath] = { 168 'source_kind': project.source_kind, 169 'target_kind': project.target_kind, 170 'module': project.upstream_module, 171 'subdir': project.subdir, 172 'upstream_repos': project.upstream_repos, 173 'upstream_revision': project.upstream_revision, 174 } 175 176 177 class TailorizedProject(object): 178 """ 179 A TailorizedProject has two main capabilities: it may be bootstrapped 180 from an upstream repository or brought in sync with current upstream 27 A Tailorizer has two main capabilities: its able to bootstrap a 28 new Project, or brought it in sync with its current upstream 181 29 revision. 182 30 """ 183 31 184 def __init__(self, root, verbose=False, config=None): 185 import logging 186 from os import makedirs 187 from os.path import join, exists, split 188 189 self.root = root 190 if not exists(root): 191 makedirs(root) 192 193 self.verbose = verbose 194 self.logger = logging.getLogger('tailor.%s' % split(root)[1]) 195 hdlr = logging.FileHandler(join(root, LOG_FILENAME)) 196 formatter = logging.Formatter('%(asctime)s [%(levelname)s] %(message)s') 197 hdlr.setFormatter(formatter) 198 self.logger.addHandler(hdlr) 199 self.logger.setLevel(logging.INFO) 200 201 self.source_kind = self.target_kind = None 202 203 self.config = config 204 205 def migrateConfiguration(self): 206 self.__loadOldStatus() 207 self.__saveStatus() 208 209 def __saveOldStatus(self): 210 from os.path import join 211 212 statusfilename = join(self.root, STATUS_FILENAME) 213 f = open(statusfilename, 'w') 214 print >>f, self.source_kind 215 print >>f, self.target_kind 216 print >>f, self.upstream_module 217 print >>f, self.upstream_repos 218 print >>f, self.upstream_revision 219 print >>f, self.subdir 220 f.close() 221 222 def __saveStatus(self): 223 """ 224 Save relevant project information in a persistent way. 225 """ 226 227 if self.config: 228 self.config.saveProject(self) 229 else: 230 self.__saveOldStatus() 231 232 def __loadOldStatus(self): 233 from os.path import join, split 234 235 statusfilename = join(self.root, STATUS_FILENAME) 236 f = open(statusfilename) 237 self.source_kind = f.readline()[:-1] 238 self.target_kind = f.readline()[:-1] 239 self.upstream_module = f.readline()[:-1] 240 self.upstream_repos = f.readline()[:-1] 241 self.upstream_revision = f.readline()[:-1] 242 subdir = f.readline() 243 if subdir: 244 self.subdir = subdir[:-1] 245 else: 246 self.subdir = split(self.upstream_module)[1] 247 f.close() 248 249 def __loadStatus(self): 250 """ 251 Load relevant project information. 252 """ 253 254 if self.config: 255 self.config.loadProject(self) 256 else: 257 self.__loadOldStatus() 258 259 # Fix old configs 260 261 if self.source_kind == 'svn' and not '/' in self.upstream_module: 262 self.logger.warning('OLD config values for SVN') 263 print "The project at '%s' contains old values for" % self.root 264 print "the upstream repository (%s)" % self.upstream_repos 265 print "and module (%s)." % self.upstream_module 266 print "Please correct them, specifying the exact URL of the" 267 print "root of the SVN repository and then the prefix path up" 268 print "to the point you want, that must start with a slash." 269 print "This usually means splitting the repository URL above in" 270 print "two parts. For example, that could be" 271 272 crepo = self.upstream_repos 273 example_split = crepo.rfind('/', 6, crepo.rfind('/')) 274 if example_split > 0: 275 example_repo = crepo[:example_split] 276 example_module = crepo[example_split:] 277 else: 278 example_repo = 'http://svn.plone.org/collective' 279 example_module = '/ATContentTypes/trunk' 280 281 print " Repository=%s" % example_repo 282 print " Module=%s" % example_module 283 print "but your situation may vary, that's just an example!" 284 print 285 try: 286 self.repository = raw_input('Repository: ') 287 self.upstream_module = raw_input('Module/prefix: ') 288 except KeyboardInterrupt: 289 self.logger.warning("Leaving old config values, stopped by user") 290 raise 291 292 def bootstrap(self, source_kind, target_kind, 293 source_repository, source_module, revision, 294 target_repository, target_module, subdir): 32 def __init__(self, project): 33 self.project = project 34 35 def bootstrap(self): 295 36 """ 296 37 Bootstrap a new tailorized module. 297 38 298 First of all prepare the target system working directory such that 299 it can host the upstream source tree. This is backend specific. 300 301 Extract a copy of the ``repository`` at given ``revision`` in the 302 ``root`` directory and initialize a target repository with its content. 303 304 The actual information on the project are stored in a text file. 39 First of all prepare the target system working directory such 40 that it can host the upstream source tree. This is backend 41 specific. 42 43 Then extract a copy of the upstream repository and import its 44 content into the target repository. 305 45 """ 306 46 307 from os.path import split 308 309 if source_kind == 'svn': 310 if not (source_module and source_module.startswith('/')): 311 raise InvocationError('With SVN the module argument is ' 312 'mandatory and must start with a "/"') 313 314 if source_repository.endswith('/'): 315 source_repository = source_repository[:-1] 316 317 if source_module and source_module.endswith('/'): 318 source_module = source_module[:-1] 319 320 if not subdir: 321 subdir = split(source_module or source_repository)[1] or '' 322 323 self.logger.info("Bootstrapping '%s'" % (self.root,)) 324 325 dwd = DualWorkingDir(source_kind, target_kind) 326 self.logger.info("getting %s revision '%s' of '%s' from '%s'" % ( 327 source_kind, revision, source_module, source_repository)) 47 self.project.log_info("Bootstrapping '%s'" % self.project.root) 328 48 329 49 try: 330 dwd.prepareWorkingDirectory(self.root, 331 target_repository, target_module) 50 self.project.prepareWorkingDirectory() 332 51 except: 333 self. logger.exception('Cannot prepare working directory!')52 self.project.log_error('Cannot prepare working directory!', True) 334 53 raise 335 54 336 55 try: 337 actual = dwd.checkoutUpstreamRevision(self.root, source_repository, 338 source_module, revision, 339 subdir=subdir, 340 logger=self.logger) 56 self.project.checkoutUpstreamRevision() 341 57 except: 342 self.logger.exception('Checkout failed!') 58 self.project.log_error("Checkout of '%s' failed!" % 59 self.project.name, True) 343 60 raise 344 61 345 # the above machinery checked out a copy under of the wc 346 # in the directory named as the last component of the module's name 347 348 if not source_module: 349 source_module = split(source_repository)[1] 350 351 try: 352 dwd.initializeNewWorkingDir(self.root, source_repository, 353 source_module, subdir, 354 actual, revision=='INITIAL') 355 except: 356 self.logger.exception('Working copy initialization failed!') 357 raise 358 359 self.source_kind = source_kind 360 self.target_kind = target_kind 361 self.upstream_repos = source_repository 362 self.upstream_module = source_module 363 self.subdir = subdir 364 self.upstream_revision = actual.revision 365 366 self.__saveStatus() 367 368 self.logger.info("Bootstrap completed") 369 370 def applyable(self, root, changeset): 371 """ 372 Print the changeset being applied. 373 """ 374 375 if self.verbose: 376 print "Changeset %s:" % changeset.revision 377 try: 378 print changeset.log 379 except UnicodeEncodeError: 380 print ">>> Non-printable changelog <<<" 381 382 return True 383 384 def applied(self, root, changeset): 385 """ 386 Save current status. 387 """ 388 389 self.upstream_revision = changeset.revision 390 self.__saveStatus() 391 if self.verbose: 392 print 393 394 def update(self, single_commit, concatenate_logs): 62 self.project.log_info("Bootstrap completed") 63 64 def update(self): 395 65 """ 396 66 Update an existing tailorized project. 397 398 Fetch the upstream changesets and apply them to the working copy.399 Use the information stored in the ``tailor.info`` file to ask just400 the new changeset since last bootstrap/synchronization.401 67 """ 402 68 403 from os.path import join 404 405 self.__loadStatus() 406 proj = join(self.root, self.subdir) 407 408 self.logger.info("Updating '%s' from revision '%s'" % ( 409 self.upstream_module, self.upstream_revision)) 410 411 if self.verbose: 412 print "\nUpdating '%s' from revision '%s'" % ( 413 self.upstream_module, self.upstream_revision) 69 self.project.log_info("Updating '%s'" % self.project.name) 414 70 415 71 try: 416 dwd = DualWorkingDir(self.source_kind, self.target_kind) 417 changesets = dwd.getPendingChangesets(proj, 418 self.upstream_repos, 419 self.upstream_module) 420 except KeyboardInterrupt: 421 print "Leaving '%s' unchanged" % proj 422 self.logger.info("Leaving '%s' unchanged, stopped by user" % proj) 423 return 72 self.project.applyPendingChangesets() 424 73 except: 425 self.logger.exception("Unable to get changes for '%s'" % proj) 74 self.project.log_error("Cannot update '%s'!" % self.project.name, 75 True) 426 76 raise 427 77 428 nchanges = len(changesets) 429 if nchanges: 430 if self.verbose: 431 print "Applying %d upstream changesets" % nchanges 432 433 try: 434 last, conflicts = dwd.applyPendingChangesets( 435 proj, self.upstream_module, 436 applyable=self.applyable, 437 applied=self.applied, logger=self.logger, 438 delayed_commit=single_commit) 439 except: 440 self.logger.exception('Upstream change application failed') 441 raise 442 443 if last: 444 if single_commit: 445 dwd.commitDelayedChangesets(proj, concatenate_logs) 446 447 self.logger.info("Update completed, now at revision '%s'" % ( 448 self.upstream_revision,)) 78 self.project.log_info("Update completed") 79 80 def __call__(self, options): 81 if options.bootstrap: 82 self.bootstrap() 449 83 else: 450 self.logger.info("Update completed with no upstream changes") 451 84 self.update() 452 85 453 86 GENERAL_OPTIONS = [ … … 635 268 Changeset.REFILL_MESSAGE = not options.dont_refill_changelogs 636 269 637 SvnWorkingDir.USE_PROPSET = options.use_svn_propset638 639 270 if options.interactive: 640 271 interactive(options, args) 641 272 elif options.configfile: 642 config = TailorConfig(options) 643 644 config(map(abspath, args)) 273 defaults = {} 274 for k,v in options.__dict__.items(): 275 defaults[k.replace('_', '-')] = v 276 277 config = Config(open(options.configfile), defaults) 278 279 if not args: 280 args = config.projects() 281 282 for projname in args: 283 project = config[projname] 284 tailorizer = Tailorizer(project) 285 tailorizer(options) 645 286 else: 646 287 # Good (?) old way
Note: See TracChangeset
for help on using the changeset viewer.
