# -*- coding:utf-8 -*-
# Copyright 2016 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Manage various config files."""
from __future__ import print_function
import ConfigParser
import functools
import os
import shlex
import sys
_path = os.path.realpath(__file__ + '/../..')
if sys.path[0] != _path:
sys.path.insert(0, _path)
del _path
# pylint: disable=wrong-import-position
import rh.hooks
import rh.shell
class Error(Exception):
"""Base exception class."""
class ValidationError(Error):
"""Config file has unknown sections/keys or other values."""
class RawConfigParser(ConfigParser.RawConfigParser):
"""Like RawConfigParser but with some default helpers."""
@staticmethod
def _check_args(name, cnt_min, cnt_max, args):
cnt = len(args)
if cnt not in (0, cnt_max - cnt_min):
raise TypeError('%s() takes %i or %i arguments (got %i)' %
(name, cnt_min, cnt_max, cnt,))
return cnt
def options(self, section, *args):
"""Return the options in |section| (with default |args|).
Args:
section: The section to look up.
args: What to return if |section| does not exist.
"""
cnt = self._check_args('options', 2, 3, args)
try:
return ConfigParser.RawConfigParser.options(self, section)
except ConfigParser.NoSectionError:
if cnt == 1:
return args[0]
raise
def get(self, section, option, *args):
"""Return the value for |option| in |section| (with default |args|)."""
cnt = self._check_args('get', 3, 4, args)
try:
return ConfigParser.RawConfigParser.get(self, section, option)
except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
if cnt == 1:
return args[0]
raise
def items(self, section, *args):
"""Return a list of (key, value) tuples for the options in |section|."""
cnt = self._check_args('items', 2, 3, args)
try:
return ConfigParser.RawConfigParser.items(self, section)
except ConfigParser.NoSectionError:
if cnt == 1:
return args[0]
raise
class PreSubmitConfig(object):
"""Config file used for per-project `repo upload` hooks."""
FILENAME = 'PREUPLOAD.cfg'
GLOBAL_FILENAME = 'GLOBAL-PREUPLOAD.cfg'
CUSTOM_HOOKS_SECTION = 'Hook Scripts'
BUILTIN_HOOKS_SECTION = 'Builtin Hooks'
BUILTIN_HOOKS_OPTIONS_SECTION = 'Builtin Hooks Options'
TOOL_PATHS_SECTION = 'Tool Paths'
OPTIONS_SECTION = 'Options'
OPTION_IGNORE_MERGED_COMMITS = 'ignore_merged_commits'
VALID_OPTIONS = (OPTION_IGNORE_MERGED_COMMITS,)
def __init__(self, paths=('',), global_paths=()):
"""Initialize.
All the config files found will be merged together in order.
Args:
paths: The directories to look for config files.
global_paths: The directories to look for global config files.
"""
config = RawConfigParser()
def _search(paths, filename):
for path in paths:
path = os.path.join(path, filename)
if os.path.exists(path):
self.paths.append(path)
try:
config.read(path)
except ConfigParser.ParsingError as e:
raise ValidationError('%s: %s' % (path, e))
self.paths = []
_search(global_paths, self.GLOBAL_FILENAME)
_search(paths, self.FILENAME)
self.config = config
self._validate()
@property
def custom_hooks(self):
"""List of custom hooks to run (their keys/names)."""
return self.config.options(self.CUSTOM_HOOKS_SECTION, [])
def custom_hook(self, hook):
"""The command to execute for |hook|."""
return shlex.split(self.config.get(self.CUSTOM_HOOKS_SECTION, hook, ''))
@property
def builtin_hooks(self):
"""List of all enabled builtin hooks (their keys/names)."""
return [k for k, v in self.config.items(self.BUILTIN_HOOKS_SECTION, ())
if rh.shell.boolean_shell_value(v, None)]
def builtin_hook_option(self, hook):
"""The options to pass to |hook|."""
return shlex.split(self.config.get(self.BUILTIN_HOOKS_OPTIONS_SECTION,
hook, ''))
@property
def tool_paths(self):
"""List of all tool paths."""
return dict(self.config.items(self.TOOL_PATHS_SECTION, ()))
def callable_hooks(self):
"""Yield a name and callback for each hook to be executed."""
for hook in self.custom_hooks:
options = rh.hooks.HookOptions(hook,
self.custom_hook(hook),
self.tool_paths)
yield (hook, functools.partial(rh.hooks.check_custom,
options=options))
for hook in self.builtin_hooks:
options = rh.hooks.HookOptions(hook,
self.builtin_hook_option(hook),
self.tool_paths)
yield (hook, functools.partial(rh.hooks.BUILTIN_HOOKS[hook],
options=options))
@property
def ignore_merged_commits(self):
"""Whether to skip hooks for merged commits."""
return rh.shell.boolean_shell_value(
self.config.get(self.OPTIONS_SECTION,
self.OPTION_IGNORE_MERGED_COMMITS, None),
False)
def _validate(self):
"""Run consistency checks on the config settings."""
config = self.config
# Reject unknown sections.
valid_sections = set((
self.CUSTOM_HOOKS_SECTION,
self.BUILTIN_HOOKS_SECTION,
self.BUILTIN_HOOKS_OPTIONS_SECTION,
self.TOOL_PATHS_SECTION,
self.OPTIONS_SECTION,
))
bad_sections = set(config.sections()) - valid_sections
if bad_sections:
raise ValidationError('%s: unknown sections: %s' %
(self.paths, bad_sections))
# Reject blank custom hooks.
for hook in self.custom_hooks:
if not config.get(self.CUSTOM_HOOKS_SECTION, hook):
raise ValidationError('%s: custom hook "%s" cannot be blank' %
(self.paths, hook))
# Reject unknown builtin hooks.
valid_builtin_hooks = set(rh.hooks.BUILTIN_HOOKS.keys())
if config.has_section(self.BUILTIN_HOOKS_SECTION):
hooks = set(config.options(self.BUILTIN_HOOKS_SECTION))
bad_hooks = hooks - valid_builtin_hooks
if bad_hooks:
raise ValidationError('%s: unknown builtin hooks: %s' %
(self.paths, bad_hooks))
elif config.has_section(self.BUILTIN_HOOKS_OPTIONS_SECTION):
raise ValidationError('Builtin hook options specified, but missing '
'builtin hook settings')
if config.has_section(self.BUILTIN_HOOKS_OPTIONS_SECTION):
hooks = set(config.options(self.BUILTIN_HOOKS_OPTIONS_SECTION))
bad_hooks = hooks - valid_builtin_hooks
if bad_hooks:
raise ValidationError('%s: unknown builtin hook options: %s' %
(self.paths, bad_hooks))
# Verify hooks are valid shell strings.
for hook in self.custom_hooks:
try:
self.custom_hook(hook)
except ValueError as e:
raise ValidationError('%s: hook "%s" command line is invalid: '
'%s' % (self.paths, hook, e))
# Verify hook options are valid shell strings.
for hook in self.builtin_hooks:
try:
self.builtin_hook_option(hook)
except ValueError as e:
raise ValidationError('%s: hook options "%s" are invalid: %s' %
(self.paths, hook, e))
# Reject unknown tools.
valid_tools = set(rh.hooks.TOOL_PATHS.keys())
if config.has_section(self.TOOL_PATHS_SECTION):
tools = set(config.options(self.TOOL_PATHS_SECTION))
bad_tools = tools - valid_tools
if bad_tools:
raise ValidationError('%s: unknown tools: %s' %
(self.paths, bad_tools))
# Reject unknown options.
valid_options = set(self.VALID_OPTIONS)
if config.has_section(self.OPTIONS_SECTION):
options = set(config.options(self.OPTIONS_SECTION))
bad_options = options - valid_options
if bad_options:
raise ValidationError('%s: unknown options: %s' %
(self.paths, bad_options))