mklog.py 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. #!/usr/bin/env python3
  2. # Copyright (C) 2020 Free Software Foundation, Inc.
  3. #
  4. # This file is part of GCC.
  5. #
  6. # GCC is free software; you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License as published by
  8. # the Free Software Foundation; either version 3, or (at your option)
  9. # any later version.
  10. #
  11. # GCC is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with GCC; see the file COPYING. If not, write to
  18. # the Free Software Foundation, 51 Franklin Street, Fifth Floor,
  19. # Boston, MA 02110-1301, USA.
  20. # This script parses a .diff file generated with 'diff -up' or 'diff -cp'
  21. # and adds a skeleton ChangeLog file to the file. It does not try to be
  22. # too smart when parsing function names, but it produces a reasonable
  23. # approximation.
  24. #
  25. # Author: Martin Liska <mliska@suse.cz>
  26. import argparse
  27. import os
  28. import re
  29. import sys
  30. from itertools import takewhile
  31. import requests
  32. from unidiff import PatchSet
  33. pr_regex = re.compile(r'(\/(\/|\*)|[Cc*!])\s+(?P<pr>PR [a-z+-]+\/[0-9]+)')
  34. dr_regex = re.compile(r'(\/(\/|\*)|[Cc*!])\s+(?P<dr>DR [0-9]+)')
  35. identifier_regex = re.compile(r'^([a-zA-Z0-9_#].*)')
  36. comment_regex = re.compile(r'^\/\*')
  37. struct_regex = re.compile(r'^(class|struct|union|enum)\s+'
  38. r'(GTY\(.*\)\s+)?([a-zA-Z0-9_]+)')
  39. macro_regex = re.compile(r'#\s*(define|undef)\s+([a-zA-Z0-9_]+)')
  40. super_macro_regex = re.compile(r'^DEF[A-Z0-9_]+\s*\(([a-zA-Z0-9_]+)')
  41. fn_regex = re.compile(r'([a-zA-Z_][^()\s]*)\s*\([^*]')
  42. template_and_param_regex = re.compile(r'<[^<>]*>')
  43. bugzilla_url = 'https://gcc.gnu.org/bugzilla/rest.cgi/bug?id=%s&' \
  44. 'include_fields=summary'
  45. function_extensions = set(['.c', '.cpp', '.C', '.cc', '.h', '.inc', '.def'])
  46. help_message = """\
  47. Generate ChangeLog template for PATCH.
  48. PATCH must be generated using diff(1)'s -up or -cp options
  49. (or their equivalent in git).
  50. """
  51. script_folder = os.path.realpath(__file__)
  52. gcc_root = os.path.dirname(os.path.dirname(script_folder))
  53. def find_changelog(path):
  54. folder = os.path.split(path)[0]
  55. while True:
  56. if os.path.exists(os.path.join(gcc_root, folder, 'ChangeLog')):
  57. return folder
  58. folder = os.path.dirname(folder)
  59. if folder == '':
  60. return folder
  61. raise AssertionError()
  62. def extract_function_name(line):
  63. if comment_regex.match(line):
  64. return None
  65. m = struct_regex.search(line)
  66. if m:
  67. # Struct declaration
  68. return m.group(1) + ' ' + m.group(3)
  69. m = macro_regex.search(line)
  70. if m:
  71. # Macro definition
  72. return m.group(2)
  73. m = super_macro_regex.search(line)
  74. if m:
  75. # Supermacro
  76. return m.group(1)
  77. m = fn_regex.search(line)
  78. if m:
  79. # Discard template and function parameters.
  80. fn = m.group(1)
  81. fn = re.sub(template_and_param_regex, '', fn)
  82. return fn.rstrip()
  83. return None
  84. def try_add_function(functions, line):
  85. fn = extract_function_name(line)
  86. if fn and fn not in functions:
  87. functions.append(fn)
  88. return bool(fn)
  89. def sort_changelog_files(changed_file):
  90. return (changed_file.is_added_file, changed_file.is_removed_file)
  91. def get_pr_titles(prs):
  92. output = ''
  93. for pr in prs:
  94. id = pr.split('/')[-1]
  95. r = requests.get(bugzilla_url % id)
  96. bugs = r.json()['bugs']
  97. if len(bugs) == 1:
  98. output += '%s - %s\n' % (pr, bugs[0]['summary'])
  99. print(output)
  100. if output:
  101. output += '\n'
  102. return output
  103. def generate_changelog(data, no_functions=False, fill_pr_titles=False):
  104. changelogs = {}
  105. changelog_list = []
  106. prs = []
  107. out = ''
  108. diff = PatchSet(data)
  109. for file in diff:
  110. changelog = find_changelog(file.path)
  111. if changelog not in changelogs:
  112. changelogs[changelog] = []
  113. changelog_list.append(changelog)
  114. changelogs[changelog].append(file)
  115. # Extract PR entries from newly added tests
  116. if 'testsuite' in file.path and file.is_added_file:
  117. for line in list(file)[0]:
  118. m = pr_regex.search(line.value)
  119. if m:
  120. pr = m.group('pr')
  121. if pr not in prs:
  122. prs.append(pr)
  123. else:
  124. m = dr_regex.search(line.value)
  125. if m:
  126. dr = m.group('dr')
  127. if dr not in prs:
  128. prs.append(dr)
  129. else:
  130. break
  131. if fill_pr_titles:
  132. out += get_pr_titles(prs)
  133. # sort ChangeLog so that 'testsuite' is at the end
  134. for changelog in sorted(changelog_list, key=lambda x: 'testsuite' in x):
  135. files = changelogs[changelog]
  136. out += '%s:\n' % os.path.join(changelog, 'ChangeLog')
  137. out += '\n'
  138. for pr in prs:
  139. out += '\t%s\n' % pr
  140. # new and deleted files should be at the end
  141. for file in sorted(files, key=sort_changelog_files):
  142. assert file.path.startswith(changelog)
  143. in_tests = 'testsuite' in changelog or 'testsuite' in file.path
  144. relative_path = file.path[len(changelog):].lstrip('/')
  145. functions = []
  146. if file.is_added_file:
  147. msg = 'New test' if in_tests else 'New file'
  148. out += '\t* %s: %s.\n' % (relative_path, msg)
  149. elif file.is_removed_file:
  150. out += '\t* %s: Removed.\n' % (relative_path)
  151. elif hasattr(file, 'is_rename') and file.is_rename:
  152. out += '\t* %s: Moved to...\n' % (relative_path)
  153. new_path = file.target_file[2:]
  154. # A file can be theoretically moved to a location that
  155. # belongs to a different ChangeLog. Let user fix it.
  156. if new_path.startswith(changelog):
  157. new_path = new_path[len(changelog):].lstrip('/')
  158. out += '\t* %s: ...here.\n' % (new_path)
  159. else:
  160. if not no_functions:
  161. for hunk in file:
  162. # Do not add function names for testsuite files
  163. extension = os.path.splitext(relative_path)[1]
  164. if not in_tests and extension in function_extensions:
  165. last_fn = None
  166. modified_visited = False
  167. success = False
  168. for line in hunk:
  169. m = identifier_regex.match(line.value)
  170. if line.is_added or line.is_removed:
  171. if not line.value.strip():
  172. continue
  173. modified_visited = True
  174. if m and try_add_function(functions,
  175. m.group(1)):
  176. last_fn = None
  177. success = True
  178. elif line.is_context:
  179. if last_fn and modified_visited:
  180. try_add_function(functions, last_fn)
  181. last_fn = None
  182. modified_visited = False
  183. success = True
  184. elif m:
  185. last_fn = m.group(1)
  186. modified_visited = False
  187. if not success:
  188. try_add_function(functions,
  189. hunk.section_header)
  190. if functions:
  191. out += '\t* %s (%s):\n' % (relative_path, functions[0])
  192. for fn in functions[1:]:
  193. out += '\t(%s):\n' % fn
  194. else:
  195. out += '\t* %s:\n' % relative_path
  196. out += '\n'
  197. return out
  198. if __name__ == '__main__':
  199. parser = argparse.ArgumentParser(description=help_message)
  200. parser.add_argument('input', nargs='?',
  201. help='Patch file (or missing, read standard input)')
  202. parser.add_argument('-s', '--no-functions', action='store_true',
  203. help='Do not generate function names in ChangeLogs')
  204. parser.add_argument('-p', '--fill-up-bug-titles', action='store_true',
  205. help='Download title of mentioned PRs')
  206. parser.add_argument('-c', '--changelog',
  207. help='Append the ChangeLog to a git commit message '
  208. 'file')
  209. args = parser.parse_args()
  210. if args.input == '-':
  211. args.input = None
  212. input = open(args.input) if args.input else sys.stdin
  213. data = input.read()
  214. output = generate_changelog(data, args.no_functions,
  215. args.fill_up_bug_titles)
  216. if args.changelog:
  217. lines = open(args.changelog).read().split('\n')
  218. start = list(takewhile(lambda l: not l.startswith('#'), lines))
  219. end = lines[len(start):]
  220. with open(args.changelog, 'w') as f:
  221. if start:
  222. # appent empty line
  223. if start[-1] != '':
  224. start.append('')
  225. else:
  226. # append 2 empty lines
  227. start = 2 * ['']
  228. f.write('\n'.join(start))
  229. f.write('\n')
  230. f.write(output)
  231. f.write('\n'.join(end))
  232. else:
  233. print(output, end='')