123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466 |
- #!/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()
|