--- /dev/null
+#!/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='')