]> git.proxmox.com Git - mirror_edk2.git/blame - BaseTools/Scripts/PatchCheck.py
BaseTools/PatchCheck.py: Check the committer email address
[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
8f38b08b
PMD
555 email = self.read_committer_email_address_from_git(commit)\r
556 self.ok &= EmailAddressCheck(email, 'Committer').ok\r
a7e173b0
JJ
557 patch = self.read_patch_from_git(commit)\r
558 self.ok &= CheckOnePatch(commit, patch).ok\r
5ac4548c
JC
559 if not commits:\r
560 print("Couldn't find commit matching: '{}'".format(rev_spec))\r
a7e173b0
JJ
561\r
562 def read_commit_list_from_git(self, rev_spec, max_count):\r
563 # Run git to get the commit patch\r
564 cmd = [ 'rev-list', '--abbrev-commit', '--no-walk' ]\r
565 if max_count is not None:\r
566 cmd.append('--max-count=' + str(max_count))\r
567 cmd.append(rev_spec)\r
568 out = self.run_git(*cmd)\r
5ac4548c 569 return out.split() if out else []\r
a7e173b0
JJ
570\r
571 def read_patch_from_git(self, commit):\r
572 # Run git to get the commit patch\r
96603b4f 573 return self.run_git('show', '--pretty=email', '--no-textconv', commit)\r
a7e173b0 574\r
8f38b08b
PMD
575 def read_committer_email_address_from_git(self, commit):\r
576 # Run git to get the committer email\r
577 return self.run_git('show', '--pretty=%cn <%ce>', '--no-patch', commit)\r
578\r
a7e173b0
JJ
579 def run_git(self, *args):\r
580 cmd = [ 'git' ]\r
581 cmd += args\r
582 p = subprocess.Popen(cmd,\r
583 stdout=subprocess.PIPE,\r
584 stderr=subprocess.STDOUT)\r
5ac4548c 585 Result = p.communicate()\r
6460513a 586 return Result[0].decode('utf-8', 'ignore') if Result[0] and Result[0].find(b"fatal")!=0 else None\r
a7e173b0
JJ
587\r
588class CheckOnePatchFile:\r
589 """Performs a patch check for a single file.\r
590\r
591 stdin is used when the filename is '-'.\r
592 """\r
593\r
594 def __init__(self, patch_filename):\r
595 if patch_filename == '-':\r
596 patch = sys.stdin.read()\r
597 patch_filename = 'stdin'\r
598 else:\r
599 f = open(patch_filename, 'rb')\r
600 patch = f.read().decode('utf-8', 'ignore')\r
601 f.close()\r
602 if Verbose.level > Verbose.ONELINE:\r
603 print('Checking patch file:', patch_filename)\r
604 self.ok = CheckOnePatch(patch_filename, patch).ok\r
605\r
606class CheckOneArg:\r
607 """Performs a patch check for a single command line argument.\r
608\r
609 The argument will be handed off to a file or git-commit based\r
610 checker.\r
611 """\r
612\r
613 def __init__(self, param, max_count=None):\r
614 self.ok = True\r
615 if param == '-' or os.path.exists(param):\r
616 checker = CheckOnePatchFile(param)\r
617 else:\r
618 checker = CheckGitCommits(param, max_count)\r
619 self.ok = checker.ok\r
620\r
621class PatchCheckApp:\r
622 """Checks patches based on the command line arguments."""\r
623\r
624 def __init__(self):\r
625 self.parse_options()\r
626 patches = self.args.patches\r
627\r
628 if len(patches) == 0:\r
629 patches = [ 'HEAD' ]\r
630\r
631 self.ok = True\r
632 self.count = None\r
633 for patch in patches:\r
634 self.process_one_arg(patch)\r
635\r
636 if self.count is not None:\r
637 self.process_one_arg('HEAD')\r
638\r
639 if self.ok:\r
640 self.retval = 0\r
641 else:\r
642 self.retval = -1\r
643\r
644 def process_one_arg(self, arg):\r
645 if len(arg) >= 2 and arg[0] == '-':\r
646 try:\r
647 self.count = int(arg[1:])\r
648 return\r
649 except ValueError:\r
650 pass\r
651 self.ok &= CheckOneArg(arg, self.count).ok\r
652 self.count = None\r
653\r
654 def parse_options(self):\r
655 parser = argparse.ArgumentParser(description=__copyright__)\r
656 parser.add_argument('--version', action='version',\r
657 version='%(prog)s ' + VersionNumber)\r
658 parser.add_argument('patches', nargs='*',\r
659 help='[patch file | git rev list]')\r
660 group = parser.add_mutually_exclusive_group()\r
661 group.add_argument("--oneline",\r
662 action="store_true",\r
663 help="Print one result per line")\r
664 group.add_argument("--silent",\r
665 action="store_true",\r
666 help="Print nothing")\r
667 self.args = parser.parse_args()\r
668 if self.args.oneline:\r
669 Verbose.level = Verbose.ONELINE\r
670 if self.args.silent:\r
671 Verbose.level = Verbose.SILENT\r
672\r
673if __name__ == "__main__":\r
674 sys.exit(PatchCheckApp().retval)\r