| #!/usr/bin/env python3 | |
| # Copyright (C) 2017-2019 Free Software Foundation, Inc. | |
| # | |
| # This file is part of GCC. | |
| # | |
| # GCC is free software; you can redistribute it and/or modify | |
| # it under the terms of the GNU General Public License as published by | |
| # the Free Software Foundation; either version 3, or (at your option) | |
| # any later version. | |
| # | |
| # GCC is distributed in the hope that it will be useful, | |
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
| # GNU General Public License for more details. | |
| # | |
| # You should have received a copy of the GNU General Public License | |
| # along with GCC; see the file COPYING. If not, write to | |
| # the Free Software Foundation, 51 Franklin Street, Fifth Floor, | |
| # Boston, MA 02110-1301, USA. | |
| # This script parses a .diff file generated with 'diff -up' or 'diff -cp' | |
| # and adds a skeleton ChangeLog file to the file. It does not try to be | |
| # too smart when parsing function names, but it produces a reasonable | |
| # approximation. | |
| # | |
| # This is a straightforward adaptation of original Perl script. | |
| # | |
| # Author: Yury Gribov <tetra2005@gmail.com> | |
| import argparse | |
| import sys | |
| import re | |
| import os.path | |
| import os | |
| import tempfile | |
| import time | |
| import shutil | |
| from subprocess import Popen, PIPE | |
| me = os.path.basename(sys.argv[0]) | |
| pr_regex = re.compile('\+(\/(\/|\*)|[Cc*!])\s+(PR [a-z+-]+\/[0-9]+)') | |
| def error(msg): | |
| sys.stderr.write("%s: error: %s\n" % (me, msg)) | |
| sys.exit(1) | |
| def warn(msg): | |
| sys.stderr.write("%s: warning: %s\n" % (me, msg)) | |
| class RegexCache(object): | |
| """Simple trick to Perl-like combined match-and-bind.""" | |
| def __init__(self): | |
| self.last_match = None | |
| def match(self, p, s): | |
| self.last_match = re.match(p, s) if isinstance(p, str) else p.match(s) | |
| return self.last_match | |
| def search(self, p, s): | |
| self.last_match = re.search(p, s) if isinstance(p, str) else p.search(s) | |
| return self.last_match | |
| def group(self, n): | |
| return self.last_match.group(n) | |
| cache = RegexCache() | |
| def run(cmd, die_on_error): | |
| """Simple wrapper for Popen.""" | |
| proc = Popen(cmd.split(' '), stderr = PIPE, stdout = PIPE) | |
| (out, err) = proc.communicate() | |
| if die_on_error and proc.returncode != 0: | |
| error("`%s` failed:\n" % (cmd, proc.stderr)) | |
| return proc.returncode, out.decode(), err | |
| def read_user_info(): | |
| dot_mklog_format_msg = """\ | |
| The .mklog format is: | |
| NAME = ... | |
| EMAIL = ... | |
| """ | |
| # First try to read .mklog config | |
| mklog_conf = os.path.expanduser('~/.mklog') | |
| if os.path.exists(mklog_conf): | |
| attrs = {} | |
| f = open(mklog_conf) | |
| for s in f: | |
| if cache.match(r'^\s*([a-zA-Z0-9_]+)\s*=\s*(.*?)\s*$', s): | |
| attrs[cache.group(1)] = cache.group(2) | |
| f.close() | |
| if 'NAME' not in attrs: | |
| error("'NAME' not present in .mklog") | |
| if 'EMAIL' not in attrs: | |
| error("'EMAIL' not present in .mklog") | |
| return attrs['NAME'], attrs['EMAIL'] | |
| # Otherwise go with git | |
| rc1, name, _ = run('git config user.name', False) | |
| name = name.rstrip() | |
| rc2, email, _ = run('git config user.email', False) | |
| email = email.rstrip() | |
| if rc1 != 0 or rc2 != 0: | |
| error("""\ | |
| Could not read git user.name and user.email settings. | |
| Please add missing git settings, or create a %s. | |
| """ % mklog_conf) | |
| return name, email | |
| def get_parent_changelog (s): | |
| """See which ChangeLog this file change should go to.""" | |
| if s.find('\\') == -1 and s.find('/') == -1: | |
| return "ChangeLog", s | |
| gcc_root = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) | |
| d = s | |
| while d: | |
| clname = d + "/ChangeLog" | |
| if os.path.exists(gcc_root + '/' + clname) or os.path.exists(clname): | |
| relname = s[len(d)+1:] | |
| return clname, relname | |
| d, _ = os.path.split(d) | |
| return "Unknown ChangeLog", s | |
| class FileDiff: | |
| """Class to represent changes in a single file.""" | |
| def __init__(self, filename): | |
| self.filename = filename | |
| self.hunks = [] | |
| self.clname, self.relname = get_parent_changelog(filename); | |
| def dump(self): | |
| print("Diff for %s:\n ChangeLog = %s\n rel name = %s\n" % (self.filename, self.clname, self.relname)) | |
| for i, h in enumerate(self.hunks): | |
| print("Next hunk %d:" % i) | |
| h.dump() | |
| class Hunk: | |
| """Class to represent a single hunk of changes.""" | |
| def __init__(self, hdr): | |
| self.hdr = hdr | |
| self.lines = [] | |
| self.ctx_diff = is_ctx_hunk_start(hdr) | |
| def dump(self): | |
| print('%s' % self.hdr) | |
| print('%s' % '\n'.join(self.lines)) | |
| def is_file_addition(self): | |
| """Does hunk describe addition of file?""" | |
| if self.ctx_diff: | |
| for line in self.lines: | |
| if re.match(r'^\*\*\* 0 \*\*\*\*', line): | |
| return True | |
| else: | |
| return re.match(r'^@@ -0,0 \+1.* @@', self.hdr) | |
| def is_file_removal(self): | |
| """Does hunk describe removal of file?""" | |
| if self.ctx_diff: | |
| for line in self.lines: | |
| if re.match(r'^--- 0 ----', line): | |
| return True | |
| else: | |
| return re.match(r'^@@ -1.* \+0,0 @@', self.hdr) | |
| def is_file_diff_start(s): | |
| # Don't be fooled by context diff line markers: | |
| # *** 385,391 **** | |
| return ((s.startswith('*** ') and not s.endswith('***')) | |
| or (s.startswith('--- ') and not s.endswith('---'))) | |
| def is_ctx_hunk_start(s): | |
| return re.match(r'^\*\*\*\*\*\**', s) | |
| def is_uni_hunk_start(s): | |
| return re.match(r'^@@ .* @@', s) | |
| def is_hunk_start(s): | |
| return is_ctx_hunk_start(s) or is_uni_hunk_start(s) | |
| def remove_suffixes(s): | |
| if s.startswith('a/') or s.startswith('b/'): | |
| s = s[2:] | |
| if s.endswith('.jj'): | |
| s = s[:-3] | |
| return s | |
| def find_changed_funs(hunk): | |
| """Find all functions touched by hunk. We don't try too hard | |
| to find good matches. This should return a superset | |
| of the actual set of functions in the .diff file. | |
| """ | |
| fns = [] | |
| fn = None | |
| if (cache.match(r'^\*\*\*\*\*\** ([a-zA-Z0-9_].*)', hunk.hdr) | |
| or cache.match(r'^@@ .* @@ ([a-zA-Z0-9_].*)', hunk.hdr)): | |
| fn = cache.group(1) | |
| for i, line in enumerate(hunk.lines): | |
| # Context diffs have extra whitespace after first char; | |
| # remove it to make matching easier. | |
| if hunk.ctx_diff: | |
| line = re.sub(r'^([-+! ]) ', r'\1', line) | |
| # Remember most recent identifier in hunk | |
| # that might be a function name. | |
| if cache.match(r'^[-+! ]([a-zA-Z0-9_#].*)', line): | |
| fn = cache.group(1) | |
| change = line and re.match(r'^[-+!][^-]', line) | |
| # Top-level comment cannot belong to function | |
| if re.match(r'^[-+! ]\/\*', line): | |
| fn = None | |
| if change and fn: | |
| if cache.match(r'^((class|struct|union|enum)\s+[a-zA-Z0-9_]+)', fn): | |
| # Struct declaration | |
| fn = cache.group(1) | |
| elif cache.search(r'#\s*define\s+([a-zA-Z0-9_]+)', fn): | |
| # Macro definition | |
| fn = cache.group(1) | |
| elif cache.match('^DEF[A-Z0-9_]+\s*\(([a-zA-Z0-9_]+)', fn): | |
| # Supermacro | |
| fn = cache.group(1) | |
| elif cache.search(r'([a-zA-Z_][^()\s]*)\s*\([^*]', fn): | |
| # Discard template and function parameters. | |
| fn = cache.group(1) | |
| fn = re.sub(r'<[^<>]*>', '', fn) | |
| fn = fn.rstrip() | |
| else: | |
| fn = None | |
| if fn and fn not in fns: # Avoid dups | |
| fns.append(fn) | |
| fn = None | |
| return fns | |
| def parse_patch(contents): | |
| """Parse patch contents to a sequence of FileDiffs.""" | |
| diffs = [] | |
| lines = contents.split('\n') | |
| i = 0 | |
| while i < len(lines): | |
| line = lines[i] | |
| # Diff headers look like | |
| # --- a/gcc/tree.c | |
| # +++ b/gcc/tree.c | |
| # or | |
| # *** gcc/cfgexpand.c 2013-12-25 20:07:24.800350058 +0400 | |
| # --- gcc/cfgexpand.c 2013-12-25 20:06:30.612350178 +0400 | |
| if is_file_diff_start(line): | |
| left = re.split(r'\s+', line)[1] | |
| else: | |
| i += 1 | |
| continue | |
| left = remove_suffixes(left); | |
| i += 1 | |
| line = lines[i] | |
| if not cache.match(r'^[+-][+-][+-] +(\S+)', line): | |
| error("expected filename in line %d" % i) | |
| right = remove_suffixes(cache.group(1)); | |
| # Extract real file name from left and right names. | |
| filename = None | |
| if left == right: | |
| filename = left | |
| elif left == '/dev/null': | |
| filename = right; | |
| elif right == '/dev/null': | |
| filename = left; | |
| else: | |
| comps = [] | |
| while left and right: | |
| left, l = os.path.split(left) | |
| right, r = os.path.split(right) | |
| if l != r: | |
| break | |
| comps.append(l) | |
| if not comps: | |
| error("failed to extract common name for %s and %s" % (left, right)) | |
| comps.reverse() | |
| filename = '/'.join(comps) | |
| d = FileDiff(filename) | |
| diffs.append(d) | |
| # Collect hunks for current file. | |
| hunk = None | |
| i += 1 | |
| while i < len(lines): | |
| line = lines[i] | |
| # Create new hunk when we see hunk header | |
| if is_hunk_start(line): | |
| if hunk is not None: | |
| d.hunks.append(hunk) | |
| hunk = Hunk(line) | |
| i += 1 | |
| continue | |
| # Stop when we reach next diff | |
| if (is_file_diff_start(line) | |
| or line.startswith('diff ') | |
| or line.startswith('Index: ')): | |
| i -= 1 | |
| break | |
| if hunk is not None: | |
| hunk.lines.append(line) | |
| i += 1 | |
| d.hunks.append(hunk) | |
| return diffs | |
| def get_pr_from_testcase(line): | |
| r = pr_regex.search(line) | |
| if r != None: | |
| return r.group(3) | |
| else: | |
| return None | |
| def main(): | |
| name, email = read_user_info() | |
| help_message = """\ | |
| Generate ChangeLog template for PATCH. | |
| PATCH must be generated using diff(1)'s -up or -cp options | |
| (or their equivalent in Subversion/git). | |
| """ | |
| inline_message = """\ | |
| Prepends ChangeLog to PATCH. | |
| If PATCH is not stdin, modifies PATCH in-place, | |
| otherwise writes to stdout.' | |
| """ | |
| parser = argparse.ArgumentParser(description = help_message) | |
| parser.add_argument('-v', '--verbose', action = 'store_true', help = 'Verbose messages') | |
| parser.add_argument('-i', '--inline', action = 'store_true', help = inline_message) | |
| parser.add_argument('input', nargs = '?', help = 'Patch file (or missing, read standard input)') | |
| args = parser.parse_args() | |
| if args.input == '-': | |
| args.input = None | |
| input = open(args.input) if args.input else sys.stdin | |
| contents = input.read() | |
| diffs = parse_patch(contents) | |
| if args.verbose: | |
| print("Parse results:") | |
| for d in diffs: | |
| d.dump() | |
| # Generate template ChangeLog. | |
| logs = {} | |
| prs = [] | |
| for d in diffs: | |
| log_name = d.clname | |
| logs.setdefault(log_name, '') | |
| logs[log_name] += '\t* %s' % d.relname | |
| change_msg = '' | |
| # Check if file was removed or added. | |
| # Two patterns for context and unified diff. | |
| if len(d.hunks) == 1: | |
| hunk0 = d.hunks[0] | |
| if hunk0.is_file_addition(): | |
| if re.search(r'testsuite.*(?<!\.exp)$', d.filename): | |
| change_msg = ': New test.\n' | |
| pr = get_pr_from_testcase(hunk0.lines[0]) | |
| if pr and pr not in prs: | |
| prs.append(pr) | |
| else: | |
| change_msg = ": New file.\n" | |
| elif hunk0.is_file_removal(): | |
| change_msg = ": Remove.\n" | |
| _, ext = os.path.splitext(d.filename) | |
| if (not change_msg and ext in ['.c', '.cpp', '.C', '.cc', '.h', '.inc', '.def'] | |
| and not 'testsuite' in d.filename): | |
| fns = [] | |
| for hunk in d.hunks: | |
| for fn in find_changed_funs(hunk): | |
| if fn not in fns: | |
| fns.append(fn) | |
| for fn in fns: | |
| if change_msg: | |
| change_msg += "\t(%s):\n" % fn | |
| else: | |
| change_msg = " (%s):\n" % fn | |
| logs[log_name] += change_msg if change_msg else ":\n" | |
| if args.inline and args.input: | |
| # Get a temp filename, rather than an open filehandle, because we use | |
| # the open to truncate. | |
| fd, tmp = tempfile.mkstemp("tmp.XXXXXXXX") | |
| os.close(fd) | |
| # Copy permissions to temp file | |
| # (old Pythons do not support shutil.copymode) | |
| shutil.copymode(args.input, tmp) | |
| # Open the temp file, clearing contents. | |
| out = open(tmp, 'w') | |
| else: | |
| tmp = None | |
| out = sys.stdout | |
| # Print log | |
| date = time.strftime('%Y-%m-%d') | |
| bugmsg = '' | |
| if len(prs): | |
| bugmsg = '\n'.join(['\t' + pr for pr in prs]) + '\n' | |
| for log_name, msg in sorted(logs.items()): | |
| out.write("""\ | |
| %s: | |
| %s %s <%s> | |
| %s%s\n""" % (log_name, date, name, email, bugmsg, msg)) | |
| if args.inline: | |
| # Append patch body | |
| out.write(contents) | |
| if args.input: | |
| # Write new contents atomically | |
| out.close() | |
| shutil.move(tmp, args.input) | |
| if __name__ == '__main__': | |
| main() |