]>
git.proxmox.com Git - mirror_edk2.git/blob - BaseTools/Scripts/PatchCheck.py
ca0849b77bbee5ecca9409fd81f9e18a6bbbf881
2 # Check a patch for various format issues
4 # Copyright (c) 2015 - 2020, Intel Corporation. All rights reserved.<BR>
5 # Copyright (C) 2020, Red Hat, Inc.<BR>
6 # Copyright (c) 2020, ARM Ltd. All rights reserved.<BR>
8 # SPDX-License-Identifier: BSD-2-Clause-Patent
11 from __future__
import print_function
14 __copyright__
= "Copyright (c) 2015 - 2016, Intel Corporation All rights reserved."
26 SILENT
, ONELINE
, NORMAL
= range(3)
29 class EmailAddressCheck
:
30 """Checks an email address."""
32 def __init__(self
, email
, description
):
36 self
.error('Email address is missing!')
38 if description
is None:
39 self
.error('Email description is missing!')
42 self
.description
= "'" + description
+ "'"
43 self
.check_email_address(email
)
45 def error(self
, *err
):
46 if self
.ok
and Verbose
.level
> Verbose
.ONELINE
:
47 print('The ' + self
.description
+ ' email address is not valid:')
49 if Verbose
.level
< Verbose
.NORMAL
:
53 prefix
= (' *', ' ')[count
> 0]
57 email_re1
= re
.compile(r
'(?:\s*)(.*?)(\s*)<(.+)>\s*$',
58 re
.MULTILINE|re
.IGNORECASE
)
60 def check_email_address(self
, email
):
62 mo
= self
.email_re1
.match(email
)
64 self
.error("Email format is invalid: " + email
.strip())
67 name
= mo
.group(1).strip()
69 self
.error("Name is not provided with email address: " +
72 quoted
= len(name
) > 2 and name
[0] == '"' and name
[-1] == '"'
73 if name
.find(',') >= 0 and not quoted
:
74 self
.error('Add quotes (") around name with a comma: ' +
78 self
.error("There should be a space between the name and " +
79 "email address: " + email
)
81 if mo
.group(3).find(' ') >= 0:
82 self
.error("The email address cannot contain a space: " +
85 if ' via Groups.Io' in name
and mo
.group(3).endswith('@groups.io'):
86 self
.error("Email rewritten by lists DMARC / DKIM / SPF: " +
89 class CommitMessageCheck
:
90 """Checks the contents of a git commit message."""
92 def __init__(self
, subject
, message
):
95 if subject
is None and message
is None:
96 self
.error('Commit message is missing!')
99 self
.subject
= subject
104 self
.check_contributed_under()
105 self
.check_signed_off_by()
106 self
.check_misc_signatures()
107 self
.check_overall_format()
108 self
.report_message_result()
110 url
= 'https://github.com/tianocore/tianocore.github.io/wiki/Commit-Message-Format'
112 def report_message_result(self
):
113 if Verbose
.level
< Verbose
.NORMAL
:
118 print('The commit message format passed all checks.')
124 def error(self
, *err
):
125 if self
.ok
and Verbose
.level
> Verbose
.ONELINE
:
126 print('The commit message format is not valid:')
128 if Verbose
.level
< Verbose
.NORMAL
:
132 prefix
= (' *', ' ')[count
> 0]
136 # Find 'contributed-under:' at the start of a line ignoring case and
137 # requires ':' to be present. Matches if there is white space before
138 # the tag or between the tag and the ':'.
139 contributed_under_re
= \
140 re
.compile(r
'^\s*contributed-under\s*:', re
.MULTILINE|re
.IGNORECASE
)
142 def check_contributed_under(self
):
143 match
= self
.contributed_under_re
.search(self
.msg
)
144 if match
is not None:
145 self
.error('Contributed-under! (Note: this must be ' +
146 'removed by the code contributor!)')
149 def make_signature_re(sig
, re_input
=False):
153 sub_re
= sig
.replace('-', r
'[-\s]+')
154 re_str
= (r
'^(?P<tag>' + sub_re
+
155 r
')(\s*):(\s*)(?P<value>\S.*?)(?:\s*)$')
157 return re
.compile(re_str
, re
.MULTILINE|re
.IGNORECASE
)
159 print("Tried to compile re:", re_str
)
164 (?: (?P<tag>[^:]+) \s* : \s*
167 (?: \[ (?P<updater>[^:]+) \s* : \s*
168 (?P<note>.+?) \s* \] )
170 re
.VERBOSE | re
.MULTILINE
)
172 def find_signatures(self
, sig
):
173 if not sig
.endswith('-by') and sig
!= 'Cc':
175 regex
= self
.make_signature_re(sig
)
177 sigs
= regex
.findall(self
.msg
)
179 bad_case_sigs
= filter(lambda m
: m
[0] != sig
, sigs
)
180 for s
in bad_case_sigs
:
181 self
.error("'" +s
[0] + "' should be '" + sig
+ "'")
185 self
.error('There should be no spaces between ' + sig
+
188 self
.error("There should be a space after '" + sig
+ ":'")
190 EmailAddressCheck(s
[3], sig
)
194 def check_signed_off_by(self
):
196 if self
.msg
.find(sob
) < 0:
197 self
.error('Missing Signed-off-by! (Note: this must be ' +
198 'added by the code contributor!)')
201 sobs
= self
.find_signatures('Signed-off')
204 self
.error('Invalid Signed-off-by format!')
216 def check_misc_signatures(self
):
217 for sig
in self
.sig_types
:
218 self
.find_signatures(sig
)
220 cve_re
= re
.compile('CVE-[0-9]{4}-[0-9]{5}[^0-9]')
222 def check_overall_format(self
):
223 lines
= self
.msg
.splitlines()
225 if len(lines
) >= 1 and lines
[0].endswith('\r\n'):
230 lines
.insert(0, empty_line
)
231 lines
.insert(0, self
.subject
+ empty_line
)
236 self
.error('Empty commit message!')
239 if count
>= 1 and re
.search(self
.cve_re
, lines
[0]):
241 # If CVE-xxxx-xxxxx is present in subject line, then limit length of
242 # subject line to 92 characters
244 if len(lines
[0].rstrip()) >= 93:
246 'First line of commit message (subject line) is too long (%d >= 93).' %
247 (len(lines
[0].rstrip()))
251 # If CVE-xxxx-xxxxx is not present in subject line, then limit
252 # length of subject line to 75 characters
254 if len(lines
[0].rstrip()) >= 76:
256 'First line of commit message (subject line) is too long (%d >= 76).' %
257 (len(lines
[0].rstrip()))
260 if count
>= 1 and len(lines
[0].strip()) == 0:
261 self
.error('First line of commit message (subject line) ' +
264 if count
>= 2 and lines
[1].strip() != '':
265 self
.error('Second line of commit message should be ' +
268 for i
in range(2, count
):
269 if (len(lines
[i
]) >= 76 and
270 len(lines
[i
].split()) > 1 and
271 not lines
[i
].startswith('git-svn-id:') and
272 not lines
[i
].startswith('Reviewed-by') and
273 not lines
[i
].startswith('Acked-by:') and
274 not lines
[i
].startswith('Tested-by:') and
275 not lines
[i
].startswith('Reported-by:') and
276 not lines
[i
].startswith('Suggested-by:') and
277 not lines
[i
].startswith('Signed-off-by:') and
278 not lines
[i
].startswith('Cc:')):
280 # Print a warning if body line is longer than 75 characters
283 'WARNING - Line %d of commit message is too long (%d >= 76).' %
284 (i
+ 1, len(lines
[i
]))
289 for i
in range(count
- 1, 0, -1):
291 mo
= self
.sig_block_re
.match(line
)
293 if line
.strip() == '':
295 elif last_sig_line
is not None:
296 err2
= 'Add empty line before "%s"?' % last_sig_line
297 self
.error('The line before the signature block ' +
298 'should be empty', err2
)
300 self
.error('The signature block was not found')
302 last_sig_line
= line
.strip()
304 (START
, PRE_PATCH
, PATCH
) = range(3)
307 """Checks the contents of a git diff."""
309 def __init__(self
, diff
):
311 self
.format_ok
= True
312 self
.lines
= diff
.splitlines(True)
313 self
.count
= len(self
.lines
)
317 self
.LicenseCheck(self
.lines
, self
.count
)
318 while self
.line_num
< self
.count
and self
.format_ok
:
319 line_num
= self
.line_num
321 assert(self
.line_num
> line_num
)
322 self
.report_message_result()
324 def LicenseCheck(self
, lines
, count
):
326 self
.startcheck
= False
330 if line
.startswith('--- /dev/null'):
331 nextline
= lines
[line_index
+ 1]
332 added_file
= self
.Readdedfileformat
.search(nextline
).group(1)
333 added_file_extension
= os
.path
.splitext(added_file
)[1]
334 if added_file_extension
in self
.file_extension_list
:
335 self
.startcheck
= True
337 if self
.startcheck
and self
.license_format_preflix
in line
:
338 if self
.bsd2_patent
in line
or self
.bsd3_patent
in line
:
341 for optional_license
in self
.license_optional_list
:
342 if optional_license
in line
:
344 self
.warning(added_file
)
345 if line_index
+ 1 == count
or lines
[line_index
+ 1].startswith('diff --') and self
.startcheck
:
347 error_message
= "Invalid License in: " + added_file
348 self
.error(error_message
)
349 self
.startcheck
= False
351 line_index
= line_index
+ 1
353 def warning(self
, *err
):
356 warning_format
= 'Warning: License accepted but not BSD plus patent license in'
357 print(warning_format
, line
)
360 def report_message_result(self
):
361 if Verbose
.level
< Verbose
.NORMAL
:
364 print('The code passed all checks.')
366 print('\nWARNING - The following binary files will be added ' +
367 'into the repository:')
368 for binary
in self
.new_bin
:
372 line
= self
.lines
[self
.line_num
]
374 if self
.state
in (PRE_PATCH
, PATCH
):
375 if line
.startswith('diff --git'):
377 if self
.state
== PATCH
:
378 if line
.startswith('@@ '):
379 self
.state
= PRE_PATCH
380 elif len(line
) >= 1 and line
[0] not in ' -+' and \
381 not line
.startswith('\r\n') and \
382 not line
.startswith(r
'\ No newline ') and not self
.binary
:
383 for line
in self
.lines
[self
.line_num
+ 1:]:
384 if line
.startswith('diff --git'):
385 self
.format_error('diff found after end of patch')
387 self
.line_num
= self
.count
390 if self
.state
== START
:
391 if line
.startswith('diff --git'):
392 self
.state
= PRE_PATCH
393 self
.filename
= line
[13:].split(' ', 1)[0]
394 self
.is_newfile
= False
395 self
.force_crlf
= True
396 self
.force_notabs
= True
397 if self
.filename
.endswith('.sh') or \
398 self
.filename
.startswith('BaseTools/BinWrappers/PosixLike/') or \
399 self
.filename
.startswith('BaseTools/Bin/CYGWIN_NT-5.1-i686/') or \
400 self
.filename
== 'BaseTools/BuildEnv':
402 # Do not enforce CR/LF line endings for linux shell scripts.
403 # Some linux shell scripts don't end with the ".sh" extension,
404 # they are identified by their path.
406 self
.force_crlf
= False
407 if self
.filename
== '.gitmodules' or \
408 self
.filename
== 'BaseTools/Conf/diff.order':
410 # .gitmodules and diff orderfiles are used internally by git
411 # use tabs and LF line endings. Do not enforce no tabs and
412 # do not enforce CR/LF line endings.
414 self
.force_crlf
= False
415 self
.force_notabs
= False
416 elif len(line
.rstrip()) != 0:
417 self
.format_error("didn't find diff command")
419 elif self
.state
== PRE_PATCH
:
420 if line
.startswith('@@ '):
423 elif line
.startswith('GIT binary patch') or \
424 line
.startswith('Binary files'):
428 self
.new_bin
.append(self
.filename
)
429 elif line
.startswith('new file mode 160000'):
431 # New submodule. Do not enforce CR/LF line endings
433 self
.force_crlf
= False
436 self
.is_newfile
= self
.newfile_prefix_re
.match(line
)
437 for pfx
in self
.pre_patch_prefixes
:
438 if line
.startswith(pfx
):
441 self
.format_error("didn't find diff hunk marker (@@)")
443 elif self
.state
== PATCH
:
446 elif line
.startswith('-'):
448 elif line
.startswith('+'):
449 self
.check_added_line(line
[1:])
450 elif line
.startswith('\r\n'):
452 elif line
.startswith(r
'\ No newline '):
454 elif not line
.startswith(' '):
455 self
.format_error("unexpected patch line")
458 pre_patch_prefixes
= (
472 line_endings
= ('\r\n', '\n\r', '\n', '\r')
474 newfile_prefix_re
= \
480 def added_line_error(self
, msg
, line
):
482 if self
.filename
is not None:
483 lines
.append('File: ' + self
.filename
)
484 lines
.append('Line: ' + line
)
490 DEBUG \s* \( \s* \( \s*
491 (?: DEBUG_[A-Z_]+ \s* \| \s*)*
496 def check_added_line(self
, line
):
498 for an_eol
in self
.line_endings
:
499 if line
.endswith(an_eol
):
501 line
= line
[:-len(eol
)]
503 stripped
= line
.rstrip()
505 if self
.force_crlf
and eol
!= '\r\n' and (line
.find('Subproject commit') == -1):
506 self
.added_line_error('Line ending (%s) is not CRLF' % repr(eol
),
508 if self
.force_notabs
and '\t' in line
:
509 self
.added_line_error('Tab character used', line
)
510 if len(stripped
) < len(line
):
511 self
.added_line_error('Trailing whitespace found', line
)
513 mo
= self
.old_debug_re
.search(line
)
515 self
.added_line_error('EFI_D_' + mo
.group(1) + ' was used, '
516 'but DEBUG_' + mo
.group(1) +
517 ' is now recommended', line
)
519 split_diff_re
= re
.compile(r
'''
521 ^ diff \s+ --git \s+ a/.+ \s+ b/.+ $
527 re
.IGNORECASE | re
.VERBOSE | re
.MULTILINE
)
529 def format_error(self
, err
):
530 self
.format_ok
= False
531 err
= 'Patch format error: ' + err
532 err2
= 'Line: ' + self
.lines
[self
.line_num
].rstrip()
533 self
.error(err
, err2
)
535 def error(self
, *err
):
536 if self
.ok
and Verbose
.level
> Verbose
.ONELINE
:
537 print('Code format is not valid:')
539 if Verbose
.level
< Verbose
.NORMAL
:
543 prefix
= (' *', ' ')[count
> 0]
547 license_format_preflix
= 'SPDX-License-Identifier'
549 bsd2_patent
= 'BSD-2-Clause-Patent'
551 bsd3_patent
= 'BSD-3-Clause-Patent'
553 license_optional_list
= ['BSD-2-Clause', 'BSD-3-Clause', 'MIT', 'Python-2.0', 'Zlib']
555 Readdedfileformat
= re
.compile(r
'\+\+\+ b\/(.*)\n')
557 file_extension_list
= [".c", ".h", ".inf", ".dsc", ".dec", ".py", ".bat", ".sh", ".uni", ".yaml", ".fdf", ".inc", "yml", ".asm", \
558 ".asm16", ".asl", ".vfr", ".s", ".S", ".aslc", ".nasm", ".nasmb", ".idf", ".Vfr", ".H"]
561 """Checks the contents of a git email formatted patch.
563 Various checks are performed on both the commit message and the
567 def __init__(self
, name
, patch
):
569 self
.find_patch_pieces()
571 email_check
= EmailAddressCheck(self
.author_email
, 'Author')
572 email_ok
= email_check
.ok
574 msg_check
= CommitMessageCheck(self
.commit_subject
, self
.commit_msg
)
575 msg_ok
= msg_check
.ok
578 if self
.diff
is not None:
579 diff_check
= GitDiffCheck(self
.diff
)
580 diff_ok
= diff_check
.ok
582 self
.ok
= email_ok
and msg_ok
and diff_ok
584 if Verbose
.level
== Verbose
.ONELINE
:
590 result
.append('commit message')
592 result
.append('diff content')
593 result
= 'bad ' + ' and '.join(result
)
597 git_diff_re
= re
.compile(r
'''
598 ^ diff \s+ --git \s+ a/.+ \s+ b/.+ $
600 re
.IGNORECASE | re
.VERBOSE | re
.MULTILINE
)
604 (?P<commit_message> [\s\S\r\n]* )
607 (?: ^ \s+ .+ \s+ \| \s+ \d+ \s+ \+* \-*
612 re
.IGNORECASE | re
.VERBOSE | re
.MULTILINE
)
614 subject_prefix_re
= \
617 [^\[\]]* # Allow all non-brackets
622 def find_patch_pieces(self
):
623 if sys
.version_info
< (3, 0):
624 patch
= self
.patch
.encode('ascii', 'ignore')
628 self
.commit_msg
= None
630 self
.commit_subject
= None
631 self
.commit_prefix
= None
634 if patch
.startswith('diff --git'):
638 pmail
= email
.message_from_string(patch
)
639 parts
= list(pmail
.walk())
640 assert(len(parts
) == 1)
641 assert(parts
[0].get_content_type() == 'text/plain')
642 content
= parts
[0].get_payload(decode
=True).decode('utf-8', 'ignore')
644 mo
= self
.git_diff_re
.search(content
)
646 self
.diff
= content
[mo
.start():]
647 content
= content
[:mo
.start()]
649 mo
= self
.stat_re
.search(content
)
651 self
.commit_msg
= content
653 self
.stat
= mo
.group('stat')
654 self
.commit_msg
= mo
.group('commit_message')
656 # Parse subject line from email header. The subject line may be
657 # composed of multiple parts with different encodings. Decode and
658 # combine all the parts to produce a single string with the contents of
659 # the decoded subject line.
661 parts
= email
.header
.decode_header(pmail
.get('subject'))
663 for (part
, encoding
) in parts
:
665 part
= part
.decode(encoding
)
671 subject
= subject
+ part
673 self
.commit_subject
= subject
.replace('\r\n', '')
674 self
.commit_subject
= self
.commit_subject
.replace('\n', '')
675 self
.commit_subject
= self
.subject_prefix_re
.sub('', self
.commit_subject
, 1)
677 self
.author_email
= pmail
['from']
679 class CheckGitCommits
:
680 """Reads patches from git based on the specified git revision range.
682 The patches are read from git, and then checked.
685 def __init__(self
, rev_spec
, max_count
):
686 commits
= self
.read_commit_list_from_git(rev_spec
, max_count
)
687 if len(commits
) == 1 and Verbose
.level
> Verbose
.ONELINE
:
688 commits
= [ rev_spec
]
691 for commit
in commits
:
692 if Verbose
.level
> Verbose
.ONELINE
:
697 print('Checking git commit:', commit
)
698 email
= self
.read_committer_email_address_from_git(commit
)
699 self
.ok
&= EmailAddressCheck(email
, 'Committer').ok
700 patch
= self
.read_patch_from_git(commit
)
701 self
.ok
&= CheckOnePatch(commit
, patch
).ok
703 print("Couldn't find commit matching: '{}'".format(rev_spec
))
705 def read_commit_list_from_git(self
, rev_spec
, max_count
):
706 # Run git to get the commit patch
707 cmd
= [ 'rev-list', '--abbrev-commit', '--no-walk' ]
708 if max_count
is not None:
709 cmd
.append('--max-count=' + str(max_count
))
711 out
= self
.run_git(*cmd
)
712 return out
.split() if out
else []
714 def read_patch_from_git(self
, commit
):
715 # Run git to get the commit patch
716 return self
.run_git('show', '--pretty=email', '--no-textconv',
717 '--no-use-mailmap', commit
)
719 def read_committer_email_address_from_git(self
, commit
):
720 # Run git to get the committer email
721 return self
.run_git('show', '--pretty=%cn <%ce>', '--no-patch',
722 '--no-use-mailmap', commit
)
724 def run_git(self
, *args
):
727 p
= subprocess
.Popen(cmd
,
728 stdout
=subprocess
.PIPE
,
729 stderr
=subprocess
.STDOUT
)
730 Result
= p
.communicate()
731 return Result
[0].decode('utf-8', 'ignore') if Result
[0] and Result
[0].find(b
"fatal")!=0 else None
733 class CheckOnePatchFile
:
734 """Performs a patch check for a single file.
736 stdin is used when the filename is '-'.
739 def __init__(self
, patch_filename
):
740 if patch_filename
== '-':
741 patch
= sys
.stdin
.read()
742 patch_filename
= 'stdin'
744 f
= open(patch_filename
, 'rb')
745 patch
= f
.read().decode('utf-8', 'ignore')
747 if Verbose
.level
> Verbose
.ONELINE
:
748 print('Checking patch file:', patch_filename
)
749 self
.ok
= CheckOnePatch(patch_filename
, patch
).ok
752 """Performs a patch check for a single command line argument.
754 The argument will be handed off to a file or git-commit based
758 def __init__(self
, param
, max_count
=None):
760 if param
== '-' or os
.path
.exists(param
):
761 checker
= CheckOnePatchFile(param
)
763 checker
= CheckGitCommits(param
, max_count
)
767 """Checks patches based on the command line arguments."""
771 patches
= self
.args
.patches
773 if len(patches
) == 0:
778 for patch
in patches
:
779 self
.process_one_arg(patch
)
781 if self
.count
is not None:
782 self
.process_one_arg('HEAD')
789 def process_one_arg(self
, arg
):
790 if len(arg
) >= 2 and arg
[0] == '-':
792 self
.count
= int(arg
[1:])
796 self
.ok
&= CheckOneArg(arg
, self
.count
).ok
799 def parse_options(self
):
800 parser
= argparse
.ArgumentParser(description
=__copyright__
)
801 parser
.add_argument('--version', action
='version',
802 version
='%(prog)s ' + VersionNumber
)
803 parser
.add_argument('patches', nargs
='*',
804 help='[patch file | git rev list]')
805 group
= parser
.add_mutually_exclusive_group()
806 group
.add_argument("--oneline",
808 help="Print one result per line")
809 group
.add_argument("--silent",
811 help="Print nothing")
812 self
.args
= parser
.parse_args()
813 if self
.args
.oneline
:
814 Verbose
.level
= Verbose
.ONELINE
816 Verbose
.level
= Verbose
.SILENT
818 if __name__
== "__main__":
819 sys
.exit(PatchCheckApp().retval
)