mirror of https://github.com/acidanthera/audk.git
629 lines
21 KiB
Python
Executable File
629 lines
21 KiB
Python
Executable File
## @file
|
|
# Check a patch for various format issues
|
|
#
|
|
# Copyright (c) 2015 - 2016, 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 - 2016, 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]) >= 72:
|
|
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
|
|
self.binary = False
|
|
elif line.startswith('GIT binary patch'):
|
|
self.state = PATCH
|
|
self.binary = True
|
|
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 self.binary:
|
|
pass
|
|
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)
|
|
|
|
old_debug_re = \
|
|
re.compile(r'''
|
|
DEBUG \s* \( \s* \( \s*
|
|
(?: DEBUG_[A-Z_]+ \s* \| \s*)*
|
|
EFI_D_ ([A-Z_]+)
|
|
''',
|
|
re.VERBOSE)
|
|
|
|
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)
|
|
|
|
mo = self.old_debug_re.search(line)
|
|
if mo is not None:
|
|
self.added_line_error('EFI_D_' + mo.group(1) + ' was used, '
|
|
'but DEBUG_' + mo.group(1) +
|
|
' is now recommended', 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)
|
|
|
|
subject_prefix_re = \
|
|
re.compile(r'''^
|
|
\s* (\[
|
|
[^\[\]]* # Allow all non-brackets
|
|
\])* \s*
|
|
''',
|
|
re.VERBOSE)
|
|
|
|
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', '')
|
|
self.commit_subject = self.subject_prefix_re.sub('', self.commit_subject, 1)
|
|
|
|
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)
|