]> git.ipfire.org Git - thirdparty/gcc.git/blob - contrib/mklog
Update gennews for GCC 14.
[thirdparty/gcc.git] / contrib / mklog
1 #!/usr/bin/python
2
3 # Copyright (C) 2017 Free Software Foundation, Inc.
4 #
5 # This file is part of GCC.
6 #
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.
11 #
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.
16 #
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.
21
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.
26 #
27 # This is a straightforward adaptation of original Perl script.
28 #
29 # Author: Yury Gribov <tetra2005@gmail.com>
30
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
40
41 me = os.path.basename(sys.argv[0])
42
43 def error(msg):
44 sys.stderr.write("%s: error: %s\n" % (me, msg))
45 sys.exit(1)
46
47 def warn(msg):
48 sys.stderr.write("%s: warning: %s\n" % (me, msg))
49
50 class RegexCache(object):
51 """Simple trick to Perl-like combined match-and-bind."""
52
53 def __init__(self):
54 self.last_match = None
55
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
59
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
63
64 def group(self, n):
65 return self.last_match.group(n)
66
67 cache = RegexCache()
68
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).
75
76 When PATCH is - or missing, read standard input.
77
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)
83
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
91
92 def read_user_info():
93 dot_mklog_format_msg = """\
94 The .mklog format is:
95 NAME = ...
96 EMAIL = ...
97 """
98
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']
113
114 # Otherwise go with git
115
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()
120
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)
126
127 return name, email
128
129 def get_parent_changelog (s):
130 """See which ChangeLog this file change should go to."""
131
132 if s.find('\\') == -1 and s.find('/') == -1:
133 return "ChangeLog", s
134
135 gcc_root = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
136
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)
144
145 return "Unknown ChangeLog", s
146
147 class FileDiff:
148 """Class to represent changes in a single file."""
149
150 def __init__(self, filename):
151 self.filename = filename
152 self.hunks = []
153 self.clname, self.relname = get_parent_changelog(filename);
154
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()
160
161 class Hunk:
162 """Class to represent a single hunk of changes."""
163
164 def __init__(self, hdr):
165 self.hdr = hdr
166 self.lines = []
167 self.ctx_diff = is_ctx_hunk_start(hdr)
168
169 def dump(self):
170 print '%s' % self.hdr
171 print '%s' % '\n'.join(self.lines)
172
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)
181
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)
190
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('---')))
196
197 def is_ctx_hunk_start(s):
198 return re.match(r'^\*\*\*\*\*\**', s)
199
200 def is_uni_hunk_start(s):
201 return re.match(r'^@@ .* @@', s)
202
203 def is_hunk_start(s):
204 return is_ctx_hunk_start(s) or is_uni_hunk_start(s)
205
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
212
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 """
218
219 fns = []
220 fn = None
221
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)
225
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)
231
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)
236
237 change = line and re.match(r'^[-+!][^-]', line)
238
239 # Top-level comment cannot belong to function
240 if re.match(r'^[-+! ]\/\*', line):
241 fn = None
242
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
260
261 if fn and fn not in fns: # Avoid dups
262 fns.append(fn)
263
264 fn = None
265
266 return fns
267
268 def parse_patch(contents):
269 """Parse patch contents to a sequence of FileDiffs."""
270
271 diffs = []
272
273 lines = contents.split('\n')
274
275 i = 0
276 while i < len(lines):
277 line = lines[i]
278
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
285
286 if is_file_diff_start(line):
287 left = re.split(r'\s+', line)[1]
288 else:
289 i += 1
290 continue
291
292 left = remove_suffixes(left);
293
294 i += 1
295 line = lines[i]
296
297 if not cache.match(r'^[+-][+-][+-] +(\S+)', line):
298 error("expected filename in line %d" % i)
299 right = remove_suffixes(cache.group(1));
300
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))
320
321 comps.reverse()
322 filename = '/'.join(comps)
323
324 d = FileDiff(filename)
325 diffs.append(d)
326
327 # Collect hunks for current file.
328 hunk = None
329 i += 1
330 while i < len(lines):
331 line = lines[i]
332
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
340
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
347
348 if hunk is not None:
349 hunk.lines.append(line)
350 i += 1
351
352 d.hunks.append(hunk)
353
354 return diffs
355
356 def main():
357 name, email = read_user_info()
358
359 try:
360 opts, args = getopt.getopt(sys.argv[1:], 'hiv', ['help', 'verbose', 'inline'])
361 except getopt.GetoptError, err:
362 error(str(err))
363
364 inline = False
365 verbose = 0
366
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"
376
377 if len(args) == 0:
378 args = ['-']
379
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")
386
387 contents = input.read()
388 diffs = parse_patch(contents)
389
390 if verbose:
391 print "Parse results:"
392 for d in diffs:
393 d.dump()
394
395 # Generate template ChangeLog.
396
397 logs = {}
398 for d in diffs:
399 log_name = d.clname
400
401 logs.setdefault(log_name, '')
402 logs[log_name] += '\t* %s' % d.relname
403
404 change_msg = ''
405
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"
417
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)
425
426 for fn in fns:
427 if change_msg:
428 change_msg += "\t(%s):\n" % fn
429 else:
430 change_msg = " (%s):\n" % fn
431
432 logs[log_name] += change_msg if change_msg else ":\n"
433
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)
439
440 # Copy permissions to temp file
441 # (old Pythons do not support shutil.copymode)
442 shutil.copymode(args[0], tmp)
443
444 # Open the temp file, clearing contents.
445 out = open(tmp, 'wb')
446 else:
447 tmp = None
448 out = sys.stdout
449
450 # Print log
451 date = time.strftime('%Y-%m-%d')
452 for log_name, msg in sorted(logs.iteritems()):
453 out.write("""\
454 %s:
455
456 %s %s <%s>
457
458 %s\n""" % (log_name, date, name, email, msg))
459
460 if inline:
461 # Append patch body
462 out.write(contents)
463
464 if args[0] != '-':
465 # Write new contents atomically
466 out.close()
467 shutil.move(tmp, args[0])
468
469 if __name__ == '__main__':
470 main()