Home | History | Annotate | Line # | Download | only in contrib
mklog.py revision 1.1
      1  1.1  christos #!/usr/bin/env python3
      2  1.1  christos 
      3  1.1  christos # Copyright (C) 2020 Free Software Foundation, Inc.
      4  1.1  christos #
      5  1.1  christos # This file is part of GCC.
      6  1.1  christos #
      7  1.1  christos # GCC is free software; you can redistribute it and/or modify
      8  1.1  christos # it under the terms of the GNU General Public License as published by
      9  1.1  christos # the Free Software Foundation; either version 3, or (at your option)
     10  1.1  christos # any later version.
     11  1.1  christos #
     12  1.1  christos # GCC is distributed in the hope that it will be useful,
     13  1.1  christos # but WITHOUT ANY WARRANTY; without even the implied warranty of
     14  1.1  christos # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     15  1.1  christos # GNU General Public License for more details.
     16  1.1  christos #
     17  1.1  christos # You should have received a copy of the GNU General Public License
     18  1.1  christos # along with GCC; see the file COPYING.  If not, write to
     19  1.1  christos # the Free Software Foundation, 51 Franklin Street, Fifth Floor,
     20  1.1  christos # Boston, MA 02110-1301, USA.
     21  1.1  christos 
     22  1.1  christos # This script parses a .diff file generated with 'diff -up' or 'diff -cp'
     23  1.1  christos # and adds a skeleton ChangeLog file to the file. It does not try to be
     24  1.1  christos # too smart when parsing function names, but it produces a reasonable
     25  1.1  christos # approximation.
     26  1.1  christos #
     27  1.1  christos # Author: Martin Liska <mliska (at] suse.cz>
     28  1.1  christos 
     29  1.1  christos import argparse
     30  1.1  christos import os
     31  1.1  christos import re
     32  1.1  christos import sys
     33  1.1  christos from itertools import takewhile
     34  1.1  christos 
     35  1.1  christos import requests
     36  1.1  christos 
     37  1.1  christos from unidiff import PatchSet
     38  1.1  christos 
     39  1.1  christos pr_regex = re.compile(r'(\/(\/|\*)|[Cc*!])\s+(?P<pr>PR [a-z+-]+\/[0-9]+)')
     40  1.1  christos dr_regex = re.compile(r'(\/(\/|\*)|[Cc*!])\s+(?P<dr>DR [0-9]+)')
     41  1.1  christos identifier_regex = re.compile(r'^([a-zA-Z0-9_#].*)')
     42  1.1  christos comment_regex = re.compile(r'^\/\*')
     43  1.1  christos struct_regex = re.compile(r'^(class|struct|union|enum)\s+'
     44  1.1  christos                           r'(GTY\(.*\)\s+)?([a-zA-Z0-9_]+)')
     45  1.1  christos macro_regex = re.compile(r'#\s*(define|undef)\s+([a-zA-Z0-9_]+)')
     46  1.1  christos super_macro_regex = re.compile(r'^DEF[A-Z0-9_]+\s*\(([a-zA-Z0-9_]+)')
     47  1.1  christos fn_regex = re.compile(r'([a-zA-Z_][^()\s]*)\s*\([^*]')
     48  1.1  christos template_and_param_regex = re.compile(r'<[^<>]*>')
     49  1.1  christos bugzilla_url = 'https://gcc.gnu.org/bugzilla/rest.cgi/bug?id=%s&' \
     50  1.1  christos                'include_fields=summary'
     51  1.1  christos 
     52  1.1  christos function_extensions = set(['.c', '.cpp', '.C', '.cc', '.h', '.inc', '.def'])
     53  1.1  christos 
     54  1.1  christos help_message = """\
     55  1.1  christos Generate ChangeLog template for PATCH.
     56  1.1  christos PATCH must be generated using diff(1)'s -up or -cp options
     57  1.1  christos (or their equivalent in git).
     58  1.1  christos """
     59  1.1  christos 
     60  1.1  christos script_folder = os.path.realpath(__file__)
     61  1.1  christos gcc_root = os.path.dirname(os.path.dirname(script_folder))
     62  1.1  christos 
     63  1.1  christos 
     64  1.1  christos def find_changelog(path):
     65  1.1  christos     folder = os.path.split(path)[0]
     66  1.1  christos     while True:
     67  1.1  christos         if os.path.exists(os.path.join(gcc_root, folder, 'ChangeLog')):
     68  1.1  christos             return folder
     69  1.1  christos         folder = os.path.dirname(folder)
     70  1.1  christos         if folder == '':
     71  1.1  christos             return folder
     72  1.1  christos     raise AssertionError()
     73  1.1  christos 
     74  1.1  christos 
     75  1.1  christos def extract_function_name(line):
     76  1.1  christos     if comment_regex.match(line):
     77  1.1  christos         return None
     78  1.1  christos     m = struct_regex.search(line)
     79  1.1  christos     if m:
     80  1.1  christos         # Struct declaration
     81  1.1  christos         return m.group(1) + ' ' + m.group(3)
     82  1.1  christos     m = macro_regex.search(line)
     83  1.1  christos     if m:
     84  1.1  christos         # Macro definition
     85  1.1  christos         return m.group(2)
     86  1.1  christos     m = super_macro_regex.search(line)
     87  1.1  christos     if m:
     88  1.1  christos         # Supermacro
     89  1.1  christos         return m.group(1)
     90  1.1  christos     m = fn_regex.search(line)
     91  1.1  christos     if m:
     92  1.1  christos         # Discard template and function parameters.
     93  1.1  christos         fn = m.group(1)
     94  1.1  christos         fn = re.sub(template_and_param_regex, '', fn)
     95  1.1  christos         return fn.rstrip()
     96  1.1  christos     return None
     97  1.1  christos 
     98  1.1  christos 
     99  1.1  christos def try_add_function(functions, line):
    100  1.1  christos     fn = extract_function_name(line)
    101  1.1  christos     if fn and fn not in functions:
    102  1.1  christos         functions.append(fn)
    103  1.1  christos     return bool(fn)
    104  1.1  christos 
    105  1.1  christos 
    106  1.1  christos def sort_changelog_files(changed_file):
    107  1.1  christos     return (changed_file.is_added_file, changed_file.is_removed_file)
    108  1.1  christos 
    109  1.1  christos 
    110  1.1  christos def get_pr_titles(prs):
    111  1.1  christos     output = ''
    112  1.1  christos     for pr in prs:
    113  1.1  christos         id = pr.split('/')[-1]
    114  1.1  christos         r = requests.get(bugzilla_url % id)
    115  1.1  christos         bugs = r.json()['bugs']
    116  1.1  christos         if len(bugs) == 1:
    117  1.1  christos             output += '%s - %s\n' % (pr, bugs[0]['summary'])
    118  1.1  christos             print(output)
    119  1.1  christos     if output:
    120  1.1  christos         output += '\n'
    121  1.1  christos     return output
    122  1.1  christos 
    123  1.1  christos 
    124  1.1  christos def generate_changelog(data, no_functions=False, fill_pr_titles=False):
    125  1.1  christos     changelogs = {}
    126  1.1  christos     changelog_list = []
    127  1.1  christos     prs = []
    128  1.1  christos     out = ''
    129  1.1  christos     diff = PatchSet(data)
    130  1.1  christos 
    131  1.1  christos     for file in diff:
    132  1.1  christos         changelog = find_changelog(file.path)
    133  1.1  christos         if changelog not in changelogs:
    134  1.1  christos             changelogs[changelog] = []
    135  1.1  christos             changelog_list.append(changelog)
    136  1.1  christos         changelogs[changelog].append(file)
    137  1.1  christos 
    138  1.1  christos         # Extract PR entries from newly added tests
    139  1.1  christos         if 'testsuite' in file.path and file.is_added_file:
    140  1.1  christos             for line in list(file)[0]:
    141  1.1  christos                 m = pr_regex.search(line.value)
    142  1.1  christos                 if m:
    143  1.1  christos                     pr = m.group('pr')
    144  1.1  christos                     if pr not in prs:
    145  1.1  christos                         prs.append(pr)
    146  1.1  christos                 else:
    147  1.1  christos                     m = dr_regex.search(line.value)
    148  1.1  christos                     if m:
    149  1.1  christos                         dr = m.group('dr')
    150  1.1  christos                         if dr not in prs:
    151  1.1  christos                             prs.append(dr)
    152  1.1  christos                     else:
    153  1.1  christos                         break
    154  1.1  christos 
    155  1.1  christos     if fill_pr_titles:
    156  1.1  christos         out += get_pr_titles(prs)
    157  1.1  christos 
    158  1.1  christos     # sort ChangeLog so that 'testsuite' is at the end
    159  1.1  christos     for changelog in sorted(changelog_list, key=lambda x: 'testsuite' in x):
    160  1.1  christos         files = changelogs[changelog]
    161  1.1  christos         out += '%s:\n' % os.path.join(changelog, 'ChangeLog')
    162  1.1  christos         out += '\n'
    163  1.1  christos         for pr in prs:
    164  1.1  christos             out += '\t%s\n' % pr
    165  1.1  christos         # new and deleted files should be at the end
    166  1.1  christos         for file in sorted(files, key=sort_changelog_files):
    167  1.1  christos             assert file.path.startswith(changelog)
    168  1.1  christos             in_tests = 'testsuite' in changelog or 'testsuite' in file.path
    169  1.1  christos             relative_path = file.path[len(changelog):].lstrip('/')
    170  1.1  christos             functions = []
    171  1.1  christos             if file.is_added_file:
    172  1.1  christos                 msg = 'New test' if in_tests else 'New file'
    173  1.1  christos                 out += '\t* %s: %s.\n' % (relative_path, msg)
    174  1.1  christos             elif file.is_removed_file:
    175  1.1  christos                 out += '\t* %s: Removed.\n' % (relative_path)
    176  1.1  christos             elif hasattr(file, 'is_rename') and file.is_rename:
    177  1.1  christos                 out += '\t* %s: Moved to...\n' % (relative_path)
    178  1.1  christos                 new_path = file.target_file[2:]
    179  1.1  christos                 # A file can be theoretically moved to a location that
    180  1.1  christos                 # belongs to a different ChangeLog.  Let user fix it.
    181  1.1  christos                 if new_path.startswith(changelog):
    182  1.1  christos                     new_path = new_path[len(changelog):].lstrip('/')
    183  1.1  christos                 out += '\t* %s: ...here.\n' % (new_path)
    184  1.1  christos             else:
    185  1.1  christos                 if not no_functions:
    186  1.1  christos                     for hunk in file:
    187  1.1  christos                         # Do not add function names for testsuite files
    188  1.1  christos                         extension = os.path.splitext(relative_path)[1]
    189  1.1  christos                         if not in_tests and extension in function_extensions:
    190  1.1  christos                             last_fn = None
    191  1.1  christos                             modified_visited = False
    192  1.1  christos                             success = False
    193  1.1  christos                             for line in hunk:
    194  1.1  christos                                 m = identifier_regex.match(line.value)
    195  1.1  christos                                 if line.is_added or line.is_removed:
    196  1.1  christos                                     if not line.value.strip():
    197  1.1  christos                                         continue
    198  1.1  christos                                     modified_visited = True
    199  1.1  christos                                     if m and try_add_function(functions,
    200  1.1  christos                                                               m.group(1)):
    201  1.1  christos                                         last_fn = None
    202  1.1  christos                                         success = True
    203  1.1  christos                                 elif line.is_context:
    204  1.1  christos                                     if last_fn and modified_visited:
    205  1.1  christos                                         try_add_function(functions, last_fn)
    206  1.1  christos                                         last_fn = None
    207  1.1  christos                                         modified_visited = False
    208  1.1  christos                                         success = True
    209  1.1  christos                                     elif m:
    210  1.1  christos                                         last_fn = m.group(1)
    211  1.1  christos                                         modified_visited = False
    212  1.1  christos                             if not success:
    213  1.1  christos                                 try_add_function(functions,
    214  1.1  christos                                                  hunk.section_header)
    215  1.1  christos                 if functions:
    216  1.1  christos                     out += '\t* %s (%s):\n' % (relative_path, functions[0])
    217  1.1  christos                     for fn in functions[1:]:
    218  1.1  christos                         out += '\t(%s):\n' % fn
    219  1.1  christos                 else:
    220  1.1  christos                     out += '\t* %s:\n' % relative_path
    221  1.1  christos         out += '\n'
    222  1.1  christos     return out
    223  1.1  christos 
    224  1.1  christos 
    225  1.1  christos if __name__ == '__main__':
    226  1.1  christos     parser = argparse.ArgumentParser(description=help_message)
    227  1.1  christos     parser.add_argument('input', nargs='?',
    228  1.1  christos                         help='Patch file (or missing, read standard input)')
    229  1.1  christos     parser.add_argument('-s', '--no-functions', action='store_true',
    230  1.1  christos                         help='Do not generate function names in ChangeLogs')
    231  1.1  christos     parser.add_argument('-p', '--fill-up-bug-titles', action='store_true',
    232  1.1  christos                         help='Download title of mentioned PRs')
    233  1.1  christos     parser.add_argument('-c', '--changelog',
    234  1.1  christos                         help='Append the ChangeLog to a git commit message '
    235  1.1  christos                              'file')
    236  1.1  christos     args = parser.parse_args()
    237  1.1  christos     if args.input == '-':
    238  1.1  christos         args.input = None
    239  1.1  christos 
    240  1.1  christos     input = open(args.input) if args.input else sys.stdin
    241  1.1  christos     data = input.read()
    242  1.1  christos     output = generate_changelog(data, args.no_functions,
    243  1.1  christos                                 args.fill_up_bug_titles)
    244  1.1  christos     if args.changelog:
    245  1.1  christos         lines = open(args.changelog).read().split('\n')
    246  1.1  christos         start = list(takewhile(lambda l: not l.startswith('#'), lines))
    247  1.1  christos         end = lines[len(start):]
    248  1.1  christos         with open(args.changelog, 'w') as f:
    249  1.1  christos             if start:
    250  1.1  christos                 # appent empty line
    251  1.1  christos                 if start[-1] != '':
    252  1.1  christos                     start.append('')
    253  1.1  christos             else:
    254  1.1  christos                 # append 2 empty lines
    255  1.1  christos                 start = 2 * ['']
    256  1.1  christos             f.write('\n'.join(start))
    257  1.1  christos             f.write('\n')
    258  1.1  christos             f.write(output)
    259  1.1  christos             f.write('\n'.join(end))
    260  1.1  christos     else:
    261  1.1  christos         print(output, end='')
    262