Home | History | Annotate | Line # | Download | only in contrib
mklog.py revision 1.1.1.1
      1  1.1  mrg #!/usr/bin/env python3
      2  1.1  mrg 
      3  1.1  mrg # Copyright (C) 2020 Free Software Foundation, Inc.
      4  1.1  mrg #
      5  1.1  mrg # This file is part of GCC.
      6  1.1  mrg #
      7  1.1  mrg # GCC is free software; you can redistribute it and/or modify
      8  1.1  mrg # it under the terms of the GNU General Public License as published by
      9  1.1  mrg # the Free Software Foundation; either version 3, or (at your option)
     10  1.1  mrg # any later version.
     11  1.1  mrg #
     12  1.1  mrg # GCC is distributed in the hope that it will be useful,
     13  1.1  mrg # but WITHOUT ANY WARRANTY; without even the implied warranty of
     14  1.1  mrg # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     15  1.1  mrg # GNU General Public License for more details.
     16  1.1  mrg #
     17  1.1  mrg # You should have received a copy of the GNU General Public License
     18  1.1  mrg # along with GCC; see the file COPYING.  If not, write to
     19  1.1  mrg # the Free Software Foundation, 51 Franklin Street, Fifth Floor,
     20  1.1  mrg # Boston, MA 02110-1301, USA.
     21  1.1  mrg 
     22  1.1  mrg # This script parses a .diff file generated with 'diff -up' or 'diff -cp'
     23  1.1  mrg # and adds a skeleton ChangeLog file to the file. It does not try to be
     24  1.1  mrg # too smart when parsing function names, but it produces a reasonable
     25  1.1  mrg # approximation.
     26  1.1  mrg #
     27  1.1  mrg # Author: Martin Liska <mliska (at] suse.cz>
     28  1.1  mrg 
     29  1.1  mrg import argparse
     30  1.1  mrg import datetime
     31  1.1  mrg import os
     32  1.1  mrg import re
     33  1.1  mrg import subprocess
     34  1.1  mrg import sys
     35  1.1  mrg from itertools import takewhile
     36  1.1  mrg 
     37  1.1  mrg import requests
     38  1.1  mrg 
     39  1.1  mrg from unidiff import PatchSet
     40  1.1  mrg 
     41  1.1  mrg pr_regex = re.compile(r'(\/(\/|\*)|[Cc*!])\s+(?P<pr>PR [a-z+-]+\/[0-9]+)')
     42  1.1  mrg prnum_regex = re.compile(r'PR (?P<comp>[a-z+-]+)/(?P<num>[0-9]+)')
     43  1.1  mrg dr_regex = re.compile(r'(\/(\/|\*)|[Cc*!])\s+(?P<dr>DR [0-9]+)')
     44  1.1  mrg dg_regex = re.compile(r'{\s+dg-(error|warning)')
     45  1.1  mrg pr_filename_regex = re.compile(r'(^|[\W_])[Pp][Rr](?P<pr>\d{4,})')
     46  1.1  mrg identifier_regex = re.compile(r'^([a-zA-Z0-9_#].*)')
     47  1.1  mrg comment_regex = re.compile(r'^\/\*')
     48  1.1  mrg struct_regex = re.compile(r'^(class|struct|union|enum)\s+'
     49  1.1  mrg                           r'(GTY\(.*\)\s+)?([a-zA-Z0-9_]+)')
     50  1.1  mrg macro_regex = re.compile(r'#\s*(define|undef)\s+([a-zA-Z0-9_]+)')
     51  1.1  mrg super_macro_regex = re.compile(r'^DEF[A-Z0-9_]+\s*\(([a-zA-Z0-9_]+)')
     52  1.1  mrg fn_regex = re.compile(r'([a-zA-Z_][^()\s]*)\s*\([^*]')
     53  1.1  mrg template_and_param_regex = re.compile(r'<[^<>]*>')
     54  1.1  mrg md_def_regex = re.compile(r'\(define.*\s+"(.*)"')
     55  1.1  mrg bugzilla_url = 'https://gcc.gnu.org/bugzilla/rest.cgi/bug?id=%s&' \
     56  1.1  mrg                'include_fields=summary,component'
     57  1.1  mrg 
     58  1.1  mrg function_extensions = {'.c', '.cpp', '.C', '.cc', '.h', '.inc', '.def', '.md'}
     59  1.1  mrg 
     60  1.1  mrg # NB: Makefile.in isn't listed as it's not always generated.
     61  1.1  mrg generated_files = {'aclocal.m4', 'config.h.in', 'configure'}
     62  1.1  mrg 
     63  1.1  mrg help_message = """\
     64  1.1  mrg Generate ChangeLog template for PATCH.
     65  1.1  mrg PATCH must be generated using diff(1)'s -up or -cp options
     66  1.1  mrg (or their equivalent in git).
     67  1.1  mrg """
     68  1.1  mrg 
     69  1.1  mrg script_folder = os.path.realpath(__file__)
     70  1.1  mrg root = os.path.dirname(os.path.dirname(script_folder))
     71  1.1  mrg 
     72  1.1  mrg firstpr = ''
     73  1.1  mrg 
     74  1.1  mrg 
     75  1.1  mrg def find_changelog(path):
     76  1.1  mrg     folder = os.path.split(path)[0]
     77  1.1  mrg     while True:
     78  1.1  mrg         if os.path.exists(os.path.join(root, folder, 'ChangeLog')):
     79  1.1  mrg             return folder
     80  1.1  mrg         folder = os.path.dirname(folder)
     81  1.1  mrg         if folder == '':
     82  1.1  mrg             return folder
     83  1.1  mrg     raise AssertionError()
     84  1.1  mrg 
     85  1.1  mrg 
     86  1.1  mrg def extract_function_name(line):
     87  1.1  mrg     if comment_regex.match(line):
     88  1.1  mrg         return None
     89  1.1  mrg     m = struct_regex.search(line)
     90  1.1  mrg     if m:
     91  1.1  mrg         # Struct declaration
     92  1.1  mrg         return m.group(1) + ' ' + m.group(3)
     93  1.1  mrg     m = macro_regex.search(line)
     94  1.1  mrg     if m:
     95  1.1  mrg         # Macro definition
     96  1.1  mrg         return m.group(2)
     97  1.1  mrg     m = super_macro_regex.search(line)
     98  1.1  mrg     if m:
     99  1.1  mrg         # Supermacro
    100  1.1  mrg         return m.group(1)
    101  1.1  mrg     m = fn_regex.search(line)
    102  1.1  mrg     if m:
    103  1.1  mrg         # Discard template and function parameters.
    104  1.1  mrg         fn = m.group(1)
    105  1.1  mrg         fn = re.sub(template_and_param_regex, '', fn)
    106  1.1  mrg         return fn.rstrip()
    107  1.1  mrg     return None
    108  1.1  mrg 
    109  1.1  mrg 
    110  1.1  mrg def try_add_function(functions, line):
    111  1.1  mrg     fn = extract_function_name(line)
    112  1.1  mrg     if fn and fn not in functions:
    113  1.1  mrg         functions.append(fn)
    114  1.1  mrg     return bool(fn)
    115  1.1  mrg 
    116  1.1  mrg 
    117  1.1  mrg def sort_changelog_files(changed_file):
    118  1.1  mrg     return (changed_file.is_added_file, changed_file.is_removed_file)
    119  1.1  mrg 
    120  1.1  mrg 
    121  1.1  mrg def get_pr_titles(prs):
    122  1.1  mrg     output = []
    123  1.1  mrg     for idx, pr in enumerate(prs):
    124  1.1  mrg         pr_id = pr.split('/')[-1]
    125  1.1  mrg         r = requests.get(bugzilla_url % pr_id)
    126  1.1  mrg         bugs = r.json()['bugs']
    127  1.1  mrg         if len(bugs) == 1:
    128  1.1  mrg             prs[idx] = 'PR %s/%s' % (bugs[0]['component'], pr_id)
    129  1.1  mrg             out = '%s - %s\n' % (prs[idx], bugs[0]['summary'])
    130  1.1  mrg             if out not in output:
    131  1.1  mrg                 output.append(out)
    132  1.1  mrg     if output:
    133  1.1  mrg         output.append('')
    134  1.1  mrg     return '\n'.join(output)
    135  1.1  mrg 
    136  1.1  mrg 
    137  1.1  mrg def generate_changelog(data, no_functions=False, fill_pr_titles=False,
    138  1.1  mrg                        additional_prs=None):
    139  1.1  mrg     changelogs = {}
    140  1.1  mrg     changelog_list = []
    141  1.1  mrg     prs = []
    142  1.1  mrg     out = ''
    143  1.1  mrg     diff = PatchSet(data)
    144  1.1  mrg     global firstpr
    145  1.1  mrg 
    146  1.1  mrg     if additional_prs:
    147  1.1  mrg         prs = [pr for pr in additional_prs if pr not in prs]
    148  1.1  mrg     for file in diff:
    149  1.1  mrg         # skip files that can't be parsed
    150  1.1  mrg         if file.path == '/dev/null':
    151  1.1  mrg             continue
    152  1.1  mrg         changelog = find_changelog(file.path)
    153  1.1  mrg         if changelog not in changelogs:
    154  1.1  mrg             changelogs[changelog] = []
    155  1.1  mrg             changelog_list.append(changelog)
    156  1.1  mrg         changelogs[changelog].append(file)
    157  1.1  mrg 
    158  1.1  mrg         # Extract PR entries from newly added tests
    159  1.1  mrg         if 'testsuite' in file.path and file.is_added_file:
    160  1.1  mrg             # Only search first ten lines as later lines may
    161  1.1  mrg             # contains commented code which a note that it
    162  1.1  mrg             # has not been tested due to a certain PR or DR.
    163  1.1  mrg             this_file_prs = []
    164  1.1  mrg             for line in list(file)[0][0:10]:
    165  1.1  mrg                 m = pr_regex.search(line.value)
    166  1.1  mrg                 if m:
    167  1.1  mrg                     pr = m.group('pr')
    168  1.1  mrg                     if pr not in prs:
    169  1.1  mrg                         prs.append(pr)
    170  1.1  mrg                         this_file_prs.append(pr.split('/')[-1])
    171  1.1  mrg                 else:
    172  1.1  mrg                     m = dr_regex.search(line.value)
    173  1.1  mrg                     if m:
    174  1.1  mrg                         dr = m.group('dr')
    175  1.1  mrg                         if dr not in prs:
    176  1.1  mrg                             prs.append(dr)
    177  1.1  mrg                             this_file_prs.append(dr.split('/')[-1])
    178  1.1  mrg                     elif dg_regex.search(line.value):
    179  1.1  mrg                         # Found dg-warning/dg-error line
    180  1.1  mrg                         break
    181  1.1  mrg             # PR number in the file name
    182  1.1  mrg             fname = os.path.basename(file.path)
    183  1.1  mrg             m = pr_filename_regex.search(fname)
    184  1.1  mrg             if m:
    185  1.1  mrg                 pr = m.group('pr')
    186  1.1  mrg                 pr2 = 'PR ' + pr
    187  1.1  mrg                 if pr not in this_file_prs and pr2 not in prs:
    188  1.1  mrg                     prs.append(pr2)
    189  1.1  mrg 
    190  1.1  mrg     if prs:
    191  1.1  mrg         firstpr = prs[0]
    192  1.1  mrg 
    193  1.1  mrg     if fill_pr_titles:
    194  1.1  mrg         out += get_pr_titles(prs)
    195  1.1  mrg 
    196  1.1  mrg     # print list of PR entries before ChangeLog entries
    197  1.1  mrg     if prs:
    198  1.1  mrg         if not out:
    199  1.1  mrg             out += '\n'
    200  1.1  mrg         for pr in prs:
    201  1.1  mrg             out += '\t%s\n' % pr
    202  1.1  mrg         out += '\n'
    203  1.1  mrg 
    204  1.1  mrg     # sort ChangeLog so that 'testsuite' is at the end
    205  1.1  mrg     for changelog in sorted(changelog_list, key=lambda x: 'testsuite' in x):
    206  1.1  mrg         files = changelogs[changelog]
    207  1.1  mrg         out += '%s:\n' % os.path.join(changelog, 'ChangeLog')
    208  1.1  mrg         out += '\n'
    209  1.1  mrg         # new and deleted files should be at the end
    210  1.1  mrg         for file in sorted(files, key=sort_changelog_files):
    211  1.1  mrg             assert file.path.startswith(changelog)
    212  1.1  mrg             in_tests = 'testsuite' in changelog or 'testsuite' in file.path
    213  1.1  mrg             relative_path = file.path[len(changelog):].lstrip('/')
    214  1.1  mrg             functions = []
    215  1.1  mrg             if file.is_added_file:
    216  1.1  mrg                 msg = 'New test' if in_tests else 'New file'
    217  1.1  mrg                 out += '\t* %s: %s.\n' % (relative_path, msg)
    218  1.1  mrg             elif file.is_removed_file:
    219  1.1  mrg                 out += '\t* %s: Removed.\n' % (relative_path)
    220  1.1  mrg             elif hasattr(file, 'is_rename') and file.is_rename:
    221  1.1  mrg                 out += '\t* %s: Moved to...\n' % (relative_path)
    222  1.1  mrg                 new_path = file.target_file[2:]
    223  1.1  mrg                 # A file can be theoretically moved to a location that
    224  1.1  mrg                 # belongs to a different ChangeLog.  Let user fix it.
    225  1.1  mrg                 if new_path.startswith(changelog):
    226  1.1  mrg                     new_path = new_path[len(changelog):].lstrip('/')
    227  1.1  mrg                 out += '\t* %s: ...here.\n' % (new_path)
    228  1.1  mrg             elif os.path.basename(file.path) in generated_files:
    229  1.1  mrg                 out += '\t* %s: Regenerate.\n' % (relative_path)
    230  1.1  mrg             else:
    231  1.1  mrg                 if not no_functions:
    232  1.1  mrg                     for hunk in file:
    233  1.1  mrg                         # Do not add function names for testsuite files
    234  1.1  mrg                         extension = os.path.splitext(relative_path)[1]
    235  1.1  mrg                         if not in_tests and extension in function_extensions:
    236  1.1  mrg                             last_fn = None
    237  1.1  mrg                             modified_visited = False
    238  1.1  mrg                             success = False
    239  1.1  mrg                             for line in hunk:
    240  1.1  mrg                                 m = identifier_regex.match(line.value)
    241  1.1  mrg                                 if line.is_added or line.is_removed:
    242  1.1  mrg                                     # special-case definition in .md files
    243  1.1  mrg                                     m2 = md_def_regex.match(line.value)
    244  1.1  mrg                                     if extension == '.md' and m2:
    245  1.1  mrg                                         fn = m2.group(1)
    246  1.1  mrg                                         if fn not in functions:
    247  1.1  mrg                                             functions.append(fn)
    248  1.1  mrg                                             last_fn = None
    249  1.1  mrg                                             success = True
    250  1.1  mrg 
    251  1.1  mrg                                     if not line.value.strip():
    252  1.1  mrg                                         continue
    253  1.1  mrg                                     modified_visited = True
    254  1.1  mrg                                     if m and try_add_function(functions,
    255  1.1  mrg                                                               m.group(1)):
    256  1.1  mrg                                         last_fn = None
    257  1.1  mrg                                         success = True
    258  1.1  mrg                                 elif line.is_context:
    259  1.1  mrg                                     if last_fn and modified_visited:
    260  1.1  mrg                                         try_add_function(functions, last_fn)
    261  1.1  mrg                                         last_fn = None
    262  1.1  mrg                                         modified_visited = False
    263  1.1  mrg                                         success = True
    264  1.1  mrg                                     elif m:
    265  1.1  mrg                                         last_fn = m.group(1)
    266  1.1  mrg                                         modified_visited = False
    267  1.1  mrg                             if not success:
    268  1.1  mrg                                 try_add_function(functions,
    269  1.1  mrg                                                  hunk.section_header)
    270  1.1  mrg                 if functions:
    271  1.1  mrg                     out += '\t* %s (%s):\n' % (relative_path, functions[0])
    272  1.1  mrg                     for fn in functions[1:]:
    273  1.1  mrg                         out += '\t(%s):\n' % fn
    274  1.1  mrg                 else:
    275  1.1  mrg                     out += '\t* %s:\n' % relative_path
    276  1.1  mrg         out += '\n'
    277  1.1  mrg     return out
    278  1.1  mrg 
    279  1.1  mrg 
    280  1.1  mrg def update_copyright(data):
    281  1.1  mrg     current_timestamp = datetime.datetime.now().strftime('%Y-%m-%d')
    282  1.1  mrg     username = subprocess.check_output('git config user.name', shell=True,
    283  1.1  mrg                                        encoding='utf8').strip()
    284  1.1  mrg     email = subprocess.check_output('git config user.email', shell=True,
    285  1.1  mrg                                     encoding='utf8').strip()
    286  1.1  mrg 
    287  1.1  mrg     changelogs = set()
    288  1.1  mrg     diff = PatchSet(data)
    289  1.1  mrg 
    290  1.1  mrg     for file in diff:
    291  1.1  mrg         changelog = os.path.join(find_changelog(file.path), 'ChangeLog')
    292  1.1  mrg         if changelog not in changelogs:
    293  1.1  mrg             changelogs.add(changelog)
    294  1.1  mrg             with open(changelog) as f:
    295  1.1  mrg                 content = f.read()
    296  1.1  mrg             with open(changelog, 'w+') as f:
    297  1.1  mrg                 f.write(f'{current_timestamp}  {username}  <{email}>\n\n')
    298  1.1  mrg                 f.write('\tUpdate copyright years.\n\n')
    299  1.1  mrg                 f.write(content)
    300  1.1  mrg 
    301  1.1  mrg 
    302  1.1  mrg if __name__ == '__main__':
    303  1.1  mrg     parser = argparse.ArgumentParser(description=help_message)
    304  1.1  mrg     parser.add_argument('input', nargs='?',
    305  1.1  mrg                         help='Patch file (or missing, read standard input)')
    306  1.1  mrg     parser.add_argument('-b', '--pr-numbers', action='store',
    307  1.1  mrg                         type=lambda arg: arg.split(','), nargs='?',
    308  1.1  mrg                         help='Add the specified PRs (comma separated)')
    309  1.1  mrg     parser.add_argument('-s', '--no-functions', action='store_true',
    310  1.1  mrg                         help='Do not generate function names in ChangeLogs')
    311  1.1  mrg     parser.add_argument('-p', '--fill-up-bug-titles', action='store_true',
    312  1.1  mrg                         help='Download title of mentioned PRs')
    313  1.1  mrg     parser.add_argument('-d', '--directory',
    314  1.1  mrg                         help='Root directory where to search for ChangeLog '
    315  1.1  mrg                         'files')
    316  1.1  mrg     parser.add_argument('-c', '--changelog',
    317  1.1  mrg                         help='Append the ChangeLog to a git commit message '
    318  1.1  mrg                              'file')
    319  1.1  mrg     parser.add_argument('--update-copyright', action='store_true',
    320  1.1  mrg                         help='Update copyright in ChangeLog files')
    321  1.1  mrg     args = parser.parse_args()
    322  1.1  mrg     if args.input == '-':
    323  1.1  mrg         args.input = None
    324  1.1  mrg     if args.directory:
    325  1.1  mrg         root = args.directory
    326  1.1  mrg 
    327  1.1  mrg     data = open(args.input) if args.input else sys.stdin
    328  1.1  mrg     if args.update_copyright:
    329  1.1  mrg         update_copyright(data)
    330  1.1  mrg     else:
    331  1.1  mrg         output = generate_changelog(data, args.no_functions,
    332  1.1  mrg                                     args.fill_up_bug_titles, args.pr_numbers)
    333  1.1  mrg         if args.changelog:
    334  1.1  mrg             lines = open(args.changelog).read().split('\n')
    335  1.1  mrg             start = list(takewhile(lambda l: not l.startswith('#'), lines))
    336  1.1  mrg             end = lines[len(start):]
    337  1.1  mrg             with open(args.changelog, 'w') as f:
    338  1.1  mrg                 if not start or not start[0]:
    339  1.1  mrg                     # initial commit subject line 'component: [PRnnnnn]'
    340  1.1  mrg                     m = prnum_regex.match(firstpr)
    341  1.1  mrg                     if m:
    342  1.1  mrg                         title = f'{m.group("comp")}: [PR{m.group("num")}]'
    343  1.1  mrg                         start.insert(0, title)
    344  1.1  mrg                 if start:
    345  1.1  mrg                     # append empty line
    346  1.1  mrg                     if start[-1] != '':
    347  1.1  mrg                         start.append('')
    348  1.1  mrg                 else:
    349  1.1  mrg                     # append 2 empty lines
    350  1.1  mrg                     start = 2 * ['']
    351  1.1  mrg                 f.write('\n'.join(start))
    352  1.1  mrg                 f.write('\n')
    353  1.1  mrg                 f.write(output)
    354  1.1  mrg                 f.write('\n'.join(end))
    355  1.1  mrg         else:
    356  1.1  mrg             print(output, end='')
    357