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