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