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