• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
18from __future__ import print_function
19
20import ConfigParser
21import functools
22import os
23import shlex
24import sys
25
26_path = os.path.realpath(__file__ + '/../..')
27if sys.path[0] != _path:
28    sys.path.insert(0, _path)
29del _path
30
31import rh.hooks
32import rh.shell
33
34
35class Error(Exception):
36    """Base exception class."""
37
38
39class ValidationError(Error):
40    """Config file has unknown sections/keys or other values."""
41
42
43class 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
90class 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