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