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