]> git.proxmox.com Git - mirror_edk2.git/blame - BaseTools/Scripts/PatchCheck.py
ArmVirtPkg: Change use of EFI_D_* to DEBUG_*
[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
PG
369 self.filename.startswith('BaseTools/Bin/CYGWIN_NT-5.1-i686/') or \\r
370 self.filename == 'BaseTools/BuildEnv':\r
c8b8157e
MK
371 #\r
372 # Do not enforce CR/LF line endings for linux shell scripts.\r
01356d29
PG
373 # Some linux shell scripts don't end with the ".sh" extension,\r
374 # they are identified by their path.\r
c8b8157e
MK
375 #\r
376 self.force_crlf = False\r
c267eb88
LL
377 if self.filename == '.gitmodules' or \\r
378 self.filename == 'BaseTools/Conf/diff.order':\r
c8b8157e 379 #\r
c267eb88
LL
380 # .gitmodules and diff orderfiles are used internally by git\r
381 # use tabs and LF line endings. Do not enforce no tabs and\r
382 # do not enforce CR/LF line endings.\r
c8b8157e
MK
383 #\r
384 self.force_crlf = False\r
385 self.force_notabs = False\r
a7e173b0
JJ
386 elif len(line.rstrip()) != 0:\r
387 self.format_error("didn't find diff command")\r
388 self.line_num += 1\r
389 elif self.state == PRE_PATCH:\r
a7e173b0
JJ
390 if line.startswith('@@ '):\r
391 self.state = PATCH\r
103733f8 392 self.binary = False\r
6a69dd49
HW
393 elif line.startswith('GIT binary patch') or \\r
394 line.startswith('Binary files'):\r
103733f8
JJ
395 self.state = PATCH\r
396 self.binary = True\r
6a69dd49
HW
397 if self.is_newfile:\r
398 self.new_bin.append(self.filename)\r
c8b8157e
MK
399 elif line.startswith('new file mode 160000'):\r
400 #\r
401 # New submodule. Do not enforce CR/LF line endings\r
402 #\r
403 self.force_crlf = False\r
a7e173b0
JJ
404 else:\r
405 ok = False\r
6a69dd49 406 self.is_newfile = self.newfile_prefix_re.match(line)\r
a7e173b0
JJ
407 for pfx in self.pre_patch_prefixes:\r
408 if line.startswith(pfx):\r
409 ok = True\r
410 if not ok:\r
411 self.format_error("didn't find diff hunk marker (@@)")\r
412 self.line_num += 1\r
413 elif self.state == PATCH:\r
103733f8
JJ
414 if self.binary:\r
415 pass\r
aab57eff 416 elif line.startswith('-'):\r
a7e173b0
JJ
417 pass\r
418 elif line.startswith('+'):\r
419 self.check_added_line(line[1:])\r
05579e51
MK
420 elif line.startswith('\r\n'):\r
421 pass\r
a7e173b0
JJ
422 elif line.startswith(r'\ No newline '):\r
423 pass\r
424 elif not line.startswith(' '):\r
425 self.format_error("unexpected patch line")\r
426 self.line_num += 1\r
427\r
428 pre_patch_prefixes = (\r
429 '--- ',\r
430 '+++ ',\r
431 'index ',\r
432 'new file ',\r
433 'deleted file ',\r
434 'old mode ',\r
435 'new mode ',\r
436 'similarity index ',\r
1eeb5ff1
MK
437 'copy from ',\r
438 'copy to ',\r
a7e173b0 439 'rename ',\r
a7e173b0
JJ
440 )\r
441\r
442 line_endings = ('\r\n', '\n\r', '\n', '\r')\r
443\r
6a69dd49
HW
444 newfile_prefix_re = \\r
445 re.compile(r'''^\r
446 index\ 0+\.\.\r
447 ''',\r
448 re.VERBOSE)\r
a7e173b0
JJ
449\r
450 def added_line_error(self, msg, line):\r
451 lines = [ msg ]\r
6a69dd49
HW
452 if self.filename is not None:\r
453 lines.append('File: ' + self.filename)\r
a7e173b0
JJ
454 lines.append('Line: ' + line)\r
455\r
456 self.error(*lines)\r
457\r
c3926cdb
YZ
458 old_debug_re = \\r
459 re.compile(r'''\r
460 DEBUG \s* \( \s* \( \s*\r
461 (?: DEBUG_[A-Z_]+ \s* \| \s*)*\r
462 EFI_D_ ([A-Z_]+)\r
463 ''',\r
464 re.VERBOSE)\r
465\r
a7e173b0
JJ
466 def check_added_line(self, line):\r
467 eol = ''\r
468 for an_eol in self.line_endings:\r
469 if line.endswith(an_eol):\r
470 eol = an_eol\r
471 line = line[:-len(eol)]\r
472\r
473 stripped = line.rstrip()\r
474\r
ff2655d1 475 if self.force_crlf and eol != '\r\n' and (line.find('Subproject commit') == -1):\r
a7e173b0
JJ
476 self.added_line_error('Line ending (%s) is not CRLF' % repr(eol),\r
477 line)\r
c8b8157e 478 if self.force_notabs and '\t' in line:\r
a7e173b0
JJ
479 self.added_line_error('Tab character used', line)\r
480 if len(stripped) < len(line):\r
481 self.added_line_error('Trailing whitespace found', line)\r
482\r
c3926cdb
YZ
483 mo = self.old_debug_re.search(line)\r
484 if mo is not None:\r
485 self.added_line_error('EFI_D_' + mo.group(1) + ' was used, '\r
486 'but DEBUG_' + mo.group(1) +\r
487 ' is now recommended', line)\r
488\r
a7e173b0
JJ
489 split_diff_re = re.compile(r'''\r
490 (?P<cmd>\r
491 ^ diff \s+ --git \s+ a/.+ \s+ b/.+ $\r
492 )\r
493 (?P<index>\r
494 ^ index \s+ .+ $\r
495 )\r
496 ''',\r
497 re.IGNORECASE | re.VERBOSE | re.MULTILINE)\r
498\r
499 def format_error(self, err):\r
500 self.format_ok = False\r
501 err = 'Patch format error: ' + err\r
502 err2 = 'Line: ' + self.lines[self.line_num].rstrip()\r
503 self.error(err, err2)\r
504\r
505 def error(self, *err):\r
506 if self.ok and Verbose.level > Verbose.ONELINE:\r
507 print('Code format is not valid:')\r
508 self.ok = False\r
509 if Verbose.level < Verbose.NORMAL:\r
510 return\r
511 count = 0\r
512 for line in err:\r
513 prefix = (' *', ' ')[count > 0]\r
514 print(prefix, line)\r
515 count += 1\r
516\r
517class CheckOnePatch:\r
518 """Checks the contents of a git email formatted patch.\r
519\r
520 Various checks are performed on both the commit message and the\r
521 patch content.\r
522 """\r
523\r
524 def __init__(self, name, patch):\r
525 self.patch = patch\r
526 self.find_patch_pieces()\r
527\r
c0328cf3
PMD
528 email_check = EmailAddressCheck(self.author_email, 'Author')\r
529 email_ok = email_check.ok\r
530\r
84af6ea3 531 msg_check = CommitMessageCheck(self.commit_subject, self.commit_msg, self.author_email)\r
a7e173b0
JJ
532 msg_ok = msg_check.ok\r
533\r
534 diff_ok = True\r
535 if self.diff is not None:\r
536 diff_check = GitDiffCheck(self.diff)\r
537 diff_ok = diff_check.ok\r
538\r
c0328cf3 539 self.ok = email_ok and msg_ok and diff_ok\r
a7e173b0
JJ
540\r
541 if Verbose.level == Verbose.ONELINE:\r
542 if self.ok:\r
543 result = 'ok'\r
544 else:\r
545 result = list()\r
546 if not msg_ok:\r
547 result.append('commit message')\r
548 if not diff_ok:\r
549 result.append('diff content')\r
550 result = 'bad ' + ' and '.join(result)\r
551 print(name, result)\r
552\r
553\r
554 git_diff_re = re.compile(r'''\r
555 ^ diff \s+ --git \s+ a/.+ \s+ b/.+ $\r
556 ''',\r
557 re.IGNORECASE | re.VERBOSE | re.MULTILINE)\r
558\r
559 stat_re = \\r
560 re.compile(r'''\r
561 (?P<commit_message> [\s\S\r\n]* )\r
562 (?P<stat>\r
563 ^ --- $ [\r\n]+\r
564 (?: ^ \s+ .+ \s+ \| \s+ \d+ \s+ \+* \-*\r
565 $ [\r\n]+ )+\r
566 [\s\S\r\n]+\r
567 )\r
568 ''',\r
569 re.IGNORECASE | re.VERBOSE | re.MULTILINE)\r
570\r
e709bbb1
YZ
571 subject_prefix_re = \\r
572 re.compile(r'''^\r
573 \s* (\[\r
574 [^\[\]]* # Allow all non-brackets\r
575 \])* \s*\r
576 ''',\r
577 re.VERBOSE)\r
578\r
a7e173b0
JJ
579 def find_patch_pieces(self):\r
580 if sys.version_info < (3, 0):\r
581 patch = self.patch.encode('ascii', 'ignore')\r
582 else:\r
583 patch = self.patch\r
584\r
585 self.commit_msg = None\r
586 self.stat = None\r
587 self.commit_subject = None\r
588 self.commit_prefix = None\r
589 self.diff = None\r
590\r
591 if patch.startswith('diff --git'):\r
592 self.diff = patch\r
593 return\r
594\r
595 pmail = email.message_from_string(patch)\r
596 parts = list(pmail.walk())\r
597 assert(len(parts) == 1)\r
598 assert(parts[0].get_content_type() == 'text/plain')\r
599 content = parts[0].get_payload(decode=True).decode('utf-8', 'ignore')\r
600\r
601 mo = self.git_diff_re.search(content)\r
602 if mo is not None:\r
603 self.diff = content[mo.start():]\r
604 content = content[:mo.start()]\r
605\r
606 mo = self.stat_re.search(content)\r
607 if mo is None:\r
608 self.commit_msg = content\r
609 else:\r
610 self.stat = mo.group('stat')\r
611 self.commit_msg = mo.group('commit_message')\r
b112ec22
MK
612 #\r
613 # Parse subject line from email header. The subject line may be\r
614 # composed of multiple parts with different encodings. Decode and\r
615 # combine all the parts to produce a single string with the contents of\r
616 # the decoded subject line.\r
617 #\r
618 parts = email.header.decode_header(pmail.get('subject'))\r
619 subject = ''\r
620 for (part, encoding) in parts:\r
621 if encoding:\r
622 part = part.decode(encoding)\r
623 else:\r
624 try:\r
625 part = part.decode()\r
626 except:\r
627 pass\r
628 subject = subject + part\r
a7e173b0 629\r
b112ec22 630 self.commit_subject = subject.replace('\r\n', '')\r
a7e173b0 631 self.commit_subject = self.commit_subject.replace('\n', '')\r
e709bbb1 632 self.commit_subject = self.subject_prefix_re.sub('', self.commit_subject, 1)\r
a7e173b0 633\r
c0328cf3
PMD
634 self.author_email = pmail['from']\r
635\r
a7e173b0
JJ
636class CheckGitCommits:\r
637 """Reads patches from git based on the specified git revision range.\r
638\r
639 The patches are read from git, and then checked.\r
640 """\r
641\r
642 def __init__(self, rev_spec, max_count):\r
643 commits = self.read_commit_list_from_git(rev_spec, max_count)\r
644 if len(commits) == 1 and Verbose.level > Verbose.ONELINE:\r
645 commits = [ rev_spec ]\r
646 self.ok = True\r
647 blank_line = False\r
648 for commit in commits:\r
649 if Verbose.level > Verbose.ONELINE:\r
650 if blank_line:\r
651 print()\r
652 else:\r
653 blank_line = True\r
654 print('Checking git commit:', commit)\r
8f38b08b
PMD
655 email = self.read_committer_email_address_from_git(commit)\r
656 self.ok &= EmailAddressCheck(email, 'Committer').ok\r
a7e173b0
JJ
657 patch = self.read_patch_from_git(commit)\r
658 self.ok &= CheckOnePatch(commit, patch).ok\r
5ac4548c
JC
659 if not commits:\r
660 print("Couldn't find commit matching: '{}'".format(rev_spec))\r
a7e173b0
JJ
661\r
662 def read_commit_list_from_git(self, rev_spec, max_count):\r
663 # Run git to get the commit patch\r
664 cmd = [ 'rev-list', '--abbrev-commit', '--no-walk' ]\r
665 if max_count is not None:\r
666 cmd.append('--max-count=' + str(max_count))\r
667 cmd.append(rev_spec)\r
668 out = self.run_git(*cmd)\r
5ac4548c 669 return out.split() if out else []\r
a7e173b0
JJ
670\r
671 def read_patch_from_git(self, commit):\r
672 # Run git to get the commit patch\r
c37cce7a
PMD
673 return self.run_git('show', '--pretty=email', '--no-textconv',\r
674 '--no-use-mailmap', commit)\r
a7e173b0 675\r
8f38b08b
PMD
676 def read_committer_email_address_from_git(self, commit):\r
677 # Run git to get the committer email\r
c37cce7a
PMD
678 return self.run_git('show', '--pretty=%cn <%ce>', '--no-patch',\r
679 '--no-use-mailmap', commit)\r
8f38b08b 680\r
a7e173b0
JJ
681 def run_git(self, *args):\r
682 cmd = [ 'git' ]\r
683 cmd += args\r
684 p = subprocess.Popen(cmd,\r
685 stdout=subprocess.PIPE,\r
686 stderr=subprocess.STDOUT)\r
5ac4548c 687 Result = p.communicate()\r
6460513a 688 return Result[0].decode('utf-8', 'ignore') if Result[0] and Result[0].find(b"fatal")!=0 else None\r
a7e173b0
JJ
689\r
690class CheckOnePatchFile:\r
691 """Performs a patch check for a single file.\r
692\r
693 stdin is used when the filename is '-'.\r
694 """\r
695\r
696 def __init__(self, patch_filename):\r
697 if patch_filename == '-':\r
698 patch = sys.stdin.read()\r
699 patch_filename = 'stdin'\r
700 else:\r
701 f = open(patch_filename, 'rb')\r
702 patch = f.read().decode('utf-8', 'ignore')\r
703 f.close()\r
704 if Verbose.level > Verbose.ONELINE:\r
705 print('Checking patch file:', patch_filename)\r
706 self.ok = CheckOnePatch(patch_filename, patch).ok\r
707\r
708class CheckOneArg:\r
709 """Performs a patch check for a single command line argument.\r
710\r
711 The argument will be handed off to a file or git-commit based\r
712 checker.\r
713 """\r
714\r
715 def __init__(self, param, max_count=None):\r
716 self.ok = True\r
717 if param == '-' or os.path.exists(param):\r
718 checker = CheckOnePatchFile(param)\r
719 else:\r
720 checker = CheckGitCommits(param, max_count)\r
721 self.ok = checker.ok\r
722\r
723class PatchCheckApp:\r
724 """Checks patches based on the command line arguments."""\r
725\r
726 def __init__(self):\r
727 self.parse_options()\r
728 patches = self.args.patches\r
729\r
730 if len(patches) == 0:\r
731 patches = [ 'HEAD' ]\r
732\r
733 self.ok = True\r
734 self.count = None\r
735 for patch in patches:\r
736 self.process_one_arg(patch)\r
737\r
738 if self.count is not None:\r
739 self.process_one_arg('HEAD')\r
740\r
741 if self.ok:\r
742 self.retval = 0\r
743 else:\r
744 self.retval = -1\r
745\r
746 def process_one_arg(self, arg):\r
747 if len(arg) >= 2 and arg[0] == '-':\r
748 try:\r
749 self.count = int(arg[1:])\r
750 return\r
751 except ValueError:\r
752 pass\r
753 self.ok &= CheckOneArg(arg, self.count).ok\r
754 self.count = None\r
755\r
756 def parse_options(self):\r
757 parser = argparse.ArgumentParser(description=__copyright__)\r
758 parser.add_argument('--version', action='version',\r
759 version='%(prog)s ' + VersionNumber)\r
760 parser.add_argument('patches', nargs='*',\r
761 help='[patch file | git rev list]')\r
762 group = parser.add_mutually_exclusive_group()\r
763 group.add_argument("--oneline",\r
764 action="store_true",\r
765 help="Print one result per line")\r
766 group.add_argument("--silent",\r
767 action="store_true",\r
768 help="Print nothing")\r
769 self.args = parser.parse_args()\r
770 if self.args.oneline:\r
771 Verbose.level = Verbose.ONELINE\r
772 if self.args.silent:\r
773 Verbose.level = Verbose.SILENT\r
774\r
775if __name__ == "__main__":\r
776 sys.exit(PatchCheckApp().retval)\r