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