source: tailor/vcpx/tailor.py @ 183

Revision 183, 18.2 KB checked in by lele@…, 8 years ago (diff)

Use InvocationError? in place of OptionError?

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        f.close()
213
214    def __saveStatus(self):
215        """
216        Save relevant project information in a persistent way.
217        """
218
219        if self.config:
220            self.config.saveProject(self)
221        else:
222            self.__saveOldStatus()
223
224    def __loadOldStatus(self):
225        from os.path import join, split
226
227        statusfilename = join(self.root, STATUS_FILENAME)
228        f = open(statusfilename)
229        (srck, dstk,
230         module, upstream_repos, upstream_revision) = f.readlines()
231        self.source_kind = srck[:-1]
232        self.target_kind = dstk[:-1]
233        self.module = module[:-1]
234        self.subdir = split(self.module)[1]
235        self.upstream_repos = upstream_repos[:-1]
236        self.upstream_revision = upstream_revision[:-1]
237        f.close()
238
239    def __loadStatus(self):
240        """
241        Load relevant project information.
242        """
243
244        if self.config:
245            self.config.loadProject(self)
246        else:
247            self.__loadOldStatus()
248           
249    def bootstrap(self, source_kind, target_kind,
250                  repository, module, revision, subdir):
251        """
252        Bootstrap a new tailorized module.
253
254        Extract a copy of the `repository` at given `revision` in the `root`
255        directory and initialize a target repository with its content.
256
257        The actual information on the project are stored in a text file.
258        """
259
260        self.logger.info("Bootstrapping '%s'" % (self.root,))
261
262        dwd = DualWorkingDir(source_kind, target_kind)
263        self.logger.info("getting %s revision '%s' of '%s' from '%s'" % (
264            source_kind, revision, module, repository))
265        actual = dwd.checkoutUpstreamRevision(self.root, repository,
266                                              module, revision,
267                                              subdir=subdir,
268                                              logger=self.logger)
269
270        # the above machinery checked out a copy under of the wc
271        # in the directory named as the last component of the module's name
272
273        if not subdir:
274            subdir = module
275           
276        dwd.initializeNewWorkingDir(self.root, repository, subdir, actual)
277
278        self.source_kind = source_kind
279        self.target_kind = target_kind
280        self.upstream_repos = repository
281        self.module = module
282        self.subdir = subdir
283        self.upstream_revision = actual
284
285        self.__saveStatus()
286
287        self.logger.info("Bootstrap completed")
288
289    def applyable(self, root, changeset):
290        """
291        Print the changeset being applied.
292        """
293
294        if self.verbose:
295            print "Changeset %s:" % changeset.revision
296            print changeset.log
297
298        return True
299   
300    def applied(self, root, changeset):
301        """
302        Save current status.
303        """
304
305        self.upstream_revision = changeset.revision
306        self.__saveStatus()
307        if self.verbose:
308            print
309
310    def update(self, single_commit, concatenate_logs):
311        """
312        Update an existing tailorized project.
313
314        Fetch the upstream changesets and apply them to the working copy.
315        Use the information stored in the `tailor.info` file to ask just
316        the new changeset since last bootstrap/synchronization.
317        """
318       
319        from os.path import join
320       
321        self.__loadStatus()
322
323        proj = join(self.root, self.subdir)
324        self.logger.info("Updating '%s' from revision '%s'" % (
325            self.module, self.upstream_revision))
326
327        if self.verbose:
328            print "\nUpdating '%s' from revision '%s'" % (self.module,
329                                                          self.upstream_revision)
330       
331        dwd = DualWorkingDir(self.source_kind, self.target_kind)
332        changesets = dwd.getUpstreamChangesets(proj, self.upstream_revision)
333
334        nchanges = len(changesets)
335        if nchanges:
336            if self.verbose:
337                print "Applying %d upstream changesets" % nchanges
338               
339            l,c = dwd.applyUpstreamChangesets(proj, changesets,
340                                              applyable=self.applyable,
341                                              applied=self.applied,
342                                              logger=self.logger,
343                                              delayed_commit=single_commit)
344            if l:
345                if single_commit:
346                    dwd.commitDelayedChangesets(proj, concatenate_logs)
347
348                self.logger.info("Update completed, now at revision '%s'" % (
349                    self.upstream_revision,))
350        else:
351            self.logger.info("Update completed with no upstream changes")
352
353
354GENERAL_OPTIONS = [
355    make_option("-D", "--debug", dest="debug",
356                action="store_true", default=False,
357                help="Print each executed command."),
358    make_option("-v", "--verbose", dest="verbose",
359                action="store_true", default=False,
360                help="Be verbose, echoing the changelog of each applied "
361                     "changeset to stdout."),
362    make_option("--configfile", metavar="CONFNAME",
363                help="Centralized storage of projects info.  With this "
364                     "option and no other arguments tailor will update "
365                     "every project found in the config file."),
366    make_option("--migrate-config", dest="migrate",
367                action="store_true", default=False,
368                help="Migrate old configuration to new centralized storage."),
369]   
370
371UPDATE_OPTIONS = [
372    make_option("--update", action="store_true", default=True,
373                help="Update the given repositories, fetching upstream "
374                     "changesets, applying and re-registering each one. "
375                     "This is the default behaviour."),
376    make_option("-S", "--single-commit", action="store_true", default=False,
377                help="Do a single, final commit on the target VC, effectively "
378                     "grouping together all upstream changeset into a single "
379                     "one, from the target VC point of view."),
380    make_option("-C", "--concatenate-logs", action="store_true", default=False,
381                help="With --single-commit, concatenate each changeset "
382                     "message log to the final changelog, instead of just "
383                     "the name of the patch."),
384]
385
386BOOTSTRAP_OPTIONS = [
387    make_option("-b", "--bootstrap", action="store_true", default=False,
388                help="Bootstrap mode, that is the initial copy of the "
389                     "upstream tree, given as an URI (see -R) and maybe "
390                     "a revision (-r).  This overrides --update."),
391    make_option("-s", "--source-kind", dest="source_kind", metavar="VC-KIND",
392                help="Select the backend for the upstream source "
393                     "version control VC-KIND. Default is 'cvs'.",
394                default="cvs"),
395    make_option("-t", "--target-kind", dest="target_kind", metavar="VC-KIND",
396                help="Select VC-KIND as backend for the shadow repository, "
397                     "with 'darcs' as default.",
398                default="darcs"),
399    make_option("-R", "--repository", dest="repository", metavar="REPOS",
400                help="Specify the upstream repository, from where bootstrap "
401                     "will checkout the module.  REPOS syntax depends on "
402                     "the source version control kind."),
403    make_option("-m", "--module", dest="module", metavar="MODULE",
404                help="Specify the module to checkout at bootstrap time."),
405    make_option("-r", "--revision", dest="revision", metavar="REV",
406                help="Specify the revision bootstrap should checkout.  REV "
407                     "must be a valid 'name' for a revision in the upstream "
408                     "version control kind.  For CVS it may be a tag/branch. "
409                     "'HEAD', the default, means the latest version in all "
410                     "backends.",
411                default="HEAD"),
412    make_option("--subdir", metavar="DIR",
413                help="Force the subdirectory where the checkout will happen, "
414                     "by default it's the tail part of the module name."),
415]
416
417class ExistingProjectError(Exception):
418    """
419    Raised when, in bootstrap mode, the directory for the project is already
420    there.
421    """
422
423class UnknownProjectError(Exception):
424    """
425    Raised when, in normal mode, the directory for the project does not
426    exist.
427    """
428
429class ProjectNotTailored(Exception):
430    """
431    Raised when trying to do something on a project that has not been
432    tailored.
433    """
434   
435def main():
436    """
437    Script entry point.
438
439    Parse the command line options and arguments, and for each
440    specified working copy directory (the current working directory by
441    default) execute the tailorization steps.
442    """
443   
444    from os import getcwd, chdir
445    from os.path import abspath, exists, join
446    from shwrap import SystemCommand
447   
448    parser = OptionParser(usage='%prog [options] [project ...]',
449                          option_list=GENERAL_OPTIONS)
450   
451    bsoptions = OptionGroup(parser, "Bootstrap options")
452    bsoptions.add_options(BOOTSTRAP_OPTIONS)
453
454    upoptions = OptionGroup(parser, "Update options")
455    upoptions.add_options(UPDATE_OPTIONS)
456   
457    parser.add_option_group(bsoptions)
458    parser.add_option_group(upoptions)
459   
460    options, args = parser.parse_args()
461   
462    SystemCommand.VERBOSE = options.debug
463
464    if options.configfile:
465        config = TailorConfig(options)
466
467        config(map(abspath, args))
468    else:
469        # Good (?) old way
470       
471        config = None
472       
473        base = getcwd()
474       
475        if len(args) == 0:
476            args.append(base)
477
478        while args:
479            chdir(base)
480
481            proj = args.pop(0)
482            root = abspath(proj)
483
484            if options.bootstrap:
485                if exists(join(root, STATUS_FILENAME)):
486                    raise ExistingProjectError(
487                        "Project %r cannot be bootstrapped twice" % proj)
488
489                if not options.repository:
490                    raise InvocationError('Need a repository to bootstrap %r' %
491                                          proj)
492            else:
493                if not exists(proj):
494                    raise UnknownProjectError("Project %r does not exist" %
495                                              proj)
496
497                if not exists(join(root, STATUS_FILENAME)):
498                    raise UnknownProjectError(
499                        "%r is not a tailorized project" % proj)
500
501            tailored = TailorizedProject(root, options.verbose, config)
502
503            if options.bootstrap:
504                tailored.bootstrap(options.source_kind, options.target_kind,
505                                   options.repository,
506                                   options.module,
507                                   options.revision,
508                                   options.subdir)
509            elif options.update:
510                tailored.update(options.single_commit,
511                                options.concatenate_logs)
512
Note: See TracBrowser for help on using the repository browser.