mirror of git://gcc.gnu.org/git/gcc.git
				
				
				
			
		
			
				
	
	
		
			471 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
			
		
		
	
	
			471 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
| #!/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 can not 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()
 |