]> git.proxmox.com Git - mirror_edk2.git/blame - BaseTools/Scripts/PatchCheck.py
Maintainers.txt: Add Gary Lin as the reviewer for LsiScsi driver
[mirror_edk2.git] / BaseTools / Scripts / PatchCheck.py
CommitLineData
a7e173b0
JJ
1## @file\r
2# Check a patch for various format issues\r
3#\r
b112ec22 4# Copyright (c) 2015 - 2020, Intel Corporation. All rights reserved.<BR>\r
8ffa47fb 5# Copyright (C) 2020, Red Hat, Inc.<BR>\r
01356d29 6# Copyright (c) 2020, ARM Ltd. All rights reserved.<BR>\r
a7e173b0 7#\r
2e351cbe 8# SPDX-License-Identifier: BSD-2-Clause-Patent\r
a7e173b0
JJ
9#\r
10\r
11from __future__ import print_function\r
12\r
13VersionNumber = '0.1'\r
e6140670 14__copyright__ = "Copyright (c) 2015 - 2016, Intel Corporation All rights reserved."\r
a7e173b0
JJ
15\r
16import email\r
17import argparse\r
18import os\r
19import re\r
20import subprocess\r
21import sys\r
22\r
f56d52c7
LL
23import email.header\r
24\r
a7e173b0
JJ
25class Verbose:\r
26 SILENT, ONELINE, NORMAL = range(3)\r
27 level = NORMAL\r
28\r
8ffa47fb
PMD
29class EmailAddressCheck:\r
30 """Checks an email address."""\r
31\r
8120390a 32 def __init__(self, email, description):\r
8ffa47fb
PMD
33 self.ok = True\r
34\r
35 if email is None:\r
36 self.error('Email address is missing!')\r
37 return\r
8120390a
PMD
38 if description is None:\r
39 self.error('Email description is missing!')\r
40 return\r
8ffa47fb 41\r
8120390a 42 self.description = "'" + description + "'"\r
8ffa47fb
PMD
43 self.check_email_address(email)\r
44\r
45 def error(self, *err):\r
46 if self.ok and Verbose.level > Verbose.ONELINE:\r
8120390a 47 print('The ' + self.description + ' email address is not valid:')\r
8ffa47fb
PMD
48 self.ok = False\r
49 if Verbose.level < Verbose.NORMAL:\r
50 return\r
51 count = 0\r
52 for line in err:\r
53 prefix = (' *', ' ')[count > 0]\r
54 print(prefix, line)\r
55 count += 1\r
56\r
57 email_re1 = re.compile(r'(?:\s*)(.*?)(\s*)<(.+)>\s*$',\r
58 re.MULTILINE|re.IGNORECASE)\r
59\r
60 def check_email_address(self, email):\r
61 email = email.strip()\r
62 mo = self.email_re1.match(email)\r
63 if mo is None:\r
64 self.error("Email format is invalid: " + email.strip())\r
65 return\r
66\r
67 name = mo.group(1).strip()\r
68 if name == '':\r
69 self.error("Name is not provided with email address: " +\r
70 email)\r
71 else:\r
72 quoted = len(name) > 2 and name[0] == '"' and name[-1] == '"'\r
73 if name.find(',') >= 0 and not quoted:\r
74 self.error('Add quotes (") around name with a comma: ' +\r
75 name)\r
76\r
77 if mo.group(2) == '':\r
78 self.error("There should be a space between the name and " +\r
79 "email address: " + email)\r
80\r
81 if mo.group(3).find(' ') >= 0:\r
82 self.error("The email address cannot contain a space: " +\r
83 mo.group(3))\r
84\r
38ed2ff3
PMD
85 if ' via Groups.Io' in name and mo.group(3).endswith('@groups.io'):\r
86 self.error("Email rewritten by lists DMARC / DKIM / SPF: " +\r
87 email)\r
88\r
a7e173b0
JJ
89class CommitMessageCheck:\r
90 """Checks the contents of a git commit message."""\r
91\r
92 def __init__(self, subject, message):\r
93 self.ok = True\r
94\r
95 if subject is None and message is None:\r
96 self.error('Commit message is missing!')\r
97 return\r
98\r
99 self.subject = subject\r
100 self.msg = message\r
101\r
b112ec22
MK
102 print (subject)\r
103\r
a7e173b0
JJ
104 self.check_contributed_under()\r
105 self.check_signed_off_by()\r
106 self.check_misc_signatures()\r
107 self.check_overall_format()\r
108 self.report_message_result()\r
109\r
110 url = 'https://github.com/tianocore/tianocore.github.io/wiki/Commit-Message-Format'\r
111\r
112 def report_message_result(self):\r
113 if Verbose.level < Verbose.NORMAL:\r
114 return\r
115 if self.ok:\r
116 # All checks passed\r
117 return_code = 0\r
118 print('The commit message format passed all checks.')\r
119 else:\r
120 return_code = 1\r
121 if not self.ok:\r
122 print(self.url)\r
123\r
124 def error(self, *err):\r
125 if self.ok and Verbose.level > Verbose.ONELINE:\r
126 print('The commit message format is not valid:')\r
127 self.ok = False\r
128 if Verbose.level < Verbose.NORMAL:\r
129 return\r
130 count = 0\r
131 for line in err:\r
132 prefix = (' *', ' ')[count > 0]\r
133 print(prefix, line)\r
134 count += 1\r
135\r
a2813610
MK
136 # Find 'contributed-under:' at the start of a line ignoring case and\r
137 # requires ':' to be present. Matches if there is white space before\r
138 # the tag or between the tag and the ':'.\r
139 contributed_under_re = \\r
140 re.compile(r'^\s*contributed-under\s*:', re.MULTILINE|re.IGNORECASE)\r
141\r
a7e173b0 142 def check_contributed_under(self):\r
a2813610
MK
143 match = self.contributed_under_re.search(self.msg)\r
144 if match is not None:\r
145 self.error('Contributed-under! (Note: this must be ' +\r
146 'removed by the code contributor!)')\r
a7e173b0
JJ
147\r
148 @staticmethod\r
149 def make_signature_re(sig, re_input=False):\r
150 if re_input:\r
151 sub_re = sig\r
152 else:\r
153 sub_re = sig.replace('-', r'[-\s]+')\r
154 re_str = (r'^(?P<tag>' + sub_re +\r
155 r')(\s*):(\s*)(?P<value>\S.*?)(?:\s*)$')\r
156 try:\r
157 return re.compile(re_str, re.MULTILINE|re.IGNORECASE)\r
158 except Exception:\r
159 print("Tried to compile re:", re_str)\r
160 raise\r
161\r
162 sig_block_re = \\r
163 re.compile(r'''^\r
164 (?: (?P<tag>[^:]+) \s* : \s*\r
165 (?P<value>\S.*?) )\r
166 |\r
167 (?: \[ (?P<updater>[^:]+) \s* : \s*\r
168 (?P<note>.+?) \s* \] )\r
169 \s* $''',\r
170 re.VERBOSE | re.MULTILINE)\r
171\r
172 def find_signatures(self, sig):\r
173 if not sig.endswith('-by') and sig != 'Cc':\r
174 sig += '-by'\r
175 regex = self.make_signature_re(sig)\r
176\r
177 sigs = regex.findall(self.msg)\r
178\r
179 bad_case_sigs = filter(lambda m: m[0] != sig, sigs)\r
180 for s in bad_case_sigs:\r
181 self.error("'" +s[0] + "' should be '" + sig + "'")\r
182\r
183 for s in sigs:\r
184 if s[1] != '':\r
185 self.error('There should be no spaces between ' + sig +\r
186 " and the ':'")\r
187 if s[2] != ' ':\r
188 self.error("There should be a space after '" + sig + ":'")\r
189\r
8120390a 190 EmailAddressCheck(s[3], sig)\r
a7e173b0
JJ
191\r
192 return sigs\r
193\r
a7e173b0
JJ
194 def check_signed_off_by(self):\r
195 sob='Signed-off-by'\r
196 if self.msg.find(sob) < 0:\r
197 self.error('Missing Signed-off-by! (Note: this must be ' +\r
198 'added by the code contributor!)')\r
199 return\r
200\r
201 sobs = self.find_signatures('Signed-off')\r
202\r
203 if len(sobs) == 0:\r
204 self.error('Invalid Signed-off-by format!')\r
205 return\r
206\r
207 sig_types = (\r
208 'Reviewed',\r
209 'Reported',\r
210 'Tested',\r
211 'Suggested',\r
212 'Acked',\r
213 'Cc'\r
214 )\r
215\r
216 def check_misc_signatures(self):\r
217 for sig in self.sig_types:\r
218 self.find_signatures(sig)\r
219\r
b112ec22
MK
220 cve_re = re.compile('CVE-[0-9]{4}-[0-9]{5}[^0-9]')\r
221\r
a7e173b0
JJ
222 def check_overall_format(self):\r
223 lines = self.msg.splitlines()\r
224\r
225 if len(lines) >= 1 and lines[0].endswith('\r\n'):\r
226 empty_line = '\r\n'\r
227 else:\r
228 empty_line = '\n'\r
229\r
230 lines.insert(0, empty_line)\r
231 lines.insert(0, self.subject + empty_line)\r
232\r
233 count = len(lines)\r
234\r
235 if count <= 0:\r
236 self.error('Empty commit message!')\r
237 return\r
238\r
b112ec22
MK
239 if count >= 1 and re.search(self.cve_re, lines[0]):\r
240 #\r
241 # If CVE-xxxx-xxxxx is present in subject line, then limit length of\r
242 # subject line to 92 characters\r
243 #\r
244 if len(lines[0].rstrip()) >= 93:\r
245 self.error(\r
246 'First line of commit message (subject line) is too long (%d >= 93).' %\r
247 (len(lines[0].rstrip()))\r
248 )\r
249 else:\r
250 #\r
251 # If CVE-xxxx-xxxxx is not present in subject line, then limit\r
252 # length of subject line to 75 characters\r
253 #\r
254 if len(lines[0].rstrip()) >= 76:\r
255 self.error(\r
256 'First line of commit message (subject line) is too long (%d >= 76).' %\r
257 (len(lines[0].rstrip()))\r
258 )\r
a7e173b0
JJ
259\r
260 if count >= 1 and len(lines[0].strip()) == 0:\r
261 self.error('First line of commit message (subject line) ' +\r
262 'is empty.')\r
263\r
264 if count >= 2 and lines[1].strip() != '':\r
265 self.error('Second line of commit message should be ' +\r
266 'empty.')\r
267\r
268 for i in range(2, count):\r
e6140670 269 if (len(lines[i]) >= 76 and\r
a7e173b0
JJ
270 len(lines[i].split()) > 1 and\r
271 not lines[i].startswith('git-svn-id:')):\r
b112ec22
MK
272 #\r
273 # Print a warning if body line is longer than 75 characters\r
274 #\r
275 print(\r
276 'WARNING - Line %d of commit message is too long (%d >= 76).' %\r
277 (i + 1, len(lines[i]))\r
278 )\r
279 print(lines[i])\r
a7e173b0
JJ
280\r
281 last_sig_line = None\r
282 for i in range(count - 1, 0, -1):\r
283 line = lines[i]\r
284 mo = self.sig_block_re.match(line)\r
285 if mo is None:\r
286 if line.strip() == '':\r
287 break\r
288 elif last_sig_line is not None:\r
289 err2 = 'Add empty line before "%s"?' % last_sig_line\r
290 self.error('The line before the signature block ' +\r
291 'should be empty', err2)\r
292 else:\r
293 self.error('The signature block was not found')\r
294 break\r
295 last_sig_line = line.strip()\r
296\r
297(START, PRE_PATCH, PATCH) = range(3)\r
298\r
299class GitDiffCheck:\r
300 """Checks the contents of a git diff."""\r
301\r
302 def __init__(self, diff):\r
303 self.ok = True\r
304 self.format_ok = True\r
305 self.lines = diff.splitlines(True)\r
306 self.count = len(self.lines)\r
307 self.line_num = 0\r
308 self.state = START\r
6a69dd49 309 self.new_bin = []\r
a4cfb842 310 self.LicenseCheck(self.lines, self.count)\r
a7e173b0
JJ
311 while self.line_num < self.count and self.format_ok:\r
312 line_num = self.line_num\r
313 self.run()\r
314 assert(self.line_num > line_num)\r
315 self.report_message_result()\r
316\r
a4cfb842
SZ
317 def LicenseCheck(self, lines, count):\r
318 self.ok = True\r
319 self.startcheck = False\r
320 self.license = True\r
321 line_index = 0\r
322 for line in lines:\r
323 if line.startswith('--- /dev/null'):\r
324 nextline = lines[line_index + 1]\r
325 added_file = self.Readdedfileformat.search(nextline).group(1)\r
326 added_file_extension = os.path.splitext(added_file)[1]\r
327 if added_file_extension in self.file_extension_list:\r
328 self.startcheck = True\r
329 self.license = False\r
330 if self.startcheck and self.license_format_preflix in line:\r
331 if self.bsd2_patent in line or self.bsd3_patent in line:\r
332 self.license = True\r
333 else:\r
334 for optional_license in self.license_optional_list:\r
335 if optional_license in line:\r
336 self.license = True\r
337 self.warning(added_file)\r
338 if line_index + 1 == count or lines[line_index + 1].startswith('diff --') and self.startcheck:\r
339 if not self.license:\r
340 error_message = "Invalid License in: " + added_file\r
341 self.error(error_message)\r
342 self.startcheck = False\r
343 self.license = True\r
344 line_index = line_index + 1\r
345\r
346 def warning(self, *err):\r
347 count = 0\r
348 for line in err:\r
349 warning_format = 'Warning: License accepted but not BSD plus patent license in'\r
350 print(warning_format, line)\r
351 count += 1\r
352\r
a7e173b0
JJ
353 def report_message_result(self):\r
354 if Verbose.level < Verbose.NORMAL:\r
355 return\r
356 if self.ok:\r
357 print('The code passed all checks.')\r
6a69dd49
HW
358 if self.new_bin:\r
359 print('\nWARNING - The following binary files will be added ' +\r
360 'into the repository:')\r
361 for binary in self.new_bin:\r
362 print(' ' + binary)\r
a7e173b0
JJ
363\r
364 def run(self):\r
365 line = self.lines[self.line_num]\r
366\r
367 if self.state in (PRE_PATCH, PATCH):\r
368 if line.startswith('diff --git'):\r
369 self.state = START\r
370 if self.state == PATCH:\r
371 if line.startswith('@@ '):\r
372 self.state = PRE_PATCH\r
373 elif len(line) >= 1 and line[0] not in ' -+' and \\r
05579e51 374 not line.startswith('\r\n') and \\r
aab57eff 375 not line.startswith(r'\ No newline ') and not self.binary:\r
a7e173b0
JJ
376 for line in self.lines[self.line_num + 1:]:\r
377 if line.startswith('diff --git'):\r
378 self.format_error('diff found after end of patch')\r
379 break\r
380 self.line_num = self.count\r
381 return\r
382\r
383 if self.state == START:\r
384 if line.startswith('diff --git'):\r
385 self.state = PRE_PATCH\r
ccaa7754 386 self.filename = line[13:].split(' ', 1)[0]\r
6a69dd49 387 self.is_newfile = False\r
c8b8157e
MK
388 self.force_crlf = True\r
389 self.force_notabs = True\r
01356d29
PG
390 if self.filename.endswith('.sh') or \\r
391 self.filename.startswith('BaseTools/BinWrappers/PosixLike/') or \\r
392 self.filename.startswith('BaseTools/Bin/CYGWIN_NT-5.1-i686/') or \\r
393 self.filename == 'BaseTools/BuildEnv':\r
c8b8157e
MK
394 #\r
395 # Do not enforce CR/LF line endings for linux shell scripts.\r
01356d29
PG
396 # Some linux shell scripts don't end with the ".sh" extension,\r
397 # they are identified by their path.\r
c8b8157e
MK
398 #\r
399 self.force_crlf = False\r
c267eb88
LL
400 if self.filename == '.gitmodules' or \\r
401 self.filename == 'BaseTools/Conf/diff.order':\r
c8b8157e 402 #\r
c267eb88
LL
403 # .gitmodules and diff orderfiles are used internally by git\r
404 # use tabs and LF line endings. Do not enforce no tabs and\r
405 # do not enforce CR/LF line endings.\r
c8b8157e
MK
406 #\r
407 self.force_crlf = False\r
408 self.force_notabs = False\r
a7e173b0
JJ
409 elif len(line.rstrip()) != 0:\r
410 self.format_error("didn't find diff command")\r
411 self.line_num += 1\r
412 elif self.state == PRE_PATCH:\r
a7e173b0
JJ
413 if line.startswith('@@ '):\r
414 self.state = PATCH\r
103733f8 415 self.binary = False\r
6a69dd49
HW
416 elif line.startswith('GIT binary patch') or \\r
417 line.startswith('Binary files'):\r
103733f8
JJ
418 self.state = PATCH\r
419 self.binary = True\r
6a69dd49
HW
420 if self.is_newfile:\r
421 self.new_bin.append(self.filename)\r
c8b8157e
MK
422 elif line.startswith('new file mode 160000'):\r
423 #\r
424 # New submodule. Do not enforce CR/LF line endings\r
425 #\r
426 self.force_crlf = False\r
a7e173b0
JJ
427 else:\r
428 ok = False\r
6a69dd49 429 self.is_newfile = self.newfile_prefix_re.match(line)\r
a7e173b0
JJ
430 for pfx in self.pre_patch_prefixes:\r
431 if line.startswith(pfx):\r
432 ok = True\r
433 if not ok:\r
434 self.format_error("didn't find diff hunk marker (@@)")\r
435 self.line_num += 1\r
436 elif self.state == PATCH:\r
103733f8
JJ
437 if self.binary:\r
438 pass\r
aab57eff 439 elif line.startswith('-'):\r
a7e173b0
JJ
440 pass\r
441 elif line.startswith('+'):\r
442 self.check_added_line(line[1:])\r
05579e51
MK
443 elif line.startswith('\r\n'):\r
444 pass\r
a7e173b0
JJ
445 elif line.startswith(r'\ No newline '):\r
446 pass\r
447 elif not line.startswith(' '):\r
448 self.format_error("unexpected patch line")\r
449 self.line_num += 1\r
450\r
451 pre_patch_prefixes = (\r
452 '--- ',\r
453 '+++ ',\r
454 'index ',\r
455 'new file ',\r
456 'deleted file ',\r
457 'old mode ',\r
458 'new mode ',\r
459 'similarity index ',\r
1eeb5ff1
MK
460 'copy from ',\r
461 'copy to ',\r
a7e173b0 462 'rename ',\r
a7e173b0
JJ
463 )\r
464\r
465 line_endings = ('\r\n', '\n\r', '\n', '\r')\r
466\r
6a69dd49
HW
467 newfile_prefix_re = \\r
468 re.compile(r'''^\r
469 index\ 0+\.\.\r
470 ''',\r
471 re.VERBOSE)\r
a7e173b0
JJ
472\r
473 def added_line_error(self, msg, line):\r
474 lines = [ msg ]\r
6a69dd49
HW
475 if self.filename is not None:\r
476 lines.append('File: ' + self.filename)\r
a7e173b0
JJ
477 lines.append('Line: ' + line)\r
478\r
479 self.error(*lines)\r
480\r
c3926cdb
YZ
481 old_debug_re = \\r
482 re.compile(r'''\r
483 DEBUG \s* \( \s* \( \s*\r
484 (?: DEBUG_[A-Z_]+ \s* \| \s*)*\r
485 EFI_D_ ([A-Z_]+)\r
486 ''',\r
487 re.VERBOSE)\r
488\r
a7e173b0
JJ
489 def check_added_line(self, line):\r
490 eol = ''\r
491 for an_eol in self.line_endings:\r
492 if line.endswith(an_eol):\r
493 eol = an_eol\r
494 line = line[:-len(eol)]\r
495\r
496 stripped = line.rstrip()\r
497\r
498 if self.force_crlf and eol != '\r\n':\r
499 self.added_line_error('Line ending (%s) is not CRLF' % repr(eol),\r
500 line)\r
c8b8157e 501 if self.force_notabs and '\t' in line:\r
a7e173b0
JJ
502 self.added_line_error('Tab character used', line)\r
503 if len(stripped) < len(line):\r
504 self.added_line_error('Trailing whitespace found', line)\r
505\r
c3926cdb
YZ
506 mo = self.old_debug_re.search(line)\r
507 if mo is not None:\r
508 self.added_line_error('EFI_D_' + mo.group(1) + ' was used, '\r
509 'but DEBUG_' + mo.group(1) +\r
510 ' is now recommended', line)\r
511\r
a7e173b0
JJ
512 split_diff_re = re.compile(r'''\r
513 (?P<cmd>\r
514 ^ diff \s+ --git \s+ a/.+ \s+ b/.+ $\r
515 )\r
516 (?P<index>\r
517 ^ index \s+ .+ $\r
518 )\r
519 ''',\r
520 re.IGNORECASE | re.VERBOSE | re.MULTILINE)\r
521\r
522 def format_error(self, err):\r
523 self.format_ok = False\r
524 err = 'Patch format error: ' + err\r
525 err2 = 'Line: ' + self.lines[self.line_num].rstrip()\r
526 self.error(err, err2)\r
527\r
528 def error(self, *err):\r
529 if self.ok and Verbose.level > Verbose.ONELINE:\r
530 print('Code format is not valid:')\r
531 self.ok = False\r
532 if Verbose.level < Verbose.NORMAL:\r
533 return\r
534 count = 0\r
535 for line in err:\r
536 prefix = (' *', ' ')[count > 0]\r
537 print(prefix, line)\r
538 count += 1\r
539\r
a4cfb842
SZ
540 license_format_preflix = 'SPDX-License-Identifier'\r
541\r
542 bsd2_patent = 'BSD-2-Clause-Patent'\r
543\r
544 bsd3_patent = 'BSD-3-Clause-Patent'\r
545\r
546 license_optional_list = ['BSD-2-Clause', 'BSD-3-Clause', 'MIT', 'Python-2.0', 'Zlib']\r
547\r
548 Readdedfileformat = re.compile(r'\+\+\+ b\/(.*)\n')\r
549\r
550 file_extension_list = [".c", ".h", ".inf", ".dsc", ".dec", ".py", ".bat", ".sh", ".uni", ".yaml", ".fdf", ".inc", "yml", ".asm", \\r
551 ".asm16", ".asl", ".vfr", ".s", ".S", ".aslc", ".nasm", ".nasmb", ".idf", ".Vfr", ".H"]\r
552\r
a7e173b0
JJ
553class CheckOnePatch:\r
554 """Checks the contents of a git email formatted patch.\r
555\r
556 Various checks are performed on both the commit message and the\r
557 patch content.\r
558 """\r
559\r
560 def __init__(self, name, patch):\r
561 self.patch = patch\r
562 self.find_patch_pieces()\r
563\r
c0328cf3
PMD
564 email_check = EmailAddressCheck(self.author_email, 'Author')\r
565 email_ok = email_check.ok\r
566\r
a7e173b0
JJ
567 msg_check = CommitMessageCheck(self.commit_subject, self.commit_msg)\r
568 msg_ok = msg_check.ok\r
569\r
570 diff_ok = True\r
571 if self.diff is not None:\r
572 diff_check = GitDiffCheck(self.diff)\r
573 diff_ok = diff_check.ok\r
574\r
c0328cf3 575 self.ok = email_ok and msg_ok and diff_ok\r
a7e173b0
JJ
576\r
577 if Verbose.level == Verbose.ONELINE:\r
578 if self.ok:\r
579 result = 'ok'\r
580 else:\r
581 result = list()\r
582 if not msg_ok:\r
583 result.append('commit message')\r
584 if not diff_ok:\r
585 result.append('diff content')\r
586 result = 'bad ' + ' and '.join(result)\r
587 print(name, result)\r
588\r
589\r
590 git_diff_re = re.compile(r'''\r
591 ^ diff \s+ --git \s+ a/.+ \s+ b/.+ $\r
592 ''',\r
593 re.IGNORECASE | re.VERBOSE | re.MULTILINE)\r
594\r
595 stat_re = \\r
596 re.compile(r'''\r
597 (?P<commit_message> [\s\S\r\n]* )\r
598 (?P<stat>\r
599 ^ --- $ [\r\n]+\r
600 (?: ^ \s+ .+ \s+ \| \s+ \d+ \s+ \+* \-*\r
601 $ [\r\n]+ )+\r
602 [\s\S\r\n]+\r
603 )\r
604 ''',\r
605 re.IGNORECASE | re.VERBOSE | re.MULTILINE)\r
606\r
e709bbb1
YZ
607 subject_prefix_re = \\r
608 re.compile(r'''^\r
609 \s* (\[\r
610 [^\[\]]* # Allow all non-brackets\r
611 \])* \s*\r
612 ''',\r
613 re.VERBOSE)\r
614\r
a7e173b0
JJ
615 def find_patch_pieces(self):\r
616 if sys.version_info < (3, 0):\r
617 patch = self.patch.encode('ascii', 'ignore')\r
618 else:\r
619 patch = self.patch\r
620\r
621 self.commit_msg = None\r
622 self.stat = None\r
623 self.commit_subject = None\r
624 self.commit_prefix = None\r
625 self.diff = None\r
626\r
627 if patch.startswith('diff --git'):\r
628 self.diff = patch\r
629 return\r
630\r
631 pmail = email.message_from_string(patch)\r
632 parts = list(pmail.walk())\r
633 assert(len(parts) == 1)\r
634 assert(parts[0].get_content_type() == 'text/plain')\r
635 content = parts[0].get_payload(decode=True).decode('utf-8', 'ignore')\r
636\r
637 mo = self.git_diff_re.search(content)\r
638 if mo is not None:\r
639 self.diff = content[mo.start():]\r
640 content = content[:mo.start()]\r
641\r
642 mo = self.stat_re.search(content)\r
643 if mo is None:\r
644 self.commit_msg = content\r
645 else:\r
646 self.stat = mo.group('stat')\r
647 self.commit_msg = mo.group('commit_message')\r
b112ec22
MK
648 #\r
649 # Parse subject line from email header. The subject line may be\r
650 # composed of multiple parts with different encodings. Decode and\r
651 # combine all the parts to produce a single string with the contents of\r
652 # the decoded subject line.\r
653 #\r
654 parts = email.header.decode_header(pmail.get('subject'))\r
655 subject = ''\r
656 for (part, encoding) in parts:\r
657 if encoding:\r
658 part = part.decode(encoding)\r
659 else:\r
660 try:\r
661 part = part.decode()\r
662 except:\r
663 pass\r
664 subject = subject + part\r
a7e173b0 665\r
b112ec22 666 self.commit_subject = subject.replace('\r\n', '')\r
a7e173b0 667 self.commit_subject = self.commit_subject.replace('\n', '')\r
e709bbb1 668 self.commit_subject = self.subject_prefix_re.sub('', self.commit_subject, 1)\r
a7e173b0 669\r
c0328cf3
PMD
670 self.author_email = pmail['from']\r
671\r
a7e173b0
JJ
672class CheckGitCommits:\r
673 """Reads patches from git based on the specified git revision range.\r
674\r
675 The patches are read from git, and then checked.\r
676 """\r
677\r
678 def __init__(self, rev_spec, max_count):\r
679 commits = self.read_commit_list_from_git(rev_spec, max_count)\r
680 if len(commits) == 1 and Verbose.level > Verbose.ONELINE:\r
681 commits = [ rev_spec ]\r
682 self.ok = True\r
683 blank_line = False\r
684 for commit in commits:\r
685 if Verbose.level > Verbose.ONELINE:\r
686 if blank_line:\r
687 print()\r
688 else:\r
689 blank_line = True\r
690 print('Checking git commit:', commit)\r
8f38b08b
PMD
691 email = self.read_committer_email_address_from_git(commit)\r
692 self.ok &= EmailAddressCheck(email, 'Committer').ok\r
a7e173b0
JJ
693 patch = self.read_patch_from_git(commit)\r
694 self.ok &= CheckOnePatch(commit, patch).ok\r
5ac4548c
JC
695 if not commits:\r
696 print("Couldn't find commit matching: '{}'".format(rev_spec))\r
a7e173b0
JJ
697\r
698 def read_commit_list_from_git(self, rev_spec, max_count):\r
699 # Run git to get the commit patch\r
700 cmd = [ 'rev-list', '--abbrev-commit', '--no-walk' ]\r
701 if max_count is not None:\r
702 cmd.append('--max-count=' + str(max_count))\r
703 cmd.append(rev_spec)\r
704 out = self.run_git(*cmd)\r
5ac4548c 705 return out.split() if out else []\r
a7e173b0
JJ
706\r
707 def read_patch_from_git(self, commit):\r
708 # Run git to get the commit patch\r
c37cce7a
PMD
709 return self.run_git('show', '--pretty=email', '--no-textconv',\r
710 '--no-use-mailmap', commit)\r
a7e173b0 711\r
8f38b08b
PMD
712 def read_committer_email_address_from_git(self, commit):\r
713 # Run git to get the committer email\r
c37cce7a
PMD
714 return self.run_git('show', '--pretty=%cn <%ce>', '--no-patch',\r
715 '--no-use-mailmap', commit)\r
8f38b08b 716\r
a7e173b0
JJ
717 def run_git(self, *args):\r
718 cmd = [ 'git' ]\r
719 cmd += args\r
720 p = subprocess.Popen(cmd,\r
721 stdout=subprocess.PIPE,\r
722 stderr=subprocess.STDOUT)\r
5ac4548c 723 Result = p.communicate()\r
6460513a 724 return Result[0].decode('utf-8', 'ignore') if Result[0] and Result[0].find(b"fatal")!=0 else None\r
a7e173b0
JJ
725\r
726class CheckOnePatchFile:\r
727 """Performs a patch check for a single file.\r
728\r
729 stdin is used when the filename is '-'.\r
730 """\r
731\r
732 def __init__(self, patch_filename):\r
733 if patch_filename == '-':\r
734 patch = sys.stdin.read()\r
735 patch_filename = 'stdin'\r
736 else:\r
737 f = open(patch_filename, 'rb')\r
738 patch = f.read().decode('utf-8', 'ignore')\r
739 f.close()\r
740 if Verbose.level > Verbose.ONELINE:\r
741 print('Checking patch file:', patch_filename)\r
742 self.ok = CheckOnePatch(patch_filename, patch).ok\r
743\r
744class CheckOneArg:\r
745 """Performs a patch check for a single command line argument.\r
746\r
747 The argument will be handed off to a file or git-commit based\r
748 checker.\r
749 """\r
750\r
751 def __init__(self, param, max_count=None):\r
752 self.ok = True\r
753 if param == '-' or os.path.exists(param):\r
754 checker = CheckOnePatchFile(param)\r
755 else:\r
756 checker = CheckGitCommits(param, max_count)\r
757 self.ok = checker.ok\r
758\r
759class PatchCheckApp:\r
760 """Checks patches based on the command line arguments."""\r
761\r
762 def __init__(self):\r
763 self.parse_options()\r
764 patches = self.args.patches\r
765\r
766 if len(patches) == 0:\r
767 patches = [ 'HEAD' ]\r
768\r
769 self.ok = True\r
770 self.count = None\r
771 for patch in patches:\r
772 self.process_one_arg(patch)\r
773\r
774 if self.count is not None:\r
775 self.process_one_arg('HEAD')\r
776\r
777 if self.ok:\r
778 self.retval = 0\r
779 else:\r
780 self.retval = -1\r
781\r
782 def process_one_arg(self, arg):\r
783 if len(arg) >= 2 and arg[0] == '-':\r
784 try:\r
785 self.count = int(arg[1:])\r
786 return\r
787 except ValueError:\r
788 pass\r
789 self.ok &= CheckOneArg(arg, self.count).ok\r
790 self.count = None\r
791\r
792 def parse_options(self):\r
793 parser = argparse.ArgumentParser(description=__copyright__)\r
794 parser.add_argument('--version', action='version',\r
795 version='%(prog)s ' + VersionNumber)\r
796 parser.add_argument('patches', nargs='*',\r
797 help='[patch file | git rev list]')\r
798 group = parser.add_mutually_exclusive_group()\r
799 group.add_argument("--oneline",\r
800 action="store_true",\r
801 help="Print one result per line")\r
802 group.add_argument("--silent",\r
803 action="store_true",\r
804 help="Print nothing")\r
805 self.args = parser.parse_args()\r
806 if self.args.oneline:\r
807 Verbose.level = Verbose.ONELINE\r
808 if self.args.silent:\r
809 Verbose.level = Verbose.SILENT\r
810\r
811if __name__ == "__main__":\r
812 sys.exit(PatchCheckApp().retval)\r