On 10/09/15 19:13, Bjorge, Erik C wrote: > The patch has some trailing white spaces
Apparently, Jordan forgot to run the script on the patch that adds the script! ;) Laszlo > but other than that it works fine. > > Reviewed-by: Erik Bjorge <erik.c.bjo...@intel.com> > > -----Original Message----- > From: Justen, Jordan L > Sent: Wednesday, October 7, 2015 7:53 PM > To: edk2-devel@lists.01.org > Cc: Justen, Jordan L <jordan.l.jus...@intel.com>; Bjorge, Erik C > <erik.c.bjo...@intel.com>; Zhu, Yonghong <yonghong....@intel.com>; Gao, > Liming <liming....@intel.com> > Subject: [PATCH] BaseTools/Scripts: Add PatchCheck.py script > > This script can be used to check some expected rules for EDK II patches. It > only works on git formatted patches. > > It checks both the commit message and the lines that are added in the patch > diff. > > In the commit message it verifies line lengths, signature formats, and the > Contributed-under tag. > > In the patch, it checks that line endings are CRLF for all files that don't > have a .sh extension. It verifies that no trailing whitespace is present and > that tab characters are not used. > > Patch contributors should use this script prior to submitting their patches. > Package maintainers can also use it to verify incoming patches. > > It can also be run by specifying a git revision list, so actual patch files > are not always required. > > For example, to checkout this last 5 patches in your git branch you can run: > > python PatchCheck.py HEAD~5.. > > Or, a shortcut (like git log): > > python PatchCheck.py -5 > > The --oneline option works similar to git log --oneline. > > The --silent option enables silent operation. > > The script supports python 2.7 and python 3. > > Contributed-under: TianoCore Contribution Agreement 1.0 > Signed-off-by: Jordan Justen <jordan.l.jus...@intel.com> > Cc: Erik Bjorge <erik.c.bjo...@intel.com> > Cc: Yonghong Zhu <yonghong....@intel.com> > Cc: Liming Gao <liming....@intel.com> > --- > BaseTools/Scripts/PatchCheck.py | 607 > ++++++++++++++++++++++++++++++++++++++++ > 1 file changed, 607 insertions(+) > create mode 100644 BaseTools/Scripts/PatchCheck.py > > diff --git a/BaseTools/Scripts/PatchCheck.py > b/BaseTools/Scripts/PatchCheck.py new file mode 100644 index 0000000..340a997 > --- /dev/null > +++ b/BaseTools/Scripts/PatchCheck.py > @@ -0,0 +1,607 @@ > +## @file > +# Check a patch for various format issues # # Copyright (c) 2015, > +Intel Corporation. All rights reserved.<BR> # # This program and the > +accompanying materials are licensed and made # available under the > +terms and conditions of the BSD License which # accompanies this > +distribution. The full text of the license may be # found at > +http://opensource.org/licenses/bsd-license.php > +# > +# THE PROGRAM IS DISTRIBUTED UNDER THE BSD LICENSE ON AN "AS IS" > +# BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS OF ANY KIND, EITHER # > +EXPRESS OR IMPLIED. > +# > + > +from __future__ import print_function > + > +VersionNumber = '0.1' > +__copyright__ = "Copyright (c) 2015, Intel Corporation All rights reserved." > + > +import email > +import argparse > +import os > +import re > +import subprocess > +import sys > + > +class Verbose: > + SILENT, ONELINE, NORMAL = range(3) > + level = NORMAL > + > +class CommitMessageCheck: > + """Checks the contents of a git commit message.""" > + > + def __init__(self, subject, message): > + self.ok = True > + > + if subject is None and message is None: > + self.error('Commit message is missing!') > + return > + > + self.subject = subject > + self.msg = message > + > + self.check_contributed_under() > + self.check_signed_off_by() > + self.check_misc_signatures() > + self.check_overall_format() > + self.report_message_result() > + > + url = > 'https://github.com/tianocore/tianocore.github.io/wiki/Commit-Message-Format' > + > + def report_message_result(self): > + if Verbose.level < Verbose.NORMAL: > + return > + if self.ok: > + # All checks passed > + return_code = 0 > + print('The commit message format passed all checks.') > + else: > + return_code = 1 > + if not self.ok: > + print(self.url) > + > + def error(self, *err): > + if self.ok and Verbose.level > Verbose.ONELINE: > + print('The commit message format is not valid:') > + self.ok = False > + if Verbose.level < Verbose.NORMAL: > + return > + count = 0 > + for line in err: > + prefix = (' *', ' ')[count > 0] > + print(prefix, line) > + count += 1 > + > + def check_contributed_under(self): > + cu_msg='Contributed-under: TianoCore Contribution Agreement 1.0' > + if self.msg.find(cu_msg) < 0: > + self.error('Missing Contributed-under! (Note: this must be ' + > + 'added by the code contributor!)') > + > + @staticmethod > + def make_signature_re(sig, re_input=False): > + if re_input: > + sub_re = sig > + else: > + sub_re = sig.replace('-', r'[-\s]+') > + re_str = (r'^(?P<tag>' + sub_re + > + r')(\s*):(\s*)(?P<value>\S.*?)(?:\s*)$') > + try: > + return re.compile(re_str, re.MULTILINE|re.IGNORECASE) > + except Exception: > + print("Tried to compile re:", re_str) > + raise > + > + sig_block_re = \ > + re.compile(r'''^ > + (?: (?P<tag>[^:]+) \s* : \s* > + (?P<value>\S.*?) ) > + | > + (?: \[ (?P<updater>[^:]+) \s* : \s* > + (?P<note>.+?) \s* \] ) > + \s* $''', > + re.VERBOSE | re.MULTILINE) > + > + def find_signatures(self, sig): > + if not sig.endswith('-by') and sig != 'Cc': > + sig += '-by' > + regex = self.make_signature_re(sig) > + > + sigs = regex.findall(self.msg) > + > + bad_case_sigs = filter(lambda m: m[0] != sig, sigs) > + for s in bad_case_sigs: > + self.error("'" +s[0] + "' should be '" + sig + "'") > + > + for s in sigs: > + if s[1] != '': > + self.error('There should be no spaces between ' + sig + > + " and the ':'") > + if s[2] != ' ': > + self.error("There should be a space after '" + sig + > + ":'") > + > + self.check_email_address(s[3]) > + > + return sigs > + > + email_re1 = re.compile(r'(?:\s*)(.*?)(\s*)<(.+)>\s*$', > + re.MULTILINE|re.IGNORECASE) > + > + def check_email_address(self, email): > + email = email.strip() > + mo = self.email_re1.match(email) > + if mo is None: > + self.error("Email format is invalid: " + email.strip()) > + return > + > + name = mo.group(1).strip() > + if name == '': > + self.error("Name is not provided with email address: " + > + email) > + else: > + quoted = len(name) > 2 and name[0] == '"' and name[-1] == '"' > + if name.find(',') >= 0 and not quoted: > + self.error('Add quotes (") around name with a comma: ' + > + name) > + > + if mo.group(2) == '': > + self.error("There should be a space between the name and " + > + "email address: " + email) > + > + if mo.group(3).find(' ') >= 0: > + self.error("The email address cannot contain a space: " + > + mo.group(3)) > + > + def check_signed_off_by(self): > + sob='Signed-off-by' > + if self.msg.find(sob) < 0: > + self.error('Missing Signed-off-by! (Note: this must be ' + > + 'added by the code contributor!)') > + return > + > + sobs = self.find_signatures('Signed-off') > + > + if len(sobs) == 0: > + self.error('Invalid Signed-off-by format!') > + return > + > + sig_types = ( > + 'Reviewed', > + 'Reported', > + 'Tested', > + 'Suggested', > + 'Acked', > + 'Cc' > + ) > + > + def check_misc_signatures(self): > + for sig in self.sig_types: > + self.find_signatures(sig) > + > + def check_overall_format(self): > + lines = self.msg.splitlines() > + > + if len(lines) >= 1 and lines[0].endswith('\r\n'): > + empty_line = '\r\n' > + else: > + empty_line = '\n' > + > + lines.insert(0, empty_line) > + lines.insert(0, self.subject + empty_line) > + > + count = len(lines) > + > + if count <= 0: > + self.error('Empty commit message!') > + return > + > + if count >= 1 and len(lines[0]) > 76: > + self.error('First line of commit message (subject line) ' + > + 'is too long.') > + > + if count >= 1 and len(lines[0].strip()) == 0: > + self.error('First line of commit message (subject line) ' + > + 'is empty.') > + > + if count >= 2 and lines[1].strip() != '': > + self.error('Second line of commit message should be ' + > + 'empty.') > + > + for i in range(2, count): > + if (len(lines[i]) > 76 and > + len(lines[i].split()) > 1 and > + not lines[i].startswith('git-svn-id:')): > + self.error('Line %d of commit message is too long.' % > + (i + 1)) > + > + last_sig_line = None > + for i in range(count - 1, 0, -1): > + line = lines[i] > + mo = self.sig_block_re.match(line) > + if mo is None: > + if line.strip() == '': > + break > + elif last_sig_line is not None: > + err2 = 'Add empty line before "%s"?' % last_sig_line > + self.error('The line before the signature block ' + > + 'should be empty', err2) > + else: > + self.error('The signature block was not found') > + break > + last_sig_line = line.strip() > + > +(START, PRE_PATCH, PATCH) = range(3) > + > +class GitDiffCheck: > + """Checks the contents of a git diff.""" > + > + def __init__(self, diff): > + self.ok = True > + self.format_ok = True > + self.lines = diff.splitlines(True) > + self.count = len(self.lines) > + self.line_num = 0 > + self.state = START > + while self.line_num < self.count and self.format_ok: > + line_num = self.line_num > + self.run() > + assert(self.line_num > line_num) > + self.report_message_result() > + > + def report_message_result(self): > + if Verbose.level < Verbose.NORMAL: > + return > + if self.ok: > + print('The code passed all checks.') > + > + def run(self): > + line = self.lines[self.line_num] > + > + if self.state in (PRE_PATCH, PATCH): > + if line.startswith('diff --git'): > + self.state = START > + if self.state == PATCH: > + if line.startswith('@@ '): > + self.state = PRE_PATCH > + elif len(line) >= 1 and line[0] not in ' -+' and \ > + not line.startswith(r'\ No newline '): > + for line in self.lines[self.line_num + 1:]: > + if line.startswith('diff --git'): > + self.format_error('diff found after end of patch') > + break > + self.line_num = self.count > + return > + > + if self.state == START: > + if line.startswith('diff --git'): > + self.state = PRE_PATCH > + self.set_filename(None) > + elif len(line.rstrip()) != 0: > + self.format_error("didn't find diff command") > + self.line_num += 1 > + elif self.state == PRE_PATCH: > + if line.startswith('+++ b/'): > + self.set_filename(line[6:].rstrip()) > + if line.startswith('@@ '): > + self.state = PATCH > + else: > + ok = False > + for pfx in self.pre_patch_prefixes: > + if line.startswith(pfx): > + ok = True > + if not ok: > + self.format_error("didn't find diff hunk marker (@@)") > + self.line_num += 1 > + elif self.state == PATCH: > + if line.startswith('-'): > + pass > + elif line.startswith('+'): > + self.check_added_line(line[1:]) > + elif line.startswith(r'\ No newline '): > + pass > + elif not line.startswith(' '): > + self.format_error("unexpected patch line") > + self.line_num += 1 > + > + pre_patch_prefixes = ( > + '--- ', > + '+++ ', > + 'index ', > + 'new file ', > + 'deleted file ', > + 'old mode ', > + 'new mode ', > + 'similarity index ', > + 'rename ', > + 'Binary files ', > + ) > + > + line_endings = ('\r\n', '\n\r', '\n', '\r') > + > + def set_filename(self, filename): > + self.hunk_filename = filename > + if filename: > + self.force_crlf = not filename.endswith('.sh') > + else: > + self.force_crlf = True > + > + def added_line_error(self, msg, line): > + lines = [ msg ] > + if self.hunk_filename is not None: > + lines.append('File: ' + self.hunk_filename) > + lines.append('Line: ' + line) > + > + self.error(*lines) > + > + def check_added_line(self, line): > + eol = '' > + for an_eol in self.line_endings: > + if line.endswith(an_eol): > + eol = an_eol > + line = line[:-len(eol)] > + > + stripped = line.rstrip() > + > + if self.force_crlf and eol != '\r\n': > + self.added_line_error('Line ending (%s) is not CRLF' % repr(eol), > + line) > + if '\t' in line: > + self.added_line_error('Tab character used', line) > + if len(stripped) < len(line): > + self.added_line_error('Trailing whitespace found', line) > + > + split_diff_re = re.compile(r''' > + (?P<cmd> > + ^ diff \s+ --git \s+ a/.+ \s+ b/.+ $ > + ) > + (?P<index> > + ^ index \s+ .+ $ > + ) > + ''', > + re.IGNORECASE | re.VERBOSE | > + re.MULTILINE) > + > + def format_error(self, err): > + self.format_ok = False > + err = 'Patch format error: ' + err > + err2 = 'Line: ' + self.lines[self.line_num].rstrip() > + self.error(err, err2) > + > + def error(self, *err): > + if self.ok and Verbose.level > Verbose.ONELINE: > + print('Code format is not valid:') > + self.ok = False > + if Verbose.level < Verbose.NORMAL: > + return > + count = 0 > + for line in err: > + prefix = (' *', ' ')[count > 0] > + print(prefix, line) > + count += 1 > + > +class CheckOnePatch: > + """Checks the contents of a git email formatted patch. > + > + Various checks are performed on both the commit message and the > + patch content. > + """ > + > + def __init__(self, name, patch): > + self.patch = patch > + self.find_patch_pieces() > + > + msg_check = CommitMessageCheck(self.commit_subject, self.commit_msg) > + msg_ok = msg_check.ok > + > + diff_ok = True > + if self.diff is not None: > + diff_check = GitDiffCheck(self.diff) > + diff_ok = diff_check.ok > + > + self.ok = msg_ok and diff_ok > + > + if Verbose.level == Verbose.ONELINE: > + if self.ok: > + result = 'ok' > + else: > + result = list() > + if not msg_ok: > + result.append('commit message') > + if not diff_ok: > + result.append('diff content') > + result = 'bad ' + ' and '.join(result) > + print(name, result) > + > + > + git_diff_re = re.compile(r''' > + ^ diff \s+ --git \s+ a/.+ \s+ b/.+ $ > + ''', > + re.IGNORECASE | re.VERBOSE | re.MULTILINE) > + > + stat_re = \ > + re.compile(r''' > + (?P<commit_message> [\s\S\r\n]* ) > + (?P<stat> > + ^ --- $ [\r\n]+ > + (?: ^ \s+ .+ \s+ \| \s+ \d+ \s+ \+* \-* > + $ [\r\n]+ )+ > + [\s\S\r\n]+ > + ) > + ''', > + re.IGNORECASE | re.VERBOSE | re.MULTILINE) > + > + def find_patch_pieces(self): > + if sys.version_info < (3, 0): > + patch = self.patch.encode('ascii', 'ignore') > + else: > + patch = self.patch > + > + self.commit_msg = None > + self.stat = None > + self.commit_subject = None > + self.commit_prefix = None > + self.diff = None > + > + if patch.startswith('diff --git'): > + self.diff = patch > + return > + > + pmail = email.message_from_string(patch) > + parts = list(pmail.walk()) > + assert(len(parts) == 1) > + assert(parts[0].get_content_type() == 'text/plain') > + content = parts[0].get_payload(decode=True).decode('utf-8', > + 'ignore') > + > + mo = self.git_diff_re.search(content) > + if mo is not None: > + self.diff = content[mo.start():] > + content = content[:mo.start()] > + > + mo = self.stat_re.search(content) > + if mo is None: > + self.commit_msg = content > + else: > + self.stat = mo.group('stat') > + self.commit_msg = mo.group('commit_message') > + > + self.commit_subject = pmail['subject'].replace('\r\n', '') > + self.commit_subject = self.commit_subject.replace('\n', '') > + > + pfx_start = self.commit_subject.find('[') > + if pfx_start >= 0: > + pfx_end = self.commit_subject.find(']') > + if pfx_end > pfx_start: > + self.commit_prefix = self.commit_subject[pfx_start + 1 : > pfx_end] > + self.commit_subject = self.commit_subject[pfx_end + 1 > + :].lstrip() > + > + > +class CheckGitCommits: > + """Reads patches from git based on the specified git revision range. > + > + The patches are read from git, and then checked. > + """ > + > + def __init__(self, rev_spec, max_count): > + commits = self.read_commit_list_from_git(rev_spec, max_count) > + if len(commits) == 1 and Verbose.level > Verbose.ONELINE: > + commits = [ rev_spec ] > + self.ok = True > + blank_line = False > + for commit in commits: > + if Verbose.level > Verbose.ONELINE: > + if blank_line: > + print() > + else: > + blank_line = True > + print('Checking git commit:', commit) > + patch = self.read_patch_from_git(commit) > + self.ok &= CheckOnePatch(commit, patch).ok > + > + def read_commit_list_from_git(self, rev_spec, max_count): > + # Run git to get the commit patch > + cmd = [ 'rev-list', '--abbrev-commit', '--no-walk' ] > + if max_count is not None: > + cmd.append('--max-count=' + str(max_count)) > + cmd.append(rev_spec) > + out = self.run_git(*cmd) > + return out.split() > + > + def read_patch_from_git(self, commit): > + # Run git to get the commit patch > + return self.run_git('show', '--pretty=email', commit) > + > + def run_git(self, *args): > + cmd = [ 'git' ] > + cmd += args > + p = subprocess.Popen(cmd, > + stdout=subprocess.PIPE, > + stderr=subprocess.STDOUT) > + return p.communicate()[0].decode('utf-8', 'ignore') > + > +class CheckOnePatchFile: > + """Performs a patch check for a single file. > + > + stdin is used when the filename is '-'. > + """ > + > + def __init__(self, patch_filename): > + if patch_filename == '-': > + patch = sys.stdin.read() > + patch_filename = 'stdin' > + else: > + f = open(patch_filename, 'rb') > + patch = f.read().decode('utf-8', 'ignore') > + f.close() > + if Verbose.level > Verbose.ONELINE: > + print('Checking patch file:', patch_filename) > + self.ok = CheckOnePatch(patch_filename, patch).ok > + > +class CheckOneArg: > + """Performs a patch check for a single command line argument. > + > + The argument will be handed off to a file or git-commit based > + checker. > + """ > + > + def __init__(self, param, max_count=None): > + self.ok = True > + if param == '-' or os.path.exists(param): > + checker = CheckOnePatchFile(param) > + else: > + checker = CheckGitCommits(param, max_count) > + self.ok = checker.ok > + > +class PatchCheckApp: > + """Checks patches based on the command line arguments.""" > + > + def __init__(self): > + self.parse_options() > + patches = self.args.patches > + > + if len(patches) == 0: > + patches = [ 'HEAD' ] > + > + self.ok = True > + self.count = None > + for patch in patches: > + self.process_one_arg(patch) > + > + if self.count is not None: > + self.process_one_arg('HEAD') > + > + if self.ok: > + self.retval = 0 > + else: > + self.retval = -1 > + > + def process_one_arg(self, arg): > + if len(arg) >= 2 and arg[0] == '-': > + try: > + self.count = int(arg[1:]) > + return > + except ValueError: > + pass > + self.ok &= CheckOneArg(arg, self.count).ok > + self.count = None > + > + def parse_options(self): > + parser = argparse.ArgumentParser(description=__copyright__) > + parser.add_argument('--version', action='version', > + version='%(prog)s ' + VersionNumber) > + parser.add_argument('patches', nargs='*', > + help='[patch file | git rev list]') > + group = parser.add_mutually_exclusive_group() > + group.add_argument("--oneline", > + action="store_true", > + help="Print one result per line") > + group.add_argument("--silent", > + action="store_true", > + help="Print nothing") > + self.args = parser.parse_args() > + if self.args.oneline: > + Verbose.level = Verbose.ONELINE > + if self.args.silent: > + Verbose.level = Verbose.SILENT > + > +if __name__ == "__main__": > + sys.exit(PatchCheckApp().retval) > -- > 2.5.1 > > _______________________________________________ > edk2-devel mailing list > edk2-devel@lists.01.org > https://lists.01.org/mailman/listinfo/edk2-devel > _______________________________________________ edk2-devel mailing list edk2-devel@lists.01.org https://lists.01.org/mailman/listinfo/edk2-devel