| #!/usr/bin/env python | |
| # | |
| # Licensed to the Apache Software Foundation (ASF) under one or more | |
| # contributor license agreements. See the NOTICE file distributed with | |
| # this work for additional information regarding copyright ownership. | |
| # The ASF licenses this file to You 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. | |
| # | |
| """Subversion pre-commit hook script that runs user configured commands | |
| to validate files in the commit and reject the commit if the commands | |
| exit with a non-zero exit code. The script expects a validate-files.conf | |
| file placed in the conf dir under the repo the commit is for. | |
| Note: As changed file paths $FILE are always represented as a Unicode (Py3) | |
| or UTF-8 (Py2) strings, you might need to set apropriate locale and | |
| PYTHONIOENCODING environment variable for this script and | |
| commands to handle non-ascii path and command outputs, especially | |
| you want to use svnlook cat command to inspect file contents.""" | |
| import sys | |
| import os | |
| import subprocess | |
| import fnmatch | |
| # Deal with the rename of ConfigParser to configparser in Python3 | |
| try: | |
| # Python >= 3.0 | |
| import configparser | |
| ConfigParser = configparser.ConfigParser | |
| except ImportError: | |
| # Python < 3.0 | |
| import ConfigParser as configparser | |
| ConfigParser = configparser.SafeConfigParser | |
| class Config(ConfigParser): | |
| """Superclass of SafeConfigParser with some customizations | |
| for this script""" | |
| def optionxform(self, option): | |
| """Redefine optionxform so option names are case sensitive""" | |
| return option | |
| def getlist(self, section, option): | |
| """Returns value of option as a list using whitespace to | |
| split entries""" | |
| value = self.get(section, option) | |
| if value: | |
| return value.split() | |
| else: | |
| return None | |
| def get_matching_rules(self, repo): | |
| """Return list of unique rules names that apply to a given repo""" | |
| rules = {} | |
| for option in self.options('repositories'): | |
| if fnmatch.fnmatch(repo, option): | |
| for rule in self.getlist('repositories', option): | |
| rules[rule] = True | |
| return rules.keys() | |
| def get_rule_section_name(self, rule): | |
| """Given a rule name provide the section name it is defined in.""" | |
| return 'rule:%s' % (rule) | |
| class Commands: | |
| """Class to handle logic of running commands""" | |
| def __init__(self, config): | |
| self.config = config | |
| def svnlook_changed(self, repo, txn): | |
| """Provide list of files changed in txn of repo""" | |
| svnlook = self.config.get('DEFAULT', 'svnlook') | |
| cmd = "'%s' changed -t '%s' '%s'" % (svnlook, txn, repo) | |
| p = subprocess.Popen(cmd, shell=True, | |
| stdout=subprocess.PIPE, stderr=subprocess.PIPE) | |
| changed = [] | |
| while True: | |
| line = p.stdout.readline() | |
| if not line: | |
| break | |
| line = line.strip() | |
| text_mod = line[0:1] | |
| # Only if the contents of the file changed (by addition or update) | |
| # directories always end in / in the svnlook changed output | |
| if line[-1:] != b"/" and (text_mod == b"A" or text_mod == b"U"): | |
| changed_path = line[4:] | |
| if not isinstance(changed_path, str): | |
| # svnlook always uses UTF-8 for internal path | |
| changed_path = changed_path.decode('utf-8') | |
| changed.append(changed_path) | |
| # wait on the command to finish so we can get the | |
| # returncode/stderr output | |
| data = p.communicate() | |
| if p.returncode != 0: | |
| err_mesg = data[1] | |
| if sys.stderr.encoding: | |
| err_mesg =err_mesg.decode(sys.stderr.encoding, | |
| 'backslashreplace') | |
| sys.stderr.write(err_mesg) | |
| sys.exit(2) | |
| return changed | |
| def user_command(self, section, repo, txn, fn): | |
| """ Run the command defined for a given section. | |
| Replaces $REPO, $TXN and $FILE with the repo, txn and fn arguments | |
| in the defined command. | |
| Returns a tuple of the exit code and the stderr output of the command""" | |
| cmd = self.config.get(section, 'command') | |
| cmd_env = os.environ.copy() | |
| cmd_env['REPO'] = repo | |
| cmd_env['TXN'] = txn | |
| cmd_env['FILE'] = fn | |
| p = subprocess.Popen(cmd, shell=True, env=cmd_env, stderr=subprocess.PIPE) | |
| data = p.communicate() | |
| err_mesg = data[1] | |
| if sys.stderr.encoding: | |
| err_mesg = err_mesg.decode(sys.stderr.encoding, | |
| 'backslashreplace') | |
| return (p.returncode, err_mesg) | |
| def main(repo, txn): | |
| exitcode = 0 | |
| config = Config() | |
| config.read(os.path.join(repo, 'conf', 'validate-files.conf')) | |
| commands = Commands(config) | |
| rules = config.get_matching_rules(repo) | |
| # no matching rules so nothing to do | |
| if len(rules) == 0: | |
| sys.exit(0) | |
| changed = commands.svnlook_changed(repo, txn) | |
| # this shouldn't ever happen | |
| if len(changed) == 0: | |
| sys.exit(0) | |
| for rule in rules: | |
| section = config.get_rule_section_name(rule) | |
| pattern = config.get(section, 'pattern') | |
| # skip leading slashes if present in the pattern | |
| if pattern[0] == '/': pattern = pattern[1:] | |
| for fn in fnmatch.filter(changed, pattern): | |
| (returncode, err_mesg) = commands.user_command(section, repo, | |
| txn, fn) | |
| if returncode != 0: | |
| sys.stderr.write( | |
| "\nError validating file '%s' with rule '%s' " \ | |
| "(exit code %d):\n" % (fn, rule, returncode)) | |
| sys.stderr.write(err_mesg) | |
| exitcode = 1 | |
| return exitcode | |
| if __name__ == "__main__": | |
| if len(sys.argv) != 3: | |
| sys.stderr.write("invalid args\n") | |
| sys.exit(0) | |
| try: | |
| sys.exit(main(sys.argv[1], sys.argv[2])) | |
| except configparser.Error as e: | |
| sys.stderr.write("Error with the validate-files.conf: %s\n" % e) | |
| sys.exit(2) |