Warray-bounds-4.C: Skip AIX.
[official-gcc.git] / contrib / mklog
blob1a0e82d1ddd09194486c391a2dbd5ca10e39f878
1 #!/usr/bin/env python3
3 # Copyright (C) 2017-2019 Free Software Foundation, Inc.
5 # This file is part of GCC.
7 # GCC is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3, or (at your option)
10 # any later version.
12 # GCC is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with GCC; see the file COPYING.  If not, write to
19 # the Free Software Foundation, 51 Franklin Street, Fifth Floor,
20 # Boston, MA 02110-1301, USA.
22 # This script parses a .diff file generated with 'diff -up' or 'diff -cp'
23 # and adds a skeleton ChangeLog file to the file. It does not try to be
24 # too smart when parsing function names, but it produces a reasonable
25 # approximation.
27 # This is a straightforward adaptation of original Perl script.
29 # Author: Yury Gribov <tetra2005@gmail.com>
31 import argparse
32 import sys
33 import re
34 import os.path
35 import os
36 import tempfile
37 import time
38 import shutil
39 from subprocess import Popen, PIPE
41 me = os.path.basename(sys.argv[0])
43 pr_regex = re.compile('\+(\/(\/|\*)|[Cc*!])\s+(PR [a-z+-]+\/[0-9]+)')
45 def error(msg):
46   sys.stderr.write("%s: error: %s\n" % (me, msg))
47   sys.exit(1)
49 def warn(msg):
50   sys.stderr.write("%s: warning: %s\n" % (me, msg))
52 class RegexCache(object):
53   """Simple trick to Perl-like combined match-and-bind."""
55   def __init__(self):
56     self.last_match = None
58   def match(self, p, s):
59     self.last_match = re.match(p, s) if isinstance(p, str) else p.match(s)
60     return self.last_match
62   def search(self, p, s):
63     self.last_match = re.search(p, s) if isinstance(p, str) else p.search(s)
64     return self.last_match
66   def group(self, n):
67     return self.last_match.group(n)
69 cache = RegexCache()
71 def run(cmd, die_on_error):
72   """Simple wrapper for Popen."""
73   proc = Popen(cmd.split(' '), stderr = PIPE, stdout = PIPE)
74   (out, err) = proc.communicate()
75   if die_on_error and proc.returncode != 0:
76     error("`%s` failed:\n" % (cmd, proc.stderr))
77   return proc.returncode, out.decode(), err
79 def read_user_info():
80   dot_mklog_format_msg = """\
81 The .mklog format is:
82 NAME = ...
83 EMAIL = ...
84 """
86   # First try to read .mklog config
87   mklog_conf = os.path.expanduser('~/.mklog')
88   if os.path.exists(mklog_conf):
89     attrs = {}
90     f = open(mklog_conf)
91     for s in f:
92       if cache.match(r'^\s*([a-zA-Z0-9_]+)\s*=\s*(.*?)\s*$', s):
93         attrs[cache.group(1)] = cache.group(2)
94     f.close()
95     if 'NAME' not in attrs:
96       error("'NAME' not present in .mklog")
97     if 'EMAIL' not in attrs:
98       error("'EMAIL' not present in .mklog")
99     return attrs['NAME'], attrs['EMAIL']
101   # Otherwise go with git
103   rc1, name, _ = run('git config user.name', False)
104   name = name.rstrip()
105   rc2, email, _ = run('git config user.email', False)
106   email = email.rstrip()
108   if rc1 != 0 or rc2 != 0:
109     error("""\
110 Could not read git user.name and user.email settings.
111 Please add missing git settings, or create a %s.
112 """ % mklog_conf)
114   return name, email
116 def get_parent_changelog (s):
117   """See which ChangeLog this file change should go to."""
119   if s.find('\\') == -1 and s.find('/') == -1:
120     return "ChangeLog", s
122   gcc_root = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
124   d = s
125   while d:
126     clname = d + "/ChangeLog"
127     if os.path.exists(gcc_root + '/' + clname) or os.path.exists(clname):
128       relname = s[len(d)+1:]
129       return clname, relname
130     d, _ = os.path.split(d)
132   return "Unknown ChangeLog", s
134 class FileDiff:
135   """Class to represent changes in a single file."""
137   def __init__(self, filename):
138     self.filename = filename
139     self.hunks = []
140     self.clname, self.relname = get_parent_changelog(filename);
142   def dump(self):
143     print("Diff for %s:\n  ChangeLog = %s\n  rel name = %s\n" % (self.filename, self.clname, self.relname))
144     for i, h in enumerate(self.hunks):
145       print("Next hunk %d:" % i)
146       h.dump()
148 class Hunk:
149   """Class to represent a single hunk of changes."""
151   def __init__(self, hdr):
152     self.hdr = hdr
153     self.lines = []
154     self.ctx_diff = is_ctx_hunk_start(hdr)
156   def dump(self):
157     print('%s' % self.hdr)
158     print('%s' % '\n'.join(self.lines))
160   def is_file_addition(self):
161     """Does hunk describe addition of file?"""
162     if self.ctx_diff:
163       for line in self.lines:
164         if re.match(r'^\*\*\* 0 \*\*\*\*', line):
165           return True
166     else:
167       return re.match(r'^@@ -0,0 \+1.* @@', self.hdr)
169   def is_file_removal(self):
170     """Does hunk describe removal of file?"""
171     if self.ctx_diff:
172       for line in self.lines:
173         if re.match(r'^--- 0 ----', line):
174           return True
175     else:
176       return re.match(r'^@@ -1.* \+0,0 @@', self.hdr)
178 def is_file_diff_start(s):
179   # Don't be fooled by context diff line markers:
180   #   *** 385,391 ****
181   return ((s.startswith('***') and not s.endswith('***'))
182           or (s.startswith('---') and not s.endswith('---')))
184 def is_ctx_hunk_start(s):
185   return re.match(r'^\*\*\*\*\*\**', s)
187 def is_uni_hunk_start(s):
188   return re.match(r'^@@ .* @@', s)
190 def is_hunk_start(s):
191   return is_ctx_hunk_start(s) or is_uni_hunk_start(s)
193 def remove_suffixes(s):
194   if s.startswith('a/') or s.startswith('b/'):
195     s = s[2:]
196   if s.endswith('.jj'):
197     s = s[:-3]
198   return s
200 def find_changed_funs(hunk):
201   """Find all functions touched by hunk.  We don't try too hard
202      to find good matches.  This should return a superset
203      of the actual set of functions in the .diff file.
204   """
206   fns = []
207   fn = None
209   if (cache.match(r'^\*\*\*\*\*\** ([a-zA-Z0-9_].*)', hunk.hdr)
210       or cache.match(r'^@@ .* @@ ([a-zA-Z0-9_].*)', hunk.hdr)):
211     fn = cache.group(1)
213   for i, line in enumerate(hunk.lines):
214     # Context diffs have extra whitespace after first char;
215     # remove it to make matching easier.
216     if hunk.ctx_diff:
217       line = re.sub(r'^([-+! ]) ', r'\1', line)
219     # Remember most recent identifier in hunk
220     # that might be a function name.
221     if cache.match(r'^[-+! ]([a-zA-Z0-9_#].*)', line):
222       fn = cache.group(1)
224     change = line and re.match(r'^[-+!][^-]', line)
226     # Top-level comment cannot belong to function
227     if re.match(r'^[-+! ]\/\*', line):
228       fn = None
230     if change and fn:
231       if cache.match(r'^((class|struct|union|enum)\s+[a-zA-Z0-9_]+)', fn):
232         # Struct declaration
233         fn = cache.group(1)
234       elif cache.search(r'#\s*define\s+([a-zA-Z0-9_]+)', fn):
235         # Macro definition
236         fn = cache.group(1)
237       elif cache.match('^DEF[A-Z0-9_]+\s*\(([a-zA-Z0-9_]+)', fn):
238         # Supermacro
239         fn = cache.group(1)
240       elif cache.search(r'([a-zA-Z_][^()\s]*)\s*\([^*]', fn):
241         # Discard template and function parameters.
242         fn = cache.group(1)
243         fn = re.sub(r'<[^<>]*>', '', fn)
244         fn = fn.rstrip()
245       else:
246         fn = None
248       if fn and fn not in fns:  # Avoid dups
249         fns.append(fn)
251       fn = None
253   return fns
255 def parse_patch(contents):
256   """Parse patch contents to a sequence of FileDiffs."""
258   diffs = []
260   lines = contents.split('\n')
262   i = 0
263   while i < len(lines):
264     line = lines[i]
266     # Diff headers look like
267     #   --- a/gcc/tree.c
268     #   +++ b/gcc/tree.c
269     # or
270     #   *** gcc/cfgexpand.c     2013-12-25 20:07:24.800350058 +0400
271     #   --- gcc/cfgexpand.c     2013-12-25 20:06:30.612350178 +0400
273     if is_file_diff_start(line):
274       left = re.split(r'\s+', line)[1]
275     else:
276       i += 1
277       continue
279     left = remove_suffixes(left);
281     i += 1
282     line = lines[i]
284     if not cache.match(r'^[+-][+-][+-] +(\S+)', line):
285       error("expected filename in line %d" % i)
286     right = remove_suffixes(cache.group(1));
288     # Extract real file name from left and right names.
289     filename = None
290     if left == right:
291       filename = left
292     elif left == '/dev/null':
293       filename = right;
294     elif right == '/dev/null':
295       filename = left;
296     else:
297       comps = []
298       while left and right:
299         left, l = os.path.split(left)
300         right, r = os.path.split(right)
301         if l != r:
302           break
303         comps.append(l)
305       if not comps:
306         error("failed to extract common name for %s and %s" % (left, right))
308       comps.reverse()
309       filename = '/'.join(comps)
311     d = FileDiff(filename)
312     diffs.append(d)
314     # Collect hunks for current file.
315     hunk = None
316     i += 1
317     while i < len(lines):
318       line = lines[i]
320       # Create new hunk when we see hunk header
321       if is_hunk_start(line):
322         if hunk is not None:
323           d.hunks.append(hunk)
324         hunk = Hunk(line)
325         i += 1
326         continue
328       # Stop when we reach next diff
329       if (is_file_diff_start(line)
330           or line.startswith('diff ')
331           or line.startswith('Index: ')):
332         i -= 1
333         break
335       if hunk is not None:
336         hunk.lines.append(line)
337       i += 1
339     d.hunks.append(hunk)
341   return diffs
344 def get_pr_from_testcase(line):
345     r = pr_regex.search(line)
346     if r != None:
347         return r.group(3)
348     else:
349         return None
351 def main():
352   name, email = read_user_info()
354   help_message =  """\
355 Generate ChangeLog template for PATCH.
356 PATCH must be generated using diff(1)'s -up or -cp options
357 (or their equivalent in Subversion/git).
360   inline_message = """\
361 Prepends ChangeLog to PATCH.
362 If PATCH is not stdin, modifies PATCH in-place,
363 otherwise writes to stdout.'
366   parser = argparse.ArgumentParser(description = help_message)
367   parser.add_argument('-v', '--verbose', action = 'store_true', help = 'Verbose messages')
368   parser.add_argument('-i', '--inline', action = 'store_true', help = inline_message)
369   parser.add_argument('input', nargs = '?', help = 'Patch file (or missing, read standard input)')
370   args = parser.parse_args()
371   if args.input == '-':
372       args.input = None
373   input = open(args.input) if args.input else sys.stdin
374   contents = input.read()
375   diffs = parse_patch(contents)
377   if args.verbose:
378     print("Parse results:")
379     for d in diffs:
380       d.dump()
382   # Generate template ChangeLog.
384   logs = {}
385   prs = []
386   for d in diffs:
387     log_name = d.clname
389     logs.setdefault(log_name, '')
390     logs[log_name] += '\t* %s' % d.relname
392     change_msg = ''
394     # Check if file was removed or added.
395     # Two patterns for context and unified diff.
396     if len(d.hunks) == 1:
397       hunk0 = d.hunks[0]
398       if hunk0.is_file_addition():
399         if re.search(r'testsuite.*(?<!\.exp)$', d.filename):
400           change_msg = ': New test.\n'
401           pr = get_pr_from_testcase(hunk0.lines[0])
402           if pr and pr not in prs:
403               prs.append(pr)
404         else:
405           change_msg = ": New file.\n"
406       elif hunk0.is_file_removal():
407         change_msg = ": Remove.\n"
409     _, ext = os.path.splitext(d.filename)
410     if (not change_msg and ext in ['.c', '.cpp', '.C', '.cc', '.h', '.inc', '.def']
411         and not 'testsuite' in d.filename):
412       fns = []
413       for hunk in d.hunks:
414         for fn in find_changed_funs(hunk):
415           if fn not in fns:
416             fns.append(fn)
418       for fn in fns:
419         if change_msg:
420           change_msg += "\t(%s):\n" % fn
421         else:
422           change_msg = " (%s):\n" % fn
424     logs[log_name] += change_msg if change_msg else ":\n"
426   if args.inline and args.input:
427     # Get a temp filename, rather than an open filehandle, because we use
428     # the open to truncate.
429     fd, tmp = tempfile.mkstemp("tmp.XXXXXXXX")
430     os.close(fd)
432     # Copy permissions to temp file
433     # (old Pythons do not support shutil.copymode)
434     shutil.copymode(args.input, tmp)
436     # Open the temp file, clearing contents.
437     out = open(tmp, 'w')
438   else:
439     tmp = None
440     out = sys.stdout
442   # Print log
443   date = time.strftime('%Y-%m-%d')
444   bugmsg = ''
445   if len(prs):
446     bugmsg = '\n'.join(['\t' + pr for pr in prs]) + '\n'
448   for log_name, msg in sorted(logs.items()):
449     out.write("""\
452 %s  %s  <%s>
454 %s%s\n""" % (log_name, date, name, email, bugmsg, msg))
456   if args.inline:
457     # Append patch body
458     out.write(contents)
460     if args.input:
461       # Write new contents atomically
462       out.close()
463       shutil.move(tmp, args.input)
465 if __name__ == '__main__':
466     main()