blob: 71692512630567cb26fd889479eec1a87b5711f2 [file] [log] [blame]
#!/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)