]>
git.proxmox.com Git - mirror_edk2.git/blob - BaseTools/Scripts/PatchCheck.py
2 # Check a patch for various format issues
4 # Copyright (c) 2015, Intel Corporation. All rights reserved.<BR>
6 # This program and the accompanying materials are licensed and made
7 # available under the terms and conditions of the BSD License which
8 # accompanies this distribution. The full text of the license may be
9 # found at http://opensource.org/licenses/bsd-license.php
11 # THE PROGRAM IS DISTRIBUTED UNDER THE BSD LICENSE ON AN "AS IS"
12 # BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS OF ANY KIND, EITHER
16 from __future__
import print_function
19 __copyright__
= "Copyright (c) 2015, Intel Corporation All rights reserved."
29 SILENT
, ONELINE
, NORMAL
= range(3)
32 class CommitMessageCheck
:
33 """Checks the contents of a git commit message."""
35 def __init__(self
, subject
, message
):
38 if subject
is None and message
is None:
39 self
.error('Commit message is missing!')
42 self
.subject
= subject
45 self
.check_contributed_under()
46 self
.check_signed_off_by()
47 self
.check_misc_signatures()
48 self
.check_overall_format()
49 self
.report_message_result()
51 url
= 'https://github.com/tianocore/tianocore.github.io/wiki/Commit-Message-Format'
53 def report_message_result(self
):
54 if Verbose
.level
< Verbose
.NORMAL
:
59 print('The commit message format passed all checks.')
65 def error(self
, *err
):
66 if self
.ok
and Verbose
.level
> Verbose
.ONELINE
:
67 print('The commit message format is not valid:')
69 if Verbose
.level
< Verbose
.NORMAL
:
73 prefix
= (' *', ' ')[count
> 0]
77 def check_contributed_under(self
):
78 cu_msg
='Contributed-under: TianoCore Contribution Agreement 1.0'
79 if self
.msg
.find(cu_msg
) < 0:
80 self
.error('Missing Contributed-under! (Note: this must be ' +
81 'added by the code contributor!)')
84 def make_signature_re(sig
, re_input
=False):
88 sub_re
= sig
.replace('-', r
'[-\s]+')
89 re_str
= (r
'^(?P<tag>' + sub_re
+
90 r
')(\s*):(\s*)(?P<value>\S.*?)(?:\s*)$')
92 return re
.compile(re_str
, re
.MULTILINE|re
.IGNORECASE
)
94 print("Tried to compile re:", re_str
)
99 (?: (?P<tag>[^:]+) \s* : \s*
102 (?: \[ (?P<updater>[^:]+) \s* : \s*
103 (?P<note>.+?) \s* \] )
105 re
.VERBOSE | re
.MULTILINE
)
107 def find_signatures(self
, sig
):
108 if not sig
.endswith('-by') and sig
!= 'Cc':
110 regex
= self
.make_signature_re(sig
)
112 sigs
= regex
.findall(self
.msg
)
114 bad_case_sigs
= filter(lambda m
: m
[0] != sig
, sigs
)
115 for s
in bad_case_sigs
:
116 self
.error("'" +s
[0] + "' should be '" + sig
+ "'")
120 self
.error('There should be no spaces between ' + sig
+
123 self
.error("There should be a space after '" + sig
+ ":'")
125 self
.check_email_address(s
[3])
129 email_re1
= re
.compile(r
'(?:\s*)(.*?)(\s*)<(.+)>\s*$',
130 re
.MULTILINE|re
.IGNORECASE
)
132 def check_email_address(self
, email
):
133 email
= email
.strip()
134 mo
= self
.email_re1
.match(email
)
136 self
.error("Email format is invalid: " + email
.strip())
139 name
= mo
.group(1).strip()
141 self
.error("Name is not provided with email address: " +
144 quoted
= len(name
) > 2 and name
[0] == '"' and name
[-1] == '"'
145 if name
.find(',') >= 0 and not quoted
:
146 self
.error('Add quotes (") around name with a comma: ' +
149 if mo
.group(2) == '':
150 self
.error("There should be a space between the name and " +
151 "email address: " + email
)
153 if mo
.group(3).find(' ') >= 0:
154 self
.error("The email address cannot contain a space: " +
157 def check_signed_off_by(self
):
159 if self
.msg
.find(sob
) < 0:
160 self
.error('Missing Signed-off-by! (Note: this must be ' +
161 'added by the code contributor!)')
164 sobs
= self
.find_signatures('Signed-off')
167 self
.error('Invalid Signed-off-by format!')
179 def check_misc_signatures(self
):
180 for sig
in self
.sig_types
:
181 self
.find_signatures(sig
)
183 def check_overall_format(self
):
184 lines
= self
.msg
.splitlines()
186 if len(lines
) >= 1 and lines
[0].endswith('\r\n'):
191 lines
.insert(0, empty_line
)
192 lines
.insert(0, self
.subject
+ empty_line
)
197 self
.error('Empty commit message!')
200 if count
>= 1 and len(lines
[0]) > 76:
201 self
.error('First line of commit message (subject line) ' +
204 if count
>= 1 and len(lines
[0].strip()) == 0:
205 self
.error('First line of commit message (subject line) ' +
208 if count
>= 2 and lines
[1].strip() != '':
209 self
.error('Second line of commit message should be ' +
212 for i
in range(2, count
):
213 if (len(lines
[i
]) > 76 and
214 len(lines
[i
].split()) > 1 and
215 not lines
[i
].startswith('git-svn-id:')):
216 self
.error('Line %d of commit message is too long.' % (i
+ 1))
219 for i
in range(count
- 1, 0, -1):
221 mo
= self
.sig_block_re
.match(line
)
223 if line
.strip() == '':
225 elif last_sig_line
is not None:
226 err2
= 'Add empty line before "%s"?' % last_sig_line
227 self
.error('The line before the signature block ' +
228 'should be empty', err2
)
230 self
.error('The signature block was not found')
232 last_sig_line
= line
.strip()
234 (START
, PRE_PATCH
, PATCH
) = range(3)
237 """Checks the contents of a git diff."""
239 def __init__(self
, diff
):
241 self
.format_ok
= True
242 self
.lines
= diff
.splitlines(True)
243 self
.count
= len(self
.lines
)
246 while self
.line_num
< self
.count
and self
.format_ok
:
247 line_num
= self
.line_num
249 assert(self
.line_num
> line_num
)
250 self
.report_message_result()
252 def report_message_result(self
):
253 if Verbose
.level
< Verbose
.NORMAL
:
256 print('The code passed all checks.')
259 line
= self
.lines
[self
.line_num
]
261 if self
.state
in (PRE_PATCH
, PATCH
):
262 if line
.startswith('diff --git'):
264 if self
.state
== PATCH
:
265 if line
.startswith('@@ '):
266 self
.state
= PRE_PATCH
267 elif len(line
) >= 1 and line
[0] not in ' -+' and \
268 not line
.startswith(r
'\ No newline '):
269 for line
in self
.lines
[self
.line_num
+ 1:]:
270 if line
.startswith('diff --git'):
271 self
.format_error('diff found after end of patch')
273 self
.line_num
= self
.count
276 if self
.state
== START
:
277 if line
.startswith('diff --git'):
278 self
.state
= PRE_PATCH
279 self
.set_filename(None)
280 elif len(line
.rstrip()) != 0:
281 self
.format_error("didn't find diff command")
283 elif self
.state
== PRE_PATCH
:
284 if line
.startswith('+++ b/'):
285 self
.set_filename(line
[6:].rstrip())
286 if line
.startswith('@@ '):
289 elif line
.startswith('GIT binary patch'):
294 for pfx
in self
.pre_patch_prefixes
:
295 if line
.startswith(pfx
):
298 self
.format_error("didn't find diff hunk marker (@@)")
300 elif self
.state
== PATCH
:
303 if line
.startswith('-'):
305 elif line
.startswith('+'):
306 self
.check_added_line(line
[1:])
307 elif line
.startswith(r
'\ No newline '):
309 elif not line
.startswith(' '):
310 self
.format_error("unexpected patch line")
313 pre_patch_prefixes
= (
326 line_endings
= ('\r\n', '\n\r', '\n', '\r')
328 def set_filename(self
, filename
):
329 self
.hunk_filename
= filename
331 self
.force_crlf
= not filename
.endswith('.sh')
333 self
.force_crlf
= True
335 def added_line_error(self
, msg
, line
):
337 if self
.hunk_filename
is not None:
338 lines
.append('File: ' + self
.hunk_filename
)
339 lines
.append('Line: ' + line
)
343 def check_added_line(self
, line
):
345 for an_eol
in self
.line_endings
:
346 if line
.endswith(an_eol
):
348 line
= line
[:-len(eol
)]
350 stripped
= line
.rstrip()
352 if self
.force_crlf
and eol
!= '\r\n':
353 self
.added_line_error('Line ending (%s) is not CRLF' % repr(eol
),
356 self
.added_line_error('Tab character used', line
)
357 if len(stripped
) < len(line
):
358 self
.added_line_error('Trailing whitespace found', line
)
360 split_diff_re
= re
.compile(r
'''
362 ^ diff \s+ --git \s+ a/.+ \s+ b/.+ $
368 re
.IGNORECASE | re
.VERBOSE | re
.MULTILINE
)
370 def format_error(self
, err
):
371 self
.format_ok
= False
372 err
= 'Patch format error: ' + err
373 err2
= 'Line: ' + self
.lines
[self
.line_num
].rstrip()
374 self
.error(err
, err2
)
376 def error(self
, *err
):
377 if self
.ok
and Verbose
.level
> Verbose
.ONELINE
:
378 print('Code format is not valid:')
380 if Verbose
.level
< Verbose
.NORMAL
:
384 prefix
= (' *', ' ')[count
> 0]
389 """Checks the contents of a git email formatted patch.
391 Various checks are performed on both the commit message and the
395 def __init__(self
, name
, patch
):
397 self
.find_patch_pieces()
399 msg_check
= CommitMessageCheck(self
.commit_subject
, self
.commit_msg
)
400 msg_ok
= msg_check
.ok
403 if self
.diff
is not None:
404 diff_check
= GitDiffCheck(self
.diff
)
405 diff_ok
= diff_check
.ok
407 self
.ok
= msg_ok
and diff_ok
409 if Verbose
.level
== Verbose
.ONELINE
:
415 result
.append('commit message')
417 result
.append('diff content')
418 result
= 'bad ' + ' and '.join(result
)
422 git_diff_re
= re
.compile(r
'''
423 ^ diff \s+ --git \s+ a/.+ \s+ b/.+ $
425 re
.IGNORECASE | re
.VERBOSE | re
.MULTILINE
)
429 (?P<commit_message> [\s\S\r\n]* )
432 (?: ^ \s+ .+ \s+ \| \s+ \d+ \s+ \+* \-*
437 re
.IGNORECASE | re
.VERBOSE | re
.MULTILINE
)
439 def find_patch_pieces(self
):
440 if sys
.version_info
< (3, 0):
441 patch
= self
.patch
.encode('ascii', 'ignore')
445 self
.commit_msg
= None
447 self
.commit_subject
= None
448 self
.commit_prefix
= None
451 if patch
.startswith('diff --git'):
455 pmail
= email
.message_from_string(patch
)
456 parts
= list(pmail
.walk())
457 assert(len(parts
) == 1)
458 assert(parts
[0].get_content_type() == 'text/plain')
459 content
= parts
[0].get_payload(decode
=True).decode('utf-8', 'ignore')
461 mo
= self
.git_diff_re
.search(content
)
463 self
.diff
= content
[mo
.start():]
464 content
= content
[:mo
.start()]
466 mo
= self
.stat_re
.search(content
)
468 self
.commit_msg
= content
470 self
.stat
= mo
.group('stat')
471 self
.commit_msg
= mo
.group('commit_message')
473 self
.commit_subject
= pmail
['subject'].replace('\r\n', '')
474 self
.commit_subject
= self
.commit_subject
.replace('\n', '')
476 pfx_start
= self
.commit_subject
.find('[')
478 pfx_end
= self
.commit_subject
.find(']')
479 if pfx_end
> pfx_start
:
480 self
.commit_prefix
= self
.commit_subject
[pfx_start
+ 1 : pfx_end
]
481 self
.commit_subject
= self
.commit_subject
[pfx_end
+ 1 :].lstrip()
484 class CheckGitCommits
:
485 """Reads patches from git based on the specified git revision range.
487 The patches are read from git, and then checked.
490 def __init__(self
, rev_spec
, max_count
):
491 commits
= self
.read_commit_list_from_git(rev_spec
, max_count
)
492 if len(commits
) == 1 and Verbose
.level
> Verbose
.ONELINE
:
493 commits
= [ rev_spec
]
496 for commit
in commits
:
497 if Verbose
.level
> Verbose
.ONELINE
:
502 print('Checking git commit:', commit
)
503 patch
= self
.read_patch_from_git(commit
)
504 self
.ok
&= CheckOnePatch(commit
, patch
).ok
506 def read_commit_list_from_git(self
, rev_spec
, max_count
):
507 # Run git to get the commit patch
508 cmd
= [ 'rev-list', '--abbrev-commit', '--no-walk' ]
509 if max_count
is not None:
510 cmd
.append('--max-count=' + str(max_count
))
512 out
= self
.run_git(*cmd
)
515 def read_patch_from_git(self
, commit
):
516 # Run git to get the commit patch
517 return self
.run_git('show', '--pretty=email', commit
)
519 def run_git(self
, *args
):
522 p
= subprocess
.Popen(cmd
,
523 stdout
=subprocess
.PIPE
,
524 stderr
=subprocess
.STDOUT
)
525 return p
.communicate()[0].decode('utf-8', 'ignore')
527 class CheckOnePatchFile
:
528 """Performs a patch check for a single file.
530 stdin is used when the filename is '-'.
533 def __init__(self
, patch_filename
):
534 if patch_filename
== '-':
535 patch
= sys
.stdin
.read()
536 patch_filename
= 'stdin'
538 f
= open(patch_filename
, 'rb')
539 patch
= f
.read().decode('utf-8', 'ignore')
541 if Verbose
.level
> Verbose
.ONELINE
:
542 print('Checking patch file:', patch_filename
)
543 self
.ok
= CheckOnePatch(patch_filename
, patch
).ok
546 """Performs a patch check for a single command line argument.
548 The argument will be handed off to a file or git-commit based
552 def __init__(self
, param
, max_count
=None):
554 if param
== '-' or os
.path
.exists(param
):
555 checker
= CheckOnePatchFile(param
)
557 checker
= CheckGitCommits(param
, max_count
)
561 """Checks patches based on the command line arguments."""
565 patches
= self
.args
.patches
567 if len(patches
) == 0:
572 for patch
in patches
:
573 self
.process_one_arg(patch
)
575 if self
.count
is not None:
576 self
.process_one_arg('HEAD')
583 def process_one_arg(self
, arg
):
584 if len(arg
) >= 2 and arg
[0] == '-':
586 self
.count
= int(arg
[1:])
590 self
.ok
&= CheckOneArg(arg
, self
.count
).ok
593 def parse_options(self
):
594 parser
= argparse
.ArgumentParser(description
=__copyright__
)
595 parser
.add_argument('--version', action
='version',
596 version
='%(prog)s ' + VersionNumber
)
597 parser
.add_argument('patches', nargs
='*',
598 help='[patch file | git rev list]')
599 group
= parser
.add_mutually_exclusive_group()
600 group
.add_argument("--oneline",
602 help="Print one result per line")
603 group
.add_argument("--silent",
605 help="Print nothing")
606 self
.args
= parser
.parse_args()
607 if self
.args
.oneline
:
608 Verbose
.level
= Verbose
.ONELINE
610 Verbose
.level
= Verbose
.SILENT
612 if __name__
== "__main__":
613 sys
.exit(PatchCheckApp().retval
)