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