source: tailor/vcpx/tailor.py @ 188

Revision 188, 18.6 KB checked in by lele@…, 8 years ago (diff)

Better error messages

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