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