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