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