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