diff --git a/.shippable.yml b/.shippable.yml index 723b87f06d0..dbedb24a3e4 100644 --- a/.shippable.yml +++ b/.shippable.yml @@ -56,7 +56,7 @@ build: echo " => New documentation warnings/errors"; fi; echo "- Verify commit message and coding style"; - ./scripts/ci/check-compliance.py || true; + ./scripts/ci/check-compliance.py --commits ${COMMIT_RANGE} || true; fi; - > if [ "$JOB_TRIGGERED_BY_NAME" = "daily-verify" ]; then diff --git a/scripts/ci/check-compliance.py b/scripts/ci/check-compliance.py index 68daabcb6ad..7e5d6ab5b73 100755 --- a/scripts/ci/check-compliance.py +++ b/scripts/ci/check-compliance.py @@ -4,15 +4,64 @@ import subprocess import re import os import xml.etree.ElementTree as ET +from email.utils import parseaddr +import sh +import logging +import argparse +import check_identity +if "ZEPHYR_BASE" not in os.environ: + logging.error("$ZEPHYR_BASE environment variable undefined.\n") + exit(1) + +logger = None DOCS_WARNING_FILE = "doc.warnings" -commit_range = os.environ['COMMIT_RANGE'] -cwd = os.environ['ZEPHYR_BASE'] +repository_path = os.environ['ZEPHYR_BASE'] +sh_special_args = { + '_tty_out': False, + '_cwd': repository_path +} -def run_gitlint(tc): + +def init_logs(): + global logger + log_lev = os.environ.get('LOG_LEVEL', None) + level = logging.INFO + if log_lev == "DEBUG": + level = logging.DEBUG + elif log_lev == "ERROR": + level = logging.ERROR + + console = logging.StreamHandler() + format = logging.Formatter('%(levelname)-8s: %(message)s') + console.setFormatter(format) + logger = logging.getLogger('') + logger.addHandler(console) + logger.setLevel(level) + + logging.debug("Log init completed") + +def parse_args(): + parser = argparse.ArgumentParser( + description="Check for coding style and documentation warnings.") + parser.add_argument('-c', '--commits', default=None, + help="Commit range in the form: a..b") + return parser.parse_args() + +def get_shas(refspec): + + sha_list = sh.git("rev-list", + '--max-count={0}'.format(-1 if "." in refspec else 1), + refspec, **sh_special_args).split() + + return sha_list + + + +def run_gitlint(tc, commit_range): proc = subprocess.Popen('gitlint --commits %s' %(commit_range), - cwd=cwd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + cwd=repository_path, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) msg = "" if proc.wait() != 0: @@ -26,13 +75,13 @@ def run_gitlint(tc): return 0 -def run_checkpatch(tc): +def run_checkpatch(tc, commit_range): output = None out = "" diff = subprocess.Popen(('git', 'diff', '%s' %(commit_range)), stdout=subprocess.PIPE) try: - output = subprocess.check_output(('%s/scripts/checkpatch.pl' %cwd, + output = subprocess.check_output(('%s/scripts/checkpatch.pl' %repository_path, '--mailback', '--no-tree', '-'), stdin=diff.stdout, stderr=subprocess.STDOUT, shell=True) @@ -46,7 +95,7 @@ def run_checkpatch(tc): return 0 -def check_doc(tc): +def check_doc(tc, range): if os.path.exists(DOCS_WARNING_FILE) and os.path.getsize(DOCS_WARNING_FILE) > 0: with open(DOCS_WARNING_FILE, "r") as f: @@ -75,7 +124,7 @@ tests = { } } -def run_tests(): +def run_tests(range): run = "Commit Message / Documentation / Coding Style" eleTestsuite = None fails = 0 @@ -94,8 +143,7 @@ def run_tests(): eleTestcase = ET.SubElement(eleTestsuite, 'testcase', classname="%s" %(test), name="%s" %(tests[test]['name']), time="0") - fails += tests[test]['call'](eleTestcase) - + fails += tests[test]['call'](eleTestcase, range) eleTestsuite.set("tests", "%s" %total) eleTestsuite.set("failures", "%s" %fails) @@ -106,6 +154,16 @@ def run_tests(): f.close() return fails -fails = run_tests() -print(fails) -sys.exit(fails) + +def main(): + args = parse_args() + if not args.commits: + exit(1) + fails = run_tests(args.commits) + print(fails) + sys.exit(fails) + +if __name__ == "__main__": + #init_logs() + main() + diff --git a/scripts/ci/check_identity.py b/scripts/ci/check_identity.py new file mode 100755 index 00000000000..750202ca159 --- /dev/null +++ b/scripts/ci/check_identity.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 + +# A script to check email identities in commit message. We verify the author +# has signed off using the same email address +import sys +import re, os +from email.utils import parseaddr +import sh +import logging +import argparse + +if "ZEPHYR_BASE" not in os.environ: + logging.error("$ZEPHYR_BASE environment variable undefined.\n") + exit(1) + +logger = None + +repository_path = os.environ['ZEPHYR_BASE'] +sh_special_args = { + '_tty_out': False, + '_cwd': repository_path +} + +def init_logs(): + global logger + log_lev = os.environ.get('LOG_LEVEL', None) + level = logging.INFO + if log_lev == "DEBUG": + level = logging.DEBUG + elif log_lev == "ERROR": + level = logging.ERROR + + console = logging.StreamHandler() + format = logging.Formatter('%(levelname)-8s: %(message)s') + console.setFormatter(format) + logger = logging.getLogger('') + logger.addHandler(console) + logger.setLevel(level) + + logging.debug("Log init completed") + +def parse_args(): + parser = argparse.ArgumentParser( + description="Verify that author identity matches Signed-off-by") + parser.add_argument('-c', '--commits', default=None, + help="Commit range in the form: a..b") + return parser.parse_args() + +def get_shas(refspec): + + sha_list = sh.git("rev-list", + '--max-count={0}'.format(-1 if "." in refspec else 1), + refspec, **sh_special_args).split() + + return sha_list + +def verify_signed_off(tc, commit): + + signed = [] + author = "" + sha = "" + parsed_addr = None + for line in commit.split("\n"): + match = re.search("^commit\s([^\s]*)", line) + if match: + sha = match.group(1) + match = re.search("^Author:\s(.*)", line) + if match: + author = match.group(1) + parsed_addr = parseaddr(author) + match = re.search("signed-off-by:\s(.*)", line, re.IGNORECASE) + if match: + signed.append(match.group(1)) + + error1 = "%s: author email (%s) needs to match one of the signed-off-by entries." %(sha, author) + error2 = "%s: author email (%s) does not follow the syntax: First Last ." %(sha, author) + error = 0 + if tc: + failure = None + if author not in signed: + failure = ET.SubElement(tc, 'failure', type="failure", message="identity error on range: %s" %commit_range) + failure.text = error1 + error = 1 + if not parsed_addr or len(parsed_addr[0].split(" ")) < 2: + if not failure: + failure = ET.SubElement(tc, 'failure', type="failure", message="identity error on range: %s" %commit_range) + failure.text = error2 + else: + failure.text = failure.text + "\n" + error2 + error = 1 + + else: + if author not in signed: + print(error1) + error = 1 + + if not parsed_addr or len(parsed_addr[0].split(" ")) < 2: + print(error2) + error = 1 + + return error + +def main(): + args = parse_args() + if not args.commits: + exit(1) + for f in get_shas(args.commits): + commit = sh.git("log","--decorate=short", "-n 1", f, **sh_special_args) + verify_signed_off(None, commit) + +if __name__ == "__main__": + main() +