mklog.py revision 1.1.1.1 1 1.1 mrg #!/usr/bin/env python3
2 1.1 mrg
3 1.1 mrg # Copyright (C) 2020 Free Software Foundation, Inc.
4 1.1 mrg #
5 1.1 mrg # This file is part of GCC.
6 1.1 mrg #
7 1.1 mrg # GCC is free software; you can redistribute it and/or modify
8 1.1 mrg # it under the terms of the GNU General Public License as published by
9 1.1 mrg # the Free Software Foundation; either version 3, or (at your option)
10 1.1 mrg # any later version.
11 1.1 mrg #
12 1.1 mrg # GCC is distributed in the hope that it will be useful,
13 1.1 mrg # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 1.1 mrg # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 1.1 mrg # GNU General Public License for more details.
16 1.1 mrg #
17 1.1 mrg # You should have received a copy of the GNU General Public License
18 1.1 mrg # along with GCC; see the file COPYING. If not, write to
19 1.1 mrg # the Free Software Foundation, 51 Franklin Street, Fifth Floor,
20 1.1 mrg # Boston, MA 02110-1301, USA.
21 1.1 mrg
22 1.1 mrg # This script parses a .diff file generated with 'diff -up' or 'diff -cp'
23 1.1 mrg # and adds a skeleton ChangeLog file to the file. It does not try to be
24 1.1 mrg # too smart when parsing function names, but it produces a reasonable
25 1.1 mrg # approximation.
26 1.1 mrg #
27 1.1 mrg # Author: Martin Liska <mliska (at] suse.cz>
28 1.1 mrg
29 1.1 mrg import argparse
30 1.1 mrg import datetime
31 1.1 mrg import os
32 1.1 mrg import re
33 1.1 mrg import subprocess
34 1.1 mrg import sys
35 1.1 mrg from itertools import takewhile
36 1.1 mrg
37 1.1 mrg import requests
38 1.1 mrg
39 1.1 mrg from unidiff import PatchSet
40 1.1 mrg
41 1.1 mrg pr_regex = re.compile(r'(\/(\/|\*)|[Cc*!])\s+(?P<pr>PR [a-z+-]+\/[0-9]+)')
42 1.1 mrg prnum_regex = re.compile(r'PR (?P<comp>[a-z+-]+)/(?P<num>[0-9]+)')
43 1.1 mrg dr_regex = re.compile(r'(\/(\/|\*)|[Cc*!])\s+(?P<dr>DR [0-9]+)')
44 1.1 mrg dg_regex = re.compile(r'{\s+dg-(error|warning)')
45 1.1 mrg pr_filename_regex = re.compile(r'(^|[\W_])[Pp][Rr](?P<pr>\d{4,})')
46 1.1 mrg identifier_regex = re.compile(r'^([a-zA-Z0-9_#].*)')
47 1.1 mrg comment_regex = re.compile(r'^\/\*')
48 1.1 mrg struct_regex = re.compile(r'^(class|struct|union|enum)\s+'
49 1.1 mrg r'(GTY\(.*\)\s+)?([a-zA-Z0-9_]+)')
50 1.1 mrg macro_regex = re.compile(r'#\s*(define|undef)\s+([a-zA-Z0-9_]+)')
51 1.1 mrg super_macro_regex = re.compile(r'^DEF[A-Z0-9_]+\s*\(([a-zA-Z0-9_]+)')
52 1.1 mrg fn_regex = re.compile(r'([a-zA-Z_][^()\s]*)\s*\([^*]')
53 1.1 mrg template_and_param_regex = re.compile(r'<[^<>]*>')
54 1.1 mrg md_def_regex = re.compile(r'\(define.*\s+"(.*)"')
55 1.1 mrg bugzilla_url = 'https://gcc.gnu.org/bugzilla/rest.cgi/bug?id=%s&' \
56 1.1 mrg 'include_fields=summary,component'
57 1.1 mrg
58 1.1 mrg function_extensions = {'.c', '.cpp', '.C', '.cc', '.h', '.inc', '.def', '.md'}
59 1.1 mrg
60 1.1 mrg # NB: Makefile.in isn't listed as it's not always generated.
61 1.1 mrg generated_files = {'aclocal.m4', 'config.h.in', 'configure'}
62 1.1 mrg
63 1.1 mrg help_message = """\
64 1.1 mrg Generate ChangeLog template for PATCH.
65 1.1 mrg PATCH must be generated using diff(1)'s -up or -cp options
66 1.1 mrg (or their equivalent in git).
67 1.1 mrg """
68 1.1 mrg
69 1.1 mrg script_folder = os.path.realpath(__file__)
70 1.1 mrg root = os.path.dirname(os.path.dirname(script_folder))
71 1.1 mrg
72 1.1 mrg firstpr = ''
73 1.1 mrg
74 1.1 mrg
75 1.1 mrg def find_changelog(path):
76 1.1 mrg folder = os.path.split(path)[0]
77 1.1 mrg while True:
78 1.1 mrg if os.path.exists(os.path.join(root, folder, 'ChangeLog')):
79 1.1 mrg return folder
80 1.1 mrg folder = os.path.dirname(folder)
81 1.1 mrg if folder == '':
82 1.1 mrg return folder
83 1.1 mrg raise AssertionError()
84 1.1 mrg
85 1.1 mrg
86 1.1 mrg def extract_function_name(line):
87 1.1 mrg if comment_regex.match(line):
88 1.1 mrg return None
89 1.1 mrg m = struct_regex.search(line)
90 1.1 mrg if m:
91 1.1 mrg # Struct declaration
92 1.1 mrg return m.group(1) + ' ' + m.group(3)
93 1.1 mrg m = macro_regex.search(line)
94 1.1 mrg if m:
95 1.1 mrg # Macro definition
96 1.1 mrg return m.group(2)
97 1.1 mrg m = super_macro_regex.search(line)
98 1.1 mrg if m:
99 1.1 mrg # Supermacro
100 1.1 mrg return m.group(1)
101 1.1 mrg m = fn_regex.search(line)
102 1.1 mrg if m:
103 1.1 mrg # Discard template and function parameters.
104 1.1 mrg fn = m.group(1)
105 1.1 mrg fn = re.sub(template_and_param_regex, '', fn)
106 1.1 mrg return fn.rstrip()
107 1.1 mrg return None
108 1.1 mrg
109 1.1 mrg
110 1.1 mrg def try_add_function(functions, line):
111 1.1 mrg fn = extract_function_name(line)
112 1.1 mrg if fn and fn not in functions:
113 1.1 mrg functions.append(fn)
114 1.1 mrg return bool(fn)
115 1.1 mrg
116 1.1 mrg
117 1.1 mrg def sort_changelog_files(changed_file):
118 1.1 mrg return (changed_file.is_added_file, changed_file.is_removed_file)
119 1.1 mrg
120 1.1 mrg
121 1.1 mrg def get_pr_titles(prs):
122 1.1 mrg output = []
123 1.1 mrg for idx, pr in enumerate(prs):
124 1.1 mrg pr_id = pr.split('/')[-1]
125 1.1 mrg r = requests.get(bugzilla_url % pr_id)
126 1.1 mrg bugs = r.json()['bugs']
127 1.1 mrg if len(bugs) == 1:
128 1.1 mrg prs[idx] = 'PR %s/%s' % (bugs[0]['component'], pr_id)
129 1.1 mrg out = '%s - %s\n' % (prs[idx], bugs[0]['summary'])
130 1.1 mrg if out not in output:
131 1.1 mrg output.append(out)
132 1.1 mrg if output:
133 1.1 mrg output.append('')
134 1.1 mrg return '\n'.join(output)
135 1.1 mrg
136 1.1 mrg
137 1.1 mrg def generate_changelog(data, no_functions=False, fill_pr_titles=False,
138 1.1 mrg additional_prs=None):
139 1.1 mrg changelogs = {}
140 1.1 mrg changelog_list = []
141 1.1 mrg prs = []
142 1.1 mrg out = ''
143 1.1 mrg diff = PatchSet(data)
144 1.1 mrg global firstpr
145 1.1 mrg
146 1.1 mrg if additional_prs:
147 1.1 mrg prs = [pr for pr in additional_prs if pr not in prs]
148 1.1 mrg for file in diff:
149 1.1 mrg # skip files that can't be parsed
150 1.1 mrg if file.path == '/dev/null':
151 1.1 mrg continue
152 1.1 mrg changelog = find_changelog(file.path)
153 1.1 mrg if changelog not in changelogs:
154 1.1 mrg changelogs[changelog] = []
155 1.1 mrg changelog_list.append(changelog)
156 1.1 mrg changelogs[changelog].append(file)
157 1.1 mrg
158 1.1 mrg # Extract PR entries from newly added tests
159 1.1 mrg if 'testsuite' in file.path and file.is_added_file:
160 1.1 mrg # Only search first ten lines as later lines may
161 1.1 mrg # contains commented code which a note that it
162 1.1 mrg # has not been tested due to a certain PR or DR.
163 1.1 mrg this_file_prs = []
164 1.1 mrg for line in list(file)[0][0:10]:
165 1.1 mrg m = pr_regex.search(line.value)
166 1.1 mrg if m:
167 1.1 mrg pr = m.group('pr')
168 1.1 mrg if pr not in prs:
169 1.1 mrg prs.append(pr)
170 1.1 mrg this_file_prs.append(pr.split('/')[-1])
171 1.1 mrg else:
172 1.1 mrg m = dr_regex.search(line.value)
173 1.1 mrg if m:
174 1.1 mrg dr = m.group('dr')
175 1.1 mrg if dr not in prs:
176 1.1 mrg prs.append(dr)
177 1.1 mrg this_file_prs.append(dr.split('/')[-1])
178 1.1 mrg elif dg_regex.search(line.value):
179 1.1 mrg # Found dg-warning/dg-error line
180 1.1 mrg break
181 1.1 mrg # PR number in the file name
182 1.1 mrg fname = os.path.basename(file.path)
183 1.1 mrg m = pr_filename_regex.search(fname)
184 1.1 mrg if m:
185 1.1 mrg pr = m.group('pr')
186 1.1 mrg pr2 = 'PR ' + pr
187 1.1 mrg if pr not in this_file_prs and pr2 not in prs:
188 1.1 mrg prs.append(pr2)
189 1.1 mrg
190 1.1 mrg if prs:
191 1.1 mrg firstpr = prs[0]
192 1.1 mrg
193 1.1 mrg if fill_pr_titles:
194 1.1 mrg out += get_pr_titles(prs)
195 1.1 mrg
196 1.1 mrg # print list of PR entries before ChangeLog entries
197 1.1 mrg if prs:
198 1.1 mrg if not out:
199 1.1 mrg out += '\n'
200 1.1 mrg for pr in prs:
201 1.1 mrg out += '\t%s\n' % pr
202 1.1 mrg out += '\n'
203 1.1 mrg
204 1.1 mrg # sort ChangeLog so that 'testsuite' is at the end
205 1.1 mrg for changelog in sorted(changelog_list, key=lambda x: 'testsuite' in x):
206 1.1 mrg files = changelogs[changelog]
207 1.1 mrg out += '%s:\n' % os.path.join(changelog, 'ChangeLog')
208 1.1 mrg out += '\n'
209 1.1 mrg # new and deleted files should be at the end
210 1.1 mrg for file in sorted(files, key=sort_changelog_files):
211 1.1 mrg assert file.path.startswith(changelog)
212 1.1 mrg in_tests = 'testsuite' in changelog or 'testsuite' in file.path
213 1.1 mrg relative_path = file.path[len(changelog):].lstrip('/')
214 1.1 mrg functions = []
215 1.1 mrg if file.is_added_file:
216 1.1 mrg msg = 'New test' if in_tests else 'New file'
217 1.1 mrg out += '\t* %s: %s.\n' % (relative_path, msg)
218 1.1 mrg elif file.is_removed_file:
219 1.1 mrg out += '\t* %s: Removed.\n' % (relative_path)
220 1.1 mrg elif hasattr(file, 'is_rename') and file.is_rename:
221 1.1 mrg out += '\t* %s: Moved to...\n' % (relative_path)
222 1.1 mrg new_path = file.target_file[2:]
223 1.1 mrg # A file can be theoretically moved to a location that
224 1.1 mrg # belongs to a different ChangeLog. Let user fix it.
225 1.1 mrg if new_path.startswith(changelog):
226 1.1 mrg new_path = new_path[len(changelog):].lstrip('/')
227 1.1 mrg out += '\t* %s: ...here.\n' % (new_path)
228 1.1 mrg elif os.path.basename(file.path) in generated_files:
229 1.1 mrg out += '\t* %s: Regenerate.\n' % (relative_path)
230 1.1 mrg else:
231 1.1 mrg if not no_functions:
232 1.1 mrg for hunk in file:
233 1.1 mrg # Do not add function names for testsuite files
234 1.1 mrg extension = os.path.splitext(relative_path)[1]
235 1.1 mrg if not in_tests and extension in function_extensions:
236 1.1 mrg last_fn = None
237 1.1 mrg modified_visited = False
238 1.1 mrg success = False
239 1.1 mrg for line in hunk:
240 1.1 mrg m = identifier_regex.match(line.value)
241 1.1 mrg if line.is_added or line.is_removed:
242 1.1 mrg # special-case definition in .md files
243 1.1 mrg m2 = md_def_regex.match(line.value)
244 1.1 mrg if extension == '.md' and m2:
245 1.1 mrg fn = m2.group(1)
246 1.1 mrg if fn not in functions:
247 1.1 mrg functions.append(fn)
248 1.1 mrg last_fn = None
249 1.1 mrg success = True
250 1.1 mrg
251 1.1 mrg if not line.value.strip():
252 1.1 mrg continue
253 1.1 mrg modified_visited = True
254 1.1 mrg if m and try_add_function(functions,
255 1.1 mrg m.group(1)):
256 1.1 mrg last_fn = None
257 1.1 mrg success = True
258 1.1 mrg elif line.is_context:
259 1.1 mrg if last_fn and modified_visited:
260 1.1 mrg try_add_function(functions, last_fn)
261 1.1 mrg last_fn = None
262 1.1 mrg modified_visited = False
263 1.1 mrg success = True
264 1.1 mrg elif m:
265 1.1 mrg last_fn = m.group(1)
266 1.1 mrg modified_visited = False
267 1.1 mrg if not success:
268 1.1 mrg try_add_function(functions,
269 1.1 mrg hunk.section_header)
270 1.1 mrg if functions:
271 1.1 mrg out += '\t* %s (%s):\n' % (relative_path, functions[0])
272 1.1 mrg for fn in functions[1:]:
273 1.1 mrg out += '\t(%s):\n' % fn
274 1.1 mrg else:
275 1.1 mrg out += '\t* %s:\n' % relative_path
276 1.1 mrg out += '\n'
277 1.1 mrg return out
278 1.1 mrg
279 1.1 mrg
280 1.1 mrg def update_copyright(data):
281 1.1 mrg current_timestamp = datetime.datetime.now().strftime('%Y-%m-%d')
282 1.1 mrg username = subprocess.check_output('git config user.name', shell=True,
283 1.1 mrg encoding='utf8').strip()
284 1.1 mrg email = subprocess.check_output('git config user.email', shell=True,
285 1.1 mrg encoding='utf8').strip()
286 1.1 mrg
287 1.1 mrg changelogs = set()
288 1.1 mrg diff = PatchSet(data)
289 1.1 mrg
290 1.1 mrg for file in diff:
291 1.1 mrg changelog = os.path.join(find_changelog(file.path), 'ChangeLog')
292 1.1 mrg if changelog not in changelogs:
293 1.1 mrg changelogs.add(changelog)
294 1.1 mrg with open(changelog) as f:
295 1.1 mrg content = f.read()
296 1.1 mrg with open(changelog, 'w+') as f:
297 1.1 mrg f.write(f'{current_timestamp} {username} <{email}>\n\n')
298 1.1 mrg f.write('\tUpdate copyright years.\n\n')
299 1.1 mrg f.write(content)
300 1.1 mrg
301 1.1 mrg
302 1.1 mrg if __name__ == '__main__':
303 1.1 mrg parser = argparse.ArgumentParser(description=help_message)
304 1.1 mrg parser.add_argument('input', nargs='?',
305 1.1 mrg help='Patch file (or missing, read standard input)')
306 1.1 mrg parser.add_argument('-b', '--pr-numbers', action='store',
307 1.1 mrg type=lambda arg: arg.split(','), nargs='?',
308 1.1 mrg help='Add the specified PRs (comma separated)')
309 1.1 mrg parser.add_argument('-s', '--no-functions', action='store_true',
310 1.1 mrg help='Do not generate function names in ChangeLogs')
311 1.1 mrg parser.add_argument('-p', '--fill-up-bug-titles', action='store_true',
312 1.1 mrg help='Download title of mentioned PRs')
313 1.1 mrg parser.add_argument('-d', '--directory',
314 1.1 mrg help='Root directory where to search for ChangeLog '
315 1.1 mrg 'files')
316 1.1 mrg parser.add_argument('-c', '--changelog',
317 1.1 mrg help='Append the ChangeLog to a git commit message '
318 1.1 mrg 'file')
319 1.1 mrg parser.add_argument('--update-copyright', action='store_true',
320 1.1 mrg help='Update copyright in ChangeLog files')
321 1.1 mrg args = parser.parse_args()
322 1.1 mrg if args.input == '-':
323 1.1 mrg args.input = None
324 1.1 mrg if args.directory:
325 1.1 mrg root = args.directory
326 1.1 mrg
327 1.1 mrg data = open(args.input) if args.input else sys.stdin
328 1.1 mrg if args.update_copyright:
329 1.1 mrg update_copyright(data)
330 1.1 mrg else:
331 1.1 mrg output = generate_changelog(data, args.no_functions,
332 1.1 mrg args.fill_up_bug_titles, args.pr_numbers)
333 1.1 mrg if args.changelog:
334 1.1 mrg lines = open(args.changelog).read().split('\n')
335 1.1 mrg start = list(takewhile(lambda l: not l.startswith('#'), lines))
336 1.1 mrg end = lines[len(start):]
337 1.1 mrg with open(args.changelog, 'w') as f:
338 1.1 mrg if not start or not start[0]:
339 1.1 mrg # initial commit subject line 'component: [PRnnnnn]'
340 1.1 mrg m = prnum_regex.match(firstpr)
341 1.1 mrg if m:
342 1.1 mrg title = f'{m.group("comp")}: [PR{m.group("num")}]'
343 1.1 mrg start.insert(0, title)
344 1.1 mrg if start:
345 1.1 mrg # append empty line
346 1.1 mrg if start[-1] != '':
347 1.1 mrg start.append('')
348 1.1 mrg else:
349 1.1 mrg # append 2 empty lines
350 1.1 mrg start = 2 * ['']
351 1.1 mrg f.write('\n'.join(start))
352 1.1 mrg f.write('\n')
353 1.1 mrg f.write(output)
354 1.1 mrg f.write('\n'.join(end))
355 1.1 mrg else:
356 1.1 mrg print(output, end='')
357