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