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