mklog.py revision 1.1 1 1.1 christos #!/usr/bin/env python3
2 1.1 christos
3 1.1 christos # Copyright (C) 2020 Free Software Foundation, Inc.
4 1.1 christos #
5 1.1 christos # This file is part of GCC.
6 1.1 christos #
7 1.1 christos # GCC is free software; you can redistribute it and/or modify
8 1.1 christos # it under the terms of the GNU General Public License as published by
9 1.1 christos # the Free Software Foundation; either version 3, or (at your option)
10 1.1 christos # any later version.
11 1.1 christos #
12 1.1 christos # GCC is distributed in the hope that it will be useful,
13 1.1 christos # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 1.1 christos # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 1.1 christos # GNU General Public License for more details.
16 1.1 christos #
17 1.1 christos # You should have received a copy of the GNU General Public License
18 1.1 christos # along with GCC; see the file COPYING. If not, write to
19 1.1 christos # the Free Software Foundation, 51 Franklin Street, Fifth Floor,
20 1.1 christos # Boston, MA 02110-1301, USA.
21 1.1 christos
22 1.1 christos # This script parses a .diff file generated with 'diff -up' or 'diff -cp'
23 1.1 christos # and adds a skeleton ChangeLog file to the file. It does not try to be
24 1.1 christos # too smart when parsing function names, but it produces a reasonable
25 1.1 christos # approximation.
26 1.1 christos #
27 1.1 christos # Author: Martin Liska <mliska (at] suse.cz>
28 1.1 christos
29 1.1 christos import argparse
30 1.1 christos import os
31 1.1 christos import re
32 1.1 christos import sys
33 1.1 christos from itertools import takewhile
34 1.1 christos
35 1.1 christos import requests
36 1.1 christos
37 1.1 christos from unidiff import PatchSet
38 1.1 christos
39 1.1 christos pr_regex = re.compile(r'(\/(\/|\*)|[Cc*!])\s+(?P<pr>PR [a-z+-]+\/[0-9]+)')
40 1.1 christos dr_regex = re.compile(r'(\/(\/|\*)|[Cc*!])\s+(?P<dr>DR [0-9]+)')
41 1.1 christos identifier_regex = re.compile(r'^([a-zA-Z0-9_#].*)')
42 1.1 christos comment_regex = re.compile(r'^\/\*')
43 1.1 christos struct_regex = re.compile(r'^(class|struct|union|enum)\s+'
44 1.1 christos r'(GTY\(.*\)\s+)?([a-zA-Z0-9_]+)')
45 1.1 christos macro_regex = re.compile(r'#\s*(define|undef)\s+([a-zA-Z0-9_]+)')
46 1.1 christos super_macro_regex = re.compile(r'^DEF[A-Z0-9_]+\s*\(([a-zA-Z0-9_]+)')
47 1.1 christos fn_regex = re.compile(r'([a-zA-Z_][^()\s]*)\s*\([^*]')
48 1.1 christos template_and_param_regex = re.compile(r'<[^<>]*>')
49 1.1 christos bugzilla_url = 'https://gcc.gnu.org/bugzilla/rest.cgi/bug?id=%s&' \
50 1.1 christos 'include_fields=summary'
51 1.1 christos
52 1.1 christos function_extensions = set(['.c', '.cpp', '.C', '.cc', '.h', '.inc', '.def'])
53 1.1 christos
54 1.1 christos help_message = """\
55 1.1 christos Generate ChangeLog template for PATCH.
56 1.1 christos PATCH must be generated using diff(1)'s -up or -cp options
57 1.1 christos (or their equivalent in git).
58 1.1 christos """
59 1.1 christos
60 1.1 christos script_folder = os.path.realpath(__file__)
61 1.1 christos gcc_root = os.path.dirname(os.path.dirname(script_folder))
62 1.1 christos
63 1.1 christos
64 1.1 christos def find_changelog(path):
65 1.1 christos folder = os.path.split(path)[0]
66 1.1 christos while True:
67 1.1 christos if os.path.exists(os.path.join(gcc_root, folder, 'ChangeLog')):
68 1.1 christos return folder
69 1.1 christos folder = os.path.dirname(folder)
70 1.1 christos if folder == '':
71 1.1 christos return folder
72 1.1 christos raise AssertionError()
73 1.1 christos
74 1.1 christos
75 1.1 christos def extract_function_name(line):
76 1.1 christos if comment_regex.match(line):
77 1.1 christos return None
78 1.1 christos m = struct_regex.search(line)
79 1.1 christos if m:
80 1.1 christos # Struct declaration
81 1.1 christos return m.group(1) + ' ' + m.group(3)
82 1.1 christos m = macro_regex.search(line)
83 1.1 christos if m:
84 1.1 christos # Macro definition
85 1.1 christos return m.group(2)
86 1.1 christos m = super_macro_regex.search(line)
87 1.1 christos if m:
88 1.1 christos # Supermacro
89 1.1 christos return m.group(1)
90 1.1 christos m = fn_regex.search(line)
91 1.1 christos if m:
92 1.1 christos # Discard template and function parameters.
93 1.1 christos fn = m.group(1)
94 1.1 christos fn = re.sub(template_and_param_regex, '', fn)
95 1.1 christos return fn.rstrip()
96 1.1 christos return None
97 1.1 christos
98 1.1 christos
99 1.1 christos def try_add_function(functions, line):
100 1.1 christos fn = extract_function_name(line)
101 1.1 christos if fn and fn not in functions:
102 1.1 christos functions.append(fn)
103 1.1 christos return bool(fn)
104 1.1 christos
105 1.1 christos
106 1.1 christos def sort_changelog_files(changed_file):
107 1.1 christos return (changed_file.is_added_file, changed_file.is_removed_file)
108 1.1 christos
109 1.1 christos
110 1.1 christos def get_pr_titles(prs):
111 1.1 christos output = ''
112 1.1 christos for pr in prs:
113 1.1 christos id = pr.split('/')[-1]
114 1.1 christos r = requests.get(bugzilla_url % id)
115 1.1 christos bugs = r.json()['bugs']
116 1.1 christos if len(bugs) == 1:
117 1.1 christos output += '%s - %s\n' % (pr, bugs[0]['summary'])
118 1.1 christos print(output)
119 1.1 christos if output:
120 1.1 christos output += '\n'
121 1.1 christos return output
122 1.1 christos
123 1.1 christos
124 1.1 christos def generate_changelog(data, no_functions=False, fill_pr_titles=False):
125 1.1 christos changelogs = {}
126 1.1 christos changelog_list = []
127 1.1 christos prs = []
128 1.1 christos out = ''
129 1.1 christos diff = PatchSet(data)
130 1.1 christos
131 1.1 christos for file in diff:
132 1.1 christos changelog = find_changelog(file.path)
133 1.1 christos if changelog not in changelogs:
134 1.1 christos changelogs[changelog] = []
135 1.1 christos changelog_list.append(changelog)
136 1.1 christos changelogs[changelog].append(file)
137 1.1 christos
138 1.1 christos # Extract PR entries from newly added tests
139 1.1 christos if 'testsuite' in file.path and file.is_added_file:
140 1.1 christos for line in list(file)[0]:
141 1.1 christos m = pr_regex.search(line.value)
142 1.1 christos if m:
143 1.1 christos pr = m.group('pr')
144 1.1 christos if pr not in prs:
145 1.1 christos prs.append(pr)
146 1.1 christos else:
147 1.1 christos m = dr_regex.search(line.value)
148 1.1 christos if m:
149 1.1 christos dr = m.group('dr')
150 1.1 christos if dr not in prs:
151 1.1 christos prs.append(dr)
152 1.1 christos else:
153 1.1 christos break
154 1.1 christos
155 1.1 christos if fill_pr_titles:
156 1.1 christos out += get_pr_titles(prs)
157 1.1 christos
158 1.1 christos # sort ChangeLog so that 'testsuite' is at the end
159 1.1 christos for changelog in sorted(changelog_list, key=lambda x: 'testsuite' in x):
160 1.1 christos files = changelogs[changelog]
161 1.1 christos out += '%s:\n' % os.path.join(changelog, 'ChangeLog')
162 1.1 christos out += '\n'
163 1.1 christos for pr in prs:
164 1.1 christos out += '\t%s\n' % pr
165 1.1 christos # new and deleted files should be at the end
166 1.1 christos for file in sorted(files, key=sort_changelog_files):
167 1.1 christos assert file.path.startswith(changelog)
168 1.1 christos in_tests = 'testsuite' in changelog or 'testsuite' in file.path
169 1.1 christos relative_path = file.path[len(changelog):].lstrip('/')
170 1.1 christos functions = []
171 1.1 christos if file.is_added_file:
172 1.1 christos msg = 'New test' if in_tests else 'New file'
173 1.1 christos out += '\t* %s: %s.\n' % (relative_path, msg)
174 1.1 christos elif file.is_removed_file:
175 1.1 christos out += '\t* %s: Removed.\n' % (relative_path)
176 1.1 christos elif hasattr(file, 'is_rename') and file.is_rename:
177 1.1 christos out += '\t* %s: Moved to...\n' % (relative_path)
178 1.1 christos new_path = file.target_file[2:]
179 1.1 christos # A file can be theoretically moved to a location that
180 1.1 christos # belongs to a different ChangeLog. Let user fix it.
181 1.1 christos if new_path.startswith(changelog):
182 1.1 christos new_path = new_path[len(changelog):].lstrip('/')
183 1.1 christos out += '\t* %s: ...here.\n' % (new_path)
184 1.1 christos else:
185 1.1 christos if not no_functions:
186 1.1 christos for hunk in file:
187 1.1 christos # Do not add function names for testsuite files
188 1.1 christos extension = os.path.splitext(relative_path)[1]
189 1.1 christos if not in_tests and extension in function_extensions:
190 1.1 christos last_fn = None
191 1.1 christos modified_visited = False
192 1.1 christos success = False
193 1.1 christos for line in hunk:
194 1.1 christos m = identifier_regex.match(line.value)
195 1.1 christos if line.is_added or line.is_removed:
196 1.1 christos if not line.value.strip():
197 1.1 christos continue
198 1.1 christos modified_visited = True
199 1.1 christos if m and try_add_function(functions,
200 1.1 christos m.group(1)):
201 1.1 christos last_fn = None
202 1.1 christos success = True
203 1.1 christos elif line.is_context:
204 1.1 christos if last_fn and modified_visited:
205 1.1 christos try_add_function(functions, last_fn)
206 1.1 christos last_fn = None
207 1.1 christos modified_visited = False
208 1.1 christos success = True
209 1.1 christos elif m:
210 1.1 christos last_fn = m.group(1)
211 1.1 christos modified_visited = False
212 1.1 christos if not success:
213 1.1 christos try_add_function(functions,
214 1.1 christos hunk.section_header)
215 1.1 christos if functions:
216 1.1 christos out += '\t* %s (%s):\n' % (relative_path, functions[0])
217 1.1 christos for fn in functions[1:]:
218 1.1 christos out += '\t(%s):\n' % fn
219 1.1 christos else:
220 1.1 christos out += '\t* %s:\n' % relative_path
221 1.1 christos out += '\n'
222 1.1 christos return out
223 1.1 christos
224 1.1 christos
225 1.1 christos if __name__ == '__main__':
226 1.1 christos parser = argparse.ArgumentParser(description=help_message)
227 1.1 christos parser.add_argument('input', nargs='?',
228 1.1 christos help='Patch file (or missing, read standard input)')
229 1.1 christos parser.add_argument('-s', '--no-functions', action='store_true',
230 1.1 christos help='Do not generate function names in ChangeLogs')
231 1.1 christos parser.add_argument('-p', '--fill-up-bug-titles', action='store_true',
232 1.1 christos help='Download title of mentioned PRs')
233 1.1 christos parser.add_argument('-c', '--changelog',
234 1.1 christos help='Append the ChangeLog to a git commit message '
235 1.1 christos 'file')
236 1.1 christos args = parser.parse_args()
237 1.1 christos if args.input == '-':
238 1.1 christos args.input = None
239 1.1 christos
240 1.1 christos input = open(args.input) if args.input else sys.stdin
241 1.1 christos data = input.read()
242 1.1 christos output = generate_changelog(data, args.no_functions,
243 1.1 christos args.fill_up_bug_titles)
244 1.1 christos if args.changelog:
245 1.1 christos lines = open(args.changelog).read().split('\n')
246 1.1 christos start = list(takewhile(lambda l: not l.startswith('#'), lines))
247 1.1 christos end = lines[len(start):]
248 1.1 christos with open(args.changelog, 'w') as f:
249 1.1 christos if start:
250 1.1 christos # appent empty line
251 1.1 christos if start[-1] != '':
252 1.1 christos start.append('')
253 1.1 christos else:
254 1.1 christos # append 2 empty lines
255 1.1 christos start = 2 * ['']
256 1.1 christos f.write('\n'.join(start))
257 1.1 christos f.write('\n')
258 1.1 christos f.write(output)
259 1.1 christos f.write('\n'.join(end))
260 1.1 christos else:
261 1.1 christos print(output, end='')
262