pa.c (hppa_profile_hook): Remove offset adjustment.
[official-gcc.git] / contrib / mklog
blobbe1dc3a27fc4df30004afc1963fe678fcd38cd23
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 sys
32 import re
33 import os.path
34 import os
35 import getopt
36 import tempfile
37 import time
38 import shutil
39 from subprocess import Popen, PIPE
41 me = os.path.basename(sys.argv[0])
43 def error(msg):
44   sys.stderr.write("%s: error: %s\n" % (me, msg))
45   sys.exit(1)
47 def warn(msg):
48   sys.stderr.write("%s: warning: %s\n" % (me, msg))
50 class RegexCache(object):
51   """Simple trick to Perl-like combined match-and-bind."""
53   def __init__(self):
54     self.last_match = None
56   def match(self, p, s):
57     self.last_match = re.match(p, s) if isinstance(p, str) else p.match(s)
58     return self.last_match
60   def search(self, p, s):
61     self.last_match = re.search(p, s) if isinstance(p, str) else p.search(s)
62     return self.last_match
64   def group(self, n):
65     return self.last_match.group(n)
67 cache = RegexCache()
69 def print_help_and_exit():
70     print("""\
71 Usage: %s [-i | --inline] [PATCH]
72 Generate ChangeLog template for PATCH.
73 PATCH must be generated using diff(1)'s -up or -cp options
74 (or their equivalent in Subversion/git).
76 When PATCH is - or missing, read standard input.
78 When -i is used, prepends ChangeLog to PATCH.
79 If PATCH is not stdin, modifies PATCH in-place, otherwise writes
80 to stdout.
81 """ % me)
82     sys.exit(1)
84 def run(cmd, die_on_error):
85   """Simple wrapper for Popen."""
86   proc = Popen(cmd.split(' '), stderr = PIPE, stdout = PIPE)
87   (out, err) = proc.communicate()
88   if die_on_error and proc.returncode != 0:
89     error("`%s` failed:\n" % (cmd, proc.stderr))
90   return proc.returncode, out.decode(), err
92 def read_user_info():
93   dot_mklog_format_msg = """\
94 The .mklog format is:
95 NAME = ...
96 EMAIL = ...
97 """
99   # First try to read .mklog config
100   mklog_conf = os.path.expanduser('~/.mklog')
101   if os.path.exists(mklog_conf):
102     attrs = {}
103     f = open(mklog_conf, 'rb')
104     for s in f:
105       if cache.match(r'^\s*([a-zA-Z0-9_]+)\s*=\s*(.*?)\s*$', s):
106         attrs[cache.group(1)] = cache.group(2)
107     f.close()
108     if 'NAME' not in attrs:
109       error("'NAME' not present in .mklog")
110     if 'EMAIL' not in attrs:
111       error("'EMAIL' not present in .mklog")
112     return attrs['NAME'], attrs['EMAIL']
114   # Otherwise go with git
116   rc1, name, _ = run('git config user.name', False)
117   name = name.rstrip()
118   rc2, email, _ = run('git config user.email', False)
119   email = email.rstrip()
121   if rc1 != 0 or rc2 != 0:
122     error("""\
123 Could not read git user.name and user.email settings.
124 Please add missing git settings, or create a %s.
125 """ % mklog_conf)
127   return name, email
129 def get_parent_changelog (s):
130   """See which ChangeLog this file change should go to."""
132   if s.find('\\') == -1 and s.find('/') == -1:
133     return "ChangeLog", s
135   gcc_root = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
137   d = s
138   while d:
139     clname = d + "/ChangeLog"
140     if os.path.exists(gcc_root + '/' + clname) or os.path.exists(clname):
141       relname = s[len(d)+1:]
142       return clname, relname
143     d, _ = os.path.split(d)
145   return "Unknown ChangeLog", s
147 class FileDiff:
148   """Class to represent changes in a single file."""
150   def __init__(self, filename):
151     self.filename = filename
152     self.hunks = []
153     self.clname, self.relname = get_parent_changelog(filename);
155   def dump(self):
156     print("Diff for %s:\n  ChangeLog = %s\n  rel name = %s\n" % (self.filename, self.clname, self.relname))
157     for i, h in enumerate(self.hunks):
158       print("Next hunk %d:" % i)
159       h.dump()
161 class Hunk:
162   """Class to represent a single hunk of changes."""
164   def __init__(self, hdr):
165     self.hdr = hdr
166     self.lines = []
167     self.ctx_diff = is_ctx_hunk_start(hdr)
169   def dump(self):
170     print('%s' % self.hdr)
171     print('%s' % '\n'.join(self.lines))
173   def is_file_addition(self):
174     """Does hunk describe addition of file?"""
175     if self.ctx_diff:
176       for line in self.lines:
177         if re.match(r'^\*\*\* 0 \*\*\*\*', line):
178           return True
179     else:
180       return re.match(r'^@@ -0,0 \+1.* @@', self.hdr)
182   def is_file_removal(self):
183     """Does hunk describe removal of file?"""
184     if self.ctx_diff:
185       for line in self.lines:
186         if re.match(r'^--- 0 ----', line):
187           return True
188     else:
189       return re.match(r'^@@ -1.* \+0,0 @@', self.hdr)
191 def is_file_diff_start(s):
192   # Don't be fooled by context diff line markers:
193   #   *** 385,391 ****
194   return ((s.startswith('***') and not s.endswith('***'))
195           or (s.startswith('---') and not s.endswith('---')))
197 def is_ctx_hunk_start(s):
198   return re.match(r'^\*\*\*\*\*\**', s)
200 def is_uni_hunk_start(s):
201   return re.match(r'^@@ .* @@', s)
203 def is_hunk_start(s):
204   return is_ctx_hunk_start(s) or is_uni_hunk_start(s)
206 def remove_suffixes(s):
207   if s.startswith('a/') or s.startswith('b/'):
208     s = s[2:]
209   if s.endswith('.jj'):
210     s = s[:-3]
211   return s
213 def find_changed_funs(hunk):
214   """Find all functions touched by hunk.  We don't try too hard
215      to find good matches.  This should return a superset
216      of the actual set of functions in the .diff file.
217   """
219   fns = []
220   fn = None
222   if (cache.match(r'^\*\*\*\*\*\** ([a-zA-Z0-9_].*)', hunk.hdr)
223       or cache.match(r'^@@ .* @@ ([a-zA-Z0-9_].*)', hunk.hdr)):
224     fn = cache.group(1)
226   for i, line in enumerate(hunk.lines):
227     # Context diffs have extra whitespace after first char;
228     # remove it to make matching easier.
229     if hunk.ctx_diff:
230       line = re.sub(r'^([-+! ]) ', r'\1', line)
232     # Remember most recent identifier in hunk
233     # that might be a function name.
234     if cache.match(r'^[-+! ]([a-zA-Z0-9_#].*)', line):
235       fn = cache.group(1)
237     change = line and re.match(r'^[-+!][^-]', line)
239     # Top-level comment cannot belong to function
240     if re.match(r'^[-+! ]\/\*', line):
241       fn = None
243     if change and fn:
244       if cache.match(r'^((class|struct|union|enum)\s+[a-zA-Z0-9_]+)', fn):
245         # Struct declaration
246         fn = cache.group(1)
247       elif cache.search(r'#\s*define\s+([a-zA-Z0-9_]+)', fn):
248         # Macro definition
249         fn = cache.group(1)
250       elif cache.match('^DEF[A-Z0-9_]+\s*\(([a-zA-Z0-9_]+)', fn):
251         # Supermacro
252         fn = cache.group(1)
253       elif cache.search(r'([a-zA-Z_][^()\s]*)\s*\([^*]', fn):
254         # Discard template and function parameters.
255         fn = cache.group(1)
256         fn = re.sub(r'<[^<>]*>', '', fn)
257         fn = fn.rstrip()
258       else:
259         fn = None
261       if fn and fn not in fns:  # Avoid dups
262         fns.append(fn)
264       fn = None
266   return fns
268 def parse_patch(contents):
269   """Parse patch contents to a sequence of FileDiffs."""
271   diffs = []
273   lines = contents.split('\n')
275   i = 0
276   while i < len(lines):
277     line = lines[i]
279     # Diff headers look like
280     #   --- a/gcc/tree.c
281     #   +++ b/gcc/tree.c
282     # or
283     #   *** gcc/cfgexpand.c     2013-12-25 20:07:24.800350058 +0400
284     #   --- gcc/cfgexpand.c     2013-12-25 20:06:30.612350178 +0400
286     if is_file_diff_start(line):
287       left = re.split(r'\s+', line)[1]
288     else:
289       i += 1
290       continue
292     left = remove_suffixes(left);
294     i += 1
295     line = lines[i]
297     if not cache.match(r'^[+-][+-][+-] +(\S+)', line):
298       error("expected filename in line %d" % i)
299     right = remove_suffixes(cache.group(1));
301     # Extract real file name from left and right names.
302     filename = None
303     if left == right:
304       filename = left
305     elif left == '/dev/null':
306       filename = right;
307     elif right == '/dev/null':
308       filename = left;
309     else:
310       comps = []
311       while left and right:
312         left, l = os.path.split(left)
313         right, r = os.path.split(right)
314         if l != r:
315           break
316         comps.append(l)
317     
318       if not comps:
319         error("failed to extract common name for %s and %s" % (left, right))
321       comps.reverse()
322       filename = '/'.join(comps)
324     d = FileDiff(filename)
325     diffs.append(d)
327     # Collect hunks for current file.
328     hunk = None
329     i += 1
330     while i < len(lines):
331       line = lines[i]
333       # Create new hunk when we see hunk header
334       if is_hunk_start(line):
335         if hunk is not None:
336           d.hunks.append(hunk)
337         hunk = Hunk(line)
338         i += 1
339         continue
341       # Stop when we reach next diff
342       if (is_file_diff_start(line)
343           or line.startswith('diff ')
344           or line.startswith('Index: ')):
345         i -= 1
346         break
348       if hunk is not None:
349         hunk.lines.append(line)
350       i += 1
352     d.hunks.append(hunk)
354   return diffs
356 def main():
357   name, email = read_user_info()
359   try:
360     opts, args = getopt.getopt(sys.argv[1:], 'hiv', ['help', 'verbose', 'inline'])
361   except getopt.GetoptError as err:
362     error(str(err))
364   inline = False
365   verbose = 0
367   for o, a in opts:
368     if o in ('-h', '--help'):
369       print_help_and_exit()
370     elif o in ('-i', '--inline'):
371       inline = True
372     elif o in ('-v', '--verbose'):
373       verbose += 1
374     else:
375       assert False, "unhandled option"
377   if len(args) == 0:
378     args = ['-']
380   if len(args) == 1 and args[0] == '-':
381     input = sys.stdin
382   elif len(args) == 1:
383     input = open(args[0])
384   else:
385     error("too many arguments; for more details run with -h")
387   contents = input.read()
388   diffs = parse_patch(contents)
390   if verbose:
391     print("Parse results:")
392     for d in diffs:
393       d.dump()
395   # Generate template ChangeLog.
397   logs = {}
398   for d in diffs:
399     log_name = d.clname
401     logs.setdefault(log_name, '')
402     logs[log_name] += '\t* %s' % d.relname
404     change_msg = ''
406     # Check if file was removed or added.
407     # Two patterns for context and unified diff.
408     if len(d.hunks) == 1:
409       hunk0 = d.hunks[0]
410       if hunk0.is_file_addition():
411         if re.search(r'testsuite.*(?<!\.exp)$', d.filename):
412           change_msg = ': New test.\n'
413         else:
414           change_msg = ": New file.\n"
415       elif hunk0.is_file_removal():
416         change_msg = ": Remove.\n"
418     _, ext = os.path.splitext(d.filename)
419     if not change_msg and ext in ['.c', '.cpp', '.C', '.cc', '.h', '.inc', '.def']:
420       fns = []
421       for hunk in d.hunks:
422         for fn in find_changed_funs(hunk):
423           if fn not in fns:
424             fns.append(fn)
426       for fn in fns:
427         if change_msg:
428           change_msg += "\t(%s):\n" % fn
429         else:
430           change_msg = " (%s):\n" % fn
432     logs[log_name] += change_msg if change_msg else ":\n"
434   if inline and args[0] != '-':
435     # Get a temp filename, rather than an open filehandle, because we use
436     # the open to truncate.
437     fd, tmp = tempfile.mkstemp("tmp.XXXXXXXX")
438     os.close(fd)
440     # Copy permissions to temp file
441     # (old Pythons do not support shutil.copymode)
442     shutil.copymode(args[0], tmp)
444     # Open the temp file, clearing contents.
445     out = open(tmp, 'w')
446   else:
447     tmp = None
448     out = sys.stdout
450   # Print log
451   date = time.strftime('%Y-%m-%d')
452   for log_name, msg in sorted(logs.items()):
453     out.write("""\
456 %s  %s  <%s>
458 %s\n""" % (log_name, date, name, email, msg))
460   if inline:
461     # Append patch body
462     out.write(contents)
464     if args[0] != '-':
465       # Write new contents atomically
466       out.close()
467       shutil.move(tmp, args[0])
469 if __name__ == '__main__':
470     main()