From 6c6471f6cd70b793711795398106ccb63b6d80b4 Mon Sep 17 00:00:00 2001 From: ygribov Date: Wed, 2 Aug 2017 21:01:08 +0000 Subject: Added Python implementation of mklog. git-svn-id: svn+ssh://gcc.gnu.org/svn/gcc/trunk@250836 138bc75d-0d04-0410-961f-82ee72b054a4 --- contrib/ChangeLog | 4 + contrib/mklog | 470 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 474 insertions(+) create mode 100755 contrib/mklog diff --git a/contrib/ChangeLog b/contrib/ChangeLog index 684ece4eadd..5fa6e032be4 100644 --- a/contrib/ChangeLog +++ b/contrib/ChangeLog @@ -1,3 +1,7 @@ +2017-08-02 Yury Gribov + + * mklog: Added new implementation. + 2017-08-02 Yury Gribov * mklog: Rename to ... diff --git a/contrib/mklog b/contrib/mklog new file mode 100755 index 00000000000..0622d2e2e3d --- /dev/null +++ b/contrib/mklog @@ -0,0 +1,470 @@ +#!/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 + +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.*(? + +%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() -- cgit v1.2.1