]>
git.proxmox.com Git - mirror_edk2.git/blob - BaseTools/Scripts/PatchCheck.py
3b6d77081e7e08ae852d6e5a1d2d09bf7ca28dda
2 # Check a patch for various format issues
4 # Copyright (c) 2015 - 2019, Intel Corporation. All rights reserved.<BR>
5 # Copyright (C) 2020, Red Hat, Inc.<BR>
7 # SPDX-License-Identifier: BSD-2-Clause-Patent
10 from __future__
import print_function
13 __copyright__
= "Copyright (c) 2015 - 2016, Intel Corporation All rights reserved."
23 SILENT
, ONELINE
, NORMAL
= range(3)
26 class EmailAddressCheck
:
27 """Checks an email address."""
29 def __init__(self
, email
):
33 self
.error('Email address is missing!')
36 self
.check_email_address(email
)
38 def error(self
, *err
):
39 if self
.ok
and Verbose
.level
> Verbose
.ONELINE
:
40 print('The email address is not valid:')
42 if Verbose
.level
< Verbose
.NORMAL
:
46 prefix
= (' *', ' ')[count
> 0]
50 email_re1
= re
.compile(r
'(?:\s*)(.*?)(\s*)<(.+)>\s*$',
51 re
.MULTILINE|re
.IGNORECASE
)
53 def check_email_address(self
, email
):
55 mo
= self
.email_re1
.match(email
)
57 self
.error("Email format is invalid: " + email
.strip())
60 name
= mo
.group(1).strip()
62 self
.error("Name is not provided with email address: " +
65 quoted
= len(name
) > 2 and name
[0] == '"' and name
[-1] == '"'
66 if name
.find(',') >= 0 and not quoted
:
67 self
.error('Add quotes (") around name with a comma: ' +
71 self
.error("There should be a space between the name and " +
72 "email address: " + email
)
74 if mo
.group(3).find(' ') >= 0:
75 self
.error("The email address cannot contain a space: " +
78 class CommitMessageCheck
:
79 """Checks the contents of a git commit message."""
81 def __init__(self
, subject
, message
):
84 if subject
is None and message
is None:
85 self
.error('Commit message is missing!')
88 self
.subject
= subject
91 self
.check_contributed_under()
92 self
.check_signed_off_by()
93 self
.check_misc_signatures()
94 self
.check_overall_format()
95 self
.report_message_result()
97 url
= 'https://github.com/tianocore/tianocore.github.io/wiki/Commit-Message-Format'
99 def report_message_result(self
):
100 if Verbose
.level
< Verbose
.NORMAL
:
105 print('The commit message format passed all checks.')
111 def error(self
, *err
):
112 if self
.ok
and Verbose
.level
> Verbose
.ONELINE
:
113 print('The commit message format is not valid:')
115 if Verbose
.level
< Verbose
.NORMAL
:
119 prefix
= (' *', ' ')[count
> 0]
123 # Find 'contributed-under:' at the start of a line ignoring case and
124 # requires ':' to be present. Matches if there is white space before
125 # the tag or between the tag and the ':'.
126 contributed_under_re
= \
127 re
.compile(r
'^\s*contributed-under\s*:', re
.MULTILINE|re
.IGNORECASE
)
129 def check_contributed_under(self
):
130 match
= self
.contributed_under_re
.search(self
.msg
)
131 if match
is not None:
132 self
.error('Contributed-under! (Note: this must be ' +
133 'removed by the code contributor!)')
136 def make_signature_re(sig
, re_input
=False):
140 sub_re
= sig
.replace('-', r
'[-\s]+')
141 re_str
= (r
'^(?P<tag>' + sub_re
+
142 r
')(\s*):(\s*)(?P<value>\S.*?)(?:\s*)$')
144 return re
.compile(re_str
, re
.MULTILINE|re
.IGNORECASE
)
146 print("Tried to compile re:", re_str
)
151 (?: (?P<tag>[^:]+) \s* : \s*
154 (?: \[ (?P<updater>[^:]+) \s* : \s*
155 (?P<note>.+?) \s* \] )
157 re
.VERBOSE | re
.MULTILINE
)
159 def find_signatures(self
, sig
):
160 if not sig
.endswith('-by') and sig
!= 'Cc':
162 regex
= self
.make_signature_re(sig
)
164 sigs
= regex
.findall(self
.msg
)
166 bad_case_sigs
= filter(lambda m
: m
[0] != sig
, sigs
)
167 for s
in bad_case_sigs
:
168 self
.error("'" +s
[0] + "' should be '" + sig
+ "'")
172 self
.error('There should be no spaces between ' + sig
+
175 self
.error("There should be a space after '" + sig
+ ":'")
177 EmailAddressCheck(s
[3])
181 def check_signed_off_by(self
):
183 if self
.msg
.find(sob
) < 0:
184 self
.error('Missing Signed-off-by! (Note: this must be ' +
185 'added by the code contributor!)')
188 sobs
= self
.find_signatures('Signed-off')
191 self
.error('Invalid Signed-off-by format!')
203 def check_misc_signatures(self
):
204 for sig
in self
.sig_types
:
205 self
.find_signatures(sig
)
207 def check_overall_format(self
):
208 lines
= self
.msg
.splitlines()
210 if len(lines
) >= 1 and lines
[0].endswith('\r\n'):
215 lines
.insert(0, empty_line
)
216 lines
.insert(0, self
.subject
+ empty_line
)
221 self
.error('Empty commit message!')
224 if count
>= 1 and len(lines
[0].rstrip()) >= 72:
225 self
.error('First line of commit message (subject line) ' +
228 if count
>= 1 and len(lines
[0].strip()) == 0:
229 self
.error('First line of commit message (subject line) ' +
232 if count
>= 2 and lines
[1].strip() != '':
233 self
.error('Second line of commit message should be ' +
236 for i
in range(2, count
):
237 if (len(lines
[i
]) >= 76 and
238 len(lines
[i
].split()) > 1 and
239 not lines
[i
].startswith('git-svn-id:')):
240 self
.error('Line %d of commit message is too long.' % (i
+ 1))
243 for i
in range(count
- 1, 0, -1):
245 mo
= self
.sig_block_re
.match(line
)
247 if line
.strip() == '':
249 elif last_sig_line
is not None:
250 err2
= 'Add empty line before "%s"?' % last_sig_line
251 self
.error('The line before the signature block ' +
252 'should be empty', err2
)
254 self
.error('The signature block was not found')
256 last_sig_line
= line
.strip()
258 (START
, PRE_PATCH
, PATCH
) = range(3)
261 """Checks the contents of a git diff."""
263 def __init__(self
, diff
):
265 self
.format_ok
= True
266 self
.lines
= diff
.splitlines(True)
267 self
.count
= len(self
.lines
)
271 while self
.line_num
< self
.count
and self
.format_ok
:
272 line_num
= self
.line_num
274 assert(self
.line_num
> line_num
)
275 self
.report_message_result()
277 def report_message_result(self
):
278 if Verbose
.level
< Verbose
.NORMAL
:
281 print('The code passed all checks.')
283 print('\nWARNING - The following binary files will be added ' +
284 'into the repository:')
285 for binary
in self
.new_bin
:
289 line
= self
.lines
[self
.line_num
]
291 if self
.state
in (PRE_PATCH
, PATCH
):
292 if line
.startswith('diff --git'):
294 if self
.state
== PATCH
:
295 if line
.startswith('@@ '):
296 self
.state
= PRE_PATCH
297 elif len(line
) >= 1 and line
[0] not in ' -+' and \
298 not line
.startswith('\r\n') and \
299 not line
.startswith(r
'\ No newline ') and not self
.binary
:
300 for line
in self
.lines
[self
.line_num
+ 1:]:
301 if line
.startswith('diff --git'):
302 self
.format_error('diff found after end of patch')
304 self
.line_num
= self
.count
307 if self
.state
== START
:
308 if line
.startswith('diff --git'):
309 self
.state
= PRE_PATCH
310 self
.filename
= line
[13:].split(' ', 1)[0]
311 self
.is_newfile
= False
312 self
.force_crlf
= not self
.filename
.endswith('.sh')
313 elif len(line
.rstrip()) != 0:
314 self
.format_error("didn't find diff command")
316 elif self
.state
== PRE_PATCH
:
317 if line
.startswith('@@ '):
320 elif line
.startswith('GIT binary patch') or \
321 line
.startswith('Binary files'):
325 self
.new_bin
.append(self
.filename
)
328 self
.is_newfile
= self
.newfile_prefix_re
.match(line
)
329 for pfx
in self
.pre_patch_prefixes
:
330 if line
.startswith(pfx
):
333 self
.format_error("didn't find diff hunk marker (@@)")
335 elif self
.state
== PATCH
:
338 elif line
.startswith('-'):
340 elif line
.startswith('+'):
341 self
.check_added_line(line
[1:])
342 elif line
.startswith('\r\n'):
344 elif line
.startswith(r
'\ No newline '):
346 elif not line
.startswith(' '):
347 self
.format_error("unexpected patch line")
350 pre_patch_prefixes
= (
364 line_endings
= ('\r\n', '\n\r', '\n', '\r')
366 newfile_prefix_re
= \
372 def added_line_error(self
, msg
, line
):
374 if self
.filename
is not None:
375 lines
.append('File: ' + self
.filename
)
376 lines
.append('Line: ' + line
)
382 DEBUG \s* \( \s* \( \s*
383 (?: DEBUG_[A-Z_]+ \s* \| \s*)*
388 def check_added_line(self
, line
):
390 for an_eol
in self
.line_endings
:
391 if line
.endswith(an_eol
):
393 line
= line
[:-len(eol
)]
395 stripped
= line
.rstrip()
397 if self
.force_crlf
and eol
!= '\r\n':
398 self
.added_line_error('Line ending (%s) is not CRLF' % repr(eol
),
401 self
.added_line_error('Tab character used', line
)
402 if len(stripped
) < len(line
):
403 self
.added_line_error('Trailing whitespace found', line
)
405 mo
= self
.old_debug_re
.search(line
)
407 self
.added_line_error('EFI_D_' + mo
.group(1) + ' was used, '
408 'but DEBUG_' + mo
.group(1) +
409 ' is now recommended', line
)
411 split_diff_re
= re
.compile(r
'''
413 ^ diff \s+ --git \s+ a/.+ \s+ b/.+ $
419 re
.IGNORECASE | re
.VERBOSE | re
.MULTILINE
)
421 def format_error(self
, err
):
422 self
.format_ok
= False
423 err
= 'Patch format error: ' + err
424 err2
= 'Line: ' + self
.lines
[self
.line_num
].rstrip()
425 self
.error(err
, err2
)
427 def error(self
, *err
):
428 if self
.ok
and Verbose
.level
> Verbose
.ONELINE
:
429 print('Code format is not valid:')
431 if Verbose
.level
< Verbose
.NORMAL
:
435 prefix
= (' *', ' ')[count
> 0]
440 """Checks the contents of a git email formatted patch.
442 Various checks are performed on both the commit message and the
446 def __init__(self
, name
, patch
):
448 self
.find_patch_pieces()
450 msg_check
= CommitMessageCheck(self
.commit_subject
, self
.commit_msg
)
451 msg_ok
= msg_check
.ok
454 if self
.diff
is not None:
455 diff_check
= GitDiffCheck(self
.diff
)
456 diff_ok
= diff_check
.ok
458 self
.ok
= msg_ok
and diff_ok
460 if Verbose
.level
== Verbose
.ONELINE
:
466 result
.append('commit message')
468 result
.append('diff content')
469 result
= 'bad ' + ' and '.join(result
)
473 git_diff_re
= re
.compile(r
'''
474 ^ diff \s+ --git \s+ a/.+ \s+ b/.+ $
476 re
.IGNORECASE | re
.VERBOSE | re
.MULTILINE
)
480 (?P<commit_message> [\s\S\r\n]* )
483 (?: ^ \s+ .+ \s+ \| \s+ \d+ \s+ \+* \-*
488 re
.IGNORECASE | re
.VERBOSE | re
.MULTILINE
)
490 subject_prefix_re
= \
493 [^\[\]]* # Allow all non-brackets
498 def find_patch_pieces(self
):
499 if sys
.version_info
< (3, 0):
500 patch
= self
.patch
.encode('ascii', 'ignore')
504 self
.commit_msg
= None
506 self
.commit_subject
= None
507 self
.commit_prefix
= None
510 if patch
.startswith('diff --git'):
514 pmail
= email
.message_from_string(patch
)
515 parts
= list(pmail
.walk())
516 assert(len(parts
) == 1)
517 assert(parts
[0].get_content_type() == 'text/plain')
518 content
= parts
[0].get_payload(decode
=True).decode('utf-8', 'ignore')
520 mo
= self
.git_diff_re
.search(content
)
522 self
.diff
= content
[mo
.start():]
523 content
= content
[:mo
.start()]
525 mo
= self
.stat_re
.search(content
)
527 self
.commit_msg
= content
529 self
.stat
= mo
.group('stat')
530 self
.commit_msg
= mo
.group('commit_message')
532 self
.commit_subject
= pmail
['subject'].replace('\r\n', '')
533 self
.commit_subject
= self
.commit_subject
.replace('\n', '')
534 self
.commit_subject
= self
.subject_prefix_re
.sub('', self
.commit_subject
, 1)
536 class CheckGitCommits
:
537 """Reads patches from git based on the specified git revision range.
539 The patches are read from git, and then checked.
542 def __init__(self
, rev_spec
, max_count
):
543 commits
= self
.read_commit_list_from_git(rev_spec
, max_count
)
544 if len(commits
) == 1 and Verbose
.level
> Verbose
.ONELINE
:
545 commits
= [ rev_spec
]
548 for commit
in commits
:
549 if Verbose
.level
> Verbose
.ONELINE
:
554 print('Checking git commit:', commit
)
555 patch
= self
.read_patch_from_git(commit
)
556 self
.ok
&= CheckOnePatch(commit
, patch
).ok
558 print("Couldn't find commit matching: '{}'".format(rev_spec
))
560 def read_commit_list_from_git(self
, rev_spec
, max_count
):
561 # Run git to get the commit patch
562 cmd
= [ 'rev-list', '--abbrev-commit', '--no-walk' ]
563 if max_count
is not None:
564 cmd
.append('--max-count=' + str(max_count
))
566 out
= self
.run_git(*cmd
)
567 return out
.split() if out
else []
569 def read_patch_from_git(self
, commit
):
570 # Run git to get the commit patch
571 return self
.run_git('show', '--pretty=email', '--no-textconv', commit
)
573 def run_git(self
, *args
):
576 p
= subprocess
.Popen(cmd
,
577 stdout
=subprocess
.PIPE
,
578 stderr
=subprocess
.STDOUT
)
579 Result
= p
.communicate()
580 return Result
[0].decode('utf-8', 'ignore') if Result
[0] and Result
[0].find(b
"fatal")!=0 else None
582 class CheckOnePatchFile
:
583 """Performs a patch check for a single file.
585 stdin is used when the filename is '-'.
588 def __init__(self
, patch_filename
):
589 if patch_filename
== '-':
590 patch
= sys
.stdin
.read()
591 patch_filename
= 'stdin'
593 f
= open(patch_filename
, 'rb')
594 patch
= f
.read().decode('utf-8', 'ignore')
596 if Verbose
.level
> Verbose
.ONELINE
:
597 print('Checking patch file:', patch_filename
)
598 self
.ok
= CheckOnePatch(patch_filename
, patch
).ok
601 """Performs a patch check for a single command line argument.
603 The argument will be handed off to a file or git-commit based
607 def __init__(self
, param
, max_count
=None):
609 if param
== '-' or os
.path
.exists(param
):
610 checker
= CheckOnePatchFile(param
)
612 checker
= CheckGitCommits(param
, max_count
)
616 """Checks patches based on the command line arguments."""
620 patches
= self
.args
.patches
622 if len(patches
) == 0:
627 for patch
in patches
:
628 self
.process_one_arg(patch
)
630 if self
.count
is not None:
631 self
.process_one_arg('HEAD')
638 def process_one_arg(self
, arg
):
639 if len(arg
) >= 2 and arg
[0] == '-':
641 self
.count
= int(arg
[1:])
645 self
.ok
&= CheckOneArg(arg
, self
.count
).ok
648 def parse_options(self
):
649 parser
= argparse
.ArgumentParser(description
=__copyright__
)
650 parser
.add_argument('--version', action
='version',
651 version
='%(prog)s ' + VersionNumber
)
652 parser
.add_argument('patches', nargs
='*',
653 help='[patch file | git rev list]')
654 group
= parser
.add_mutually_exclusive_group()
655 group
.add_argument("--oneline",
657 help="Print one result per line")
658 group
.add_argument("--silent",
660 help="Print nothing")
661 self
.args
= parser
.parse_args()
662 if self
.args
.oneline
:
663 Verbose
.level
= Verbose
.ONELINE
665 Verbose
.level
= Verbose
.SILENT
667 if __name__
== "__main__":
668 sys
.exit(PatchCheckApp().retval
)