PatchCheck.py 27 KB

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