PageRenderTime 42ms CodeModel.GetById 28ms RepoModel.GetById 0ms app.codeStats 0ms

/edk2/BaseTools/Scripts/PatchCheck.py

https://gitlab.com/envieidoc/Clover
Python | 613 lines | 582 code | 15 blank | 16 comment | 16 complexity | 929344e50af0498d71f918aaea82fddd MD5 | raw file
  1. ## @file
  2. # Check a patch for various format issues
  3. #
  4. # Copyright (c) 2015, Intel Corporation. All rights reserved.<BR>
  5. #
  6. # This program and the accompanying materials are licensed and made
  7. # available under the terms and conditions of the BSD License which
  8. # accompanies this distribution. The full text of the license may be
  9. # found at http://opensource.org/licenses/bsd-license.php
  10. #
  11. # THE PROGRAM IS DISTRIBUTED UNDER THE BSD LICENSE ON AN "AS IS"
  12. # BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS OF ANY KIND, EITHER
  13. # EXPRESS OR IMPLIED.
  14. #
  15. from __future__ import print_function
  16. VersionNumber = '0.1'
  17. __copyright__ = "Copyright (c) 2015, Intel Corporation All rights reserved."
  18. import email
  19. import argparse
  20. import os
  21. import re
  22. import subprocess
  23. import sys
  24. class Verbose:
  25. SILENT, ONELINE, NORMAL = range(3)
  26. level = NORMAL
  27. class CommitMessageCheck:
  28. """Checks the contents of a git commit message."""
  29. def __init__(self, subject, message):
  30. self.ok = True
  31. if subject is None and message is None:
  32. self.error('Commit message is missing!')
  33. return
  34. self.subject = subject
  35. self.msg = message
  36. self.check_contributed_under()
  37. self.check_signed_off_by()
  38. self.check_misc_signatures()
  39. self.check_overall_format()
  40. self.report_message_result()
  41. url = 'https://github.com/tianocore/tianocore.github.io/wiki/Commit-Message-Format'
  42. def report_message_result(self):
  43. if Verbose.level < Verbose.NORMAL:
  44. return
  45. if self.ok:
  46. # All checks passed
  47. return_code = 0
  48. print('The commit message format passed all checks.')
  49. else:
  50. return_code = 1
  51. if not self.ok:
  52. print(self.url)
  53. def error(self, *err):
  54. if self.ok and Verbose.level > Verbose.ONELINE:
  55. print('The commit message format is not valid:')
  56. self.ok = False
  57. if Verbose.level < Verbose.NORMAL:
  58. return
  59. count = 0
  60. for line in err:
  61. prefix = (' *', ' ')[count > 0]
  62. print(prefix, line)
  63. count += 1
  64. def check_contributed_under(self):
  65. cu_msg='Contributed-under: TianoCore Contribution Agreement 1.0'
  66. if self.msg.find(cu_msg) < 0:
  67. self.error('Missing Contributed-under! (Note: this must be ' +
  68. 'added by the code contributor!)')
  69. @staticmethod
  70. def make_signature_re(sig, re_input=False):
  71. if re_input:
  72. sub_re = sig
  73. else:
  74. sub_re = sig.replace('-', r'[-\s]+')
  75. re_str = (r'^(?P<tag>' + sub_re +
  76. r')(\s*):(\s*)(?P<value>\S.*?)(?:\s*)$')
  77. try:
  78. return re.compile(re_str, re.MULTILINE|re.IGNORECASE)
  79. except Exception:
  80. print("Tried to compile re:", re_str)
  81. raise
  82. sig_block_re = \
  83. re.compile(r'''^
  84. (?: (?P<tag>[^:]+) \s* : \s*
  85. (?P<value>\S.*?) )
  86. |
  87. (?: \[ (?P<updater>[^:]+) \s* : \s*
  88. (?P<note>.+?) \s* \] )
  89. \s* $''',
  90. re.VERBOSE | re.MULTILINE)
  91. def find_signatures(self, sig):
  92. if not sig.endswith('-by') and sig != 'Cc':
  93. sig += '-by'
  94. regex = self.make_signature_re(sig)
  95. sigs = regex.findall(self.msg)
  96. bad_case_sigs = filter(lambda m: m[0] != sig, sigs)
  97. for s in bad_case_sigs:
  98. self.error("'" +s[0] + "' should be '" + sig + "'")
  99. for s in sigs:
  100. if s[1] != '':
  101. self.error('There should be no spaces between ' + sig +
  102. " and the ':'")
  103. if s[2] != ' ':
  104. self.error("There should be a space after '" + sig + ":'")
  105. self.check_email_address(s[3])
  106. return sigs
  107. email_re1 = re.compile(r'(?:\s*)(.*?)(\s*)<(.+)>\s*$',
  108. re.MULTILINE|re.IGNORECASE)
  109. def check_email_address(self, email):
  110. email = email.strip()
  111. mo = self.email_re1.match(email)
  112. if mo is None:
  113. self.error("Email format is invalid: " + email.strip())
  114. return
  115. name = mo.group(1).strip()
  116. if name == '':
  117. self.error("Name is not provided with email address: " +
  118. email)
  119. else:
  120. quoted = len(name) > 2 and name[0] == '"' and name[-1] == '"'
  121. if name.find(',') >= 0 and not quoted:
  122. self.error('Add quotes (") around name with a comma: ' +
  123. name)
  124. if mo.group(2) == '':
  125. self.error("There should be a space between the name and " +
  126. "email address: " + email)
  127. if mo.group(3).find(' ') >= 0:
  128. self.error("The email address cannot contain a space: " +
  129. mo.group(3))
  130. def check_signed_off_by(self):
  131. sob='Signed-off-by'
  132. if self.msg.find(sob) < 0:
  133. self.error('Missing Signed-off-by! (Note: this must be ' +
  134. 'added by the code contributor!)')
  135. return
  136. sobs = self.find_signatures('Signed-off')
  137. if len(sobs) == 0:
  138. self.error('Invalid Signed-off-by format!')
  139. return
  140. sig_types = (
  141. 'Reviewed',
  142. 'Reported',
  143. 'Tested',
  144. 'Suggested',
  145. 'Acked',
  146. 'Cc'
  147. )
  148. def check_misc_signatures(self):
  149. for sig in self.sig_types:
  150. self.find_signatures(sig)
  151. def check_overall_format(self):
  152. lines = self.msg.splitlines()
  153. if len(lines) >= 1 and lines[0].endswith('\r\n'):
  154. empty_line = '\r\n'
  155. else:
  156. empty_line = '\n'
  157. lines.insert(0, empty_line)
  158. lines.insert(0, self.subject + empty_line)
  159. count = len(lines)
  160. if count <= 0:
  161. self.error('Empty commit message!')
  162. return
  163. if count >= 1 and len(lines[0]) > 76:
  164. self.error('First line of commit message (subject line) ' +
  165. 'is too long.')
  166. if count >= 1 and len(lines[0].strip()) == 0:
  167. self.error('First line of commit message (subject line) ' +
  168. 'is empty.')
  169. if count >= 2 and lines[1].strip() != '':
  170. self.error('Second line of commit message should be ' +
  171. 'empty.')
  172. for i in range(2, count):
  173. if (len(lines[i]) > 76 and
  174. len(lines[i].split()) > 1 and
  175. not lines[i].startswith('git-svn-id:')):
  176. self.error('Line %d of commit message is too long.' % (i + 1))
  177. last_sig_line = None
  178. for i in range(count - 1, 0, -1):
  179. line = lines[i]
  180. mo = self.sig_block_re.match(line)
  181. if mo is None:
  182. if line.strip() == '':
  183. break
  184. elif last_sig_line is not None:
  185. err2 = 'Add empty line before "%s"?' % last_sig_line
  186. self.error('The line before the signature block ' +
  187. 'should be empty', err2)
  188. else:
  189. self.error('The signature block was not found')
  190. break
  191. last_sig_line = line.strip()
  192. (START, PRE_PATCH, PATCH) = range(3)
  193. class GitDiffCheck:
  194. """Checks the contents of a git diff."""
  195. def __init__(self, diff):
  196. self.ok = True
  197. self.format_ok = True
  198. self.lines = diff.splitlines(True)
  199. self.count = len(self.lines)
  200. self.line_num = 0
  201. self.state = START
  202. while self.line_num < self.count and self.format_ok:
  203. line_num = self.line_num
  204. self.run()
  205. assert(self.line_num > line_num)
  206. self.report_message_result()
  207. def report_message_result(self):
  208. if Verbose.level < Verbose.NORMAL:
  209. return
  210. if self.ok:
  211. print('The code passed all checks.')
  212. def run(self):
  213. line = self.lines[self.line_num]
  214. if self.state in (PRE_PATCH, PATCH):
  215. if line.startswith('diff --git'):
  216. self.state = START
  217. if self.state == PATCH:
  218. if line.startswith('@@ '):
  219. self.state = PRE_PATCH
  220. elif len(line) >= 1 and line[0] not in ' -+' and \
  221. not line.startswith(r'\ No newline '):
  222. for line in self.lines[self.line_num + 1:]:
  223. if line.startswith('diff --git'):
  224. self.format_error('diff found after end of patch')
  225. break
  226. self.line_num = self.count
  227. return
  228. if self.state == START:
  229. if line.startswith('diff --git'):
  230. self.state = PRE_PATCH
  231. self.set_filename(None)
  232. elif len(line.rstrip()) != 0:
  233. self.format_error("didn't find diff command")
  234. self.line_num += 1
  235. elif self.state == PRE_PATCH:
  236. if line.startswith('+++ b/'):
  237. self.set_filename(line[6:].rstrip())
  238. if line.startswith('@@ '):
  239. self.state = PATCH
  240. self.binary = False
  241. elif line.startswith('GIT binary patch'):
  242. self.state = PATCH
  243. self.binary = True
  244. else:
  245. ok = False
  246. for pfx in self.pre_patch_prefixes:
  247. if line.startswith(pfx):
  248. ok = True
  249. if not ok:
  250. self.format_error("didn't find diff hunk marker (@@)")
  251. self.line_num += 1
  252. elif self.state == PATCH:
  253. if self.binary:
  254. pass
  255. if line.startswith('-'):
  256. pass
  257. elif line.startswith('+'):
  258. self.check_added_line(line[1:])
  259. elif line.startswith(r'\ No newline '):
  260. pass
  261. elif not line.startswith(' '):
  262. self.format_error("unexpected patch line")
  263. self.line_num += 1
  264. pre_patch_prefixes = (
  265. '--- ',
  266. '+++ ',
  267. 'index ',
  268. 'new file ',
  269. 'deleted file ',
  270. 'old mode ',
  271. 'new mode ',
  272. 'similarity index ',
  273. 'rename ',
  274. 'Binary files ',
  275. )
  276. line_endings = ('\r\n', '\n\r', '\n', '\r')
  277. def set_filename(self, filename):
  278. self.hunk_filename = filename
  279. if filename:
  280. self.force_crlf = not filename.endswith('.sh')
  281. else:
  282. self.force_crlf = True
  283. def added_line_error(self, msg, line):
  284. lines = [ msg ]
  285. if self.hunk_filename is not None:
  286. lines.append('File: ' + self.hunk_filename)
  287. lines.append('Line: ' + line)
  288. self.error(*lines)
  289. def check_added_line(self, line):
  290. eol = ''
  291. for an_eol in self.line_endings:
  292. if line.endswith(an_eol):
  293. eol = an_eol
  294. line = line[:-len(eol)]
  295. stripped = line.rstrip()
  296. if self.force_crlf and eol != '\r\n':
  297. self.added_line_error('Line ending (%s) is not CRLF' % repr(eol),
  298. line)
  299. if '\t' in line:
  300. self.added_line_error('Tab character used', line)
  301. if len(stripped) < len(line):
  302. self.added_line_error('Trailing whitespace found', line)
  303. split_diff_re = re.compile(r'''
  304. (?P<cmd>
  305. ^ diff \s+ --git \s+ a/.+ \s+ b/.+ $
  306. )
  307. (?P<index>
  308. ^ index \s+ .+ $
  309. )
  310. ''',
  311. re.IGNORECASE | re.VERBOSE | re.MULTILINE)
  312. def format_error(self, err):
  313. self.format_ok = False
  314. err = 'Patch format error: ' + err
  315. err2 = 'Line: ' + self.lines[self.line_num].rstrip()
  316. self.error(err, err2)
  317. def error(self, *err):
  318. if self.ok and Verbose.level > Verbose.ONELINE:
  319. print('Code format is not valid:')
  320. self.ok = False
  321. if Verbose.level < Verbose.NORMAL:
  322. return
  323. count = 0
  324. for line in err:
  325. prefix = (' *', ' ')[count > 0]
  326. print(prefix, line)
  327. count += 1
  328. class CheckOnePatch:
  329. """Checks the contents of a git email formatted patch.
  330. Various checks are performed on both the commit message and the
  331. patch content.
  332. """
  333. def __init__(self, name, patch):
  334. self.patch = patch
  335. self.find_patch_pieces()
  336. msg_check = CommitMessageCheck(self.commit_subject, self.commit_msg)
  337. msg_ok = msg_check.ok
  338. diff_ok = True
  339. if self.diff is not None:
  340. diff_check = GitDiffCheck(self.diff)
  341. diff_ok = diff_check.ok
  342. self.ok = msg_ok and diff_ok
  343. if Verbose.level == Verbose.ONELINE:
  344. if self.ok:
  345. result = 'ok'
  346. else:
  347. result = list()
  348. if not msg_ok:
  349. result.append('commit message')
  350. if not diff_ok:
  351. result.append('diff content')
  352. result = 'bad ' + ' and '.join(result)
  353. print(name, result)
  354. git_diff_re = re.compile(r'''
  355. ^ diff \s+ --git \s+ a/.+ \s+ b/.+ $
  356. ''',
  357. re.IGNORECASE | re.VERBOSE | re.MULTILINE)
  358. stat_re = \
  359. re.compile(r'''
  360. (?P<commit_message> [\s\S\r\n]* )
  361. (?P<stat>
  362. ^ --- $ [\r\n]+
  363. (?: ^ \s+ .+ \s+ \| \s+ \d+ \s+ \+* \-*
  364. $ [\r\n]+ )+
  365. [\s\S\r\n]+
  366. )
  367. ''',
  368. re.IGNORECASE | re.VERBOSE | re.MULTILINE)
  369. def find_patch_pieces(self):
  370. if sys.version_info < (3, 0):
  371. patch = self.patch.encode('ascii', 'ignore')
  372. else:
  373. patch = self.patch
  374. self.commit_msg = None
  375. self.stat = None
  376. self.commit_subject = None
  377. self.commit_prefix = None
  378. self.diff = None
  379. if patch.startswith('diff --git'):
  380. self.diff = patch
  381. return
  382. pmail = email.message_from_string(patch)
  383. parts = list(pmail.walk())
  384. assert(len(parts) == 1)
  385. assert(parts[0].get_content_type() == 'text/plain')
  386. content = parts[0].get_payload(decode=True).decode('utf-8', 'ignore')
  387. mo = self.git_diff_re.search(content)
  388. if mo is not None:
  389. self.diff = content[mo.start():]
  390. content = content[:mo.start()]
  391. mo = self.stat_re.search(content)
  392. if mo is None:
  393. self.commit_msg = content
  394. else:
  395. self.stat = mo.group('stat')
  396. self.commit_msg = mo.group('commit_message')
  397. self.commit_subject = pmail['subject'].replace('\r\n', '')
  398. self.commit_subject = self.commit_subject.replace('\n', '')
  399. pfx_start = self.commit_subject.find('[')
  400. if pfx_start >= 0:
  401. pfx_end = self.commit_subject.find(']')
  402. if pfx_end > pfx_start:
  403. self.commit_prefix = self.commit_subject[pfx_start + 1 : pfx_end]
  404. self.commit_subject = self.commit_subject[pfx_end + 1 :].lstrip()
  405. class CheckGitCommits:
  406. """Reads patches from git based on the specified git revision range.
  407. The patches are read from git, and then checked.
  408. """
  409. def __init__(self, rev_spec, max_count):
  410. commits = self.read_commit_list_from_git(rev_spec, max_count)
  411. if len(commits) == 1 and Verbose.level > Verbose.ONELINE:
  412. commits = [ rev_spec ]
  413. self.ok = True
  414. blank_line = False
  415. for commit in commits:
  416. if Verbose.level > Verbose.ONELINE:
  417. if blank_line:
  418. print()
  419. else:
  420. blank_line = True
  421. print('Checking git commit:', commit)
  422. patch = self.read_patch_from_git(commit)
  423. self.ok &= CheckOnePatch(commit, patch).ok
  424. def read_commit_list_from_git(self, rev_spec, max_count):
  425. # Run git to get the commit patch
  426. cmd = [ 'rev-list', '--abbrev-commit', '--no-walk' ]
  427. if max_count is not None:
  428. cmd.append('--max-count=' + str(max_count))
  429. cmd.append(rev_spec)
  430. out = self.run_git(*cmd)
  431. return out.split()
  432. def read_patch_from_git(self, commit):
  433. # Run git to get the commit patch
  434. return self.run_git('show', '--pretty=email', commit)
  435. def run_git(self, *args):
  436. cmd = [ 'git' ]
  437. cmd += args
  438. p = subprocess.Popen(cmd,
  439. stdout=subprocess.PIPE,
  440. stderr=subprocess.STDOUT)
  441. return p.communicate()[0].decode('utf-8', 'ignore')
  442. class CheckOnePatchFile:
  443. """Performs a patch check for a single file.
  444. stdin is used when the filename is '-'.
  445. """
  446. def __init__(self, patch_filename):
  447. if patch_filename == '-':
  448. patch = sys.stdin.read()
  449. patch_filename = 'stdin'
  450. else:
  451. f = open(patch_filename, 'rb')
  452. patch = f.read().decode('utf-8', 'ignore')
  453. f.close()
  454. if Verbose.level > Verbose.ONELINE:
  455. print('Checking patch file:', patch_filename)
  456. self.ok = CheckOnePatch(patch_filename, patch).ok
  457. class CheckOneArg:
  458. """Performs a patch check for a single command line argument.
  459. The argument will be handed off to a file or git-commit based
  460. checker.
  461. """
  462. def __init__(self, param, max_count=None):
  463. self.ok = True
  464. if param == '-' or os.path.exists(param):
  465. checker = CheckOnePatchFile(param)
  466. else:
  467. checker = CheckGitCommits(param, max_count)
  468. self.ok = checker.ok
  469. class PatchCheckApp:
  470. """Checks patches based on the command line arguments."""
  471. def __init__(self):
  472. self.parse_options()
  473. patches = self.args.patches
  474. if len(patches) == 0:
  475. patches = [ 'HEAD' ]
  476. self.ok = True
  477. self.count = None
  478. for patch in patches:
  479. self.process_one_arg(patch)
  480. if self.count is not None:
  481. self.process_one_arg('HEAD')
  482. if self.ok:
  483. self.retval = 0
  484. else:
  485. self.retval = -1
  486. def process_one_arg(self, arg):
  487. if len(arg) >= 2 and arg[0] == '-':
  488. try:
  489. self.count = int(arg[1:])
  490. return
  491. except ValueError:
  492. pass
  493. self.ok &= CheckOneArg(arg, self.count).ok
  494. self.count = None
  495. def parse_options(self):
  496. parser = argparse.ArgumentParser(description=__copyright__)
  497. parser.add_argument('--version', action='version',
  498. version='%(prog)s ' + VersionNumber)
  499. parser.add_argument('patches', nargs='*',
  500. help='[patch file | git rev list]')
  501. group = parser.add_mutually_exclusive_group()
  502. group.add_argument("--oneline",
  503. action="store_true",
  504. help="Print one result per line")
  505. group.add_argument("--silent",
  506. action="store_true",
  507. help="Print nothing")
  508. self.args = parser.parse_args()
  509. if self.args.oneline:
  510. Verbose.level = Verbose.ONELINE
  511. if self.args.silent:
  512. Verbose.level = Verbose.SILENT
  513. if __name__ == "__main__":
  514. sys.exit(PatchCheckApp().retval)