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