## @file\r
# Check a patch for various format issues\r
#\r
-# Copyright (c) 2015 - 2019, Intel Corporation. All rights reserved.<BR>\r
+# Copyright (c) 2015 - 2021, Intel Corporation. All rights reserved.<BR>\r
+# Copyright (C) 2020, Red Hat, Inc.<BR>\r
+# Copyright (c) 2020, ARM Ltd. All rights reserved.<BR>\r
#\r
# SPDX-License-Identifier: BSD-2-Clause-Patent\r
#\r
import subprocess\r
import sys\r
\r
+import email.header\r
+\r
class Verbose:\r
SILENT, ONELINE, NORMAL = range(3)\r
level = NORMAL\r
\r
+class EmailAddressCheck:\r
+ """Checks an email address."""\r
+\r
+ def __init__(self, email, description):\r
+ self.ok = True\r
+\r
+ if email is None:\r
+ self.error('Email address is missing!')\r
+ return\r
+ if description is None:\r
+ self.error('Email description is missing!')\r
+ return\r
+\r
+ self.description = "'" + description + "'"\r
+ self.check_email_address(email)\r
+\r
+ def error(self, *err):\r
+ if self.ok and Verbose.level > Verbose.ONELINE:\r
+ print('The ' + self.description + ' email address is not valid:')\r
+ self.ok = False\r
+ if Verbose.level < Verbose.NORMAL:\r
+ return\r
+ count = 0\r
+ for line in err:\r
+ prefix = (' *', ' ')[count > 0]\r
+ print(prefix, line)\r
+ count += 1\r
+\r
+ email_re1 = re.compile(r'(?:\s*)(.*?)(\s*)<(.+)>\s*$',\r
+ re.MULTILINE|re.IGNORECASE)\r
+\r
+ def check_email_address(self, email):\r
+ email = email.strip()\r
+ mo = self.email_re1.match(email)\r
+ if mo is None:\r
+ self.error("Email format is invalid: " + email.strip())\r
+ return\r
+\r
+ name = mo.group(1).strip()\r
+ if name == '':\r
+ self.error("Name is not provided with email address: " +\r
+ email)\r
+ else:\r
+ quoted = len(name) > 2 and name[0] == '"' and name[-1] == '"'\r
+ if name.find(',') >= 0 and not quoted:\r
+ self.error('Add quotes (") around name with a comma: ' +\r
+ name)\r
+\r
+ if mo.group(2) == '':\r
+ self.error("There should be a space between the name and " +\r
+ "email address: " + email)\r
+\r
+ if mo.group(3).find(' ') >= 0:\r
+ self.error("The email address cannot contain a space: " +\r
+ mo.group(3))\r
+\r
+ if ' via Groups.Io' in name and mo.group(3).endswith('@groups.io'):\r
+ self.error("Email rewritten by lists DMARC / DKIM / SPF: " +\r
+ email)\r
+\r
class CommitMessageCheck:\r
"""Checks the contents of a git commit message."""\r
\r
- def __init__(self, subject, message):\r
+ def __init__(self, subject, message, author_email):\r
self.ok = True\r
\r
if subject is None and message is None:\r
self.error('Commit message is missing!')\r
return\r
\r
+ MergifyMerge = False\r
+ if "mergify[bot]@users.noreply.github.com" in author_email:\r
+ if "Merge branch" in subject:\r
+ MergifyMerge = True\r
+\r
self.subject = subject\r
self.msg = message\r
\r
+ print (subject)\r
+\r
self.check_contributed_under()\r
- self.check_signed_off_by()\r
- self.check_misc_signatures()\r
- self.check_overall_format()\r
+ if not MergifyMerge:\r
+ self.check_signed_off_by()\r
+ self.check_misc_signatures()\r
+ self.check_overall_format()\r
self.report_message_result()\r
\r
url = 'https://github.com/tianocore/tianocore.github.io/wiki/Commit-Message-Format'\r
if s[2] != ' ':\r
self.error("There should be a space after '" + sig + ":'")\r
\r
- self.check_email_address(s[3])\r
+ EmailAddressCheck(s[3], sig)\r
\r
return sigs\r
\r
- email_re1 = re.compile(r'(?:\s*)(.*?)(\s*)<(.+)>\s*$',\r
- re.MULTILINE|re.IGNORECASE)\r
-\r
- def check_email_address(self, email):\r
- email = email.strip()\r
- mo = self.email_re1.match(email)\r
- if mo is None:\r
- self.error("Email format is invalid: " + email.strip())\r
- return\r
-\r
- name = mo.group(1).strip()\r
- if name == '':\r
- self.error("Name is not provided with email address: " +\r
- email)\r
- else:\r
- quoted = len(name) > 2 and name[0] == '"' and name[-1] == '"'\r
- if name.find(',') >= 0 and not quoted:\r
- self.error('Add quotes (") around name with a comma: ' +\r
- name)\r
-\r
- if mo.group(2) == '':\r
- self.error("There should be a space between the name and " +\r
- "email address: " + email)\r
-\r
- if mo.group(3).find(' ') >= 0:\r
- self.error("The email address cannot contain a space: " +\r
- mo.group(3))\r
-\r
def check_signed_off_by(self):\r
sob='Signed-off-by'\r
if self.msg.find(sob) < 0:\r
for sig in self.sig_types:\r
self.find_signatures(sig)\r
\r
+ cve_re = re.compile('CVE-[0-9]{4}-[0-9]{5}[^0-9]')\r
+\r
def check_overall_format(self):\r
lines = self.msg.splitlines()\r
\r
self.error('Empty commit message!')\r
return\r
\r
- if count >= 1 and len(lines[0]) >= 72:\r
- self.error('First line of commit message (subject line) ' +\r
- 'is too long.')\r
+ if count >= 1 and re.search(self.cve_re, lines[0]):\r
+ #\r
+ # If CVE-xxxx-xxxxx is present in subject line, then limit length of\r
+ # subject line to 92 characters\r
+ #\r
+ if len(lines[0].rstrip()) >= 93:\r
+ self.error(\r
+ 'First line of commit message (subject line) is too long (%d >= 93).' %\r
+ (len(lines[0].rstrip()))\r
+ )\r
+ else:\r
+ #\r
+ # If CVE-xxxx-xxxxx is not present in subject line, then limit\r
+ # length of subject line to 75 characters\r
+ #\r
+ if len(lines[0].rstrip()) >= 76:\r
+ self.error(\r
+ 'First line of commit message (subject line) is too long (%d >= 76).' %\r
+ (len(lines[0].rstrip()))\r
+ )\r
\r
if count >= 1 and len(lines[0].strip()) == 0:\r
self.error('First line of commit message (subject line) ' +\r
for i in range(2, count):\r
if (len(lines[i]) >= 76 and\r
len(lines[i].split()) > 1 and\r
- not lines[i].startswith('git-svn-id:')):\r
- self.error('Line %d of commit message is too long.' % (i + 1))\r
+ not lines[i].startswith('git-svn-id:') and\r
+ not lines[i].startswith('Reviewed-by') and\r
+ not lines[i].startswith('Acked-by:') and\r
+ not lines[i].startswith('Tested-by:') and\r
+ not lines[i].startswith('Reported-by:') and\r
+ not lines[i].startswith('Suggested-by:') and\r
+ not lines[i].startswith('Signed-off-by:') and\r
+ not lines[i].startswith('Cc:')):\r
+ #\r
+ # Print a warning if body line is longer than 75 characters\r
+ #\r
+ print(\r
+ 'WARNING - Line %d of commit message is too long (%d >= 76).' %\r
+ (i + 1, len(lines[i]))\r
+ )\r
+ print(lines[i])\r
\r
last_sig_line = None\r
for i in range(count - 1, 0, -1):\r
self.state = PRE_PATCH\r
self.filename = line[13:].split(' ', 1)[0]\r
self.is_newfile = False\r
- self.force_crlf = not self.filename.endswith('.sh')\r
+ self.force_crlf = True\r
+ self.force_notabs = True\r
+ if self.filename.endswith('.sh') or \\r
+ self.filename.startswith('BaseTools/BinWrappers/PosixLike/') or \\r
+ self.filename.startswith('BaseTools/BinPipWrappers/PosixLike/') or \\r
+ self.filename == 'BaseTools/BuildEnv':\r
+ #\r
+ # Do not enforce CR/LF line endings for linux shell scripts.\r
+ # Some linux shell scripts don't end with the ".sh" extension,\r
+ # they are identified by their path.\r
+ #\r
+ self.force_crlf = False\r
+ if self.filename == '.gitmodules' or \\r
+ self.filename == 'BaseTools/Conf/diff.order':\r
+ #\r
+ # .gitmodules and diff orderfiles are used internally by git\r
+ # use tabs and LF line endings. Do not enforce no tabs and\r
+ # do not enforce CR/LF line endings.\r
+ #\r
+ self.force_crlf = False\r
+ self.force_notabs = False\r
+ if os.path.basename(self.filename) == 'GNUmakefile' or \\r
+ os.path.basename(self.filename) == 'Makefile':\r
+ self.force_notabs = False\r
elif len(line.rstrip()) != 0:\r
self.format_error("didn't find diff command")\r
self.line_num += 1\r
self.binary = True\r
if self.is_newfile:\r
self.new_bin.append(self.filename)\r
+ elif line.startswith('new file mode 160000'):\r
+ #\r
+ # New submodule. Do not enforce CR/LF line endings\r
+ #\r
+ self.force_crlf = False\r
else:\r
ok = False\r
self.is_newfile = self.newfile_prefix_re.match(line)\r
\r
stripped = line.rstrip()\r
\r
- if self.force_crlf and eol != '\r\n':\r
+ if self.force_crlf and eol != '\r\n' and (line.find('Subproject commit') == -1):\r
self.added_line_error('Line ending (%s) is not CRLF' % repr(eol),\r
line)\r
- if '\t' in line:\r
+ if self.force_notabs and '\t' in line:\r
self.added_line_error('Tab character used', line)\r
if len(stripped) < len(line):\r
self.added_line_error('Trailing whitespace found', line)\r
self.patch = patch\r
self.find_patch_pieces()\r
\r
- msg_check = CommitMessageCheck(self.commit_subject, self.commit_msg)\r
+ email_check = EmailAddressCheck(self.author_email, 'Author')\r
+ email_ok = email_check.ok\r
+\r
+ msg_check = CommitMessageCheck(self.commit_subject, self.commit_msg, self.author_email)\r
msg_ok = msg_check.ok\r
\r
diff_ok = True\r
diff_check = GitDiffCheck(self.diff)\r
diff_ok = diff_check.ok\r
\r
- self.ok = msg_ok and diff_ok\r
+ self.ok = email_ok and msg_ok and diff_ok\r
\r
if Verbose.level == Verbose.ONELINE:\r
if self.ok:\r
else:\r
self.stat = mo.group('stat')\r
self.commit_msg = mo.group('commit_message')\r
+ #\r
+ # Parse subject line from email header. The subject line may be\r
+ # composed of multiple parts with different encodings. Decode and\r
+ # combine all the parts to produce a single string with the contents of\r
+ # the decoded subject line.\r
+ #\r
+ parts = email.header.decode_header(pmail.get('subject'))\r
+ subject = ''\r
+ for (part, encoding) in parts:\r
+ if encoding:\r
+ part = part.decode(encoding)\r
+ else:\r
+ try:\r
+ part = part.decode()\r
+ except:\r
+ pass\r
+ subject = subject + part\r
\r
- self.commit_subject = pmail['subject'].replace('\r\n', '')\r
+ self.commit_subject = subject.replace('\r\n', '')\r
self.commit_subject = self.commit_subject.replace('\n', '')\r
self.commit_subject = self.subject_prefix_re.sub('', self.commit_subject, 1)\r
\r
+ self.author_email = pmail['from']\r
+\r
class CheckGitCommits:\r
"""Reads patches from git based on the specified git revision range.\r
\r
else:\r
blank_line = True\r
print('Checking git commit:', commit)\r
+ email = self.read_committer_email_address_from_git(commit)\r
+ self.ok &= EmailAddressCheck(email, 'Committer').ok\r
patch = self.read_patch_from_git(commit)\r
self.ok &= CheckOnePatch(commit, patch).ok\r
if not commits:\r
\r
def read_patch_from_git(self, commit):\r
# Run git to get the commit patch\r
- return self.run_git('show', '--pretty=email', commit)\r
+ return self.run_git('show', '--pretty=email', '--no-textconv',\r
+ '--no-use-mailmap', commit)\r
+\r
+ def read_committer_email_address_from_git(self, commit):\r
+ # Run git to get the committer email\r
+ return self.run_git('show', '--pretty=%cn <%ce>', '--no-patch',\r
+ '--no-use-mailmap', commit)\r
\r
def run_git(self, *args):\r
cmd = [ 'git' ]\r