source: tailor/vcpx/tailor.py @ 131

Revision 131, 16.4 KB checked in by lele@…, 9 years ago (diff)

Fix cut'n'paste error

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