]> git.proxmox.com Git - mirror_edk2.git/blame - BaseTools/Scripts/PatchCheck.py
BaseTools/Scripts: Ignore the CRLF check when upgrade submodule.
[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 270 len(lines[i].split()) > 1 and\r
3d9d66ad
SZ
271 not lines[i].startswith('git-svn-id:') and\r
272 not lines[i].startswith('Reviewed-by') and\r
273 not lines[i].startswith('Acked-by:') and\r
274 not lines[i].startswith('Tested-by:') and\r
275 not lines[i].startswith('Reported-by:') and\r
276 not lines[i].startswith('Suggested-by:') and\r
277 not lines[i].startswith('Signed-off-by:') and\r
278 not lines[i].startswith('Cc:')):\r
b112ec22
MK
279 #\r
280 # Print a warning if body line is longer than 75 characters\r
281 #\r
282 print(\r
283 'WARNING - Line %d of commit message is too long (%d >= 76).' %\r
284 (i + 1, len(lines[i]))\r
285 )\r
286 print(lines[i])\r
a7e173b0
JJ
287\r
288 last_sig_line = None\r
289 for i in range(count - 1, 0, -1):\r
290 line = lines[i]\r
291 mo = self.sig_block_re.match(line)\r
292 if mo is None:\r
293 if line.strip() == '':\r
294 break\r
295 elif last_sig_line is not None:\r
296 err2 = 'Add empty line before "%s"?' % last_sig_line\r
297 self.error('The line before the signature block ' +\r
298 'should be empty', err2)\r
299 else:\r
300 self.error('The signature block was not found')\r
301 break\r
302 last_sig_line = line.strip()\r
303\r
304(START, PRE_PATCH, PATCH) = range(3)\r
305\r
306class GitDiffCheck:\r
307 """Checks the contents of a git diff."""\r
308\r
309 def __init__(self, diff):\r
310 self.ok = True\r
311 self.format_ok = True\r
312 self.lines = diff.splitlines(True)\r
313 self.count = len(self.lines)\r
314 self.line_num = 0\r
315 self.state = START\r
6a69dd49 316 self.new_bin = []\r
a4cfb842 317 self.LicenseCheck(self.lines, self.count)\r
a7e173b0
JJ
318 while self.line_num < self.count and self.format_ok:\r
319 line_num = self.line_num\r
320 self.run()\r
321 assert(self.line_num > line_num)\r
322 self.report_message_result()\r
323\r
a4cfb842
SZ
324 def LicenseCheck(self, lines, count):\r
325 self.ok = True\r
326 self.startcheck = False\r
327 self.license = True\r
328 line_index = 0\r
329 for line in lines:\r
330 if line.startswith('--- /dev/null'):\r
331 nextline = lines[line_index + 1]\r
332 added_file = self.Readdedfileformat.search(nextline).group(1)\r
333 added_file_extension = os.path.splitext(added_file)[1]\r
334 if added_file_extension in self.file_extension_list:\r
335 self.startcheck = True\r
336 self.license = False\r
337 if self.startcheck and self.license_format_preflix in line:\r
338 if self.bsd2_patent in line or self.bsd3_patent in line:\r
339 self.license = True\r
340 else:\r
341 for optional_license in self.license_optional_list:\r
342 if optional_license in line:\r
343 self.license = True\r
344 self.warning(added_file)\r
345 if line_index + 1 == count or lines[line_index + 1].startswith('diff --') and self.startcheck:\r
346 if not self.license:\r
347 error_message = "Invalid License in: " + added_file\r
348 self.error(error_message)\r
349 self.startcheck = False\r
350 self.license = True\r
351 line_index = line_index + 1\r
352\r
353 def warning(self, *err):\r
354 count = 0\r
355 for line in err:\r
356 warning_format = 'Warning: License accepted but not BSD plus patent license in'\r
357 print(warning_format, line)\r
358 count += 1\r
359\r
a7e173b0
JJ
360 def report_message_result(self):\r
361 if Verbose.level < Verbose.NORMAL:\r
362 return\r
363 if self.ok:\r
364 print('The code passed all checks.')\r
6a69dd49
HW
365 if self.new_bin:\r
366 print('\nWARNING - The following binary files will be added ' +\r
367 'into the repository:')\r
368 for binary in self.new_bin:\r
369 print(' ' + binary)\r
a7e173b0
JJ
370\r
371 def run(self):\r
372 line = self.lines[self.line_num]\r
373\r
374 if self.state in (PRE_PATCH, PATCH):\r
375 if line.startswith('diff --git'):\r
376 self.state = START\r
377 if self.state == PATCH:\r
378 if line.startswith('@@ '):\r
379 self.state = PRE_PATCH\r
380 elif len(line) >= 1 and line[0] not in ' -+' and \\r
05579e51 381 not line.startswith('\r\n') and \\r
aab57eff 382 not line.startswith(r'\ No newline ') and not self.binary:\r
a7e173b0
JJ
383 for line in self.lines[self.line_num + 1:]:\r
384 if line.startswith('diff --git'):\r
385 self.format_error('diff found after end of patch')\r
386 break\r
387 self.line_num = self.count\r
388 return\r
389\r
390 if self.state == START:\r
391 if line.startswith('diff --git'):\r
392 self.state = PRE_PATCH\r
ccaa7754 393 self.filename = line[13:].split(' ', 1)[0]\r
6a69dd49 394 self.is_newfile = False\r
c8b8157e
MK
395 self.force_crlf = True\r
396 self.force_notabs = True\r
01356d29
PG
397 if self.filename.endswith('.sh') or \\r
398 self.filename.startswith('BaseTools/BinWrappers/PosixLike/') or \\r
399 self.filename.startswith('BaseTools/Bin/CYGWIN_NT-5.1-i686/') or \\r
400 self.filename == 'BaseTools/BuildEnv':\r
c8b8157e
MK
401 #\r
402 # Do not enforce CR/LF line endings for linux shell scripts.\r
01356d29
PG
403 # Some linux shell scripts don't end with the ".sh" extension,\r
404 # they are identified by their path.\r
c8b8157e
MK
405 #\r
406 self.force_crlf = False\r
c267eb88
LL
407 if self.filename == '.gitmodules' or \\r
408 self.filename == 'BaseTools/Conf/diff.order':\r
c8b8157e 409 #\r
c267eb88
LL
410 # .gitmodules and diff orderfiles are used internally by git\r
411 # use tabs and LF line endings. Do not enforce no tabs and\r
412 # do not enforce CR/LF line endings.\r
c8b8157e
MK
413 #\r
414 self.force_crlf = False\r
415 self.force_notabs = False\r
a7e173b0
JJ
416 elif len(line.rstrip()) != 0:\r
417 self.format_error("didn't find diff command")\r
418 self.line_num += 1\r
419 elif self.state == PRE_PATCH:\r
a7e173b0
JJ
420 if line.startswith('@@ '):\r
421 self.state = PATCH\r
103733f8 422 self.binary = False\r
6a69dd49
HW
423 elif line.startswith('GIT binary patch') or \\r
424 line.startswith('Binary files'):\r
103733f8
JJ
425 self.state = PATCH\r
426 self.binary = True\r
6a69dd49
HW
427 if self.is_newfile:\r
428 self.new_bin.append(self.filename)\r
c8b8157e
MK
429 elif line.startswith('new file mode 160000'):\r
430 #\r
431 # New submodule. Do not enforce CR/LF line endings\r
432 #\r
433 self.force_crlf = False\r
a7e173b0
JJ
434 else:\r
435 ok = False\r
6a69dd49 436 self.is_newfile = self.newfile_prefix_re.match(line)\r
a7e173b0
JJ
437 for pfx in self.pre_patch_prefixes:\r
438 if line.startswith(pfx):\r
439 ok = True\r
440 if not ok:\r
441 self.format_error("didn't find diff hunk marker (@@)")\r
442 self.line_num += 1\r
443 elif self.state == PATCH:\r
103733f8
JJ
444 if self.binary:\r
445 pass\r
aab57eff 446 elif line.startswith('-'):\r
a7e173b0
JJ
447 pass\r
448 elif line.startswith('+'):\r
449 self.check_added_line(line[1:])\r
05579e51
MK
450 elif line.startswith('\r\n'):\r
451 pass\r
a7e173b0
JJ
452 elif line.startswith(r'\ No newline '):\r
453 pass\r
454 elif not line.startswith(' '):\r
455 self.format_error("unexpected patch line")\r
456 self.line_num += 1\r
457\r
458 pre_patch_prefixes = (\r
459 '--- ',\r
460 '+++ ',\r
461 'index ',\r
462 'new file ',\r
463 'deleted file ',\r
464 'old mode ',\r
465 'new mode ',\r
466 'similarity index ',\r
1eeb5ff1
MK
467 'copy from ',\r
468 'copy to ',\r
a7e173b0 469 'rename ',\r
a7e173b0
JJ
470 )\r
471\r
472 line_endings = ('\r\n', '\n\r', '\n', '\r')\r
473\r
6a69dd49
HW
474 newfile_prefix_re = \\r
475 re.compile(r'''^\r
476 index\ 0+\.\.\r
477 ''',\r
478 re.VERBOSE)\r
a7e173b0
JJ
479\r
480 def added_line_error(self, msg, line):\r
481 lines = [ msg ]\r
6a69dd49
HW
482 if self.filename is not None:\r
483 lines.append('File: ' + self.filename)\r
a7e173b0
JJ
484 lines.append('Line: ' + line)\r
485\r
486 self.error(*lines)\r
487\r
c3926cdb
YZ
488 old_debug_re = \\r
489 re.compile(r'''\r
490 DEBUG \s* \( \s* \( \s*\r
491 (?: DEBUG_[A-Z_]+ \s* \| \s*)*\r
492 EFI_D_ ([A-Z_]+)\r
493 ''',\r
494 re.VERBOSE)\r
495\r
a7e173b0
JJ
496 def check_added_line(self, line):\r
497 eol = ''\r
498 for an_eol in self.line_endings:\r
499 if line.endswith(an_eol):\r
500 eol = an_eol\r
501 line = line[:-len(eol)]\r
502\r
503 stripped = line.rstrip()\r
504\r
ff2655d1 505 if self.force_crlf and eol != '\r\n' and (line.find('Subproject commit') == -1):\r
a7e173b0
JJ
506 self.added_line_error('Line ending (%s) is not CRLF' % repr(eol),\r
507 line)\r
c8b8157e 508 if self.force_notabs and '\t' in line:\r
a7e173b0
JJ
509 self.added_line_error('Tab character used', line)\r
510 if len(stripped) < len(line):\r
511 self.added_line_error('Trailing whitespace found', line)\r
512\r
c3926cdb
YZ
513 mo = self.old_debug_re.search(line)\r
514 if mo is not None:\r
515 self.added_line_error('EFI_D_' + mo.group(1) + ' was used, '\r
516 'but DEBUG_' + mo.group(1) +\r
517 ' is now recommended', line)\r
518\r
a7e173b0
JJ
519 split_diff_re = re.compile(r'''\r
520 (?P<cmd>\r
521 ^ diff \s+ --git \s+ a/.+ \s+ b/.+ $\r
522 )\r
523 (?P<index>\r
524 ^ index \s+ .+ $\r
525 )\r
526 ''',\r
527 re.IGNORECASE | re.VERBOSE | re.MULTILINE)\r
528\r
529 def format_error(self, err):\r
530 self.format_ok = False\r
531 err = 'Patch format error: ' + err\r
532 err2 = 'Line: ' + self.lines[self.line_num].rstrip()\r
533 self.error(err, err2)\r
534\r
535 def error(self, *err):\r
536 if self.ok and Verbose.level > Verbose.ONELINE:\r
537 print('Code format is not valid:')\r
538 self.ok = False\r
539 if Verbose.level < Verbose.NORMAL:\r
540 return\r
541 count = 0\r
542 for line in err:\r
543 prefix = (' *', ' ')[count > 0]\r
544 print(prefix, line)\r
545 count += 1\r
546\r
a4cfb842
SZ
547 license_format_preflix = 'SPDX-License-Identifier'\r
548\r
549 bsd2_patent = 'BSD-2-Clause-Patent'\r
550\r
551 bsd3_patent = 'BSD-3-Clause-Patent'\r
552\r
553 license_optional_list = ['BSD-2-Clause', 'BSD-3-Clause', 'MIT', 'Python-2.0', 'Zlib']\r
554\r
555 Readdedfileformat = re.compile(r'\+\+\+ b\/(.*)\n')\r
556\r
557 file_extension_list = [".c", ".h", ".inf", ".dsc", ".dec", ".py", ".bat", ".sh", ".uni", ".yaml", ".fdf", ".inc", "yml", ".asm", \\r
558 ".asm16", ".asl", ".vfr", ".s", ".S", ".aslc", ".nasm", ".nasmb", ".idf", ".Vfr", ".H"]\r
559\r
a7e173b0
JJ
560class CheckOnePatch:\r
561 """Checks the contents of a git email formatted patch.\r
562\r
563 Various checks are performed on both the commit message and the\r
564 patch content.\r
565 """\r
566\r
567 def __init__(self, name, patch):\r
568 self.patch = patch\r
569 self.find_patch_pieces()\r
570\r
c0328cf3
PMD
571 email_check = EmailAddressCheck(self.author_email, 'Author')\r
572 email_ok = email_check.ok\r
573\r
a7e173b0
JJ
574 msg_check = CommitMessageCheck(self.commit_subject, self.commit_msg)\r
575 msg_ok = msg_check.ok\r
576\r
577 diff_ok = True\r
578 if self.diff is not None:\r
579 diff_check = GitDiffCheck(self.diff)\r
580 diff_ok = diff_check.ok\r
581\r
c0328cf3 582 self.ok = email_ok and msg_ok and diff_ok\r
a7e173b0
JJ
583\r
584 if Verbose.level == Verbose.ONELINE:\r
585 if self.ok:\r
586 result = 'ok'\r
587 else:\r
588 result = list()\r
589 if not msg_ok:\r
590 result.append('commit message')\r
591 if not diff_ok:\r
592 result.append('diff content')\r
593 result = 'bad ' + ' and '.join(result)\r
594 print(name, result)\r
595\r
596\r
597 git_diff_re = re.compile(r'''\r
598 ^ diff \s+ --git \s+ a/.+ \s+ b/.+ $\r
599 ''',\r
600 re.IGNORECASE | re.VERBOSE | re.MULTILINE)\r
601\r
602 stat_re = \\r
603 re.compile(r'''\r
604 (?P<commit_message> [\s\S\r\n]* )\r
605 (?P<stat>\r
606 ^ --- $ [\r\n]+\r
607 (?: ^ \s+ .+ \s+ \| \s+ \d+ \s+ \+* \-*\r
608 $ [\r\n]+ )+\r
609 [\s\S\r\n]+\r
610 )\r
611 ''',\r
612 re.IGNORECASE | re.VERBOSE | re.MULTILINE)\r
613\r
e709bbb1
YZ
614 subject_prefix_re = \\r
615 re.compile(r'''^\r
616 \s* (\[\r
617 [^\[\]]* # Allow all non-brackets\r
618 \])* \s*\r
619 ''',\r
620 re.VERBOSE)\r
621\r
a7e173b0
JJ
622 def find_patch_pieces(self):\r
623 if sys.version_info < (3, 0):\r
624 patch = self.patch.encode('ascii', 'ignore')\r
625 else:\r
626 patch = self.patch\r
627\r
628 self.commit_msg = None\r
629 self.stat = None\r
630 self.commit_subject = None\r
631 self.commit_prefix = None\r
632 self.diff = None\r
633\r
634 if patch.startswith('diff --git'):\r
635 self.diff = patch\r
636 return\r
637\r
638 pmail = email.message_from_string(patch)\r
639 parts = list(pmail.walk())\r
640 assert(len(parts) == 1)\r
641 assert(parts[0].get_content_type() == 'text/plain')\r
642 content = parts[0].get_payload(decode=True).decode('utf-8', 'ignore')\r
643\r
644 mo = self.git_diff_re.search(content)\r
645 if mo is not None:\r
646 self.diff = content[mo.start():]\r
647 content = content[:mo.start()]\r
648\r
649 mo = self.stat_re.search(content)\r
650 if mo is None:\r
651 self.commit_msg = content\r
652 else:\r
653 self.stat = mo.group('stat')\r
654 self.commit_msg = mo.group('commit_message')\r
b112ec22
MK
655 #\r
656 # Parse subject line from email header. The subject line may be\r
657 # composed of multiple parts with different encodings. Decode and\r
658 # combine all the parts to produce a single string with the contents of\r
659 # the decoded subject line.\r
660 #\r
661 parts = email.header.decode_header(pmail.get('subject'))\r
662 subject = ''\r
663 for (part, encoding) in parts:\r
664 if encoding:\r
665 part = part.decode(encoding)\r
666 else:\r
667 try:\r
668 part = part.decode()\r
669 except:\r
670 pass\r
671 subject = subject + part\r
a7e173b0 672\r
b112ec22 673 self.commit_subject = subject.replace('\r\n', '')\r
a7e173b0 674 self.commit_subject = self.commit_subject.replace('\n', '')\r
e709bbb1 675 self.commit_subject = self.subject_prefix_re.sub('', self.commit_subject, 1)\r
a7e173b0 676\r
c0328cf3
PMD
677 self.author_email = pmail['from']\r
678\r
a7e173b0
JJ
679class CheckGitCommits:\r
680 """Reads patches from git based on the specified git revision range.\r
681\r
682 The patches are read from git, and then checked.\r
683 """\r
684\r
685 def __init__(self, rev_spec, max_count):\r
686 commits = self.read_commit_list_from_git(rev_spec, max_count)\r
687 if len(commits) == 1 and Verbose.level > Verbose.ONELINE:\r
688 commits = [ rev_spec ]\r
689 self.ok = True\r
690 blank_line = False\r
691 for commit in commits:\r
692 if Verbose.level > Verbose.ONELINE:\r
693 if blank_line:\r
694 print()\r
695 else:\r
696 blank_line = True\r
697 print('Checking git commit:', commit)\r
8f38b08b
PMD
698 email = self.read_committer_email_address_from_git(commit)\r
699 self.ok &= EmailAddressCheck(email, 'Committer').ok\r
a7e173b0
JJ
700 patch = self.read_patch_from_git(commit)\r
701 self.ok &= CheckOnePatch(commit, patch).ok\r
5ac4548c
JC
702 if not commits:\r
703 print("Couldn't find commit matching: '{}'".format(rev_spec))\r
a7e173b0
JJ
704\r
705 def read_commit_list_from_git(self, rev_spec, max_count):\r
706 # Run git to get the commit patch\r
707 cmd = [ 'rev-list', '--abbrev-commit', '--no-walk' ]\r
708 if max_count is not None:\r
709 cmd.append('--max-count=' + str(max_count))\r
710 cmd.append(rev_spec)\r
711 out = self.run_git(*cmd)\r
5ac4548c 712 return out.split() if out else []\r
a7e173b0
JJ
713\r
714 def read_patch_from_git(self, commit):\r
715 # Run git to get the commit patch\r
c37cce7a
PMD
716 return self.run_git('show', '--pretty=email', '--no-textconv',\r
717 '--no-use-mailmap', commit)\r
a7e173b0 718\r
8f38b08b
PMD
719 def read_committer_email_address_from_git(self, commit):\r
720 # Run git to get the committer email\r
c37cce7a
PMD
721 return self.run_git('show', '--pretty=%cn <%ce>', '--no-patch',\r
722 '--no-use-mailmap', commit)\r
8f38b08b 723\r
a7e173b0
JJ
724 def run_git(self, *args):\r
725 cmd = [ 'git' ]\r
726 cmd += args\r
727 p = subprocess.Popen(cmd,\r
728 stdout=subprocess.PIPE,\r
729 stderr=subprocess.STDOUT)\r
5ac4548c 730 Result = p.communicate()\r
6460513a 731 return Result[0].decode('utf-8', 'ignore') if Result[0] and Result[0].find(b"fatal")!=0 else None\r
a7e173b0
JJ
732\r
733class CheckOnePatchFile:\r
734 """Performs a patch check for a single file.\r
735\r
736 stdin is used when the filename is '-'.\r
737 """\r
738\r
739 def __init__(self, patch_filename):\r
740 if patch_filename == '-':\r
741 patch = sys.stdin.read()\r
742 patch_filename = 'stdin'\r
743 else:\r
744 f = open(patch_filename, 'rb')\r
745 patch = f.read().decode('utf-8', 'ignore')\r
746 f.close()\r
747 if Verbose.level > Verbose.ONELINE:\r
748 print('Checking patch file:', patch_filename)\r
749 self.ok = CheckOnePatch(patch_filename, patch).ok\r
750\r
751class CheckOneArg:\r
752 """Performs a patch check for a single command line argument.\r
753\r
754 The argument will be handed off to a file or git-commit based\r
755 checker.\r
756 """\r
757\r
758 def __init__(self, param, max_count=None):\r
759 self.ok = True\r
760 if param == '-' or os.path.exists(param):\r
761 checker = CheckOnePatchFile(param)\r
762 else:\r
763 checker = CheckGitCommits(param, max_count)\r
764 self.ok = checker.ok\r
765\r
766class PatchCheckApp:\r
767 """Checks patches based on the command line arguments."""\r
768\r
769 def __init__(self):\r
770 self.parse_options()\r
771 patches = self.args.patches\r
772\r
773 if len(patches) == 0:\r
774 patches = [ 'HEAD' ]\r
775\r
776 self.ok = True\r
777 self.count = None\r
778 for patch in patches:\r
779 self.process_one_arg(patch)\r
780\r
781 if self.count is not None:\r
782 self.process_one_arg('HEAD')\r
783\r
784 if self.ok:\r
785 self.retval = 0\r
786 else:\r
787 self.retval = -1\r
788\r
789 def process_one_arg(self, arg):\r
790 if len(arg) >= 2 and arg[0] == '-':\r
791 try:\r
792 self.count = int(arg[1:])\r
793 return\r
794 except ValueError:\r
795 pass\r
796 self.ok &= CheckOneArg(arg, self.count).ok\r
797 self.count = None\r
798\r
799 def parse_options(self):\r
800 parser = argparse.ArgumentParser(description=__copyright__)\r
801 parser.add_argument('--version', action='version',\r
802 version='%(prog)s ' + VersionNumber)\r
803 parser.add_argument('patches', nargs='*',\r
804 help='[patch file | git rev list]')\r
805 group = parser.add_mutually_exclusive_group()\r
806 group.add_argument("--oneline",\r
807 action="store_true",\r
808 help="Print one result per line")\r
809 group.add_argument("--silent",\r
810 action="store_true",\r
811 help="Print nothing")\r
812 self.args = parser.parse_args()\r
813 if self.args.oneline:\r
814 Verbose.level = Verbose.ONELINE\r
815 if self.args.silent:\r
816 Verbose.level = Verbose.SILENT\r
817\r
818if __name__ == "__main__":\r
819 sys.exit(PatchCheckApp().retval)\r