Home | History | Annotate | Download | only in rh
      1 # -*- coding:utf-8 -*-
      2 # Copyright 2016 The Android Open Source Project
      3 #
      4 # Licensed under the Apache License, Version 2.0 (the "License");
      5 # you may not use this file except in compliance with the License.
      6 # You may obtain a copy of the License at
      7 #
      8 #      http://www.apache.org/licenses/LICENSE-2.0
      9 #
     10 # Unless required by applicable law or agreed to in writing, software
     11 # distributed under the License is distributed on an "AS IS" BASIS,
     12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13 # See the License for the specific language governing permissions and
     14 # limitations under the License.
     15 
     16 """Manage various config files."""
     17 
     18 from __future__ import print_function
     19 
     20 import ConfigParser
     21 import functools
     22 import os
     23 import shlex
     24 import sys
     25 
     26 _path = os.path.realpath(__file__ + '/../..')
     27 if sys.path[0] != _path:
     28     sys.path.insert(0, _path)
     29 del _path
     30 
     31 import rh.hooks
     32 import rh.shell
     33 
     34 
     35 class Error(Exception):
     36     """Base exception class."""
     37 
     38 
     39 class ValidationError(Error):
     40     """Config file has unknown sections/keys or other values."""
     41 
     42 
     43 class RawConfigParser(ConfigParser.RawConfigParser):
     44     """Like RawConfigParser but with some default helpers."""
     45 
     46     @staticmethod
     47     def _check_args(name, cnt_min, cnt_max, args):
     48         cnt = len(args)
     49         if cnt not in (0, cnt_max - cnt_min):
     50             raise TypeError('%s() takes %i or %i arguments (got %i)' %
     51                             (name, cnt_min, cnt_max, cnt,))
     52         return cnt
     53 
     54     def options(self, section, *args):
     55         """Return the options in |section| (with default |args|).
     56 
     57         Args:
     58           section: The section to look up.
     59           args: What to return if |section| does not exist.
     60         """
     61         cnt = self._check_args('options', 2, 3, args)
     62         try:
     63             return ConfigParser.RawConfigParser.options(self, section)
     64         except ConfigParser.NoSectionError:
     65             if cnt == 1:
     66                 return args[0]
     67             raise
     68 
     69     def get(self, section, option, *args):
     70         """Return the value for |option| in |section| (with default |args|)."""
     71         cnt = self._check_args('get', 3, 4, args)
     72         try:
     73             return ConfigParser.RawConfigParser.get(self, section, option)
     74         except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
     75             if cnt == 1:
     76                 return args[0]
     77             raise
     78 
     79     def items(self, section, *args):
     80         """Return a list of (key, value) tuples for the options in |section|."""
     81         cnt = self._check_args('items', 2, 3, args)
     82         try:
     83             return ConfigParser.RawConfigParser.items(self, section)
     84         except ConfigParser.NoSectionError:
     85             if cnt == 1:
     86                 return args[0]
     87             raise
     88 
     89 
     90 class PreSubmitConfig(object):
     91     """Config file used for per-project `repo upload` hooks."""
     92 
     93     FILENAME = 'PREUPLOAD.cfg'
     94     GLOBAL_FILENAME = 'GLOBAL-PREUPLOAD.cfg'
     95 
     96     CUSTOM_HOOKS_SECTION = 'Hook Scripts'
     97     BUILTIN_HOOKS_SECTION = 'Builtin Hooks'
     98     BUILTIN_HOOKS_OPTIONS_SECTION = 'Builtin Hooks Options'
     99     TOOL_PATHS_SECTION = 'Tool Paths'
    100     OPTIONS_SECTION = 'Options'
    101 
    102     OPTION_IGNORE_MERGED_COMMITS = 'ignore_merged_commits'
    103     VALID_OPTIONS = (OPTION_IGNORE_MERGED_COMMITS,)
    104 
    105     def __init__(self, paths=('',), global_paths=()):
    106         """Initialize.
    107 
    108         All the config files found will be merged together in order.
    109 
    110         Args:
    111           paths: The directories to look for config files.
    112           global_paths: The directories to look for global config files.
    113         """
    114         config = RawConfigParser()
    115 
    116         def _search(paths, filename):
    117             for path in paths:
    118                 path = os.path.join(path, filename)
    119                 if os.path.exists(path):
    120                     self.paths.append(path)
    121                     try:
    122                         config.read(path)
    123                     except ConfigParser.ParsingError as e:
    124                         raise ValidationError('%s: %s' % (path, e))
    125 
    126         self.paths = []
    127         _search(global_paths, self.GLOBAL_FILENAME)
    128         _search(paths, self.FILENAME)
    129 
    130         self.config = config
    131 
    132         self._validate()
    133 
    134     @property
    135     def custom_hooks(self):
    136         """List of custom hooks to run (their keys/names)."""
    137         return self.config.options(self.CUSTOM_HOOKS_SECTION, [])
    138 
    139     def custom_hook(self, hook):
    140         """The command to execute for |hook|."""
    141         return shlex.split(self.config.get(self.CUSTOM_HOOKS_SECTION, hook, ''))
    142 
    143     @property
    144     def builtin_hooks(self):
    145         """List of all enabled builtin hooks (their keys/names)."""
    146         return [k for k, v in self.config.items(self.BUILTIN_HOOKS_SECTION, ())
    147                 if rh.shell.boolean_shell_value(v, None)]
    148 
    149     def builtin_hook_option(self, hook):
    150         """The options to pass to |hook|."""
    151         return shlex.split(self.config.get(self.BUILTIN_HOOKS_OPTIONS_SECTION,
    152                                            hook, ''))
    153 
    154     @property
    155     def tool_paths(self):
    156         """List of all tool paths."""
    157         return dict(self.config.items(self.TOOL_PATHS_SECTION, ()))
    158 
    159     def callable_hooks(self):
    160         """Yield a name and callback for each hook to be executed."""
    161         for hook in self.custom_hooks:
    162             options = rh.hooks.HookOptions(hook,
    163                                            self.custom_hook(hook),
    164                                            self.tool_paths)
    165             yield (hook, functools.partial(rh.hooks.check_custom,
    166                                            options=options))
    167 
    168         for hook in self.builtin_hooks:
    169             options = rh.hooks.HookOptions(hook,
    170                                            self.builtin_hook_option(hook),
    171                                            self.tool_paths)
    172             yield (hook, functools.partial(rh.hooks.BUILTIN_HOOKS[hook],
    173                                            options=options))
    174 
    175     @property
    176     def ignore_merged_commits(self):
    177         """Whether to skip hooks for merged commits."""
    178         return rh.shell.boolean_shell_value(
    179             self.config.get(self.OPTIONS_SECTION,
    180                             self.OPTION_IGNORE_MERGED_COMMITS, None),
    181             False)
    182 
    183     def _validate(self):
    184         """Run consistency checks on the config settings."""
    185         config = self.config
    186 
    187         # Reject unknown sections.
    188         valid_sections = set((
    189             self.CUSTOM_HOOKS_SECTION,
    190             self.BUILTIN_HOOKS_SECTION,
    191             self.BUILTIN_HOOKS_OPTIONS_SECTION,
    192             self.TOOL_PATHS_SECTION,
    193             self.OPTIONS_SECTION,
    194         ))
    195         bad_sections = set(config.sections()) - valid_sections
    196         if bad_sections:
    197             raise ValidationError('%s: unknown sections: %s' %
    198                                   (self.paths, bad_sections))
    199 
    200         # Reject blank custom hooks.
    201         for hook in self.custom_hooks:
    202             if not config.get(self.CUSTOM_HOOKS_SECTION, hook):
    203                 raise ValidationError('%s: custom hook "%s" cannot be blank' %
    204                                       (self.paths, hook))
    205 
    206         # Reject unknown builtin hooks.
    207         valid_builtin_hooks = set(rh.hooks.BUILTIN_HOOKS.keys())
    208         if config.has_section(self.BUILTIN_HOOKS_SECTION):
    209             hooks = set(config.options(self.BUILTIN_HOOKS_SECTION))
    210             bad_hooks = hooks - valid_builtin_hooks
    211             if bad_hooks:
    212                 raise ValidationError('%s: unknown builtin hooks: %s' %
    213                                       (self.paths, bad_hooks))
    214         elif config.has_section(self.BUILTIN_HOOKS_OPTIONS_SECTION):
    215             raise ValidationError('Builtin hook options specified, but missing '
    216                                   'builtin hook settings')
    217 
    218         if config.has_section(self.BUILTIN_HOOKS_OPTIONS_SECTION):
    219             hooks = set(config.options(self.BUILTIN_HOOKS_OPTIONS_SECTION))
    220             bad_hooks = hooks - valid_builtin_hooks
    221             if bad_hooks:
    222                 raise ValidationError('%s: unknown builtin hook options: %s' %
    223                                       (self.paths, bad_hooks))
    224 
    225         # Verify hooks are valid shell strings.
    226         for hook in self.custom_hooks:
    227             try:
    228                 self.custom_hook(hook)
    229             except ValueError as e:
    230                 raise ValidationError('%s: hook "%s" command line is invalid: '
    231                                       '%s' % (self.paths, hook, e))
    232 
    233         # Verify hook options are valid shell strings.
    234         for hook in self.builtin_hooks:
    235             try:
    236                 self.builtin_hook_option(hook)
    237             except ValueError as e:
    238                 raise ValidationError('%s: hook options "%s" are invalid: %s' %
    239                                       (self.paths, hook, e))
    240 
    241         # Reject unknown tools.
    242         valid_tools = set(rh.hooks.TOOL_PATHS.keys())
    243         if config.has_section(self.TOOL_PATHS_SECTION):
    244             tools = set(config.options(self.TOOL_PATHS_SECTION))
    245             bad_tools = tools - valid_tools
    246             if bad_tools:
    247                 raise ValidationError('%s: unknown tools: %s' %
    248                                       (self.paths, bad_tools))
    249 
    250         # Reject unknown options.
    251         valid_options = set(self.VALID_OPTIONS)
    252         if config.has_section(self.OPTIONS_SECTION):
    253             options = set(config.options(self.OPTIONS_SECTION))
    254             bad_options = options - valid_options
    255             if bad_options:
    256                 raise ValidationError('%s: unknown options: %s' %
    257                                       (self.paths, bad_options))
    258