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