check_copyright.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603
  1. #!/usr/bin/env python
  2. # SPDX-FileCopyrightText: 2021 Espressif Systems (Shanghai) CO LTD
  3. # SPDX-License-Identifier: Apache-2.0
  4. """
  5. Check files for copyright headers:
  6. - file not on ignore list:
  7. - old Espressif copyright -> replace with SPDX
  8. - SPDX with invalid year or old company name -> replace with valid SPDX
  9. - other SPDX copyright -> PASS
  10. - non-SPDX copyright -> FAIL
  11. - no copyright -> insert Espressif copyright
  12. - file on ignore list:
  13. - old Espressif copyright -> replace with SPDX, remove from ignore list
  14. - SPDX with invalid year or company format -> replace with valid SPDX and remove from ignore list
  15. else -> keep on ignore list
  16. """
  17. import argparse
  18. import ast
  19. import configparser
  20. import datetime
  21. import os
  22. import re
  23. import sys
  24. import textwrap
  25. from typing import List, Tuple
  26. import pathspec
  27. import yaml
  28. # importing the whole comment_parser causes a crash when running inside of gitbash environment on Windows.
  29. from comment_parser.parsers import c_parser, python_parser
  30. from comment_parser.parsers.common import Comment
  31. from thefuzz import fuzz
  32. IDF_PATH = os.getenv('IDF_PATH', os.getcwd())
  33. IGNORE_LIST_FN = os.path.join(IDF_PATH, 'tools/ci/check_copyright_ignore.txt')
  34. CONFIG_FN = os.path.join(IDF_PATH, 'tools', 'ci', 'check_copyright_config.yaml')
  35. CHECK_FAIL_MESSAGE = textwrap.dedent('''\
  36. To make a file pass the test, it needs to contain both:
  37. an SPDX-FileCopyrightText and an SPDX-License-Identifier with an allowed license for the section.
  38. More information about SPDX license identifiers can be found here:
  39. https://spdx.github.io/spdx-spec/appendix-V-using-SPDX-short-identifiers-in-source-files/
  40. To have this hook automatically insert the standard Espressif copyright notice,
  41. ensure the word "copyright" is not in any comment up to line 30 and the file is not on the ignore list.
  42. Below is a list of files, which failed the copyright check.
  43. ''')
  44. CHECK_MODIFY_MESSAGE = textwrap.dedent('''\
  45. Above is a list of files, which were modified. Please check their contents, stage them and run the commit again!
  46. Files prefixed with "(ignore)" were on the ignore list at the time of invoking this script.
  47. They may have been removed if noted above.
  48. ''')
  49. CHECK_FOOTER_MESSAGE = textwrap.dedent('''\
  50. Additional information about this hook and copyright headers may be found here:
  51. https://docs.espressif.com/projects/esp-idf/en/latest/esp32/contribute/copyright-guide.html
  52. ''')
  53. # This is an old header style, which this script
  54. # attempts to detect and replace with a new SPDX license identifier
  55. OLD_APACHE_HEADER = textwrap.dedent('''\
  56. Copyright 2015-2019 Espressif Systems (Shanghai) PTE LTD
  57. Licensed under the Apache License, Version 2.0 (the "License");
  58. you may not use this file except in compliance with the License.
  59. You may obtain a copy of the License at
  60. http://www.apache.org/licenses/LICENSE-2.0
  61. Unless required by applicable law or agreed to in writing, software
  62. distributed under the License is distributed on an "AS IS" BASIS,
  63. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  64. See the License for the specific language governing permissions and
  65. limitations under the License.
  66. ''')
  67. # New headers to be used
  68. NEW_APACHE_HEADER_PYTHON = textwrap.dedent('''\
  69. # SPDX-FileCopyrightText: {years} Espressif Systems (Shanghai) CO LTD
  70. # SPDX-License-Identifier: Apache-2.0
  71. ''')
  72. NEW_APACHE_HEADER = textwrap.dedent('''\
  73. /*
  74. * SPDX-FileCopyrightText: {years} Espressif Systems (Shanghai) CO LTD
  75. *
  76. * SPDX-License-Identifier: Apache-2.0
  77. */
  78. ''')
  79. # filetype -> mime
  80. MIME = {
  81. 'python': 'text/x-python',
  82. 'c': 'text/x-c',
  83. 'cpp': 'text/x-c++'
  84. }
  85. # mime -> parser
  86. MIME_PARSER = {
  87. 'text/x-c': c_parser,
  88. 'text/x-c++': c_parser,
  89. 'text/x-python': python_parser,
  90. }
  91. # terminal color output
  92. TERMINAL_RESET = '\33[0m'
  93. TERMINAL_BOLD = '\33[1m'
  94. TERMINAL_YELLOW = '\33[93m'
  95. TERMINAL_GREEN = '\33[92m'
  96. TERMINAL_RED = '\33[91m'
  97. TERMINAL_GRAY = '\33[90m'
  98. class UnsupportedFileType(Exception):
  99. """Exception raised for unsupported file types.
  100. Attributes:
  101. file_name -- input file which caused the error
  102. message -- explanation of the error
  103. """
  104. def __init__(self, file_name: str, message: str = 'this file type is not supported') -> None:
  105. self.fine_name = file_name
  106. self.message = message
  107. super().__init__(self.message)
  108. def __str__(self) -> str:
  109. return f'{self.fine_name}: {self.message}'
  110. class NotFound(Exception):
  111. """Exception raised when something is not found.
  112. Attributes:
  113. thing -- what was not found
  114. """
  115. def __init__(self, thing: str = 'something') -> None:
  116. self.thing = thing
  117. super().__init__(self.thing)
  118. def __str__(self) -> str:
  119. return f'{self.thing} was not found'
  120. class CustomFile:
  121. """
  122. Custom data object to hold file name and if it's on the ignore list
  123. and to make it easier to print
  124. """
  125. def __init__(self, file_name: str, is_on_ignore_list: bool) -> None:
  126. self.file_name = file_name
  127. self.is_on_ignore_list = is_on_ignore_list
  128. def __str__(self) -> str:
  129. if self.is_on_ignore_list:
  130. return f'(ignore) {self.file_name}'
  131. return f' {self.file_name}'
  132. class CommentHolder(Comment):
  133. """
  134. Hold the comment, its line number and when it is multiline,
  135. also store if it's the first in a comment block
  136. """
  137. def __init__(self, text: str, line_number: int, multiline: bool = False, first_in_multiline: bool = False):
  138. """
  139. Args:
  140. text: String text of comment.
  141. line_number: Line number (int) comment was found on.
  142. multiline: bool is it multiline
  143. first_in_multiline: bool if multiline, is it first in that comment block
  144. """
  145. super(self.__class__, self).__init__(text, line_number, multiline)
  146. self._first_in_multiline = first_in_multiline and multiline
  147. def is_first_in_multiline(self) -> bool:
  148. """
  149. Returns whether this comment was a first in a multiline comment.
  150. """
  151. return self._first_in_multiline
  152. def get_file_mime(fn: str) -> str:
  153. """
  154. Return the mime type based on file's extension
  155. """
  156. if fn.endswith('.py'):
  157. return MIME['python']
  158. if fn.endswith(('.cpp', '.hpp')):
  159. return MIME['cpp']
  160. if fn.endswith(('.c', '.h', '.ld')):
  161. return MIME['c']
  162. raise UnsupportedFileType(fn)
  163. def get_comments(code: str, mime: str) -> list:
  164. """
  165. Extracts all comments from source code and does a multiline split
  166. """
  167. parser = MIME_PARSER[mime]
  168. comments = parser.extract_comments(code)
  169. new_comments = []
  170. for comment in comments:
  171. if comment.is_multiline():
  172. comment_lines = comment.text().splitlines()
  173. for line_number, line in enumerate(comment_lines, start=comment.line_number()):
  174. # the third argument of Comment is a bool multiline. Store the relative line number inside the multiline comment
  175. new_comments.append(CommentHolder(line, line_number, True, line_number == comment.line_number()))
  176. else:
  177. new_comments.append(CommentHolder(comment.text(), comment.line_number()))
  178. return new_comments
  179. def has_valid_copyright(file_name: str, mime: str, is_on_ignore: bool, config_section: configparser.SectionProxy,
  180. args: argparse.Namespace) -> Tuple[bool, bool]:
  181. """
  182. Detects if a file has a valid SPDX copyright notice.
  183. returns: Tuple[valid, modified]
  184. """
  185. detected_licenses = []
  186. detected_notices = []
  187. detected_contributors = []
  188. valid, modified = False, False
  189. with open(file_name, 'r') as f:
  190. code = f.read()
  191. comments = get_comments(code, mime)
  192. code_lines = code.splitlines()
  193. if not code_lines: # file is empty
  194. print(f'{TERMINAL_YELLOW}"{file_name}" is empty!{TERMINAL_RESET}')
  195. valid = True
  196. return valid, modified
  197. if args.replace:
  198. try:
  199. year, line = detect_old_header_style(file_name, comments, args)
  200. except NotFound as e:
  201. if args.debug:
  202. print(f'{TERMINAL_GRAY}{e} in {file_name}{TERMINAL_RESET}')
  203. else:
  204. code_lines = replace_copyright(code_lines, year, line, mime, file_name)
  205. valid = True
  206. for comment in comments:
  207. if comment.line_number() > args.max_lines:
  208. break
  209. matches = re.search(r'SPDX-FileCopyrightText: ?(.*)', comment.text(), re.IGNORECASE)
  210. if matches:
  211. detected_notices.append((matches.group(1), comment.line_number()))
  212. try:
  213. year = extract_year_from_espressif_notice(matches.group(1))
  214. except NotFound as e:
  215. if args.verbose:
  216. print(f'{TERMINAL_GRAY}Not an {e.thing} {file_name}:{comment.line_number()}{TERMINAL_RESET}')
  217. else:
  218. template = '// SPDX-FileCopyrightText: ' + config_section['espressif_copyright']
  219. if comment.is_multiline():
  220. template = ' * SPDX-FileCopyrightText: ' + config_section['espressif_copyright']
  221. if comment.is_first_in_multiline():
  222. template = '/* SPDX-FileCopyrightText: ' + config_section['espressif_copyright']
  223. if mime == MIME['python']:
  224. template = '# SPDX-FileCopyrightText: ' + config_section['espressif_copyright']
  225. code_lines[comment.line_number() - 1] = template.format(years=format_years(year, file_name))
  226. matches = re.search(r'SPDX-FileContributor: ?(.*)', comment.text(), re.IGNORECASE)
  227. if matches:
  228. detected_contributors.append((matches.group(1), comment.line_number()))
  229. try:
  230. year = extract_year_from_espressif_notice(matches.group(1))
  231. except NotFound as e:
  232. if args.debug:
  233. print(f'{TERMINAL_GRAY}Not an {e.thing} {file_name}:{comment.line_number()}{TERMINAL_RESET}')
  234. else:
  235. template = '// SPDX-FileContributor: ' + config_section['espressif_copyright']
  236. if comment.is_multiline():
  237. template = ' * SPDX-FileContributor: ' + config_section['espressif_copyright']
  238. if comment.is_first_in_multiline():
  239. template = '/* SPDX-FileContributor: ' + config_section['espressif_copyright']
  240. if mime == MIME['python']:
  241. template = '# SPDX-FileContributor: ' + config_section['espressif_copyright']
  242. code_lines[comment.line_number() - 1] = template.format(years=format_years(year, file_name))
  243. matches = re.search(r'SPDX-License-Identifier: ?(.*)', comment.text(), re.IGNORECASE)
  244. if matches:
  245. detected_licenses.append((matches.group(1), comment.line_number()))
  246. if not is_on_ignore and not contains_any_copyright(comments, args):
  247. code_lines = insert_copyright(code_lines, file_name, mime, config_section)
  248. print(f'"{file_name}": inserted copyright notice - please check the content and run commit again!')
  249. valid = True
  250. new_code = '\n'.join(code_lines) + '\n'
  251. if code != new_code:
  252. with open(file_name, 'w') as f:
  253. f.write(new_code)
  254. modified = True
  255. if detected_licenses and detected_notices:
  256. valid = True
  257. if args.debug:
  258. print(f'{file_name} notices: {detected_notices}')
  259. print(f'{file_name} licenses: {detected_licenses}')
  260. if detected_licenses:
  261. for detected_license, line_number in detected_licenses:
  262. allowed_licenses = ast.literal_eval(config_section['allowed_licenses'])
  263. if not allowed_license_combination(detected_license, allowed_licenses):
  264. valid = False
  265. print(f'{TERMINAL_RED}{file_name}:{line_number} License "{detected_license}" is not allowed! Allowed licenses: {allowed_licenses}.')
  266. return valid, modified
  267. def contains_any_copyright(comments: list, args: argparse.Namespace) -> bool:
  268. """
  269. Return True if any comment contain the word "copyright"
  270. """
  271. return any(
  272. comment.line_number() <= args.max_lines
  273. and re.search(r'copyright', comment.text(), re.IGNORECASE)
  274. for comment in comments
  275. )
  276. def insert_copyright(code_lines: list, file_name: str, mime: str, config_section: configparser.SectionProxy) -> list:
  277. """
  278. Insert a copyright notice in the beginning of a file, respecting a potential shebang
  279. """
  280. new_code_lines = []
  281. # if first line contains a shebang, keep it first
  282. if code_lines[0].startswith('#!'):
  283. new_code_lines.append(code_lines[0])
  284. del code_lines[0]
  285. template = config_section['new_notice_c']
  286. if mime == MIME['python']:
  287. template = config_section['new_notice_python']
  288. new_code_lines.extend(template.format(license=config_section['license_for_new_files'], years=format_years(0, file_name)).splitlines())
  289. new_code_lines.extend(code_lines)
  290. return new_code_lines
  291. def extract_year_from_espressif_notice(notice: str) -> int:
  292. """
  293. Extracts copyright year (creation date) from a Espressif copyright notice
  294. """
  295. matches = re.search(r'(\d{4})(?:-\d{4})? Espressif Systems', notice, re.IGNORECASE)
  296. if matches:
  297. return int(matches.group(1))
  298. raise NotFound('Espressif copyright notice')
  299. def replace_copyright(code_lines: list, year: int, line: int, mime: str, file_name: str) -> list:
  300. """
  301. Replaces old header style with new SPDX form.
  302. """
  303. # replace from line number (line) to line number (line + number of lines in the OLD HEADER)
  304. # with new header depending on file type
  305. end = line + OLD_APACHE_HEADER.count('\n')
  306. del code_lines[line - 1:end - 1]
  307. template = NEW_APACHE_HEADER
  308. if mime == MIME['python']:
  309. template = NEW_APACHE_HEADER_PYTHON
  310. code_lines[line - 1:line - 1] = template.format(years=format_years(year, file_name)).splitlines()
  311. print(f'{TERMINAL_BOLD}"{file_name}": replacing old Apache-2.0 header (lines: {line}-{end}) with the new SPDX header.{TERMINAL_RESET}')
  312. return code_lines
  313. def detect_old_header_style(file_name: str, comments: list, args: argparse.Namespace) -> Tuple[int, int]:
  314. """
  315. Detects old header style (Apache-2.0) and extracts the year and line number.
  316. returns: Tuple[year, comment line number]
  317. """
  318. comments_text = str()
  319. for comment in comments:
  320. if comment.line_number() > args.max_lines:
  321. break
  322. comments_text = f'{comments_text}\n{comment.text().strip()}'
  323. ratio = fuzz.partial_ratio(comments_text, OLD_APACHE_HEADER)
  324. if args.debug:
  325. print(f'{TERMINAL_GRAY}ratio for {file_name}: {ratio}{TERMINAL_RESET}')
  326. if ratio > args.fuzzy_ratio:
  327. for comment in comments:
  328. # only check up to line number MAX_LINES
  329. if comment.line_number() > args.max_lines:
  330. break
  331. try:
  332. year = extract_year_from_espressif_notice(comment.text())
  333. except NotFound:
  334. pass
  335. else:
  336. return (year, comment.line_number())
  337. raise NotFound('Old Espressif header')
  338. def format_years(past: int, file_name: str) -> str:
  339. """
  340. Function to format a year:
  341. - just current year -> output: [year]
  342. - some year in the past -> output: [past year]-[current year]
  343. """
  344. today = datetime.datetime.now().year
  345. if past == 0:
  346. # use the current year
  347. past = today
  348. if past == today:
  349. return str(past)
  350. if past > today or past < 1972:
  351. error_msg = f'{file_name}: invalid year in the copyright header detected. ' \
  352. + 'Check your system clock and the copyright header.'
  353. raise ValueError(error_msg)
  354. return '{past}-{today}'.format(past=past, today=today)
  355. def check_copyrights(args: argparse.Namespace, config: configparser.ConfigParser) -> Tuple[List, List]:
  356. """
  357. Main logic and for loop
  358. returns:
  359. list of files with wrong headers
  360. list of files which were modified
  361. """
  362. wrong_header_files = []
  363. modified_files = []
  364. pathspecs = {}
  365. with open(IGNORE_LIST_FN, 'r') as f:
  366. ignore_list = [item.strip() for item in f.readlines()]
  367. updated_ignore_list = ignore_list.copy()
  368. # compile the file patterns
  369. for section in config.sections():
  370. # configparser stores all values as strings
  371. patterns = ast.literal_eval(config[section]['include'])
  372. try:
  373. pathspecs[section] = pathspec.PathSpec.from_lines('gitwildmatch', patterns)
  374. except TypeError:
  375. print(f'Error while compiling file patterns. Section {section} has invalid include option. Must be a list of file patterns.')
  376. sys.exit(1)
  377. for file_name in args.filenames:
  378. try:
  379. mime = get_file_mime(file_name)
  380. except UnsupportedFileType:
  381. print(f'{TERMINAL_GRAY}"{file_name}" is not of a supported type! Skipping.{TERMINAL_RESET}')
  382. continue
  383. matched_section = 'DEFAULT'
  384. for section in config.sections():
  385. if pathspecs[section].match_file(file_name):
  386. if args.debug:
  387. print(f'{TERMINAL_GRAY}{file_name} matched {section}{TERMINAL_RESET}')
  388. matched_section = section
  389. if config[matched_section]['perform_check'] == 'False': # configparser stores all values as strings
  390. print(f'{TERMINAL_GRAY}"{file_name}" is using config section "{matched_section}" which does not perform the check! Skipping.{TERMINAL_RESET}')
  391. continue
  392. if file_name in ignore_list:
  393. if args.verbose:
  394. print(f'{TERMINAL_GRAY}"{file_name}" is on the ignore list.{TERMINAL_RESET}')
  395. valid, modified = has_valid_copyright(file_name, mime, True, config[matched_section], args)
  396. if modified:
  397. modified_files.append(CustomFile(file_name, True))
  398. if valid:
  399. if args.dont_update_ignore_list:
  400. print(f'{TERMINAL_YELLOW}"{file_name}" now has a correct copyright header - remove it from the ignore list '
  401. f'or run this script without the --dont-update-ignore-list option to do this automatically!{TERMINAL_RESET}')
  402. else:
  403. updated_ignore_list.remove(file_name)
  404. else:
  405. wrong_header_files.append(CustomFile(file_name, True))
  406. else:
  407. valid, modified = has_valid_copyright(file_name, mime, False, config[matched_section], args)
  408. if modified:
  409. modified_files.append(CustomFile(file_name, False))
  410. if not valid:
  411. wrong_header_files.append(CustomFile(file_name, False))
  412. if updated_ignore_list != ignore_list:
  413. with open(IGNORE_LIST_FN, 'w') as f:
  414. for item in updated_ignore_list:
  415. f.write(f'{item}\n')
  416. modified_files.append(CustomFile(IGNORE_LIST_FN, False))
  417. print(f'\n{TERMINAL_GREEN}Files removed from ignore list:{TERMINAL_RESET}')
  418. for file in ignore_list:
  419. if file not in updated_ignore_list:
  420. print(f' {file}')
  421. return wrong_header_files, modified_files
  422. def build_parser() -> argparse.ArgumentParser:
  423. parser = argparse.ArgumentParser(description='Check copyright headers')
  424. parser.add_argument('-v', '--verbose', action='store_true',
  425. help='print more information (useful for debugging)')
  426. parser.add_argument('-r', '--replace', action='store_true',
  427. help='tries to update copyright notices')
  428. parser.add_argument('-m', '--max-lines', type=int, default=30,
  429. help='how far to check for copyright notice in a file (default 30)')
  430. parser.add_argument('-f', '--fuzzy-ratio', type=int, default=95,
  431. help='minimum %% ratio to be considered as equal to the old header style (default 95)')
  432. parser.add_argument('-d', '--debug', action='store_true',
  433. help='print debug info')
  434. parser.add_argument('-du', '--dont-update-ignore-list', action='store_true')
  435. parser.add_argument('filenames', nargs='+', help='file(s) to check', metavar='file')
  436. return parser
  437. def debug_output(args: argparse.Namespace, config: configparser.ConfigParser) -> None:
  438. print(f'{TERMINAL_GRAY}Running with args: {args}')
  439. print(f'Config file: {CONFIG_FN}')
  440. print(f'Ignore list: {IGNORE_LIST_FN}{TERMINAL_RESET}')
  441. print(f'Sections: {config.sections()}')
  442. for section in config:
  443. print(f'section: "{section}"')
  444. for key in config[section]:
  445. print(f' {key}: "{config[section][key]}"')
  446. def allowed_license_combination(license_to_match: str, all_licenses: List[str]) -> bool:
  447. """
  448. Licenses can be combined together with the OR keyword. Therefore, a simple "in" lookup in a list is not enough.
  449. For example, if "A" and "B" are supported then "A OR B" and "B OR A" should be supported as well.
  450. """
  451. if license_to_match in all_licenses:
  452. # This is the simple case, for example, when "A" is used from the list ["A", "B"]
  453. return True
  454. # for example, if license_to_match is "A OR B" then the following split will be ["A", "B"]
  455. split_list = [sp for sp in map(str.strip, license_to_match.split(' OR ')) if len(sp) > 0]
  456. # for example, "A" and "B" needs to be in the supported list in order to match "A OR B".
  457. return all(i in all_licenses for i in split_list)
  458. def verify_config(config: configparser.ConfigParser) -> None:
  459. fail = False
  460. for section in config:
  461. license_for_new_files = config[section]['license_for_new_files']
  462. # configparser stores all values as strings
  463. allowed_licenses = ast.literal_eval(config[section]['allowed_licenses'])
  464. if not allowed_license_combination(license_for_new_files, allowed_licenses):
  465. print(f'Invalid config, section "{section}":\nDefault license for new files '
  466. f'({license_for_new_files}) is not on the allowed licenses list {allowed_licenses}.')
  467. fail = True
  468. for section in config.sections():
  469. if 'include' not in config[section]:
  470. print(f'Invalid config, section "{section}":\nSection does not have the "include" option set.')
  471. fail = True
  472. if fail:
  473. sys.exit(1)
  474. def main() -> None:
  475. args = build_parser().parse_args()
  476. config = configparser.ConfigParser()
  477. with open(CONFIG_FN, 'r') as f:
  478. yaml_dict = yaml.safe_load(f)
  479. config.read_dict(yaml_dict)
  480. if args.debug:
  481. debug_output(args, config)
  482. verify_config(config)
  483. wrong_header_files, modified_files = check_copyrights(args, config)
  484. abort_commit = bool(modified_files)
  485. num_files_wrong = 0
  486. if wrong_header_files:
  487. print(f'{TERMINAL_YELLOW}Information about this test{TERMINAL_RESET}')
  488. print(CHECK_FAIL_MESSAGE.format())
  489. print(f'{TERMINAL_YELLOW}Files which failed the copyright check:{TERMINAL_RESET}')
  490. for wrong_file in wrong_header_files:
  491. if not wrong_file.is_on_ignore_list:
  492. abort_commit = True
  493. num_files_wrong += 1
  494. print(wrong_file)
  495. if modified_files:
  496. print(f'\n{TERMINAL_YELLOW}Modified files:{TERMINAL_RESET}')
  497. for file in modified_files:
  498. print(file)
  499. print(CHECK_MODIFY_MESSAGE)
  500. num_files_processed = len(args.filenames)
  501. print(CHECK_FOOTER_MESSAGE)
  502. if abort_commit:
  503. num_files_modified = len(modified_files)
  504. print(f'{TERMINAL_RED}Processed {num_files_processed} source file{"s"[:num_files_processed^1]},', end=' ')
  505. print(f'{num_files_modified} were modified and {num_files_wrong} have an invalid copyright (excluding ones on the ignore list).{TERMINAL_RESET}')
  506. sys.exit(1) # sys.exit(1) to abort the commit
  507. # pre-commit also automatically aborts a commit if files are modified on disk
  508. print(f'{TERMINAL_GREEN}Successfully processed {num_files_processed} file{"s"[:num_files_processed^1]}.{TERMINAL_RESET}')
  509. if __name__ == '__main__':
  510. main()