summaryrefslogtreecommitdiff
path: root/BaseTools/Scripts/PatchCheck.py
diff options
context:
space:
mode:
authorkx <kx@radix.pro>2024-06-04 06:43:08 +0300
committerkx <kx@radix.pro>2024-06-04 06:43:08 +0300
commit7e2ccccace24636f29ddc210b94606abd4c7e42b (patch)
tree545d43ea12671048956cafeb12303e84d601e571 /BaseTools/Scripts/PatchCheck.py
parent27b044605cd5f6b33a3d231576003850b3fe305b (diff)
downloadedk2-trunk.tar.xz
Renormalized end-of-lines from master@27b044605cd5f6b33a3d231576003850b3fe305btrunk
Diffstat (limited to 'BaseTools/Scripts/PatchCheck.py')
-rwxr-xr-xBaseTools/Scripts/PatchCheck.py1770
1 files changed, 885 insertions, 885 deletions
diff --git a/BaseTools/Scripts/PatchCheck.py b/BaseTools/Scripts/PatchCheck.py
index 762a1a89f9..00c7ef81ef 100755
--- a/BaseTools/Scripts/PatchCheck.py
+++ b/BaseTools/Scripts/PatchCheck.py
@@ -1,885 +1,885 @@
-## @file
-# Check a patch for various format issues
-#
-# Copyright (c) 2015 - 2021, Intel Corporation. All rights reserved.<BR>
-# Copyright (C) 2020, Red Hat, Inc.<BR>
-# Copyright (c) 2020 - 2023, Arm Limited. All rights reserved.<BR>
-#
-# SPDX-License-Identifier: BSD-2-Clause-Patent
-#
-
-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
-
-import email.header
-
-class Verbose:
- SILENT, ONELINE, NORMAL = range(3)
- level = NORMAL
-
-class PatchCheckConf:
- ignore_change_id = False
- ignore_multi_package = False
-
-class EmailAddressCheck:
- """Checks an email address."""
-
- def __init__(self, email, description):
- self.ok = True
-
- if email is None:
- self.error('Email address is missing!')
- return
- if description is None:
- self.error('Email description is missing!')
- return
-
- self.description = "'" + description + "'"
- self.check_email_address(email)
-
- def error(self, *err):
- if self.ok and Verbose.level > Verbose.ONELINE:
- print('The ' + self.description + ' email address 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
-
- 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))
-
- if mo.group(3) == 'devel@edk2.groups.io':
- self.error("Email rewritten by lists DMARC / DKIM / SPF: " +
- email)
-
- if ' via groups.io' in name.lower() and mo.group(3).endswith('@groups.io'):
- self.error("Email rewritten by lists DMARC / DKIM / SPF: " +
- email)
-
-class CommitMessageCheck:
- """Checks the contents of a git commit message."""
-
- def __init__(self, subject, message, author_email):
- self.ok = True
- self.ignore_multi_package = False
-
- if subject is None and message is None:
- self.error('Commit message is missing!')
- return
-
- MergifyMerge = False
- if "mergify[bot]@users.noreply.github.com" in author_email:
- if "Merge branch" in subject:
- MergifyMerge = True
-
- self.subject = subject
- self.msg = message
-
- print (subject)
-
- self.check_contributed_under()
- if not MergifyMerge:
- self.check_signed_off_by()
- self.check_misc_signatures()
- self.check_overall_format()
- if not PatchCheckConf.ignore_change_id:
- self.check_change_id_format()
- self.check_ci_options_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
-
- # Find 'contributed-under:' at the start of a line ignoring case and
- # requires ':' to be present. Matches if there is white space before
- # the tag or between the tag and the ':'.
- contributed_under_re = \
- re.compile(r'^\s*contributed-under\s*:', re.MULTILINE|re.IGNORECASE)
-
- def check_contributed_under(self):
- match = self.contributed_under_re.search(self.msg)
- if match is not None:
- self.error('Contributed-under! (Note: this must be ' +
- 'removed 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.ok &= EmailAddressCheck(s[3], sig).ok
-
- return sigs
-
- 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 sigtype in self.sig_types:
- sigs = self.find_signatures(sigtype)
-
- cve_re = re.compile('CVE-[0-9]{4}-[0-9]{5}[^0-9]')
-
- 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 re.search(self.cve_re, lines[0]):
- #
- # If CVE-xxxx-xxxxx is present in subject line, then limit length of
- # subject line to 92 characters
- #
- if len(lines[0].rstrip()) >= 93:
- self.error(
- 'First line of commit message (subject line) is too long (%d >= 93).' %
- (len(lines[0].rstrip()))
- )
- else:
- #
- # If CVE-xxxx-xxxxx is not present in subject line, then limit
- # length of subject line to 75 characters
- #
- if len(lines[0].rstrip()) >= 76:
- self.error(
- 'First line of commit message (subject line) is too long (%d >= 76).' %
- (len(lines[0].rstrip()))
- )
-
- 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:') and
- not lines[i].startswith('Reviewed-by') and
- not lines[i].startswith('Acked-by:') and
- not lines[i].startswith('Tested-by:') and
- not lines[i].startswith('Reported-by:') and
- not lines[i].startswith('Suggested-by:') and
- not lines[i].startswith('Signed-off-by:') and
- not lines[i].startswith('Cc:')):
- #
- # Print a warning if body line is longer than 75 characters
- #
- print(
- 'WARNING - Line %d of commit message is too long (%d >= 76).' %
- (i + 1, len(lines[i]))
- )
- print(lines[i])
-
- 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()
-
- def check_change_id_format(self):
- cid='Change-Id:'
- if self.msg.find(cid) != -1:
- self.error('\"%s\" found in commit message:' % cid)
- return
-
- def check_ci_options_format(self):
- cio='Continuous-integration-options:'
- for line in self.msg.splitlines():
- if not line.startswith(cio):
- continue
- options = line.split(':', 1)[1].split()
- if 'PatchCheck.ignore-multi-package' in options:
- self.ignore_multi_package = True
-
-(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
- self.new_bin = []
- 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.')
- if self.new_bin:
- print('\nWARNING - The following binary files will be added ' +
- 'into the repository:')
- for binary in self.new_bin:
- print(' ' + binary)
-
- 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\n') and \
- not line.startswith(r'\ No newline ') and not self.binary:
- 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.filename = line[13:].split(' ', 1)[0]
- self.is_newfile = False
- self.force_crlf = True
- self.force_notabs = True
- if self.filename.endswith('.rtf'):
- self.force_crlf = False
- self.force_notabs = False
- if self.filename.endswith('.sh') or \
- self.filename.startswith('BaseTools/BinWrappers/PosixLike/') or \
- self.filename.startswith('BaseTools/BinPipWrappers/PosixLike/') or \
- self.filename == 'BaseTools/BuildEnv':
- #
- # Do not enforce CR/LF line endings for linux shell scripts.
- # Some linux shell scripts don't end with the ".sh" extension,
- # they are identified by their path.
- #
- self.force_crlf = False
- if self.filename == '.gitmodules' or \
- self.filename == 'BaseTools/Conf/diff.order':
- #
- # .gitmodules and diff orderfiles are used internally by git
- # use tabs and LF line endings. Do not enforce no tabs and
- # do not enforce CR/LF line endings.
- #
- self.force_crlf = False
- self.force_notabs = False
- if os.path.basename(self.filename) == 'GNUmakefile' or \
- os.path.basename(self.filename).lower() == 'makefile' or \
- os.path.splitext(self.filename)[1] == '.makefile' or \
- self.filename.startswith(
- 'BaseTools/Source/C/VfrCompile/Pccts/'):
- self.force_notabs = False
- 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('@@ '):
- self.state = PATCH
- self.binary = False
- elif line.startswith('GIT binary patch') or \
- line.startswith('Binary files'):
- self.state = PATCH
- self.binary = True
- if self.is_newfile:
- self.new_bin.append(self.filename)
- elif line.startswith('new file mode 160000'):
- #
- # New submodule. Do not enforce CR/LF line endings
- #
- self.force_crlf = False
- else:
- ok = False
- self.is_newfile = self.newfile_prefix_re.match(line)
- 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 or self.filename.endswith(".rtf"):
- pass
- elif line.startswith('-'):
- pass
- elif line.startswith('+'):
- self.check_added_line(line[1:])
- elif line.startswith('\r\n'):
- pass
- 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 ',
- 'copy from ',
- 'copy to ',
- 'rename ',
- )
-
- line_endings = ('\r\n', '\n\r', '\n', '\r')
-
- newfile_prefix_re = \
- re.compile(r'''^
- index\ 0+\.\.
- ''',
- re.VERBOSE)
-
- def added_line_error(self, msg, line):
- lines = [ msg ]
- if self.filename is not None:
- lines.append('File: ' + self.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' and (line.find('Subproject commit') == -1):
- self.added_line_error('Line ending (%s) is not CRLF' % repr(eol),
- line)
- if self.force_notabs and '\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)
-
- rp_file = os.path.realpath(self.filename)
- rp_script = os.path.realpath(__file__)
- if line.find('__FUNCTION__') != -1 and rp_file != rp_script:
- self.added_line_error('__FUNCTION__ was used, but __func__ '
- '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()
-
- email_check = EmailAddressCheck(self.author_email, 'Author')
- email_ok = email_check.ok
-
- msg_check = CommitMessageCheck(self.commit_subject, self.commit_msg, self.author_email)
- msg_ok = msg_check.ok
- self.ignore_multi_package = msg_check.ignore_multi_package
-
- diff_ok = True
- if self.diff is not None:
- diff_check = GitDiffCheck(self.diff)
- diff_ok = diff_check.ok
-
- self.ok = email_ok and 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')
- #
- # Parse subject line from email header. The subject line may be
- # composed of multiple parts with different encodings. Decode and
- # combine all the parts to produce a single string with the contents of
- # the decoded subject line.
- #
- parts = email.header.decode_header(pmail.get('subject'))
- subject = ''
- for (part, encoding) in parts:
- if encoding:
- part = part.decode(encoding)
- else:
- try:
- part = part.decode()
- except:
- pass
- subject = subject + part
-
- self.commit_subject = subject.replace('\r\n', '')
- self.commit_subject = self.commit_subject.replace('\n', '')
- self.commit_subject = self.subject_prefix_re.sub('', self.commit_subject, 1)
-
- self.author_email = pmail['from']
-
-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):
- dec_files = self.read_dec_files_from_git()
- 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)
- email = self.read_committer_email_address_from_git(commit)
- self.ok &= EmailAddressCheck(email, 'Committer').ok
- patch = self.read_patch_from_git(commit)
- check_patch = CheckOnePatch(commit, patch)
- self.ok &= check_patch.ok
- ignore_multi_package = check_patch.ignore_multi_package
- if PatchCheckConf.ignore_multi_package:
- ignore_multi_package = True
- prefix = 'WARNING: ' if ignore_multi_package else ''
- check_parent = self.check_parent_packages (dec_files, commit, prefix)
- if not ignore_multi_package:
- self.ok &= check_parent
-
- if not commits:
- print("Couldn't find commit matching: '{}'".format(rev_spec))
-
- def check_parent_packages(self, dec_files, commit, prefix):
- ok = True
- modified = self.get_parent_packages (dec_files, commit, 'AM')
- if len (modified) > 1:
- print("{}The commit adds/modifies files in multiple packages:".format(prefix))
- print(" *", '\n * '.join(modified))
- ok = False
- deleted = self.get_parent_packages (dec_files, commit, 'D')
- if len (deleted) > 1:
- print("{}The commit deletes files from multiple packages:".format(prefix))
- print(" *", '\n * '.join(deleted))
- ok = False
- return ok
-
- def get_parent_packages(self, dec_files, commit, filter):
- filelist = self.read_files_modified_from_git (commit, filter)
- parents = set()
- for file in filelist:
- dec_found = False
- for dec_file in dec_files:
- if os.path.commonpath([dec_file, file]):
- dec_found = True
- parents.add(dec_file)
- if not dec_found and os.path.dirname (file):
- # No DEC file found and file is in a subdir
- # Covers BaseTools, .github, .azurepipelines, .pytool
- parents.add(file.split('/')[0])
- return list(parents)
-
- def read_dec_files_from_git(self):
- # run git ls-files *.dec
- out = self.run_git('ls-files', '*.dec')
- # return list of .dec files
- try:
- return out.split()
- except:
- return []
-
- def read_files_modified_from_git(self, commit, filter):
- # run git diff-tree --no-commit-id --name-only -r <commit>
- out = self.run_git('diff-tree', '--no-commit-id', '--name-only',
- '--diff-filter=' + filter, '-r', commit)
- try:
- return out.split()
- except:
- return []
-
- 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() if out else []
-
- def read_patch_from_git(self, commit):
- # Run git to get the commit patch
- return self.run_git('show', '--pretty=email', '--no-textconv',
- '--no-use-mailmap', commit)
-
- def read_committer_email_address_from_git(self, commit):
- # Run git to get the committer email
- return self.run_git('show', '--pretty=%cn <%ce>', '--no-patch',
- '--no-use-mailmap', commit)
-
- def run_git(self, *args):
- cmd = [ 'git' ]
- cmd += args
- p = subprocess.Popen(cmd,
- stdout=subprocess.PIPE,
- stderr=subprocess.STDOUT)
- Result = p.communicate()
- return Result[0].decode('utf-8', 'ignore') if Result[0] and Result[0].find(b"fatal")!=0 else None
-
-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")
- group.add_argument("--ignore-change-id",
- action="store_true",
- help="Ignore the presence of 'Change-Id:' tags in commit message")
- group.add_argument("--ignore-multi-package",
- action="store_true",
- help="Ignore if commit modifies files in multiple packages")
- self.args = parser.parse_args()
- if self.args.oneline:
- Verbose.level = Verbose.ONELINE
- if self.args.silent:
- Verbose.level = Verbose.SILENT
- if self.args.ignore_change_id:
- PatchCheckConf.ignore_change_id = True
- if self.args.ignore_multi_package:
- PatchCheckConf.ignore_multi_package = True
-
-if __name__ == "__main__":
- sys.exit(PatchCheckApp().retval)
+## @file
+# Check a patch for various format issues
+#
+# Copyright (c) 2015 - 2021, Intel Corporation. All rights reserved.<BR>
+# Copyright (C) 2020, Red Hat, Inc.<BR>
+# Copyright (c) 2020 - 2023, Arm Limited. All rights reserved.<BR>
+#
+# SPDX-License-Identifier: BSD-2-Clause-Patent
+#
+
+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
+
+import email.header
+
+class Verbose:
+ SILENT, ONELINE, NORMAL = range(3)
+ level = NORMAL
+
+class PatchCheckConf:
+ ignore_change_id = False
+ ignore_multi_package = False
+
+class EmailAddressCheck:
+ """Checks an email address."""
+
+ def __init__(self, email, description):
+ self.ok = True
+
+ if email is None:
+ self.error('Email address is missing!')
+ return
+ if description is None:
+ self.error('Email description is missing!')
+ return
+
+ self.description = "'" + description + "'"
+ self.check_email_address(email)
+
+ def error(self, *err):
+ if self.ok and Verbose.level > Verbose.ONELINE:
+ print('The ' + self.description + ' email address 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
+
+ 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))
+
+ if mo.group(3) == 'devel@edk2.groups.io':
+ self.error("Email rewritten by lists DMARC / DKIM / SPF: " +
+ email)
+
+ if ' via groups.io' in name.lower() and mo.group(3).endswith('@groups.io'):
+ self.error("Email rewritten by lists DMARC / DKIM / SPF: " +
+ email)
+
+class CommitMessageCheck:
+ """Checks the contents of a git commit message."""
+
+ def __init__(self, subject, message, author_email):
+ self.ok = True
+ self.ignore_multi_package = False
+
+ if subject is None and message is None:
+ self.error('Commit message is missing!')
+ return
+
+ MergifyMerge = False
+ if "mergify[bot]@users.noreply.github.com" in author_email:
+ if "Merge branch" in subject:
+ MergifyMerge = True
+
+ self.subject = subject
+ self.msg = message
+
+ print (subject)
+
+ self.check_contributed_under()
+ if not MergifyMerge:
+ self.check_signed_off_by()
+ self.check_misc_signatures()
+ self.check_overall_format()
+ if not PatchCheckConf.ignore_change_id:
+ self.check_change_id_format()
+ self.check_ci_options_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
+
+ # Find 'contributed-under:' at the start of a line ignoring case and
+ # requires ':' to be present. Matches if there is white space before
+ # the tag or between the tag and the ':'.
+ contributed_under_re = \
+ re.compile(r'^\s*contributed-under\s*:', re.MULTILINE|re.IGNORECASE)
+
+ def check_contributed_under(self):
+ match = self.contributed_under_re.search(self.msg)
+ if match is not None:
+ self.error('Contributed-under! (Note: this must be ' +
+ 'removed 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.ok &= EmailAddressCheck(s[3], sig).ok
+
+ return sigs
+
+ 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 sigtype in self.sig_types:
+ sigs = self.find_signatures(sigtype)
+
+ cve_re = re.compile('CVE-[0-9]{4}-[0-9]{5}[^0-9]')
+
+ 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 re.search(self.cve_re, lines[0]):
+ #
+ # If CVE-xxxx-xxxxx is present in subject line, then limit length of
+ # subject line to 92 characters
+ #
+ if len(lines[0].rstrip()) >= 93:
+ self.error(
+ 'First line of commit message (subject line) is too long (%d >= 93).' %
+ (len(lines[0].rstrip()))
+ )
+ else:
+ #
+ # If CVE-xxxx-xxxxx is not present in subject line, then limit
+ # length of subject line to 75 characters
+ #
+ if len(lines[0].rstrip()) >= 76:
+ self.error(
+ 'First line of commit message (subject line) is too long (%d >= 76).' %
+ (len(lines[0].rstrip()))
+ )
+
+ 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:') and
+ not lines[i].startswith('Reviewed-by') and
+ not lines[i].startswith('Acked-by:') and
+ not lines[i].startswith('Tested-by:') and
+ not lines[i].startswith('Reported-by:') and
+ not lines[i].startswith('Suggested-by:') and
+ not lines[i].startswith('Signed-off-by:') and
+ not lines[i].startswith('Cc:')):
+ #
+ # Print a warning if body line is longer than 75 characters
+ #
+ print(
+ 'WARNING - Line %d of commit message is too long (%d >= 76).' %
+ (i + 1, len(lines[i]))
+ )
+ print(lines[i])
+
+ 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()
+
+ def check_change_id_format(self):
+ cid='Change-Id:'
+ if self.msg.find(cid) != -1:
+ self.error('\"%s\" found in commit message:' % cid)
+ return
+
+ def check_ci_options_format(self):
+ cio='Continuous-integration-options:'
+ for line in self.msg.splitlines():
+ if not line.startswith(cio):
+ continue
+ options = line.split(':', 1)[1].split()
+ if 'PatchCheck.ignore-multi-package' in options:
+ self.ignore_multi_package = True
+
+(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
+ self.new_bin = []
+ 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.')
+ if self.new_bin:
+ print('\nWARNING - The following binary files will be added ' +
+ 'into the repository:')
+ for binary in self.new_bin:
+ print(' ' + binary)
+
+ 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\n') and \
+ not line.startswith(r'\ No newline ') and not self.binary:
+ 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.filename = line[13:].split(' ', 1)[0]
+ self.is_newfile = False
+ self.force_crlf = True
+ self.force_notabs = True
+ if self.filename.endswith('.rtf'):
+ self.force_crlf = False
+ self.force_notabs = False
+ if self.filename.endswith('.sh') or \
+ self.filename.startswith('BaseTools/BinWrappers/PosixLike/') or \
+ self.filename.startswith('BaseTools/BinPipWrappers/PosixLike/') or \
+ self.filename == 'BaseTools/BuildEnv':
+ #
+ # Do not enforce CR/LF line endings for linux shell scripts.
+ # Some linux shell scripts don't end with the ".sh" extension,
+ # they are identified by their path.
+ #
+ self.force_crlf = False
+ if self.filename == '.gitmodules' or \
+ self.filename == 'BaseTools/Conf/diff.order':
+ #
+ # .gitmodules and diff orderfiles are used internally by git
+ # use tabs and LF line endings. Do not enforce no tabs and
+ # do not enforce CR/LF line endings.
+ #
+ self.force_crlf = False
+ self.force_notabs = False
+ if os.path.basename(self.filename) == 'GNUmakefile' or \
+ os.path.basename(self.filename).lower() == 'makefile' or \
+ os.path.splitext(self.filename)[1] == '.makefile' or \
+ self.filename.startswith(
+ 'BaseTools/Source/C/VfrCompile/Pccts/'):
+ self.force_notabs = False
+ 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('@@ '):
+ self.state = PATCH
+ self.binary = False
+ elif line.startswith('GIT binary patch') or \
+ line.startswith('Binary files'):
+ self.state = PATCH
+ self.binary = True
+ if self.is_newfile:
+ self.new_bin.append(self.filename)
+ elif line.startswith('new file mode 160000'):
+ #
+ # New submodule. Do not enforce CR/LF line endings
+ #
+ self.force_crlf = False
+ else:
+ ok = False
+ self.is_newfile = self.newfile_prefix_re.match(line)
+ 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 or self.filename.endswith(".rtf"):
+ pass
+ elif line.startswith('-'):
+ pass
+ elif line.startswith('+'):
+ self.check_added_line(line[1:])
+ elif line.startswith('\r\n'):
+ pass
+ 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 ',
+ 'copy from ',
+ 'copy to ',
+ 'rename ',
+ )
+
+ line_endings = ('\r\n', '\n\r', '\n', '\r')
+
+ newfile_prefix_re = \
+ re.compile(r'''^
+ index\ 0+\.\.
+ ''',
+ re.VERBOSE)
+
+ def added_line_error(self, msg, line):
+ lines = [ msg ]
+ if self.filename is not None:
+ lines.append('File: ' + self.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' and (line.find('Subproject commit') == -1):
+ self.added_line_error('Line ending (%s) is not CRLF' % repr(eol),
+ line)
+ if self.force_notabs and '\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)
+
+ rp_file = os.path.realpath(self.filename)
+ rp_script = os.path.realpath(__file__)
+ if line.find('__FUNCTION__') != -1 and rp_file != rp_script:
+ self.added_line_error('__FUNCTION__ was used, but __func__ '
+ '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()
+
+ email_check = EmailAddressCheck(self.author_email, 'Author')
+ email_ok = email_check.ok
+
+ msg_check = CommitMessageCheck(self.commit_subject, self.commit_msg, self.author_email)
+ msg_ok = msg_check.ok
+ self.ignore_multi_package = msg_check.ignore_multi_package
+
+ diff_ok = True
+ if self.diff is not None:
+ diff_check = GitDiffCheck(self.diff)
+ diff_ok = diff_check.ok
+
+ self.ok = email_ok and 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')
+ #
+ # Parse subject line from email header. The subject line may be
+ # composed of multiple parts with different encodings. Decode and
+ # combine all the parts to produce a single string with the contents of
+ # the decoded subject line.
+ #
+ parts = email.header.decode_header(pmail.get('subject'))
+ subject = ''
+ for (part, encoding) in parts:
+ if encoding:
+ part = part.decode(encoding)
+ else:
+ try:
+ part = part.decode()
+ except:
+ pass
+ subject = subject + part
+
+ self.commit_subject = subject.replace('\r\n', '')
+ self.commit_subject = self.commit_subject.replace('\n', '')
+ self.commit_subject = self.subject_prefix_re.sub('', self.commit_subject, 1)
+
+ self.author_email = pmail['from']
+
+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):
+ dec_files = self.read_dec_files_from_git()
+ 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)
+ email = self.read_committer_email_address_from_git(commit)
+ self.ok &= EmailAddressCheck(email, 'Committer').ok
+ patch = self.read_patch_from_git(commit)
+ check_patch = CheckOnePatch(commit, patch)
+ self.ok &= check_patch.ok
+ ignore_multi_package = check_patch.ignore_multi_package
+ if PatchCheckConf.ignore_multi_package:
+ ignore_multi_package = True
+ prefix = 'WARNING: ' if ignore_multi_package else ''
+ check_parent = self.check_parent_packages (dec_files, commit, prefix)
+ if not ignore_multi_package:
+ self.ok &= check_parent
+
+ if not commits:
+ print("Couldn't find commit matching: '{}'".format(rev_spec))
+
+ def check_parent_packages(self, dec_files, commit, prefix):
+ ok = True
+ modified = self.get_parent_packages (dec_files, commit, 'AM')
+ if len (modified) > 1:
+ print("{}The commit adds/modifies files in multiple packages:".format(prefix))
+ print(" *", '\n * '.join(modified))
+ ok = False
+ deleted = self.get_parent_packages (dec_files, commit, 'D')
+ if len (deleted) > 1:
+ print("{}The commit deletes files from multiple packages:".format(prefix))
+ print(" *", '\n * '.join(deleted))
+ ok = False
+ return ok
+
+ def get_parent_packages(self, dec_files, commit, filter):
+ filelist = self.read_files_modified_from_git (commit, filter)
+ parents = set()
+ for file in filelist:
+ dec_found = False
+ for dec_file in dec_files:
+ if os.path.commonpath([dec_file, file]):
+ dec_found = True
+ parents.add(dec_file)
+ if not dec_found and os.path.dirname (file):
+ # No DEC file found and file is in a subdir
+ # Covers BaseTools, .github, .azurepipelines, .pytool
+ parents.add(file.split('/')[0])
+ return list(parents)
+
+ def read_dec_files_from_git(self):
+ # run git ls-files *.dec
+ out = self.run_git('ls-files', '*.dec')
+ # return list of .dec files
+ try:
+ return out.split()
+ except:
+ return []
+
+ def read_files_modified_from_git(self, commit, filter):
+ # run git diff-tree --no-commit-id --name-only -r <commit>
+ out = self.run_git('diff-tree', '--no-commit-id', '--name-only',
+ '--diff-filter=' + filter, '-r', commit)
+ try:
+ return out.split()
+ except:
+ return []
+
+ 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() if out else []
+
+ def read_patch_from_git(self, commit):
+ # Run git to get the commit patch
+ return self.run_git('show', '--pretty=email', '--no-textconv',
+ '--no-use-mailmap', commit)
+
+ def read_committer_email_address_from_git(self, commit):
+ # Run git to get the committer email
+ return self.run_git('show', '--pretty=%cn <%ce>', '--no-patch',
+ '--no-use-mailmap', commit)
+
+ def run_git(self, *args):
+ cmd = [ 'git' ]
+ cmd += args
+ p = subprocess.Popen(cmd,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT)
+ Result = p.communicate()
+ return Result[0].decode('utf-8', 'ignore') if Result[0] and Result[0].find(b"fatal")!=0 else None
+
+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")
+ group.add_argument("--ignore-change-id",
+ action="store_true",
+ help="Ignore the presence of 'Change-Id:' tags in commit message")
+ group.add_argument("--ignore-multi-package",
+ action="store_true",
+ help="Ignore if commit modifies files in multiple packages")
+ self.args = parser.parse_args()
+ if self.args.oneline:
+ Verbose.level = Verbose.ONELINE
+ if self.args.silent:
+ Verbose.level = Verbose.SILENT
+ if self.args.ignore_change_id:
+ PatchCheckConf.ignore_change_id = True
+ if self.args.ignore_multi_package:
+ PatchCheckConf.ignore_multi_package = True
+
+if __name__ == "__main__":
+ sys.exit(PatchCheckApp().retval)