| #!/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.""" |
| |
| import sys |
| import os |
| import subprocess |
| import fnmatch |
| |
| # Deal with the rename of ConfigParser to configparser in Python3 |
| try: |
| # Python >= 3.0 |
| import configparser |
| except ImportError: |
| # Python < 3.0 |
| import ConfigParser as configparser |
| |
| class Config(configparser.SafeConfigParser): |
| """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.decode().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] != "/" and (text_mod == "A" or text_mod == "U"): |
| changed.append(line[4:]) |
| |
| # wait on the command to finish so we can get the |
| # returncode/stderr output |
| data = p.communicate() |
| if p.returncode != 0: |
| sys.stderr.write(data[1].decode()) |
| 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() |
| return (p.returncode, data[1].decode()) |
| |
| 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) |