| #!/usr/bin/python | 
 |  | 
 | # Copyright (C) 2017 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 sys | 
 | import re | 
 | import os.path | 
 | import os | 
 | import getopt | 
 | import tempfile | 
 | import time | 
 | import shutil | 
 | from subprocess import Popen, PIPE | 
 |  | 
 | me = os.path.basename(sys.argv[0]) | 
 |  | 
 | 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 print_help_and_exit(): | 
 |     print """\ | 
 | Usage: %s [-i | --inline] [PATCH] | 
 | Generate ChangeLog template for PATCH. | 
 | PATCH must be generated using diff(1)'s -up or -cp options | 
 | (or their equivalent in Subversion/git). | 
 |  | 
 | When PATCH is - or missing, read standard input. | 
 |  | 
 | When -i is used, prepends ChangeLog to PATCH. | 
 | If PATCH is not stdin, modifies PATCH in-place, otherwise writes | 
 | to stdout. | 
 | """ % me | 
 |     sys.exit(1) | 
 |  | 
 | 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, 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, 'rb') | 
 |     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 main(): | 
 |   name, email = read_user_info() | 
 |  | 
 |   try: | 
 |     opts, args = getopt.getopt(sys.argv[1:], 'hiv', ['help', 'verbose', 'inline']) | 
 |   except getopt.GetoptError, err: | 
 |     error(str(err)) | 
 |  | 
 |   inline = False | 
 |   verbose = 0 | 
 |  | 
 |   for o, a in opts: | 
 |     if o in ('-h', '--help'): | 
 |       print_help_and_exit() | 
 |     elif o in ('-i', '--inline'): | 
 |       inline = True | 
 |     elif o in ('-v', '--verbose'): | 
 |       verbose += 1 | 
 |     else: | 
 |       assert False, "unhandled option" | 
 |  | 
 |   if len(args) == 0: | 
 |     args = ['-'] | 
 |  | 
 |   if len(args) == 1 and args[0] == '-': | 
 |     input = sys.stdin | 
 |   elif len(args) == 1: | 
 |     input = open(args[0], 'rb') | 
 |   else: | 
 |     error("too many arguments; for more details run with -h") | 
 |  | 
 |   contents = input.read() | 
 |   diffs = parse_patch(contents) | 
 |  | 
 |   if verbose: | 
 |     print "Parse results:" | 
 |     for d in diffs: | 
 |       d.dump() | 
 |  | 
 |   # Generate template ChangeLog. | 
 |  | 
 |   logs = {} | 
 |   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' | 
 |         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']: | 
 |       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 inline and args[0] != '-': | 
 |     # 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[0], tmp) | 
 |  | 
 |     # Open the temp file, clearing contents. | 
 |     out = open(tmp, 'wb') | 
 |   else: | 
 |     tmp = None | 
 |     out = sys.stdout | 
 |  | 
 |   # Print log | 
 |   date = time.strftime('%Y-%m-%d') | 
 |   for log_name, msg in sorted(logs.iteritems()): | 
 |     out.write("""\ | 
 | %s: | 
 |  | 
 | %s  %s  <%s> | 
 |  | 
 | %s\n""" % (log_name, date, name, email, msg)) | 
 |  | 
 |   if inline: | 
 |     # Append patch body | 
 |     out.write(contents) | 
 |  | 
 |     if args[0] != '-': | 
 |       # Write new contents atomically | 
 |       out.close() | 
 |       shutil.move(tmp, args[0]) | 
 |  | 
 | if __name__ == '__main__': | 
 |     main() |