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