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