PatchCheck.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778
  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 == 'BaseTools/BuildEnv':
  311. #
  312. # Do not enforce CR/LF line endings for linux shell scripts.
  313. # Some linux shell scripts don't end with the ".sh" extension,
  314. # they are identified by their path.
  315. #
  316. self.force_crlf = False
  317. if self.filename == '.gitmodules' or \
  318. self.filename == 'BaseTools/Conf/diff.order':
  319. #
  320. # .gitmodules and diff orderfiles are used internally by git
  321. # use tabs and LF line endings. Do not enforce no tabs and
  322. # do not enforce CR/LF line endings.
  323. #
  324. self.force_crlf = False
  325. self.force_notabs = False
  326. if os.path.basename(self.filename) == 'GNUmakefile' or \
  327. os.path.basename(self.filename) == 'Makefile':
  328. self.force_notabs = False
  329. elif len(line.rstrip()) != 0:
  330. self.format_error("didn't find diff command")
  331. self.line_num += 1
  332. elif self.state == PRE_PATCH:
  333. if line.startswith('@@ '):
  334. self.state = PATCH
  335. self.binary = False
  336. elif line.startswith('GIT binary patch') or \
  337. line.startswith('Binary files'):
  338. self.state = PATCH
  339. self.binary = True
  340. if self.is_newfile:
  341. self.new_bin.append(self.filename)
  342. elif line.startswith('new file mode 160000'):
  343. #
  344. # New submodule. Do not enforce CR/LF line endings
  345. #
  346. self.force_crlf = False
  347. else:
  348. ok = False
  349. self.is_newfile = self.newfile_prefix_re.match(line)
  350. for pfx in self.pre_patch_prefixes:
  351. if line.startswith(pfx):
  352. ok = True
  353. if not ok:
  354. self.format_error("didn't find diff hunk marker (@@)")
  355. self.line_num += 1
  356. elif self.state == PATCH:
  357. if self.binary:
  358. pass
  359. elif line.startswith('-'):
  360. pass
  361. elif line.startswith('+'):
  362. self.check_added_line(line[1:])
  363. elif line.startswith('\r\n'):
  364. pass
  365. elif line.startswith(r'\ No newline '):
  366. pass
  367. elif not line.startswith(' '):
  368. self.format_error("unexpected patch line")
  369. self.line_num += 1
  370. pre_patch_prefixes = (
  371. '--- ',
  372. '+++ ',
  373. 'index ',
  374. 'new file ',
  375. 'deleted file ',
  376. 'old mode ',
  377. 'new mode ',
  378. 'similarity index ',
  379. 'copy from ',
  380. 'copy to ',
  381. 'rename ',
  382. )
  383. line_endings = ('\r\n', '\n\r', '\n', '\r')
  384. newfile_prefix_re = \
  385. re.compile(r'''^
  386. index\ 0+\.\.
  387. ''',
  388. re.VERBOSE)
  389. def added_line_error(self, msg, line):
  390. lines = [ msg ]
  391. if self.filename is not None:
  392. lines.append('File: ' + self.filename)
  393. lines.append('Line: ' + line)
  394. self.error(*lines)
  395. old_debug_re = \
  396. re.compile(r'''
  397. DEBUG \s* \( \s* \( \s*
  398. (?: DEBUG_[A-Z_]+ \s* \| \s*)*
  399. EFI_D_ ([A-Z_]+)
  400. ''',
  401. re.VERBOSE)
  402. def check_added_line(self, line):
  403. eol = ''
  404. for an_eol in self.line_endings:
  405. if line.endswith(an_eol):
  406. eol = an_eol
  407. line = line[:-len(eol)]
  408. stripped = line.rstrip()
  409. if self.force_crlf and eol != '\r\n' and (line.find('Subproject commit') == -1):
  410. self.added_line_error('Line ending (%s) is not CRLF' % repr(eol),
  411. line)
  412. if self.force_notabs and '\t' in line:
  413. self.added_line_error('Tab character used', line)
  414. if len(stripped) < len(line):
  415. self.added_line_error('Trailing whitespace found', line)
  416. mo = self.old_debug_re.search(line)
  417. if mo is not None:
  418. self.added_line_error('EFI_D_' + mo.group(1) + ' was used, '
  419. 'but DEBUG_' + mo.group(1) +
  420. ' is now recommended', line)
  421. split_diff_re = re.compile(r'''
  422. (?P<cmd>
  423. ^ diff \s+ --git \s+ a/.+ \s+ b/.+ $
  424. )
  425. (?P<index>
  426. ^ index \s+ .+ $
  427. )
  428. ''',
  429. re.IGNORECASE | re.VERBOSE | re.MULTILINE)
  430. def format_error(self, err):
  431. self.format_ok = False
  432. err = 'Patch format error: ' + err
  433. err2 = 'Line: ' + self.lines[self.line_num].rstrip()
  434. self.error(err, err2)
  435. def error(self, *err):
  436. if self.ok and Verbose.level > Verbose.ONELINE:
  437. print('Code format is not valid:')
  438. self.ok = False
  439. if Verbose.level < Verbose.NORMAL:
  440. return
  441. count = 0
  442. for line in err:
  443. prefix = (' *', ' ')[count > 0]
  444. print(prefix, line)
  445. count += 1
  446. class CheckOnePatch:
  447. """Checks the contents of a git email formatted patch.
  448. Various checks are performed on both the commit message and the
  449. patch content.
  450. """
  451. def __init__(self, name, patch):
  452. self.patch = patch
  453. self.find_patch_pieces()
  454. email_check = EmailAddressCheck(self.author_email, 'Author')
  455. email_ok = email_check.ok
  456. msg_check = CommitMessageCheck(self.commit_subject, self.commit_msg, self.author_email)
  457. msg_ok = msg_check.ok
  458. diff_ok = True
  459. if self.diff is not None:
  460. diff_check = GitDiffCheck(self.diff)
  461. diff_ok = diff_check.ok
  462. self.ok = email_ok and msg_ok and diff_ok
  463. if Verbose.level == Verbose.ONELINE:
  464. if self.ok:
  465. result = 'ok'
  466. else:
  467. result = list()
  468. if not msg_ok:
  469. result.append('commit message')
  470. if not diff_ok:
  471. result.append('diff content')
  472. result = 'bad ' + ' and '.join(result)
  473. print(name, result)
  474. git_diff_re = re.compile(r'''
  475. ^ diff \s+ --git \s+ a/.+ \s+ b/.+ $
  476. ''',
  477. re.IGNORECASE | re.VERBOSE | re.MULTILINE)
  478. stat_re = \
  479. re.compile(r'''
  480. (?P<commit_message> [\s\S\r\n]* )
  481. (?P<stat>
  482. ^ --- $ [\r\n]+
  483. (?: ^ \s+ .+ \s+ \| \s+ \d+ \s+ \+* \-*
  484. $ [\r\n]+ )+
  485. [\s\S\r\n]+
  486. )
  487. ''',
  488. re.IGNORECASE | re.VERBOSE | re.MULTILINE)
  489. subject_prefix_re = \
  490. re.compile(r'''^
  491. \s* (\[
  492. [^\[\]]* # Allow all non-brackets
  493. \])* \s*
  494. ''',
  495. re.VERBOSE)
  496. def find_patch_pieces(self):
  497. if sys.version_info < (3, 0):
  498. patch = self.patch.encode('ascii', 'ignore')
  499. else:
  500. patch = self.patch
  501. self.commit_msg = None
  502. self.stat = None
  503. self.commit_subject = None
  504. self.commit_prefix = None
  505. self.diff = None
  506. if patch.startswith('diff --git'):
  507. self.diff = patch
  508. return
  509. pmail = email.message_from_string(patch)
  510. parts = list(pmail.walk())
  511. assert(len(parts) == 1)
  512. assert(parts[0].get_content_type() == 'text/plain')
  513. content = parts[0].get_payload(decode=True).decode('utf-8', 'ignore')
  514. mo = self.git_diff_re.search(content)
  515. if mo is not None:
  516. self.diff = content[mo.start():]
  517. content = content[:mo.start()]
  518. mo = self.stat_re.search(content)
  519. if mo is None:
  520. self.commit_msg = content
  521. else:
  522. self.stat = mo.group('stat')
  523. self.commit_msg = mo.group('commit_message')
  524. #
  525. # Parse subject line from email header. The subject line may be
  526. # composed of multiple parts with different encodings. Decode and
  527. # combine all the parts to produce a single string with the contents of
  528. # the decoded subject line.
  529. #
  530. parts = email.header.decode_header(pmail.get('subject'))
  531. subject = ''
  532. for (part, encoding) in parts:
  533. if encoding:
  534. part = part.decode(encoding)
  535. else:
  536. try:
  537. part = part.decode()
  538. except:
  539. pass
  540. subject = subject + part
  541. self.commit_subject = subject.replace('\r\n', '')
  542. self.commit_subject = self.commit_subject.replace('\n', '')
  543. self.commit_subject = self.subject_prefix_re.sub('', self.commit_subject, 1)
  544. self.author_email = pmail['from']
  545. class CheckGitCommits:
  546. """Reads patches from git based on the specified git revision range.
  547. The patches are read from git, and then checked.
  548. """
  549. def __init__(self, rev_spec, max_count):
  550. commits = self.read_commit_list_from_git(rev_spec, max_count)
  551. if len(commits) == 1 and Verbose.level > Verbose.ONELINE:
  552. commits = [ rev_spec ]
  553. self.ok = True
  554. blank_line = False
  555. for commit in commits:
  556. if Verbose.level > Verbose.ONELINE:
  557. if blank_line:
  558. print()
  559. else:
  560. blank_line = True
  561. print('Checking git commit:', commit)
  562. email = self.read_committer_email_address_from_git(commit)
  563. self.ok &= EmailAddressCheck(email, 'Committer').ok
  564. patch = self.read_patch_from_git(commit)
  565. self.ok &= CheckOnePatch(commit, patch).ok
  566. if not commits:
  567. print("Couldn't find commit matching: '{}'".format(rev_spec))
  568. def read_commit_list_from_git(self, rev_spec, max_count):
  569. # Run git to get the commit patch
  570. cmd = [ 'rev-list', '--abbrev-commit', '--no-walk' ]
  571. if max_count is not None:
  572. cmd.append('--max-count=' + str(max_count))
  573. cmd.append(rev_spec)
  574. out = self.run_git(*cmd)
  575. return out.split() if out else []
  576. def read_patch_from_git(self, commit):
  577. # Run git to get the commit patch
  578. return self.run_git('show', '--pretty=email', '--no-textconv',
  579. '--no-use-mailmap', commit)
  580. def read_committer_email_address_from_git(self, commit):
  581. # Run git to get the committer email
  582. return self.run_git('show', '--pretty=%cn <%ce>', '--no-patch',
  583. '--no-use-mailmap', commit)
  584. def run_git(self, *args):
  585. cmd = [ 'git' ]
  586. cmd += args
  587. p = subprocess.Popen(cmd,
  588. stdout=subprocess.PIPE,
  589. stderr=subprocess.STDOUT)
  590. Result = p.communicate()
  591. return Result[0].decode('utf-8', 'ignore') if Result[0] and Result[0].find(b"fatal")!=0 else None
  592. class CheckOnePatchFile:
  593. """Performs a patch check for a single file.
  594. stdin is used when the filename is '-'.
  595. """
  596. def __init__(self, patch_filename):
  597. if patch_filename == '-':
  598. patch = sys.stdin.read()
  599. patch_filename = 'stdin'
  600. else:
  601. f = open(patch_filename, 'rb')
  602. patch = f.read().decode('utf-8', 'ignore')
  603. f.close()
  604. if Verbose.level > Verbose.ONELINE:
  605. print('Checking patch file:', patch_filename)
  606. self.ok = CheckOnePatch(patch_filename, patch).ok
  607. class CheckOneArg:
  608. """Performs a patch check for a single command line argument.
  609. The argument will be handed off to a file or git-commit based
  610. checker.
  611. """
  612. def __init__(self, param, max_count=None):
  613. self.ok = True
  614. if param == '-' or os.path.exists(param):
  615. checker = CheckOnePatchFile(param)
  616. else:
  617. checker = CheckGitCommits(param, max_count)
  618. self.ok = checker.ok
  619. class PatchCheckApp:
  620. """Checks patches based on the command line arguments."""
  621. def __init__(self):
  622. self.parse_options()
  623. patches = self.args.patches
  624. if len(patches) == 0:
  625. patches = [ 'HEAD' ]
  626. self.ok = True
  627. self.count = None
  628. for patch in patches:
  629. self.process_one_arg(patch)
  630. if self.count is not None:
  631. self.process_one_arg('HEAD')
  632. if self.ok:
  633. self.retval = 0
  634. else:
  635. self.retval = -1
  636. def process_one_arg(self, arg):
  637. if len(arg) >= 2 and arg[0] == '-':
  638. try:
  639. self.count = int(arg[1:])
  640. return
  641. except ValueError:
  642. pass
  643. self.ok &= CheckOneArg(arg, self.count).ok
  644. self.count = None
  645. def parse_options(self):
  646. parser = argparse.ArgumentParser(description=__copyright__)
  647. parser.add_argument('--version', action='version',
  648. version='%(prog)s ' + VersionNumber)
  649. parser.add_argument('patches', nargs='*',
  650. help='[patch file | git rev list]')
  651. group = parser.add_mutually_exclusive_group()
  652. group.add_argument("--oneline",
  653. action="store_true",
  654. help="Print one result per line")
  655. group.add_argument("--silent",
  656. action="store_true",
  657. help="Print nothing")
  658. self.args = parser.parse_args()
  659. if self.args.oneline:
  660. Verbose.level = Verbose.ONELINE
  661. if self.args.silent:
  662. Verbose.level = Verbose.SILENT
  663. if __name__ == "__main__":
  664. sys.exit(PatchCheckApp().retval)