1 1.1 mrg #!/usr/bin/env python3 2 1.1 mrg 3 1.1 mrg # Copyright (C) 2017-2019 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 # This is a straightforward adaptation of original Perl script. 28 1.1 mrg # 29 1.1 mrg # Author: Yury Gribov <tetra2005 (at] gmail.com> 30 1.1 mrg 31 1.1 mrg import argparse 32 1.1 mrg import sys 33 1.1 mrg import re 34 1.1 mrg import os.path 35 1.1 mrg import os 36 1.1 mrg import tempfile 37 1.1 mrg import time 38 1.1 mrg import shutil 39 1.1 mrg from subprocess import Popen, PIPE 40 1.1 mrg 41 1.1 mrg me = os.path.basename(sys.argv[0]) 42 1.1 mrg 43 1.1 mrg pr_regex = re.compile('\+(\/(\/|\*)|[Cc*!])\s+(PR [a-z+-]+\/[0-9]+)') 44 1.1 mrg 45 1.1 mrg def error(msg): 46 1.1 mrg sys.stderr.write("%s: error: %s\n" % (me, msg)) 47 1.1 mrg sys.exit(1) 48 1.1 mrg 49 1.1 mrg def warn(msg): 50 1.1 mrg sys.stderr.write("%s: warning: %s\n" % (me, msg)) 51 1.1 mrg 52 1.1 mrg class RegexCache(object): 53 1.1 mrg """Simple trick to Perl-like combined match-and-bind.""" 54 1.1 mrg 55 1.1 mrg def __init__(self): 56 1.1 mrg self.last_match = None 57 1.1 mrg 58 1.1 mrg def match(self, p, s): 59 1.1 mrg self.last_match = re.match(p, s) if isinstance(p, str) else p.match(s) 60 1.1 mrg return self.last_match 61 1.1 mrg 62 1.1 mrg def search(self, p, s): 63 1.1 mrg self.last_match = re.search(p, s) if isinstance(p, str) else p.search(s) 64 1.1 mrg return self.last_match 65 1.1 mrg 66 1.1 mrg def group(self, n): 67 1.1 mrg return self.last_match.group(n) 68 1.1 mrg 69 1.1 mrg cache = RegexCache() 70 1.1 mrg 71 1.1 mrg def run(cmd, die_on_error): 72 1.1 mrg """Simple wrapper for Popen.""" 73 1.1 mrg proc = Popen(cmd.split(' '), stderr = PIPE, stdout = PIPE) 74 1.1 mrg (out, err) = proc.communicate() 75 1.1 mrg if die_on_error and proc.returncode != 0: 76 1.1 mrg error("`%s` failed:\n" % (cmd, proc.stderr)) 77 1.1 mrg return proc.returncode, out.decode(), err 78 1.1 mrg 79 1.1 mrg def read_user_info(): 80 1.1 mrg dot_mklog_format_msg = """\ 81 1.1 mrg The .mklog format is: 82 1.1 mrg NAME = ... 83 1.1 mrg EMAIL = ... 84 1.1 mrg """ 85 1.1 mrg 86 1.1 mrg # First try to read .mklog config 87 1.1 mrg mklog_conf = os.path.expanduser('~/.mklog') 88 1.1 mrg if os.path.exists(mklog_conf): 89 1.1 mrg attrs = {} 90 1.1 mrg f = open(mklog_conf) 91 1.1 mrg for s in f: 92 1.1 mrg if cache.match(r'^\s*([a-zA-Z0-9_]+)\s*=\s*(.*?)\s*$', s): 93 1.1 mrg attrs[cache.group(1)] = cache.group(2) 94 1.1 mrg f.close() 95 1.1 mrg if 'NAME' not in attrs: 96 1.1 mrg error("'NAME' not present in .mklog") 97 1.1 mrg if 'EMAIL' not in attrs: 98 1.1 mrg error("'EMAIL' not present in .mklog") 99 1.1 mrg return attrs['NAME'], attrs['EMAIL'] 100 1.1 mrg 101 1.1 mrg # Otherwise go with git 102 1.1 mrg 103 1.1 mrg rc1, name, _ = run('git config user.name', False) 104 1.1 mrg name = name.rstrip() 105 1.1 mrg rc2, email, _ = run('git config user.email', False) 106 1.1 mrg email = email.rstrip() 107 1.1 mrg 108 1.1 mrg if rc1 != 0 or rc2 != 0: 109 1.1 mrg error("""\ 110 1.1 mrg Could not read git user.name and user.email settings. 111 1.1 mrg Please add missing git settings, or create a %s. 112 1.1 mrg """ % mklog_conf) 113 1.1 mrg 114 1.1 mrg return name, email 115 1.1 mrg 116 1.1 mrg def get_parent_changelog (s): 117 1.1 mrg """See which ChangeLog this file change should go to.""" 118 1.1 mrg 119 1.1 mrg if s.find('\\') == -1 and s.find('/') == -1: 120 1.1 mrg return "ChangeLog", s 121 1.1 mrg 122 1.1 mrg gcc_root = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) 123 1.1 mrg 124 1.1 mrg d = s 125 1.1 mrg while d: 126 1.1 mrg clname = d + "/ChangeLog" 127 1.1 mrg if os.path.exists(gcc_root + '/' + clname) or os.path.exists(clname): 128 1.1 mrg relname = s[len(d)+1:] 129 1.1 mrg return clname, relname 130 1.1 mrg d, _ = os.path.split(d) 131 1.1 mrg 132 1.1 mrg return "Unknown ChangeLog", s 133 1.1 mrg 134 1.1 mrg class FileDiff: 135 1.1 mrg """Class to represent changes in a single file.""" 136 1.1 mrg 137 1.1 mrg def __init__(self, filename): 138 1.1 mrg self.filename = filename 139 1.1 mrg self.hunks = [] 140 1.1 mrg self.clname, self.relname = get_parent_changelog(filename); 141 1.1 mrg 142 1.1 mrg def dump(self): 143 1.1 mrg print("Diff for %s:\n ChangeLog = %s\n rel name = %s\n" % (self.filename, self.clname, self.relname)) 144 1.1 mrg for i, h in enumerate(self.hunks): 145 1.1 mrg print("Next hunk %d:" % i) 146 1.1 mrg h.dump() 147 1.1 mrg 148 1.1 mrg class Hunk: 149 1.1 mrg """Class to represent a single hunk of changes.""" 150 1.1 mrg 151 1.1 mrg def __init__(self, hdr): 152 1.1 mrg self.hdr = hdr 153 1.1 mrg self.lines = [] 154 1.1 mrg self.ctx_diff = is_ctx_hunk_start(hdr) 155 1.1 mrg 156 1.1 mrg def dump(self): 157 1.1 mrg print('%s' % self.hdr) 158 1.1 mrg print('%s' % '\n'.join(self.lines)) 159 1.1 mrg 160 1.1 mrg def is_file_addition(self): 161 1.1 mrg """Does hunk describe addition of file?""" 162 1.1 mrg if self.ctx_diff: 163 1.1 mrg for line in self.lines: 164 1.1 mrg if re.match(r'^\*\*\* 0 \*\*\*\*', line): 165 1.1 mrg return True 166 1.1 mrg else: 167 1.1 mrg return re.match(r'^@@ -0,0 \+1.* @@', self.hdr) 168 1.1 mrg 169 1.1 mrg def is_file_removal(self): 170 1.1 mrg """Does hunk describe removal of file?""" 171 1.1 mrg if self.ctx_diff: 172 1.1 mrg for line in self.lines: 173 1.1 mrg if re.match(r'^--- 0 ----', line): 174 1.1 mrg return True 175 1.1 mrg else: 176 1.1 mrg return re.match(r'^@@ -1.* \+0,0 @@', self.hdr) 177 1.1 mrg 178 1.1 mrg def is_file_diff_start(s): 179 1.1 mrg # Don't be fooled by context diff line markers: 180 1.1 mrg # *** 385,391 **** 181 1.1 mrg return ((s.startswith('*** ') and not s.endswith('***')) 182 1.1 mrg or (s.startswith('--- ') and not s.endswith('---'))) 183 1.1 mrg 184 1.1 mrg def is_ctx_hunk_start(s): 185 1.1 mrg return re.match(r'^\*\*\*\*\*\**', s) 186 1.1 mrg 187 1.1 mrg def is_uni_hunk_start(s): 188 1.1 mrg return re.match(r'^@@ .* @@', s) 189 1.1 mrg 190 1.1 mrg def is_hunk_start(s): 191 1.1 mrg return is_ctx_hunk_start(s) or is_uni_hunk_start(s) 192 1.1 mrg 193 1.1 mrg def remove_suffixes(s): 194 1.1 mrg if s.startswith('a/') or s.startswith('b/'): 195 1.1 mrg s = s[2:] 196 1.1 mrg if s.endswith('.jj'): 197 1.1 mrg s = s[:-3] 198 1.1 mrg return s 199 1.1 mrg 200 1.1 mrg def find_changed_funs(hunk): 201 1.1 mrg """Find all functions touched by hunk. We don't try too hard 202 1.1 mrg to find good matches. This should return a superset 203 1.1 mrg of the actual set of functions in the .diff file. 204 1.1 mrg """ 205 1.1 mrg 206 1.1 mrg fns = [] 207 1.1 mrg fn = None 208 1.1 mrg 209 1.1 mrg if (cache.match(r'^\*\*\*\*\*\** ([a-zA-Z0-9_].*)', hunk.hdr) 210 1.1 mrg or cache.match(r'^@@ .* @@ ([a-zA-Z0-9_].*)', hunk.hdr)): 211 1.1 mrg fn = cache.group(1) 212 1.1 mrg 213 1.1 mrg for i, line in enumerate(hunk.lines): 214 1.1 mrg # Context diffs have extra whitespace after first char; 215 1.1 mrg # remove it to make matching easier. 216 1.1 mrg if hunk.ctx_diff: 217 1.1 mrg line = re.sub(r'^([-+! ]) ', r'\1', line) 218 1.1 mrg 219 1.1 mrg # Remember most recent identifier in hunk 220 1.1 mrg # that might be a function name. 221 1.1 mrg if cache.match(r'^[-+! ]([a-zA-Z0-9_#].*)', line): 222 1.1 mrg fn = cache.group(1) 223 1.1 mrg 224 1.1 mrg change = line and re.match(r'^[-+!][^-]', line) 225 1.1 mrg 226 1.1 mrg # Top-level comment cannot belong to function 227 1.1 mrg if re.match(r'^[-+! ]\/\*', line): 228 1.1 mrg fn = None 229 1.1 mrg 230 1.1 mrg if change and fn: 231 1.1 mrg if cache.match(r'^((class|struct|union|enum)\s+[a-zA-Z0-9_]+)', fn): 232 1.1 mrg # Struct declaration 233 1.1 mrg fn = cache.group(1) 234 1.1 mrg elif cache.search(r'#\s*define\s+([a-zA-Z0-9_]+)', fn): 235 1.1 mrg # Macro definition 236 1.1 mrg fn = cache.group(1) 237 1.1 mrg elif cache.match('^DEF[A-Z0-9_]+\s*\(([a-zA-Z0-9_]+)', fn): 238 1.1 mrg # Supermacro 239 1.1 mrg fn = cache.group(1) 240 1.1 mrg elif cache.search(r'([a-zA-Z_][^()\s]*)\s*\([^*]', fn): 241 1.1 mrg # Discard template and function parameters. 242 1.1 mrg fn = cache.group(1) 243 1.1 mrg fn = re.sub(r'<[^<>]*>', '', fn) 244 1.1 mrg fn = fn.rstrip() 245 1.1 mrg else: 246 1.1 mrg fn = None 247 1.1 mrg 248 1.1 mrg if fn and fn not in fns: # Avoid dups 249 1.1 mrg fns.append(fn) 250 1.1 mrg 251 1.1 mrg fn = None 252 1.1 mrg 253 1.1 mrg return fns 254 1.1 mrg 255 1.1 mrg def parse_patch(contents): 256 1.1 mrg """Parse patch contents to a sequence of FileDiffs.""" 257 1.1 mrg 258 1.1 mrg diffs = [] 259 1.1 mrg 260 1.1 mrg lines = contents.split('\n') 261 1.1 mrg 262 1.1 mrg i = 0 263 1.1 mrg while i < len(lines): 264 1.1 mrg line = lines[i] 265 1.1 mrg 266 1.1 mrg # Diff headers look like 267 1.1 mrg # --- a/gcc/tree.c 268 1.1 mrg # +++ b/gcc/tree.c 269 1.1 mrg # or 270 1.1 mrg # *** gcc/cfgexpand.c 2013-12-25 20:07:24.800350058 +0400 271 1.1 mrg # --- gcc/cfgexpand.c 2013-12-25 20:06:30.612350178 +0400 272 1.1 mrg 273 1.1 mrg if is_file_diff_start(line): 274 1.1 mrg left = re.split(r'\s+', line)[1] 275 1.1 mrg else: 276 1.1 mrg i += 1 277 1.1 mrg continue 278 1.1 mrg 279 1.1 mrg left = remove_suffixes(left); 280 1.1 mrg 281 1.1 mrg i += 1 282 1.1 mrg line = lines[i] 283 1.1 mrg 284 1.1 mrg if not cache.match(r'^[+-][+-][+-] +(\S+)', line): 285 1.1 mrg error("expected filename in line %d" % i) 286 1.1 mrg right = remove_suffixes(cache.group(1)); 287 1.1 mrg 288 1.1 mrg # Extract real file name from left and right names. 289 1.1 mrg filename = None 290 1.1 mrg if left == right: 291 1.1 mrg filename = left 292 1.1 mrg elif left == '/dev/null': 293 1.1 mrg filename = right; 294 1.1 mrg elif right == '/dev/null': 295 1.1 mrg filename = left; 296 1.1 mrg else: 297 1.1 mrg comps = [] 298 1.1 mrg while left and right: 299 1.1 mrg left, l = os.path.split(left) 300 1.1 mrg right, r = os.path.split(right) 301 1.1 mrg if l != r: 302 1.1 mrg break 303 1.1 mrg comps.append(l) 304 1.1 mrg 305 1.1 mrg if not comps: 306 1.1 mrg error("failed to extract common name for %s and %s" % (left, right)) 307 1.1 mrg 308 1.1 mrg comps.reverse() 309 1.1 mrg filename = '/'.join(comps) 310 1.1 mrg 311 1.1 mrg d = FileDiff(filename) 312 1.1 mrg diffs.append(d) 313 1.1 mrg 314 1.1 mrg # Collect hunks for current file. 315 1.1 mrg hunk = None 316 1.1 mrg i += 1 317 1.1 mrg while i < len(lines): 318 1.1 mrg line = lines[i] 319 1.1 mrg 320 1.1 mrg # Create new hunk when we see hunk header 321 1.1 mrg if is_hunk_start(line): 322 1.1 mrg if hunk is not None: 323 1.1 mrg d.hunks.append(hunk) 324 1.1 mrg hunk = Hunk(line) 325 1.1 mrg i += 1 326 1.1 mrg continue 327 1.1 mrg 328 1.1 mrg # Stop when we reach next diff 329 1.1 mrg if (is_file_diff_start(line) 330 1.1 mrg or line.startswith('diff ') 331 1.1 mrg or line.startswith('Index: ')): 332 1.1 mrg i -= 1 333 1.1 mrg break 334 1.1 mrg 335 1.1 mrg if hunk is not None: 336 1.1 mrg hunk.lines.append(line) 337 1.1 mrg i += 1 338 1.1 mrg 339 1.1 mrg d.hunks.append(hunk) 340 1.1 mrg 341 1.1 mrg return diffs 342 1.1 mrg 343 1.1 mrg 344 1.1 mrg def get_pr_from_testcase(line): 345 1.1 mrg r = pr_regex.search(line) 346 1.1 mrg if r != None: 347 1.1 mrg return r.group(3) 348 1.1 mrg else: 349 1.1 mrg return None 350 1.1 mrg 351 1.1 mrg def main(): 352 1.1 mrg name, email = read_user_info() 353 1.1 mrg 354 1.1 mrg help_message = """\ 355 1.1 mrg Generate ChangeLog template for PATCH. 356 1.1 mrg PATCH must be generated using diff(1)'s -up or -cp options 357 1.1 mrg (or their equivalent in Subversion/git). 358 1.1 mrg """ 359 1.1 mrg 360 1.1 mrg inline_message = """\ 361 1.1 mrg Prepends ChangeLog to PATCH. 362 1.1 mrg If PATCH is not stdin, modifies PATCH in-place, 363 1.1 mrg otherwise writes to stdout.' 364 1.1 mrg """ 365 1.1 mrg 366 1.1 mrg parser = argparse.ArgumentParser(description = help_message) 367 1.1 mrg parser.add_argument('-v', '--verbose', action = 'store_true', help = 'Verbose messages') 368 1.1 mrg parser.add_argument('-i', '--inline', action = 'store_true', help = inline_message) 369 1.1 mrg parser.add_argument('input', nargs = '?', help = 'Patch file (or missing, read standard input)') 370 1.1 mrg args = parser.parse_args() 371 1.1 mrg if args.input == '-': 372 1.1 mrg args.input = None 373 1.1 mrg input = open(args.input) if args.input else sys.stdin 374 1.1 mrg contents = input.read() 375 1.1 mrg diffs = parse_patch(contents) 376 1.1 mrg 377 1.1 mrg if args.verbose: 378 1.1 mrg print("Parse results:") 379 1.1 mrg for d in diffs: 380 1.1 mrg d.dump() 381 1.1 mrg 382 1.1 mrg # Generate template ChangeLog. 383 1.1 mrg 384 1.1 mrg logs = {} 385 1.1 mrg prs = [] 386 1.1 mrg for d in diffs: 387 1.1 mrg log_name = d.clname 388 1.1 mrg 389 1.1 mrg logs.setdefault(log_name, '') 390 1.1 mrg logs[log_name] += '\t* %s' % d.relname 391 1.1 mrg 392 1.1 mrg change_msg = '' 393 1.1 mrg 394 1.1 mrg # Check if file was removed or added. 395 1.1 mrg # Two patterns for context and unified diff. 396 1.1 mrg if len(d.hunks) == 1: 397 1.1 mrg hunk0 = d.hunks[0] 398 1.1 mrg if hunk0.is_file_addition(): 399 1.1 mrg if re.search(r'testsuite.*(?<!\.exp)$', d.filename): 400 1.1 mrg change_msg = ': New test.\n' 401 1.1 mrg pr = get_pr_from_testcase(hunk0.lines[0]) 402 1.1 mrg if pr and pr not in prs: 403 1.1 mrg prs.append(pr) 404 1.1 mrg else: 405 1.1 mrg change_msg = ": New file.\n" 406 1.1 mrg elif hunk0.is_file_removal(): 407 1.1 mrg change_msg = ": Remove.\n" 408 1.1 mrg 409 1.1 mrg _, ext = os.path.splitext(d.filename) 410 1.1 mrg if (not change_msg and ext in ['.c', '.cpp', '.C', '.cc', '.h', '.inc', '.def'] 411 1.1 mrg and not 'testsuite' in d.filename): 412 1.1 mrg fns = [] 413 1.1 mrg for hunk in d.hunks: 414 1.1 mrg for fn in find_changed_funs(hunk): 415 1.1 mrg if fn not in fns: 416 1.1 mrg fns.append(fn) 417 1.1 mrg 418 1.1 mrg for fn in fns: 419 1.1 mrg if change_msg: 420 1.1 mrg change_msg += "\t(%s):\n" % fn 421 1.1 mrg else: 422 1.1 mrg change_msg = " (%s):\n" % fn 423 1.1 mrg 424 1.1 mrg logs[log_name] += change_msg if change_msg else ":\n" 425 1.1 mrg 426 1.1 mrg if args.inline and args.input: 427 1.1 mrg # Get a temp filename, rather than an open filehandle, because we use 428 1.1 mrg # the open to truncate. 429 1.1 mrg fd, tmp = tempfile.mkstemp("tmp.XXXXXXXX") 430 1.1 mrg os.close(fd) 431 1.1 mrg 432 1.1 mrg # Copy permissions to temp file 433 1.1 mrg # (old Pythons do not support shutil.copymode) 434 1.1 mrg shutil.copymode(args.input, tmp) 435 1.1 mrg 436 1.1 mrg # Open the temp file, clearing contents. 437 1.1 mrg out = open(tmp, 'w') 438 1.1 mrg else: 439 1.1 mrg tmp = None 440 1.1 mrg out = sys.stdout 441 1.1 mrg 442 1.1 mrg # Print log 443 1.1 mrg date = time.strftime('%Y-%m-%d') 444 1.1 mrg bugmsg = '' 445 1.1 mrg if len(prs): 446 1.1 mrg bugmsg = '\n'.join(['\t' + pr for pr in prs]) + '\n' 447 1.1 mrg 448 1.1 mrg for log_name, msg in sorted(logs.items()): 449 1.1 mrg out.write("""\ 450 1.1 mrg %s: 451 1.1 mrg 452 1.1 mrg %s %s <%s> 453 1.1 mrg 454 1.1 mrg %s%s\n""" % (log_name, date, name, email, bugmsg, msg)) 455 1.1 mrg 456 1.1 mrg if args.inline: 457 1.1 mrg # Append patch body 458 1.1 mrg out.write(contents) 459 1.1 mrg 460 1.1 mrg if args.input: 461 1.1 mrg # Write new contents atomically 462 1.1 mrg out.close() 463 1.1 mrg shutil.move(tmp, args.input) 464 1.1 mrg 465 1.1 mrg if __name__ == '__main__': 466 1.1 mrg main() 467