]> git.proxmox.com Git - mirror_edk2.git/blame - BaseTools/Scripts/PatchCheck.py
CryptoPkg CLANG35: add -std=c99 to dodge OpenSSL C atomics issue
[mirror_edk2.git] / BaseTools / Scripts / PatchCheck.py
CommitLineData
a7e173b0
JJ
1## @file\r
2# Check a patch for various format issues\r
3#\r
a2813610 4# Copyright (c) 2015 - 2019, Intel Corporation. All rights reserved.<BR>\r
a7e173b0 5#\r
2e351cbe 6# SPDX-License-Identifier: BSD-2-Clause-Patent\r
a7e173b0
JJ
7#\r
8\r
9from __future__ import print_function\r
10\r
11VersionNumber = '0.1'\r
e6140670 12__copyright__ = "Copyright (c) 2015 - 2016, Intel Corporation All rights reserved."\r
a7e173b0
JJ
13\r
14import email\r
15import argparse\r
16import os\r
17import re\r
18import subprocess\r
19import sys\r
20\r
21class Verbose:\r
22 SILENT, ONELINE, NORMAL = range(3)\r
23 level = NORMAL\r
24\r
25class CommitMessageCheck:\r
26 """Checks the contents of a git commit message."""\r
27\r
28 def __init__(self, subject, message):\r
29 self.ok = True\r
30\r
31 if subject is None and message is None:\r
32 self.error('Commit message is missing!')\r
33 return\r
34\r
35 self.subject = subject\r
36 self.msg = message\r
37\r
38 self.check_contributed_under()\r
39 self.check_signed_off_by()\r
40 self.check_misc_signatures()\r
41 self.check_overall_format()\r
42 self.report_message_result()\r
43\r
44 url = 'https://github.com/tianocore/tianocore.github.io/wiki/Commit-Message-Format'\r
45\r
46 def report_message_result(self):\r
47 if Verbose.level < Verbose.NORMAL:\r
48 return\r
49 if self.ok:\r
50 # All checks passed\r
51 return_code = 0\r
52 print('The commit message format passed all checks.')\r
53 else:\r
54 return_code = 1\r
55 if not self.ok:\r
56 print(self.url)\r
57\r
58 def error(self, *err):\r
59 if self.ok and Verbose.level > Verbose.ONELINE:\r
60 print('The commit message format is not valid:')\r
61 self.ok = False\r
62 if Verbose.level < Verbose.NORMAL:\r
63 return\r
64 count = 0\r
65 for line in err:\r
66 prefix = (' *', ' ')[count > 0]\r
67 print(prefix, line)\r
68 count += 1\r
69\r
a2813610
MK
70 # Find 'contributed-under:' at the start of a line ignoring case and\r
71 # requires ':' to be present. Matches if there is white space before\r
72 # the tag or between the tag and the ':'.\r
73 contributed_under_re = \\r
74 re.compile(r'^\s*contributed-under\s*:', re.MULTILINE|re.IGNORECASE)\r
75\r
a7e173b0 76 def check_contributed_under(self):\r
a2813610
MK
77 match = self.contributed_under_re.search(self.msg)\r
78 if match is not None:\r
79 self.error('Contributed-under! (Note: this must be ' +\r
80 'removed by the code contributor!)')\r
a7e173b0
JJ
81\r
82 @staticmethod\r
83 def make_signature_re(sig, re_input=False):\r
84 if re_input:\r
85 sub_re = sig\r
86 else:\r
87 sub_re = sig.replace('-', r'[-\s]+')\r
88 re_str = (r'^(?P<tag>' + sub_re +\r
89 r')(\s*):(\s*)(?P<value>\S.*?)(?:\s*)$')\r
90 try:\r
91 return re.compile(re_str, re.MULTILINE|re.IGNORECASE)\r
92 except Exception:\r
93 print("Tried to compile re:", re_str)\r
94 raise\r
95\r
96 sig_block_re = \\r
97 re.compile(r'''^\r
98 (?: (?P<tag>[^:]+) \s* : \s*\r
99 (?P<value>\S.*?) )\r
100 |\r
101 (?: \[ (?P<updater>[^:]+) \s* : \s*\r
102 (?P<note>.+?) \s* \] )\r
103 \s* $''',\r
104 re.VERBOSE | re.MULTILINE)\r
105\r
106 def find_signatures(self, sig):\r
107 if not sig.endswith('-by') and sig != 'Cc':\r
108 sig += '-by'\r
109 regex = self.make_signature_re(sig)\r
110\r
111 sigs = regex.findall(self.msg)\r
112\r
113 bad_case_sigs = filter(lambda m: m[0] != sig, sigs)\r
114 for s in bad_case_sigs:\r
115 self.error("'" +s[0] + "' should be '" + sig + "'")\r
116\r
117 for s in sigs:\r
118 if s[1] != '':\r
119 self.error('There should be no spaces between ' + sig +\r
120 " and the ':'")\r
121 if s[2] != ' ':\r
122 self.error("There should be a space after '" + sig + ":'")\r
123\r
124 self.check_email_address(s[3])\r
125\r
126 return sigs\r
127\r
128 email_re1 = re.compile(r'(?:\s*)(.*?)(\s*)<(.+)>\s*$',\r
129 re.MULTILINE|re.IGNORECASE)\r
130\r
131 def check_email_address(self, email):\r
132 email = email.strip()\r
133 mo = self.email_re1.match(email)\r
134 if mo is None:\r
135 self.error("Email format is invalid: " + email.strip())\r
136 return\r
137\r
138 name = mo.group(1).strip()\r
139 if name == '':\r
140 self.error("Name is not provided with email address: " +\r
141 email)\r
142 else:\r
143 quoted = len(name) > 2 and name[0] == '"' and name[-1] == '"'\r
144 if name.find(',') >= 0 and not quoted:\r
145 self.error('Add quotes (") around name with a comma: ' +\r
146 name)\r
147\r
148 if mo.group(2) == '':\r
149 self.error("There should be a space between the name and " +\r
150 "email address: " + email)\r
151\r
152 if mo.group(3).find(' ') >= 0:\r
153 self.error("The email address cannot contain a space: " +\r
154 mo.group(3))\r
155\r
156 def check_signed_off_by(self):\r
157 sob='Signed-off-by'\r
158 if self.msg.find(sob) < 0:\r
159 self.error('Missing Signed-off-by! (Note: this must be ' +\r
160 'added by the code contributor!)')\r
161 return\r
162\r
163 sobs = self.find_signatures('Signed-off')\r
164\r
165 if len(sobs) == 0:\r
166 self.error('Invalid Signed-off-by format!')\r
167 return\r
168\r
169 sig_types = (\r
170 'Reviewed',\r
171 'Reported',\r
172 'Tested',\r
173 'Suggested',\r
174 'Acked',\r
175 'Cc'\r
176 )\r
177\r
178 def check_misc_signatures(self):\r
179 for sig in self.sig_types:\r
180 self.find_signatures(sig)\r
181\r
182 def check_overall_format(self):\r
183 lines = self.msg.splitlines()\r
184\r
185 if len(lines) >= 1 and lines[0].endswith('\r\n'):\r
186 empty_line = '\r\n'\r
187 else:\r
188 empty_line = '\n'\r
189\r
190 lines.insert(0, empty_line)\r
191 lines.insert(0, self.subject + empty_line)\r
192\r
193 count = len(lines)\r
194\r
195 if count <= 0:\r
196 self.error('Empty commit message!')\r
197 return\r
198\r
e6140670 199 if count >= 1 and len(lines[0]) >= 72:\r
a7e173b0
JJ
200 self.error('First line of commit message (subject line) ' +\r
201 'is too long.')\r
202\r
203 if count >= 1 and len(lines[0].strip()) == 0:\r
204 self.error('First line of commit message (subject line) ' +\r
205 'is empty.')\r
206\r
207 if count >= 2 and lines[1].strip() != '':\r
208 self.error('Second line of commit message should be ' +\r
209 'empty.')\r
210\r
211 for i in range(2, count):\r
e6140670 212 if (len(lines[i]) >= 76 and\r
a7e173b0
JJ
213 len(lines[i].split()) > 1 and\r
214 not lines[i].startswith('git-svn-id:')):\r
215 self.error('Line %d of commit message is too long.' % (i + 1))\r
216\r
217 last_sig_line = None\r
218 for i in range(count - 1, 0, -1):\r
219 line = lines[i]\r
220 mo = self.sig_block_re.match(line)\r
221 if mo is None:\r
222 if line.strip() == '':\r
223 break\r
224 elif last_sig_line is not None:\r
225 err2 = 'Add empty line before "%s"?' % last_sig_line\r
226 self.error('The line before the signature block ' +\r
227 'should be empty', err2)\r
228 else:\r
229 self.error('The signature block was not found')\r
230 break\r
231 last_sig_line = line.strip()\r
232\r
233(START, PRE_PATCH, PATCH) = range(3)\r
234\r
235class GitDiffCheck:\r
236 """Checks the contents of a git diff."""\r
237\r
238 def __init__(self, diff):\r
239 self.ok = True\r
240 self.format_ok = True\r
241 self.lines = diff.splitlines(True)\r
242 self.count = len(self.lines)\r
243 self.line_num = 0\r
244 self.state = START\r
6a69dd49 245 self.new_bin = []\r
a7e173b0
JJ
246 while self.line_num < self.count and self.format_ok:\r
247 line_num = self.line_num\r
248 self.run()\r
249 assert(self.line_num > line_num)\r
250 self.report_message_result()\r
251\r
252 def report_message_result(self):\r
253 if Verbose.level < Verbose.NORMAL:\r
254 return\r
255 if self.ok:\r
256 print('The code passed all checks.')\r
6a69dd49
HW
257 if self.new_bin:\r
258 print('\nWARNING - The following binary files will be added ' +\r
259 'into the repository:')\r
260 for binary in self.new_bin:\r
261 print(' ' + binary)\r
a7e173b0
JJ
262\r
263 def run(self):\r
264 line = self.lines[self.line_num]\r
265\r
266 if self.state in (PRE_PATCH, PATCH):\r
267 if line.startswith('diff --git'):\r
268 self.state = START\r
269 if self.state == PATCH:\r
270 if line.startswith('@@ '):\r
271 self.state = PRE_PATCH\r
272 elif len(line) >= 1 and line[0] not in ' -+' and \\r
aab57eff 273 not line.startswith(r'\ No newline ') and not self.binary:\r
a7e173b0
JJ
274 for line in self.lines[self.line_num + 1:]:\r
275 if line.startswith('diff --git'):\r
276 self.format_error('diff found after end of patch')\r
277 break\r
278 self.line_num = self.count\r
279 return\r
280\r
281 if self.state == START:\r
282 if line.startswith('diff --git'):\r
283 self.state = PRE_PATCH\r
ccaa7754 284 self.filename = line[13:].split(' ', 1)[0]\r
6a69dd49
HW
285 self.is_newfile = False\r
286 self.force_crlf = not self.filename.endswith('.sh')\r
a7e173b0
JJ
287 elif len(line.rstrip()) != 0:\r
288 self.format_error("didn't find diff command")\r
289 self.line_num += 1\r
290 elif self.state == PRE_PATCH:\r
a7e173b0
JJ
291 if line.startswith('@@ '):\r
292 self.state = PATCH\r
103733f8 293 self.binary = False\r
6a69dd49
HW
294 elif line.startswith('GIT binary patch') or \\r
295 line.startswith('Binary files'):\r
103733f8
JJ
296 self.state = PATCH\r
297 self.binary = True\r
6a69dd49
HW
298 if self.is_newfile:\r
299 self.new_bin.append(self.filename)\r
a7e173b0
JJ
300 else:\r
301 ok = False\r
6a69dd49 302 self.is_newfile = self.newfile_prefix_re.match(line)\r
a7e173b0
JJ
303 for pfx in self.pre_patch_prefixes:\r
304 if line.startswith(pfx):\r
305 ok = True\r
306 if not ok:\r
307 self.format_error("didn't find diff hunk marker (@@)")\r
308 self.line_num += 1\r
309 elif self.state == PATCH:\r
103733f8
JJ
310 if self.binary:\r
311 pass\r
aab57eff 312 elif line.startswith('-'):\r
a7e173b0
JJ
313 pass\r
314 elif line.startswith('+'):\r
315 self.check_added_line(line[1:])\r
316 elif line.startswith(r'\ No newline '):\r
317 pass\r
318 elif not line.startswith(' '):\r
319 self.format_error("unexpected patch line")\r
320 self.line_num += 1\r
321\r
322 pre_patch_prefixes = (\r
323 '--- ',\r
324 '+++ ',\r
325 'index ',\r
326 'new file ',\r
327 'deleted file ',\r
328 'old mode ',\r
329 'new mode ',\r
330 'similarity index ',\r
331 'rename ',\r
a7e173b0
JJ
332 )\r
333\r
334 line_endings = ('\r\n', '\n\r', '\n', '\r')\r
335\r
6a69dd49
HW
336 newfile_prefix_re = \\r
337 re.compile(r'''^\r
338 index\ 0+\.\.\r
339 ''',\r
340 re.VERBOSE)\r
a7e173b0
JJ
341\r
342 def added_line_error(self, msg, line):\r
343 lines = [ msg ]\r
6a69dd49
HW
344 if self.filename is not None:\r
345 lines.append('File: ' + self.filename)\r
a7e173b0
JJ
346 lines.append('Line: ' + line)\r
347\r
348 self.error(*lines)\r
349\r
c3926cdb
YZ
350 old_debug_re = \\r
351 re.compile(r'''\r
352 DEBUG \s* \( \s* \( \s*\r
353 (?: DEBUG_[A-Z_]+ \s* \| \s*)*\r
354 EFI_D_ ([A-Z_]+)\r
355 ''',\r
356 re.VERBOSE)\r
357\r
a7e173b0
JJ
358 def check_added_line(self, line):\r
359 eol = ''\r
360 for an_eol in self.line_endings:\r
361 if line.endswith(an_eol):\r
362 eol = an_eol\r
363 line = line[:-len(eol)]\r
364\r
365 stripped = line.rstrip()\r
366\r
367 if self.force_crlf and eol != '\r\n':\r
368 self.added_line_error('Line ending (%s) is not CRLF' % repr(eol),\r
369 line)\r
370 if '\t' in line:\r
371 self.added_line_error('Tab character used', line)\r
372 if len(stripped) < len(line):\r
373 self.added_line_error('Trailing whitespace found', line)\r
374\r
c3926cdb
YZ
375 mo = self.old_debug_re.search(line)\r
376 if mo is not None:\r
377 self.added_line_error('EFI_D_' + mo.group(1) + ' was used, '\r
378 'but DEBUG_' + mo.group(1) +\r
379 ' is now recommended', line)\r
380\r
a7e173b0
JJ
381 split_diff_re = re.compile(r'''\r
382 (?P<cmd>\r
383 ^ diff \s+ --git \s+ a/.+ \s+ b/.+ $\r
384 )\r
385 (?P<index>\r
386 ^ index \s+ .+ $\r
387 )\r
388 ''',\r
389 re.IGNORECASE | re.VERBOSE | re.MULTILINE)\r
390\r
391 def format_error(self, err):\r
392 self.format_ok = False\r
393 err = 'Patch format error: ' + err\r
394 err2 = 'Line: ' + self.lines[self.line_num].rstrip()\r
395 self.error(err, err2)\r
396\r
397 def error(self, *err):\r
398 if self.ok and Verbose.level > Verbose.ONELINE:\r
399 print('Code format is not valid:')\r
400 self.ok = False\r
401 if Verbose.level < Verbose.NORMAL:\r
402 return\r
403 count = 0\r
404 for line in err:\r
405 prefix = (' *', ' ')[count > 0]\r
406 print(prefix, line)\r
407 count += 1\r
408\r
409class CheckOnePatch:\r
410 """Checks the contents of a git email formatted patch.\r
411\r
412 Various checks are performed on both the commit message and the\r
413 patch content.\r
414 """\r
415\r
416 def __init__(self, name, patch):\r
417 self.patch = patch\r
418 self.find_patch_pieces()\r
419\r
420 msg_check = CommitMessageCheck(self.commit_subject, self.commit_msg)\r
421 msg_ok = msg_check.ok\r
422\r
423 diff_ok = True\r
424 if self.diff is not None:\r
425 diff_check = GitDiffCheck(self.diff)\r
426 diff_ok = diff_check.ok\r
427\r
428 self.ok = msg_ok and diff_ok\r
429\r
430 if Verbose.level == Verbose.ONELINE:\r
431 if self.ok:\r
432 result = 'ok'\r
433 else:\r
434 result = list()\r
435 if not msg_ok:\r
436 result.append('commit message')\r
437 if not diff_ok:\r
438 result.append('diff content')\r
439 result = 'bad ' + ' and '.join(result)\r
440 print(name, result)\r
441\r
442\r
443 git_diff_re = re.compile(r'''\r
444 ^ diff \s+ --git \s+ a/.+ \s+ b/.+ $\r
445 ''',\r
446 re.IGNORECASE | re.VERBOSE | re.MULTILINE)\r
447\r
448 stat_re = \\r
449 re.compile(r'''\r
450 (?P<commit_message> [\s\S\r\n]* )\r
451 (?P<stat>\r
452 ^ --- $ [\r\n]+\r
453 (?: ^ \s+ .+ \s+ \| \s+ \d+ \s+ \+* \-*\r
454 $ [\r\n]+ )+\r
455 [\s\S\r\n]+\r
456 )\r
457 ''',\r
458 re.IGNORECASE | re.VERBOSE | re.MULTILINE)\r
459\r
e709bbb1
YZ
460 subject_prefix_re = \\r
461 re.compile(r'''^\r
462 \s* (\[\r
463 [^\[\]]* # Allow all non-brackets\r
464 \])* \s*\r
465 ''',\r
466 re.VERBOSE)\r
467\r
a7e173b0
JJ
468 def find_patch_pieces(self):\r
469 if sys.version_info < (3, 0):\r
470 patch = self.patch.encode('ascii', 'ignore')\r
471 else:\r
472 patch = self.patch\r
473\r
474 self.commit_msg = None\r
475 self.stat = None\r
476 self.commit_subject = None\r
477 self.commit_prefix = None\r
478 self.diff = None\r
479\r
480 if patch.startswith('diff --git'):\r
481 self.diff = patch\r
482 return\r
483\r
484 pmail = email.message_from_string(patch)\r
485 parts = list(pmail.walk())\r
486 assert(len(parts) == 1)\r
487 assert(parts[0].get_content_type() == 'text/plain')\r
488 content = parts[0].get_payload(decode=True).decode('utf-8', 'ignore')\r
489\r
490 mo = self.git_diff_re.search(content)\r
491 if mo is not None:\r
492 self.diff = content[mo.start():]\r
493 content = content[:mo.start()]\r
494\r
495 mo = self.stat_re.search(content)\r
496 if mo is None:\r
497 self.commit_msg = content\r
498 else:\r
499 self.stat = mo.group('stat')\r
500 self.commit_msg = mo.group('commit_message')\r
501\r
502 self.commit_subject = pmail['subject'].replace('\r\n', '')\r
503 self.commit_subject = self.commit_subject.replace('\n', '')\r
e709bbb1 504 self.commit_subject = self.subject_prefix_re.sub('', self.commit_subject, 1)\r
a7e173b0
JJ
505\r
506class CheckGitCommits:\r
507 """Reads patches from git based on the specified git revision range.\r
508\r
509 The patches are read from git, and then checked.\r
510 """\r
511\r
512 def __init__(self, rev_spec, max_count):\r
513 commits = self.read_commit_list_from_git(rev_spec, max_count)\r
514 if len(commits) == 1 and Verbose.level > Verbose.ONELINE:\r
515 commits = [ rev_spec ]\r
516 self.ok = True\r
517 blank_line = False\r
518 for commit in commits:\r
519 if Verbose.level > Verbose.ONELINE:\r
520 if blank_line:\r
521 print()\r
522 else:\r
523 blank_line = True\r
524 print('Checking git commit:', commit)\r
525 patch = self.read_patch_from_git(commit)\r
526 self.ok &= CheckOnePatch(commit, patch).ok\r
5ac4548c
JC
527 if not commits:\r
528 print("Couldn't find commit matching: '{}'".format(rev_spec))\r
a7e173b0
JJ
529\r
530 def read_commit_list_from_git(self, rev_spec, max_count):\r
531 # Run git to get the commit patch\r
532 cmd = [ 'rev-list', '--abbrev-commit', '--no-walk' ]\r
533 if max_count is not None:\r
534 cmd.append('--max-count=' + str(max_count))\r
535 cmd.append(rev_spec)\r
536 out = self.run_git(*cmd)\r
5ac4548c 537 return out.split() if out else []\r
a7e173b0
JJ
538\r
539 def read_patch_from_git(self, commit):\r
540 # Run git to get the commit patch\r
541 return self.run_git('show', '--pretty=email', commit)\r
542\r
543 def run_git(self, *args):\r
544 cmd = [ 'git' ]\r
545 cmd += args\r
546 p = subprocess.Popen(cmd,\r
547 stdout=subprocess.PIPE,\r
548 stderr=subprocess.STDOUT)\r
5ac4548c 549 Result = p.communicate()\r
6460513a 550 return Result[0].decode('utf-8', 'ignore') if Result[0] and Result[0].find(b"fatal")!=0 else None\r
a7e173b0
JJ
551\r
552class CheckOnePatchFile:\r
553 """Performs a patch check for a single file.\r
554\r
555 stdin is used when the filename is '-'.\r
556 """\r
557\r
558 def __init__(self, patch_filename):\r
559 if patch_filename == '-':\r
560 patch = sys.stdin.read()\r
561 patch_filename = 'stdin'\r
562 else:\r
563 f = open(patch_filename, 'rb')\r
564 patch = f.read().decode('utf-8', 'ignore')\r
565 f.close()\r
566 if Verbose.level > Verbose.ONELINE:\r
567 print('Checking patch file:', patch_filename)\r
568 self.ok = CheckOnePatch(patch_filename, patch).ok\r
569\r
570class CheckOneArg:\r
571 """Performs a patch check for a single command line argument.\r
572\r
573 The argument will be handed off to a file or git-commit based\r
574 checker.\r
575 """\r
576\r
577 def __init__(self, param, max_count=None):\r
578 self.ok = True\r
579 if param == '-' or os.path.exists(param):\r
580 checker = CheckOnePatchFile(param)\r
581 else:\r
582 checker = CheckGitCommits(param, max_count)\r
583 self.ok = checker.ok\r
584\r
585class PatchCheckApp:\r
586 """Checks patches based on the command line arguments."""\r
587\r
588 def __init__(self):\r
589 self.parse_options()\r
590 patches = self.args.patches\r
591\r
592 if len(patches) == 0:\r
593 patches = [ 'HEAD' ]\r
594\r
595 self.ok = True\r
596 self.count = None\r
597 for patch in patches:\r
598 self.process_one_arg(patch)\r
599\r
600 if self.count is not None:\r
601 self.process_one_arg('HEAD')\r
602\r
603 if self.ok:\r
604 self.retval = 0\r
605 else:\r
606 self.retval = -1\r
607\r
608 def process_one_arg(self, arg):\r
609 if len(arg) >= 2 and arg[0] == '-':\r
610 try:\r
611 self.count = int(arg[1:])\r
612 return\r
613 except ValueError:\r
614 pass\r
615 self.ok &= CheckOneArg(arg, self.count).ok\r
616 self.count = None\r
617\r
618 def parse_options(self):\r
619 parser = argparse.ArgumentParser(description=__copyright__)\r
620 parser.add_argument('--version', action='version',\r
621 version='%(prog)s ' + VersionNumber)\r
622 parser.add_argument('patches', nargs='*',\r
623 help='[patch file | git rev list]')\r
624 group = parser.add_mutually_exclusive_group()\r
625 group.add_argument("--oneline",\r
626 action="store_true",\r
627 help="Print one result per line")\r
628 group.add_argument("--silent",\r
629 action="store_true",\r
630 help="Print nothing")\r
631 self.args = parser.parse_args()\r
632 if self.args.oneline:\r
633 Verbose.level = Verbose.ONELINE\r
634 if self.args.silent:\r
635 Verbose.level = Verbose.SILENT\r
636\r
637if __name__ == "__main__":\r
638 sys.exit(PatchCheckApp().retval)\r