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