]>
Commit | Line | Data |
---|---|---|
01f5295d YG |
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 | ||
67914693 | 239 | # Top-level comment cannot belong to function |
01f5295d YG |
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() |