PR c/53037
[official-gcc.git] / contrib / mklog
blob0622d2e2e3db9019687941d22a7d3648ec197d23
1 #!/usr/bin/python
3 # Copyright (C) 2017 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, 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.
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 can not 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)
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, 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], 'rb')
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, 'wb')
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.iteritems()):
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()