1# -*- mode: python; coding: utf-8 -*- 2# :Progetto: vcpx -- Frontend capabilities 3# :Creato: dom 04 lug 2004 00:40:54 CEST 4# :Autore: Lele Gaifax <lele@nautilus.homeip.net> 5# :Licenza: GNU General Public License 6# 7 8""" 9Implement the frontend functionalities. 10""" 11from __future__ import absolute_import 12 13from builtins import str 14__docformat__ = 'reStructuredText' 15 16__version__ = '0.9.36' 17 18from logging import getLogger 19from optparse import OptionParser, OptionGroup, Option 20from vcpx import TailorBug, TailorException 21from vcpx.config import Config, ConfigurationError 22from vcpx.project import Project 23from vcpx.source import GetUpstreamChangesetsFailure 24 25 26class EmptySourceRepository(TailorException): 27 "The source repository appears to be empty" 28 29 30class Tailorizer(Project): 31 """ 32 A Tailorizer has two main capabilities: its able to bootstrap a 33 new Project, or brought it in sync with its current upstream 34 revision. 35 """ 36 37 def _applyable(self, changeset): 38 """ 39 Print the changeset being applied. 40 """ 41 42 if self.verbose: 43 self.log.info('Changeset "%s"', changeset.revision) 44 if changeset.log: 45 self.log.info("Log message: %s", changeset.log) 46 self.log.debug("Going to apply changeset:\n%s", str(changeset)) 47 return True 48 49 def _applied(self, changeset): 50 """ 51 Separate changesets with an empty line. 52 """ 53 54 if self.verbose: 55 self.log.info('-*'*30) 56 57 def bootstrap(self): 58 """ 59 Bootstrap a new tailorized module. 60 61 First of all prepare the target system working directory such 62 that it can host the upstream source tree. This is backend 63 specific. 64 65 Then extract a copy of the upstream repository and import its 66 content into the target repository. 67 """ 68 69 self.log.info('Bootstrapping "%s" in "%s"', self.name, self.rootdir) 70 71 dwd = self.workingDir() 72 try: 73 dwd.prepareWorkingDirectory(self.source) 74 except: 75 self.log.critical('Cannot prepare working directory!', exc_info=True) 76 raise 77 78 revision = self.config.get(self.name, 'start-revision', 'INITIAL') 79 try: 80 actual = dwd.checkoutUpstreamRevision(revision) 81 except: 82 self.log.critical("Checkout of %s failed!", self.name) 83 raise 84 85 if actual is None: 86 raise EmptySourceRepository("Cannot complete the bootstrap") 87 88 try: 89 dwd.importFirstRevision(self.source, actual, 'INITIAL'==revision) 90 except: 91 self.log.critical('Could not import checked out tree in "%s"!', 92 self.rootdir, exc_info=True) 93 raise 94 95 self.log.info("Bootstrap completed") 96 97 def update(self): 98 """ 99 Update an existing tailorized project. 100 """ 101 102 self.log.info('Updating "%s" in "%s"', self.name, self.rootdir) 103 104 dwd = self.workingDir() 105 try: 106 pendings = dwd.getPendingChangesets() 107 except KeyboardInterrupt: 108 self.log.warning('Leaving "%s" unchanged, stopped by user', 109 self.name) 110 raise 111 except: 112 self.log.fatal('Unable to get changes for "%s"', self.name) 113 raise 114 115 if pendings.pending(): 116 self.log.info("Applying pending upstream changesets") 117 118 try: 119 last, conflicts = dwd.applyPendingChangesets( 120 applyable=self._applyable, applied=self._applied) 121 except KeyboardInterrupt: 122 self.log.warning('Leaving "%s" incomplete, stopped by user', 123 self.name) 124 raise 125 except: 126 self.log.fatal('Upstream change application failed') 127 raise 128 129 if last: 130 self.log.info('Update completed, now at revision "%s"', 131 last.revision) 132 else: 133 self.log.info("Update completed with no upstream changes") 134 135 def __call__(self): 136 from .shwrap import ExternalCommand 137 from .target import SynchronizableTargetWorkingDir 138 from .changes import Changeset 139 140 def pconfig(option, raw=False): 141 return self.config.get(self.name, option, raw=raw) 142 143 ExternalCommand.DEBUG = pconfig('debug') 144 145 pname_format = pconfig('patch-name-format', raw=True) 146 if pname_format is not None: 147 SynchronizableTargetWorkingDir.PATCH_NAME_FORMAT = pname_format.strip() 148 SynchronizableTargetWorkingDir.REMOVE_FIRST_LOG_LINE = pconfig('remove-first-log-line') 149 Changeset.REFILL_MESSAGE = pconfig('refill-changelogs') 150 151 try: 152 if not self.exists(): 153 self.bootstrap() 154 if pconfig('start-revision') == 'HEAD': 155 return 156 self.update() 157 except (UnicodeDecodeError, UnicodeEncodeError) as exc: 158 raise ConfigurationError('%s: it seems that the encoding ' 159 'used by either the source ("%s") or the ' 160 'target ("%s") repository ' 161 'cannot properly represent at least one ' 162 'of the characters in the upstream ' 163 'changelog. You need to use a wider ' 164 'character set, using "encoding" option, ' 165 'or even "encoding-errors-policy".' 166 % (exc, self.source.encoding, 167 self.target.encoding)) 168 except TailorBug as e: 169 self.log.fatal("Unexpected internal error, please report", exc_info=e) 170 except EmptySourceRepository as e: 171 self.log.warning("Source repository seems empty: %s", e) 172 except TailorException: 173 raise 174 except Exception as e: 175 self.log.fatal("Something unexpected!", exc_info=e) 176 177class RecogOption(Option): 178 """ 179 Make it possible to recognize an option explicitly given on the 180 command line from those simply coming out for their default value. 181 """ 182 183 def process (self, opt, value, values, parser): 184 setattr(values, '__seen_' + self.dest, True) 185 return Option.process(self, opt, value, values, parser) 186 187 188GENERAL_OPTIONS = [ 189 RecogOption("-D", "--debug", dest="debug", 190 action="store_true", default=False, 191 help="Print each executed command. This also keeps " 192 "temporary files with the upstream logs, that are " 193 "otherwise removed after use."), 194 RecogOption("-v", "--verbose", dest="verbose", 195 action="store_true", default=False, 196 help="Be verbose, echoing the changelog of each applied " 197 "changeset to stdout."), 198 RecogOption("-c", "--configfile", metavar="CONFNAME", 199 help="Centralized storage of projects info. With this " 200 "option and no other arguments tailor will update " 201 "every project found in the config file."), 202 RecogOption("--encoding", metavar="CHARSET", default=None, 203 help="Force the output encoding to given CHARSET, rather " 204 "then using the user's default settings specified " 205 "in the environment."), 206] 207 208UPDATE_OPTIONS = [ 209 RecogOption("-F", "--patch-name-format", metavar="FORMAT", 210 help="Specify the prototype that will be used " 211 "to compute the patch name. The prototype may contain " 212 "%(keyword)s such as 'author', 'date', " 213 "'revision', 'firstlogline', 'remaininglog'. It " 214 "defaults to 'Tailorized \"%(revision)s\"'; " 215 "setting it to the empty string means that tailor will " 216 "simply use the original changelog."), 217 RecogOption("-1", "--remove-first-log-line", action="store_true", 218 default=False, 219 help="Remove the first line of the upstream changelog. This " 220 "is intended to pair with --patch-name-format, " 221 "when using its 'firstlogline' variable to build the " 222 "name of the patch."), 223 RecogOption("-N", "--refill-changelogs", action="store_true", 224 default=False, 225 help="Refill every changelog, useful when upstream logs " 226 "are not uniform."), 227] 228 229BOOTSTRAP_OPTIONS = [ 230 RecogOption("-s", "--source-kind", dest="source_kind", metavar="VC-KIND", 231 help="Select the backend for the upstream source " 232 "version control VC-KIND. Default is 'cvs'.", 233 default="cvs"), 234 RecogOption("-t", "--target-kind", dest="target_kind", metavar="VC-KIND", 235 help="Select VC-KIND as backend for the shadow repository, " 236 "with 'darcs' as default.", 237 default="darcs"), 238 RecogOption("-R", "--repository", "--source-repository", 239 dest="source_repository", metavar="REPOS", 240 help="Specify the upstream repository, from where bootstrap " 241 "will checkout the module. REPOS syntax depends on " 242 "the source version control kind."), 243 RecogOption("-m", "--module", "--source-module", dest="source_module", 244 metavar="MODULE", 245 help="Specify the module to checkout at bootstrap time. " 246 "This has different meanings under the various upstream " 247 "systems: with CVS it indicates the module, while under " 248 "SVN it's the prefix of the tree you want and must begin " 249 "with a slash. Since it's used in the description of the " 250 "target repository, you may want to give it a value with " 251 "darcs too, even though it is otherwise ignored."), 252 RecogOption("-r", "--revision", "--start-revision", dest="start_revision", 253 metavar="REV", 254 help="Specify the revision bootstrap should checkout. REV " 255 "must be a valid 'name' for a revision in the upstream " 256 "version control kind. For CVS it may be either a branch " 257 "name, a timestamp or both separated by a space, and " 258 "timestamp may be 'INITIAL' to denote the beginning of " 259 "time for the given branch. Under Darcs, INITIAL is a " 260 "shortcut for the name of the first patch in the upstream " 261 "repository, otherwise it is interpreted as the name of " 262 "a tag. Under Subversion, 'INITIAL' is the first patch " 263 "that touches given repos/module, otherwise it must be " 264 "an integer revision number. " 265 "'HEAD' means the latest version in all backends.", 266 default="INITIAL"), 267 RecogOption("-T", "--target-repository", 268 dest="target_repository", metavar="REPOS", default=None, 269 help="Specify the target repository, the one that will " 270 "receive the patches coming from the source one."), 271 RecogOption("-M", "--target-module", dest="target_module", 272 metavar="MODULE", 273 help="Specify the module on the target repository that will " 274 "actually contain the upstream source tree."), 275 RecogOption("--subdir", metavar="DIR", 276 help="Force the subdirectory where the checkout will happen, " 277 "by default it's the tail part of the module name."), 278] 279 280VC_SPECIFIC_OPTIONS = [ 281 RecogOption("--use-propset", action="store_true", default=False, 282 dest="use_propset", 283 help="Use 'svn propset' to set the real date and author of " 284 "each commit, instead of appending these information to " 285 "the changelog. This requires some tweaks on the SVN " 286 "repository to enable revision propchanges."), 287 RecogOption("--ignore-arch-ids", action="store_true", default=False, 288 dest="ignore_ids", 289 help="Ignore .arch-ids directories when using a tla source."), 290] 291 292 293class ExistingProjectError(TailorException): 294 "Project seems already tailored" 295 296 297class ProjectNotTailored(TailorException): 298 "Not a tailored project" 299 300 301def main(): 302 """ 303 Script entry point. 304 305 Parse the command line options and arguments, and for each 306 specified working copy directory (the current working directory by 307 default) execute the tailorization steps. 308 """ 309 310 import sys 311 from os import getcwd 312 313 usage = "usage: \n\ 314 1. %prog [options] [project ...]\n\ 315 2. %prog test [--help] [...]" 316 parser = OptionParser(usage=usage, 317 version=__version__, 318 option_list=GENERAL_OPTIONS) 319 320 bsoptions = OptionGroup(parser, "Bootstrap options") 321 bsoptions.add_options(BOOTSTRAP_OPTIONS) 322 323 upoptions = OptionGroup(parser, "Update options") 324 upoptions.add_options(UPDATE_OPTIONS) 325 326 vcoptions = OptionGroup(parser, "VC specific options") 327 vcoptions.add_options(VC_SPECIFIC_OPTIONS) 328 329 parser.add_option_group(bsoptions) 330 parser.add_option_group(upoptions) 331 parser.add_option_group(vcoptions) 332 333 options, args = parser.parse_args() 334 335 defaults = {} 336 for k,v in list(options.__dict__.items()): 337 if k.startswith('__'): 338 continue 339 if k != 'configfile' and hasattr(options, '__seen_' + k): 340 defaults[k.replace('_', '-')] = str(v) 341 342 if options.configfile or (len(sys.argv)==2 and len(args)==1): 343 # Either we have a --configfile, or there are no options 344 # and a single argument (to support shebang style scripts) 345 346 if not options.configfile: 347 options.configfile = sys.argv[1] 348 args = None 349 350 config = Config(open(options.configfile), defaults) 351 352 if not args: 353 args = config.projects() 354 355 for projname in args: 356 tailorizer = Tailorizer(projname, config) 357 try: 358 tailorizer() 359 except GetUpstreamChangesetsFailure: 360 # Do not stop on this kind of error, but keep going 361 pass 362 else: 363 for omit in ['source-kind', 'target-kind', 364 'source-module', 'target-module', 365 'source-repository', 'target-repository', 366 'start-revision', 'subdir']: 367 if omit in defaults: 368 del defaults[omit] 369 370 config = Config(None, defaults) 371 372 config.add_section('project') 373 source = options.source_kind + ':source' 374 config.set('project', 'source', source) 375 target = options.target_kind + ':target' 376 config.set('project', 'target', target) 377 config.set('project', 'root-directory', getcwd()) 378 config.set('project', 'subdir', options.subdir or '.') 379 config.set('project', 'state-file', 'tailor.state') 380 config.set('project', 'start-revision', options.start_revision) 381 382 config.add_section(source) 383 if options.source_repository: 384 config.set(source, 'repository', options.source_repository) 385 else: 386 logger = getLogger('tailor') 387 logger.warning("By any chance you forgot either the --source-repository or the --configfile option...") 388 389 if options.source_module: 390 config.set(source, 'module', options.source_module) 391 392 config.add_section(target) 393 if options.target_repository: 394 config.set(target, 'repository', options.target_repository) 395 if options.target_module: 396 config.set(target, 'module', options.target_module) 397 398 if options.verbose: 399 sys.stderr.write("You should put the following configuration " 400 "in some file, adjust it as needed\n" 401 "and use --configfile option with that " 402 "file as argument:\n") 403 config.write(sys.stdout) 404 405 if options.debug: 406 tailorizer = Tailorizer('project', config) 407 tailorizer() 408 elif not options.verbose: 409 sys.stderr.write("Operation not performed, try --verbose\n") 410