diff options
| author | kx <kx@radix.pro> | 2024-06-04 06:43:08 +0300 |
|---|---|---|
| committer | kx <kx@radix.pro> | 2024-06-04 06:43:08 +0300 |
| commit | 7e2ccccace24636f29ddc210b94606abd4c7e42b (patch) | |
| tree | 545d43ea12671048956cafeb12303e84d601e571 /BaseTools/Scripts/PatchCheck.py | |
| parent | 27b044605cd5f6b33a3d231576003850b3fe305b (diff) | |
| download | edk2-trunk.tar.xz | |
Renormalized end-of-lines from master@27b044605cd5f6b33a3d231576003850b3fe305btrunk
Diffstat (limited to 'BaseTools/Scripts/PatchCheck.py')
| -rwxr-xr-x | BaseTools/Scripts/PatchCheck.py | 1770 |
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) |
