blob: 243edbb15c522169709902b27a8558c6e0755107 [file] [log] [blame]
#!/usr/bin/env python3
# Copyright (C) 2020 Free Software Foundation, Inc.
#
# This file is part of GCC.
#
# GCC is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3, or (at your option)
# any later version.
#
# GCC is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with GCC; see the file COPYING. If not, write to
# the Free Software Foundation, 51 Franklin Street, Fifth Floor,
# Boston, MA 02110-1301, USA.
# This script parses a .diff file generated with 'diff -up' or 'diff -cp'
# and adds a skeleton ChangeLog file to the file. It does not try to be
# too smart when parsing function names, but it produces a reasonable
# approximation.
#
# Author: Martin Liska <mliska@suse.cz>
import argparse
import os
import re
import sys
from itertools import takewhile
import requests
from unidiff import PatchSet
pr_regex = re.compile(r'(\/(\/|\*)|[Cc*!])\s+(?P<pr>PR [a-z+-]+\/[0-9]+)')
dr_regex = re.compile(r'(\/(\/|\*)|[Cc*!])\s+(?P<dr>DR [0-9]+)')
identifier_regex = re.compile(r'^([a-zA-Z0-9_#].*)')
comment_regex = re.compile(r'^\/\*')
struct_regex = re.compile(r'^(class|struct|union|enum)\s+'
r'(GTY\(.*\)\s+)?([a-zA-Z0-9_]+)')
macro_regex = re.compile(r'#\s*(define|undef)\s+([a-zA-Z0-9_]+)')
super_macro_regex = re.compile(r'^DEF[A-Z0-9_]+\s*\(([a-zA-Z0-9_]+)')
fn_regex = re.compile(r'([a-zA-Z_][^()\s]*)\s*\([^*]')
template_and_param_regex = re.compile(r'<[^<>]*>')
bugzilla_url = 'https://gcc.gnu.org/bugzilla/rest.cgi/bug?id=%s&' \
'include_fields=summary'
function_extensions = set(['.c', '.cpp', '.C', '.cc', '.h', '.inc', '.def'])
help_message = """\
Generate ChangeLog template for PATCH.
PATCH must be generated using diff(1)'s -up or -cp options
(or their equivalent in git).
"""
script_folder = os.path.realpath(__file__)
gcc_root = os.path.dirname(os.path.dirname(script_folder))
def find_changelog(path):
folder = os.path.split(path)[0]
while True:
if os.path.exists(os.path.join(gcc_root, folder, 'ChangeLog')):
return folder
folder = os.path.dirname(folder)
if folder == '':
return folder
raise AssertionError()
def extract_function_name(line):
if comment_regex.match(line):
return None
m = struct_regex.search(line)
if m:
# Struct declaration
return m.group(1) + ' ' + m.group(3)
m = macro_regex.search(line)
if m:
# Macro definition
return m.group(2)
m = super_macro_regex.search(line)
if m:
# Supermacro
return m.group(1)
m = fn_regex.search(line)
if m:
# Discard template and function parameters.
fn = m.group(1)
fn = re.sub(template_and_param_regex, '', fn)
return fn.rstrip()
return None
def try_add_function(functions, line):
fn = extract_function_name(line)
if fn and fn not in functions:
functions.append(fn)
return bool(fn)
def sort_changelog_files(changed_file):
return (changed_file.is_added_file, changed_file.is_removed_file)
def get_pr_titles(prs):
output = ''
for pr in prs:
id = pr.split('/')[-1]
r = requests.get(bugzilla_url % id)
bugs = r.json()['bugs']
if len(bugs) == 1:
output += '%s - %s\n' % (pr, bugs[0]['summary'])
print(output)
if output:
output += '\n'
return output
def generate_changelog(data, no_functions=False, fill_pr_titles=False):
changelogs = {}
changelog_list = []
prs = []
out = ''
diff = PatchSet(data)
for file in diff:
changelog = find_changelog(file.path)
if changelog not in changelogs:
changelogs[changelog] = []
changelog_list.append(changelog)
changelogs[changelog].append(file)
# Extract PR entries from newly added tests
if 'testsuite' in file.path and file.is_added_file:
for line in list(file)[0]:
m = pr_regex.search(line.value)
if m:
pr = m.group('pr')
if pr not in prs:
prs.append(pr)
else:
m = dr_regex.search(line.value)
if m:
dr = m.group('dr')
if dr not in prs:
prs.append(dr)
else:
break
if fill_pr_titles:
out += get_pr_titles(prs)
# sort ChangeLog so that 'testsuite' is at the end
for changelog in sorted(changelog_list, key=lambda x: 'testsuite' in x):
files = changelogs[changelog]
out += '%s:\n' % os.path.join(changelog, 'ChangeLog')
out += '\n'
for pr in prs:
out += '\t%s\n' % pr
# new and deleted files should be at the end
for file in sorted(files, key=sort_changelog_files):
assert file.path.startswith(changelog)
in_tests = 'testsuite' in changelog or 'testsuite' in file.path
relative_path = file.path[len(changelog):].lstrip('/')
functions = []
if file.is_added_file:
msg = 'New test' if in_tests else 'New file'
out += '\t* %s: %s.\n' % (relative_path, msg)
elif file.is_removed_file:
out += '\t* %s: Removed.\n' % (relative_path)
elif hasattr(file, 'is_rename') and file.is_rename:
out += '\t* %s: Moved to...\n' % (relative_path)
new_path = file.target_file[2:]
# A file can be theoretically moved to a location that
# belongs to a different ChangeLog. Let user fix it.
if new_path.startswith(changelog):
new_path = new_path[len(changelog):].lstrip('/')
out += '\t* %s: ...here.\n' % (new_path)
else:
if not no_functions:
for hunk in file:
# Do not add function names for testsuite files
extension = os.path.splitext(relative_path)[1]
if not in_tests and extension in function_extensions:
last_fn = None
modified_visited = False
success = False
for line in hunk:
m = identifier_regex.match(line.value)
if line.is_added or line.is_removed:
if not line.value.strip():
continue
modified_visited = True
if m and try_add_function(functions,
m.group(1)):
last_fn = None
success = True
elif line.is_context:
if last_fn and modified_visited:
try_add_function(functions, last_fn)
last_fn = None
modified_visited = False
success = True
elif m:
last_fn = m.group(1)
modified_visited = False
if not success:
try_add_function(functions,
hunk.section_header)
if functions:
out += '\t* %s (%s):\n' % (relative_path, functions[0])
for fn in functions[1:]:
out += '\t(%s):\n' % fn
else:
out += '\t* %s:\n' % relative_path
out += '\n'
return out
if __name__ == '__main__':
parser = argparse.ArgumentParser(description=help_message)
parser.add_argument('input', nargs='?',
help='Patch file (or missing, read standard input)')
parser.add_argument('-s', '--no-functions', action='store_true',
help='Do not generate function names in ChangeLogs')
parser.add_argument('-p', '--fill-up-bug-titles', action='store_true',
help='Download title of mentioned PRs')
parser.add_argument('-c', '--changelog',
help='Append the ChangeLog to a git commit message '
'file')
args = parser.parse_args()
if args.input == '-':
args.input = None
input = open(args.input) if args.input else sys.stdin
data = input.read()
output = generate_changelog(data, args.no_functions,
args.fill_up_bug_titles)
if args.changelog:
lines = open(args.changelog).read().split('\n')
start = list(takewhile(lambda l: not l.startswith('#'), lines))
end = lines[len(start):]
with open(args.changelog, 'w') as f:
if start:
# appent empty line
if start[-1] != '':
start.append('')
else:
# append 2 empty lines
start = 2 * ['']
f.write('\n'.join(start))
f.write('\n')
f.write(output)
f.write('\n'.join(end))
else:
print(output, end='')