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