]> git.ipfire.org Git - thirdparty/gcc.git/blame - contrib/mklog.py
aarch64: Add __ARM_FEATURE_PAUTH and __ARM_FEATURE_BTI ACLE defines
[thirdparty/gcc.git] / contrib / mklog.py
CommitLineData
00243d9a
ML
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
29import argparse
cf76bbf8 30import datetime
18ef76d3 31import json
00243d9a
ML
32import os
33import re
cf76bbf8 34import subprocess
00243d9a 35import sys
757dbb59 36from itertools import takewhile
00243d9a 37
4f85a52c
ML
38import requests
39
00243d9a
ML
40from unidiff import PatchSet
41
b838641b
ML
42LINE_LIMIT = 100
43TAB_WIDTH = 8
0684c8d3 44CO_AUTHORED_BY_PREFIX = 'co-authored-by: '
b838641b 45
00243d9a 46pr_regex = re.compile(r'(\/(\/|\*)|[Cc*!])\s+(?P<pr>PR [a-z+-]+\/[0-9]+)')
58e3b17f 47prnum_regex = re.compile(r'PR (?P<comp>[a-z+-]+)/(?P<num>[0-9]+)')
e7c7cdc5 48dr_regex = re.compile(r'(\/(\/|\*)|[Cc*!])\s+(?P<dr>DR [0-9]+)')
ed5ae55e 49dg_regex = re.compile(r'{\s+dg-(error|warning)')
edf0c3ff 50pr_filename_regex = re.compile(r'(^|[\W_])[Pp][Rr](?P<pr>\d{4,})')
00243d9a
ML
51identifier_regex = re.compile(r'^([a-zA-Z0-9_#].*)')
52comment_regex = re.compile(r'^\/\*')
4f85a52c
ML
53struct_regex = re.compile(r'^(class|struct|union|enum)\s+'
54 r'(GTY\(.*\)\s+)?([a-zA-Z0-9_]+)')
00243d9a
ML
55macro_regex = re.compile(r'#\s*(define|undef)\s+([a-zA-Z0-9_]+)')
56super_macro_regex = re.compile(r'^DEF[A-Z0-9_]+\s*\(([a-zA-Z0-9_]+)')
57fn_regex = re.compile(r'([a-zA-Z_][^()\s]*)\s*\([^*]')
58template_and_param_regex = re.compile(r'<[^<>]*>')
a2d04f3d 59md_def_regex = re.compile(r'\(define.*\s+"(.*)"')
25c284f1 60bugzilla_url = 'https://gcc.gnu.org/bugzilla/rest.cgi/bug?id=%s&' \
edf0c3ff 61 'include_fields=summary,component'
00243d9a 62
a2d04f3d 63function_extensions = {'.c', '.cpp', '.C', '.cc', '.h', '.inc', '.def', '.md'}
00243d9a 64
432b9f61
MF
65# NB: Makefile.in isn't listed as it's not always generated.
66generated_files = {'aclocal.m4', 'config.h.in', 'configure'}
67
00243d9a
ML
68help_message = """\
69Generate ChangeLog template for PATCH.
70PATCH must be generated using diff(1)'s -up or -cp options
71(or their equivalent in git).
72"""
73
74script_folder = os.path.realpath(__file__)
e54da1b6 75root = os.path.dirname(os.path.dirname(script_folder))
00243d9a
ML
76
77
78def find_changelog(path):
79 folder = os.path.split(path)[0]
80 while True:
e54da1b6 81 if os.path.exists(os.path.join(root, folder, 'ChangeLog')):
00243d9a
ML
82 return folder
83 folder = os.path.dirname(folder)
84 if folder == '':
85 return folder
86 raise AssertionError()
87
88
89def extract_function_name(line):
90 if comment_regex.match(line):
91 return None
92 m = struct_regex.search(line)
93 if m:
94 # Struct declaration
4f85a52c 95 return m.group(1) + ' ' + m.group(3)
00243d9a
ML
96 m = macro_regex.search(line)
97 if m:
98 # Macro definition
99 return m.group(2)
100 m = super_macro_regex.search(line)
101 if m:
102 # Supermacro
103 return m.group(1)
104 m = fn_regex.search(line)
105 if m:
106 # Discard template and function parameters.
107 fn = m.group(1)
108 fn = re.sub(template_and_param_regex, '', fn)
109 return fn.rstrip()
110 return None
111
112
113def try_add_function(functions, line):
114 fn = extract_function_name(line)
115 if fn and fn not in functions:
116 functions.append(fn)
117 return bool(fn)
118
119
120def sort_changelog_files(changed_file):
121 return (changed_file.is_added_file, changed_file.is_removed_file)
122
123
124def get_pr_titles(prs):
edf0c3ff
TB
125 output = []
126 for idx, pr in enumerate(prs):
d6d9561c
ML
127 pr_id = pr.split('/')[-1]
128 r = requests.get(bugzilla_url % pr_id)
25c284f1
ML
129 bugs = r.json()['bugs']
130 if len(bugs) == 1:
edf0c3ff
TB
131 prs[idx] = 'PR %s/%s' % (bugs[0]['component'], pr_id)
132 out = '%s - %s\n' % (prs[idx], bugs[0]['summary'])
133 if out not in output:
134 output.append(out)
25c284f1 135 if output:
edf0c3ff
TB
136 output.append('')
137 return '\n'.join(output)
00243d9a 138
4f85a52c 139
b838641b
ML
140def append_changelog_line(out, relative_path, text):
141 line = f'\t* {relative_path}:'
142 if len(line.replace('\t', ' ' * TAB_WIDTH) + ' ' + text) <= LINE_LIMIT:
143 out += f'{line} {text}\n'
144 else:
145 out += f'{line}\n'
146 out += f'\t{text}\n'
147 return out
148
149
e370a248
ML
150def get_rel_path_if_prefixed(path, folder):
151 if path.startswith(folder):
152 return path[len(folder):].lstrip('/')
153 else:
154 return path
155
156
edf0c3ff
TB
157def generate_changelog(data, no_functions=False, fill_pr_titles=False,
158 additional_prs=None):
03c0b064
ML
159 global prs
160 prs = []
161
00243d9a
ML
162 changelogs = {}
163 changelog_list = []
00243d9a
ML
164 out = ''
165 diff = PatchSet(data)
166
edf0c3ff 167 if additional_prs:
84f906df
ML
168 for apr in additional_prs:
169 if not apr.startswith('PR ') and '/' in apr:
170 apr = 'PR ' + apr
171 if apr not in prs:
172 prs.append(apr)
00243d9a 173 for file in diff:
236d6a33
ML
174 # skip files that can't be parsed
175 if file.path == '/dev/null':
176 continue
00243d9a
ML
177 changelog = find_changelog(file.path)
178 if changelog not in changelogs:
179 changelogs[changelog] = []
180 changelog_list.append(changelog)
181 changelogs[changelog].append(file)
182
183 # Extract PR entries from newly added tests
184 if 'testsuite' in file.path and file.is_added_file:
ed5ae55e
TB
185 # Only search first ten lines as later lines may
186 # contains commented code which a note that it
187 # has not been tested due to a certain PR or DR.
edf0c3ff 188 this_file_prs = []
ed5ae55e 189 for line in list(file)[0][0:10]:
00243d9a
ML
190 m = pr_regex.search(line.value)
191 if m:
192 pr = m.group('pr')
193 if pr not in prs:
194 prs.append(pr)
edf0c3ff 195 this_file_prs.append(pr.split('/')[-1])
00243d9a 196 else:
e7c7cdc5
ML
197 m = dr_regex.search(line.value)
198 if m:
199 dr = m.group('dr')
200 if dr not in prs:
201 prs.append(dr)
edf0c3ff 202 this_file_prs.append(dr.split('/')[-1])
ed5ae55e
TB
203 elif dg_regex.search(line.value):
204 # Found dg-warning/dg-error line
e7c7cdc5 205 break
edf0c3ff
TB
206 # PR number in the file name
207 fname = os.path.basename(file.path)
208 m = pr_filename_regex.search(fname)
209 if m:
210 pr = m.group('pr')
211 pr2 = 'PR ' + pr
212 if pr not in this_file_prs and pr2 not in prs:
213 prs.append(pr2)
00243d9a
ML
214
215 if fill_pr_titles:
216 out += get_pr_titles(prs)
217
fef084dc
ML
218 # print list of PR entries before ChangeLog entries
219 if prs:
220 if not out:
221 out += '\n'
222 for pr in prs:
223 out += '\t%s\n' % pr
224 out += '\n'
225
00243d9a
ML
226 # sort ChangeLog so that 'testsuite' is at the end
227 for changelog in sorted(changelog_list, key=lambda x: 'testsuite' in x):
228 files = changelogs[changelog]
229 out += '%s:\n' % os.path.join(changelog, 'ChangeLog')
230 out += '\n'
00243d9a
ML
231 # new and deleted files should be at the end
232 for file in sorted(files, key=sort_changelog_files):
233 assert file.path.startswith(changelog)
234 in_tests = 'testsuite' in changelog or 'testsuite' in file.path
e370a248 235 relative_path = get_rel_path_if_prefixed(file.path, changelog)
00243d9a
ML
236 functions = []
237 if file.is_added_file:
b838641b
ML
238 msg = 'New test.' if in_tests else 'New file.'
239 out = append_changelog_line(out, relative_path, msg)
00243d9a 240 elif file.is_removed_file:
b838641b 241 out = append_changelog_line(out, relative_path, 'Removed.')
eb78da45 242 elif hasattr(file, 'is_rename') and file.is_rename:
eb78da45
ML
243 # A file can be theoretically moved to a location that
244 # belongs to a different ChangeLog. Let user fix it.
e370a248
ML
245 #
246 # Since unidiff 0.7.0, path.file == path.target_file[2:],
247 # it used to be path.source_file[2:]
248 relative_path = get_rel_path_if_prefixed(file.source_file[2:],
249 changelog)
250 out = append_changelog_line(out, relative_path, 'Moved to...')
251 new_path = get_rel_path_if_prefixed(file.target_file[2:],
252 changelog)
253 out += f'\t* {new_path}: ...here.\n'
432b9f61
MF
254 elif os.path.basename(file.path) in generated_files:
255 out += '\t* %s: Regenerate.\n' % (relative_path)
b838641b 256 append_changelog_line(out, relative_path, 'Regenerate.')
00243d9a
ML
257 else:
258 if not no_functions:
259 for hunk in file:
260 # Do not add function names for testsuite files
261 extension = os.path.splitext(relative_path)[1]
262 if not in_tests and extension in function_extensions:
263 last_fn = None
264 modified_visited = False
265 success = False
266 for line in hunk:
267 m = identifier_regex.match(line.value)
268 if line.is_added or line.is_removed:
a2d04f3d
ML
269 # special-case definition in .md files
270 m2 = md_def_regex.match(line.value)
271 if extension == '.md' and m2:
272 fn = m2.group(1)
273 if fn not in functions:
274 functions.append(fn)
275 last_fn = None
276 success = True
277
00243d9a
ML
278 if not line.value.strip():
279 continue
280 modified_visited = True
281 if m and try_add_function(functions,
282 m.group(1)):
283 last_fn = None
284 success = True
285 elif line.is_context:
286 if last_fn and modified_visited:
287 try_add_function(functions, last_fn)
288 last_fn = None
289 modified_visited = False
290 success = True
291 elif m:
292 last_fn = m.group(1)
293 modified_visited = False
294 if not success:
295 try_add_function(functions,
296 hunk.section_header)
297 if functions:
298 out += '\t* %s (%s):\n' % (relative_path, functions[0])
299 for fn in functions[1:]:
300 out += '\t(%s):\n' % fn
301 else:
302 out += '\t* %s:\n' % relative_path
303 out += '\n'
304 return out
305
306
cf76bbf8
ML
307def update_copyright(data):
308 current_timestamp = datetime.datetime.now().strftime('%Y-%m-%d')
309 username = subprocess.check_output('git config user.name', shell=True,
310 encoding='utf8').strip()
311 email = subprocess.check_output('git config user.email', shell=True,
312 encoding='utf8').strip()
313
314 changelogs = set()
315 diff = PatchSet(data)
316
317 for file in diff:
318 changelog = os.path.join(find_changelog(file.path), 'ChangeLog')
319 if changelog not in changelogs:
320 changelogs.add(changelog)
321 with open(changelog) as f:
322 content = f.read()
323 with open(changelog, 'w+') as f:
324 f.write(f'{current_timestamp} {username} <{email}>\n\n')
325 f.write('\tUpdate copyright years.\n\n')
326 f.write(content)
327
328
0684c8d3
ML
329def skip_line_in_changelog(line):
330 if line.lower().startswith(CO_AUTHORED_BY_PREFIX) or line.startswith('#'):
331 return False
332 return True
333
334
00243d9a 335if __name__ == '__main__':
18ef76d3
ML
336 extra_args = os.getenv('GCC_MKLOG_ARGS')
337 if extra_args:
338 sys.argv += json.loads(extra_args)
339
00243d9a
ML
340 parser = argparse.ArgumentParser(description=help_message)
341 parser.add_argument('input', nargs='?',
342 help='Patch file (or missing, read standard input)')
edf0c3ff 343 parser.add_argument('-b', '--pr-numbers', action='store',
48b312b4 344 type=lambda arg: arg.split(','), nargs='?',
edf0c3ff 345 help='Add the specified PRs (comma separated)')
00243d9a
ML
346 parser.add_argument('-s', '--no-functions', action='store_true',
347 help='Do not generate function names in ChangeLogs')
348 parser.add_argument('-p', '--fill-up-bug-titles', action='store_true',
349 help='Download title of mentioned PRs')
e54da1b6 350 parser.add_argument('-d', '--directory',
c38f679e
ML
351 help='Root directory where to search for ChangeLog '
352 'files')
757dbb59
JM
353 parser.add_argument('-c', '--changelog',
354 help='Append the ChangeLog to a git commit message '
355 'file')
cf76bbf8
ML
356 parser.add_argument('--update-copyright', action='store_true',
357 help='Update copyright in ChangeLog files')
00243d9a
ML
358 args = parser.parse_args()
359 if args.input == '-':
360 args.input = None
e54da1b6
ML
361 if args.directory:
362 root = args.directory
00243d9a 363
b0451799 364 data = open(args.input, newline='\n') if args.input else sys.stdin
cf76bbf8
ML
365 if args.update_copyright:
366 update_copyright(data)
757dbb59 367 else:
cf76bbf8 368 output = generate_changelog(data, args.no_functions,
edf0c3ff 369 args.fill_up_bug_titles, args.pr_numbers)
cf76bbf8
ML
370 if args.changelog:
371 lines = open(args.changelog).read().split('\n')
0684c8d3 372 start = list(takewhile(skip_line_in_changelog, lines))
cf76bbf8
ML
373 end = lines[len(start):]
374 with open(args.changelog, 'w') as f:
58e3b17f 375 if not start or not start[0]:
03c0b064
ML
376 if len(prs) == 1:
377 # initial commit subject line 'component: [PRnnnnn]'
378 m = prnum_regex.match(prs[0])
379 if m:
380 title = f'{m.group("comp")}: [PR{m.group("num")}]'
381 start.insert(0, title)
cf76bbf8 382 if start:
58e3b17f 383 # append empty line
cf76bbf8
ML
384 if start[-1] != '':
385 start.append('')
386 else:
387 # append 2 empty lines
388 start = 2 * ['']
389 f.write('\n'.join(start))
390 f.write('\n')
391 f.write(output)
392 f.write('\n'.join(end))
393 else:
394 print(output, end='')