source: tailor/vcpx/tailor.py @ 228

Revision 228, 19.1 KB checked in by lele@…, 8 years ago (diff)

Fix the option help spacing

Line 
1#! /usr/bin/python
2# -*- mode: python; coding: utf-8 -*-
3# :Progetto: vcpx -- Frontend capabilities
4# :Creato:   dom 04 lug 2004 00:40:54 CEST
5# :Autore:   Lele Gaifax <lele@nautilus.homeip.net>
6#
7
8"""
9Implement the basic capabilities of the frontend.
10
11This implementation stores the relevant project information, needed to
12keep the whole thing going on, such as the last synced revision, in a
13unversioned file named `tailor.info` at the root.
14"""
15
16__docformat__ = 'reStructuredText'
17
18from optparse import OptionParser, OptionGroup, make_option
19from dualwd import DualWorkingDir
20from source import InvocationError
21
22STATUS_FILENAME = 'tailor.info'
23LOG_FILENAME = 'tailor.log'
24
25def relpathto(source, dest):
26    """
27    Compute the relative path needed to point `source` from `dest`.
28
29    Warning: `dest` is assumed to be a directory.
30    """
31   
32    from os.path import abspath, split, commonprefix
33   
34    source = abspath(source)
35    dest = abspath(dest)
36
37    if source.startswith(dest):
38        return source[len(dest)+1:]
39   
40    prefix = commonprefix([source, dest])
41
42    source = source[len(prefix):]
43    dest = dest[len(prefix):]
44
45    return '../' * len(dest.split('/')) + source
46
47
48class TailorConfig(object):
49    """
50    Configuration of a set of tailorized projects.
51
52    The configuration is stored in a persistent dictionary keyed on the
53    relative path of each project. The information about a single project
54    is another dictionary.
55    """
56   
57    def __init__(self, options):
58        from os.path import abspath, split
59       
60        self.options = options
61        self.configfile = abspath(options.configfile)
62        self.basedir = split(self.configfile)[0]
63       
64    def __call__(self, args):
65        from os.path import join, exists, split
66        from source import ChangesetApplicationFailure
67       
68        self.__load()
69
70        if len(args) == 0:
71            fromconfig = True
72            if self.options.bootstrap:
73                f = lambda x: not exists(x)
74            else:
75                f = exists
76               
77            args = [p for p in [join(self.basedir, r)
78                                for r in self.config.keys()] if f(p)]
79            args.sort()
80        else:
81            fromconfig = False
82           
83        try:
84            for root in args:
85                if self.options.bootstrap:               
86                    if not (fromconfig or self.options.repository):
87                        raise InvocationError('Need a repository to bootstrap '
88                                              '%r' % root, '--bootstrap')
89                else:
90                    if not self.config.has_key(relpathto(root, self.basedir)):
91                        raise UnknownProjectError("Project %r does not exist" %
92                                                  root)
93                   
94                tailored = TailorizedProject(root, self.options.verbose, self)
95
96                if self.options.bootstrap:
97                    if fromconfig:                       
98                        info = self.loadProject(root=root)
99                        self.options.source_kind = info['source_kind']
100                        self.options.target_kind = info['target_kind']
101                        self.options.repository = info['upstream_repos']
102                        self.options.module = info['module']
103                        self.options.subdir = info.get('subdir',
104                                                       split(info['module'])[1])
105                        self.options.revision = info['upstream_revision']
106                       
107                    tailored.bootstrap(self.options.source_kind,
108                                       self.options.target_kind,
109                                       self.options.repository,
110                                       self.options.module,
111                                       self.options.revision,
112                                       self.options.subdir)
113                elif self.options.migrate:
114                    tailored.migrateConfiguration()
115                elif self.options.update:
116                    try:
117                        tailored.update(self.options.single_commit,
118                                        self.options.concatenate_logs)
119                    except ChangesetApplicationFailure, e:
120                        print "Skipping '%s' because of errors:" % root, e
121        finally:
122            self.__save()
123       
124    def __save(self):
125        from pprint import pprint
126
127        configfile = open(self.configfile, 'w')
128        pprint(self.config, configfile)
129        configfile.close()
130
131    def __load(self):
132        from os.path import exists
133
134        if exists(self.options.configfile):
135            configfile = open(self.configfile)
136            self.config = eval(configfile.read())
137            configfile.close()
138        else:
139            self.config = {}
140           
141    def loadProject(self, project=None, root=None):
142        from os.path import split
143       
144        relpath = relpathto(project and project.root or root, self.basedir)
145       
146        info = self.config.get(relpath)
147        if info and project:
148            project.source_kind = info['source_kind']
149            project.target_kind = info['target_kind']
150            project.module = info['module']
151            project.subdir = info.get('subdir', split(project.module)[1])
152            project.upstream_repos = info['upstream_repos']
153            project.upstream_revision = info['upstream_revision']
154
155        return info
156       
157    def saveProject(self, project):
158        relpath = relpathto(project.root, self.basedir)
159       
160        self.config[relpath] = { 
161            'source_kind': project.source_kind,
162            'target_kind': project.target_kind,
163            'module': project.module,
164            'subdir': project.subdir,
165            'upstream_repos': project.upstream_repos,
166            'upstream_revision': project.upstream_revision,
167            }
168
169   
170class TailorizedProject(object):
171    """
172    A TailorizedProject has two main capabilities: it may be bootstrapped
173    from an upstream repository or brought in sync with current upstream
174    revision.
175    """
176   
177    def __init__(self, root, verbose=False, config=None):
178        import logging
179        from os import makedirs
180        from os.path import join, exists, split
181
182        self.root = root
183        if not exists(root):
184            makedirs(root)
185
186        self.verbose = verbose
187        self.logger = logging.getLogger('tailor.%s' % split(root)[1])
188        hdlr = logging.FileHandler(join(root, LOG_FILENAME))
189        formatter = logging.Formatter('%(asctime)s [%(levelname)s] %(message)s')
190        hdlr.setFormatter(formatter)
191        self.logger.addHandler(hdlr) 
192        self.logger.setLevel(logging.INFO)
193
194        self.source_kind = self.target_kind = None
195
196        self.config = config
197
198    def migrateConfiguration(self):
199        self.__loadOldStatus()
200        self.__saveStatus()
201       
202    def __saveOldStatus(self):
203        from os.path import join
204
205        statusfilename = join(self.root, STATUS_FILENAME)
206        f = open(statusfilename, 'w')
207        print >>f, self.source_kind
208        print >>f, self.target_kind       
209        print >>f, self.module       
210        print >>f, self.upstream_repos
211        print >>f, self.upstream_revision
212        print >>f, self.subdir
213        f.close()
214
215    def __saveStatus(self):
216        """
217        Save relevant project information in a persistent way.
218        """
219
220        if self.config:
221            self.config.saveProject(self)
222        else:
223            self.__saveOldStatus()
224
225    def __loadOldStatus(self):
226        from os.path import join, split
227
228        statusfilename = join(self.root, STATUS_FILENAME)
229        f = open(statusfilename)
230        self.source_kind = f.readline()[:-1]
231        self.target_kind = f.readline()[:-1]
232        self.module = f.readline()[:-1]
233        self.upstream_repos = f.readline()[:-1]
234        self.upstream_revision = f.readline()[:-1]
235        subdir = f.readline()
236        if subdir:
237            self.subdir = subdir[:-1]
238        else:
239            self.subdir = split(self.module)[1]           
240        f.close()
241
242    def __loadStatus(self):
243        """
244        Load relevant project information.
245        """
246
247        if self.config:
248            self.config.loadProject(self)
249        else:
250            self.__loadOldStatus()
251           
252    def bootstrap(self, source_kind, target_kind,
253                  repository, module, revision, subdir):
254        """
255        Bootstrap a new tailorized module.
256
257        Extract a copy of the `repository` at given `revision` in the `root`
258        directory and initialize a target repository with its content.
259
260        The actual information on the project are stored in a text file.
261        """
262
263        from os.path import split
264       
265        if not subdir:
266            subdir = split(module or repository)[1] or ''
267           
268        self.logger.info("Bootstrapping '%s'" % (self.root,))
269
270        dwd = DualWorkingDir(source_kind, target_kind)
271        self.logger.info("getting %s revision '%s' of '%s' from '%s'" % (
272            source_kind, revision, module, repository))
273
274        try:
275            actual = dwd.checkoutUpstreamRevision(self.root, repository,
276                                                  module, revision,
277                                                  subdir=subdir,
278                                                  logger=self.logger)
279        except:
280            self.logger.exception('Checkout failed!')
281            raise
282       
283        # the above machinery checked out a copy under of the wc
284        # in the directory named as the last component of the module's name
285
286        if not module:
287            module = split(repository)[1]
288
289        try:
290            dwd.initializeNewWorkingDir(self.root, repository, module, subdir, actual)
291        except:
292            self.logger.exception('Working copy initialization failed!')
293            raise
294       
295        self.source_kind = source_kind
296        self.target_kind = target_kind
297        self.upstream_repos = repository
298        self.module = module
299        self.subdir = subdir
300        self.upstream_revision = actual
301
302        self.__saveStatus()
303
304        self.logger.info("Bootstrap completed")
305
306    def applyable(self, root, changeset):
307        """
308        Print the changeset being applied.
309        """
310
311        if self.verbose:
312            print "Changeset %s:" % changeset.revision
313            print changeset.log
314
315        return True
316   
317    def applied(self, root, changeset):
318        """
319        Save current status.
320        """
321
322        self.upstream_revision = changeset.revision
323        self.__saveStatus()
324        if self.verbose:
325            print
326
327    def update(self, single_commit, concatenate_logs):
328        """
329        Update an existing tailorized project.
330
331        Fetch the upstream changesets and apply them to the working copy.
332        Use the information stored in the `tailor.info` file to ask just
333        the new changeset since last bootstrap/synchronization.
334        """
335       
336        from os.path import join
337
338        try:
339            self.__loadStatus()
340
341            proj = join(self.root, self.subdir)
342            self.logger.info("Updating '%s' from revision '%s'" % (
343                self.module, self.upstream_revision))
344
345            if self.verbose:
346                print "\nUpdating '%s' from revision '%s'" % (self.module,
347                                                              self.upstream_revision)
348
349            dwd = DualWorkingDir(self.source_kind, self.target_kind)
350            changesets = dwd.getUpstreamChangesets(proj, self.upstream_revision)
351        except KeyboardInterrupt:
352            print "Leaving '%s' unchanged" % proj
353            self.logger.info("Leaving '%s' unchanged, stopped by user" % proj)
354            return
355        except:
356            self.logger.exception("Unable to get changes for '%s'" % proj)
357            raise
358       
359        nchanges = len(changesets)
360        if nchanges:
361            if self.verbose:
362                print "Applying %d upstream changesets" % nchanges
363
364            try:
365                last, conflicts = dwd.applyUpstreamChangesets(
366                    proj, self.module, changesets, applyable=self.applyable,
367                    applied=self.applied, logger=self.logger,
368                    delayed_commit=single_commit)
369            except:
370                self.logger.exception('Upstream change application failed')
371                raise
372           
373            if last:
374                if single_commit:
375                    dwd.commitDelayedChangesets(proj, concatenate_logs)
376
377                self.logger.info("Update completed, now at revision '%s'" % (
378                    self.upstream_revision,))
379        else:
380            self.logger.info("Update completed with no upstream changes")
381
382
383GENERAL_OPTIONS = [
384    make_option("-D", "--debug", dest="debug",
385                action="store_true", default=False,
386                help="Print each executed command."),
387    make_option("-v", "--verbose", dest="verbose",
388                action="store_true", default=False,
389                help="Be verbose, echoing the changelog of each applied "
390                     "changeset to stdout."),
391    make_option("--configfile", metavar="CONFNAME",
392                help="Centralized storage of projects info.  With this "
393                     "option and no other arguments tailor will update "
394                     "every project found in the config file."),
395    make_option("--migrate-config", dest="migrate",
396                action="store_true", default=False,
397                help="Migrate old configuration to new centralized storage."),
398]   
399
400UPDATE_OPTIONS = [
401    make_option("--update", action="store_true", default=True,
402                help="Update the given repositories, fetching upstream "
403                     "changesets, applying and re-registering each one. "
404                     "This is the default behaviour."),
405    make_option("-S", "--single-commit", action="store_true", default=False,
406                help="Do a single, final commit on the target VC, effectively "
407                     "grouping together all upstream changeset into a single "
408                     "one, from the target VC point of view."),
409    make_option("-C", "--concatenate-logs", action="store_true", default=False,
410                help="With --single-commit, concatenate each changeset "
411                     "message log to the final changelog, instead of just "
412                     "the name of the patch."),
413]
414
415BOOTSTRAP_OPTIONS = [
416    make_option("-b", "--bootstrap", action="store_true", default=False,
417                help="Bootstrap mode, that is the initial copy of the "
418                     "upstream tree, given as an URI (see -R) and maybe "
419                     "a revision (-r).  This overrides --update."),
420    make_option("-s", "--source-kind", dest="source_kind", metavar="VC-KIND",
421                help="Select the backend for the upstream source "
422                     "version control VC-KIND. Default is 'cvs'.",
423                default="cvs"),
424    make_option("-t", "--target-kind", dest="target_kind", metavar="VC-KIND",
425                help="Select VC-KIND as backend for the shadow repository, "
426                     "with 'darcs' as default.",
427                default="darcs"),
428    make_option("-R", "--repository", dest="repository", metavar="REPOS",
429                help="Specify the upstream repository, from where bootstrap "
430                     "will checkout the module.  REPOS syntax depends on "
431                     "the source version control kind."),
432    make_option("-m", "--module", dest="module", metavar="MODULE",
433                help="Specify the module to checkout at bootstrap time. "
434                     "It's mandatory only when dealing with CVS, and used but "
435                     "not required with SVN, but since it's used in "
436                     "the description of the target repository, you "
437                     "are encouraged to give it a value with darcs too."),
438    make_option("-r", "--revision", dest="revision", metavar="REV",
439                help="Specify the revision bootstrap should checkout.  REV "
440                     "must be a valid 'name' for a revision in the upstream "
441                     "version control kind.  For CVS it may be a tag/branch. "
442                     "'HEAD', the default, means the latest version in all "
443                     "backends.",
444                default="HEAD"),
445    make_option("--subdir", metavar="DIR",
446                help="Force the subdirectory where the checkout will happen, "
447                     "by default it's the tail part of the module name."),
448]
449
450class ExistingProjectError(Exception):
451    "Project seems already tailored"
452
453class UnknownProjectError(Exception):
454    "Project does not exist"
455
456class ProjectNotTailored(Exception):
457    "Not a tailored project"
458   
459def main():
460    """
461    Script entry point.
462
463    Parse the command line options and arguments, and for each
464    specified working copy directory (the current working directory by
465    default) execute the tailorization steps.
466    """
467   
468    from os import getcwd, chdir
469    from os.path import abspath, exists, join
470    from shwrap import SystemCommand
471   
472    parser = OptionParser(usage='%prog [options] [project ...]',
473                          option_list=GENERAL_OPTIONS)
474   
475    bsoptions = OptionGroup(parser, "Bootstrap options")
476    bsoptions.add_options(BOOTSTRAP_OPTIONS)
477
478    upoptions = OptionGroup(parser, "Update options")
479    upoptions.add_options(UPDATE_OPTIONS)
480   
481    parser.add_option_group(bsoptions)
482    parser.add_option_group(upoptions)
483   
484    options, args = parser.parse_args()
485   
486    SystemCommand.VERBOSE = options.debug
487
488    if options.configfile:
489        config = TailorConfig(options)
490
491        config(map(abspath, args))
492    else:
493        # Good (?) old way
494       
495        config = None
496       
497        base = getcwd()
498       
499        if len(args) == 0:
500            args.append(base)
501
502        while args:
503            chdir(base)
504
505            proj = args.pop(0)
506            root = abspath(proj)
507
508            if options.bootstrap:
509                if exists(join(root, STATUS_FILENAME)):
510                    raise ExistingProjectError(
511                        "Project %r cannot be bootstrapped twice" % proj)
512
513                if not options.repository:
514                    raise InvocationError('Need a repository to bootstrap %r' %
515                                          proj)
516            else:
517                if not exists(proj):
518                    raise UnknownProjectError("Project %r does not exist" %
519                                              proj)
520
521                if not exists(join(root, STATUS_FILENAME)):
522                    raise UnknownProjectError(
523                        "%r is not a tailorized project" % proj)
524
525            tailored = TailorizedProject(root, options.verbose, config)
526
527            if options.bootstrap:
528                tailored.bootstrap(options.source_kind, options.target_kind,
529                                   options.repository,
530                                   options.module,
531                                   options.revision,
532                                   options.subdir)
533            elif options.update:
534                tailored.update(options.single_commit,
535                                options.concatenate_logs)
536
Note: See TracBrowser for help on using the repository browser.