Home | History | Annotate | Line # | Download | only in contrib
      1      1.1  mrg #!/usr/bin/env python3
      2      1.1  mrg 
      3  1.1.1.3  mrg # Copyright (C) 2020-2024 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 # Author: Martin Liska <mliska (at] suse.cz>
     28      1.1  mrg 
     29      1.1  mrg import argparse
     30      1.1  mrg import datetime
     31  1.1.1.3  mrg import json
     32      1.1  mrg import os
     33      1.1  mrg import re
     34      1.1  mrg import subprocess
     35      1.1  mrg import sys
     36      1.1  mrg from itertools import takewhile
     37      1.1  mrg 
     38      1.1  mrg import requests
     39      1.1  mrg 
     40      1.1  mrg from unidiff import PatchSet
     41      1.1  mrg 
     42  1.1.1.2  mrg LINE_LIMIT = 100
     43  1.1.1.2  mrg TAB_WIDTH = 8
     44  1.1.1.3  mrg 
     45  1.1.1.3  mrg # Initial commit:
     46  1.1.1.3  mrg #   +--------------------------------------------------+
     47  1.1.1.3  mrg #   | gccrs: Some title                                |
     48  1.1.1.3  mrg #   |                                                  | This is the "start"
     49  1.1.1.3  mrg #   | This is some text explaining the commit.         |
     50  1.1.1.3  mrg #   | There can be several lines.                      |
     51  1.1.1.3  mrg #   |                                                  |<------------------->
     52  1.1.1.3  mrg #   | Signed-off-by: My Name <my (at] mail.com>             | This is the "end"
     53  1.1.1.3  mrg #   +--------------------------------------------------+
     54  1.1.1.3  mrg #
     55  1.1.1.3  mrg # Results in:
     56  1.1.1.3  mrg #   +--------------------------------------------------+
     57  1.1.1.3  mrg #   | gccrs: Some title                                |
     58  1.1.1.3  mrg #   |                                                  |
     59  1.1.1.3  mrg #   | This is some text explaining the commit.         | This is the "start"
     60  1.1.1.3  mrg #   | There can be several lines.                      |
     61  1.1.1.3  mrg #   |                                                  |<------------------->
     62  1.1.1.3  mrg #   | gcc/rust/ChangeLog:                              |
     63  1.1.1.3  mrg #   |                                                  | This is the generated
     64  1.1.1.3  mrg #   |         * some_file (bla):                       | ChangeLog part
     65  1.1.1.3  mrg #   |         (foo):                                   |
     66  1.1.1.3  mrg #   |                                                  |<------------------->
     67  1.1.1.3  mrg #   | Signed-off-by: My Name <my (at] mail.com>             | This is the "end"
     68  1.1.1.3  mrg #   +--------------------------------------------------+
     69  1.1.1.3  mrg 
     70  1.1.1.3  mrg # this regex matches the first line of the "end" in the initial commit message
     71  1.1.1.3  mrg FIRST_LINE_OF_END_RE = re.compile('(?i)^(signed-off-by:|co-authored-by:|#)')
     72  1.1.1.2  mrg 
     73      1.1  mrg pr_regex = re.compile(r'(\/(\/|\*)|[Cc*!])\s+(?P<pr>PR [a-z+-]+\/[0-9]+)')
     74      1.1  mrg prnum_regex = re.compile(r'PR (?P<comp>[a-z+-]+)/(?P<num>[0-9]+)')
     75      1.1  mrg dr_regex = re.compile(r'(\/(\/|\*)|[Cc*!])\s+(?P<dr>DR [0-9]+)')
     76      1.1  mrg dg_regex = re.compile(r'{\s+dg-(error|warning)')
     77      1.1  mrg pr_filename_regex = re.compile(r'(^|[\W_])[Pp][Rr](?P<pr>\d{4,})')
     78      1.1  mrg identifier_regex = re.compile(r'^([a-zA-Z0-9_#].*)')
     79      1.1  mrg comment_regex = re.compile(r'^\/\*')
     80      1.1  mrg struct_regex = re.compile(r'^(class|struct|union|enum)\s+'
     81      1.1  mrg                           r'(GTY\(.*\)\s+)?([a-zA-Z0-9_]+)')
     82      1.1  mrg macro_regex = re.compile(r'#\s*(define|undef)\s+([a-zA-Z0-9_]+)')
     83      1.1  mrg super_macro_regex = re.compile(r'^DEF[A-Z0-9_]+\s*\(([a-zA-Z0-9_]+)')
     84      1.1  mrg fn_regex = re.compile(r'([a-zA-Z_][^()\s]*)\s*\([^*]')
     85      1.1  mrg template_and_param_regex = re.compile(r'<[^<>]*>')
     86      1.1  mrg md_def_regex = re.compile(r'\(define.*\s+"(.*)"')
     87      1.1  mrg bugzilla_url = 'https://gcc.gnu.org/bugzilla/rest.cgi/bug?id=%s&' \
     88      1.1  mrg                'include_fields=summary,component'
     89      1.1  mrg 
     90      1.1  mrg function_extensions = {'.c', '.cpp', '.C', '.cc', '.h', '.inc', '.def', '.md'}
     91      1.1  mrg 
     92      1.1  mrg # NB: Makefile.in isn't listed as it's not always generated.
     93      1.1  mrg generated_files = {'aclocal.m4', 'config.h.in', 'configure'}
     94      1.1  mrg 
     95      1.1  mrg help_message = """\
     96      1.1  mrg Generate ChangeLog template for PATCH.
     97      1.1  mrg PATCH must be generated using diff(1)'s -up or -cp options
     98      1.1  mrg (or their equivalent in git).
     99      1.1  mrg """
    100      1.1  mrg 
    101      1.1  mrg script_folder = os.path.realpath(__file__)
    102      1.1  mrg root = os.path.dirname(os.path.dirname(script_folder))
    103      1.1  mrg 
    104      1.1  mrg 
    105      1.1  mrg def find_changelog(path):
    106      1.1  mrg     folder = os.path.split(path)[0]
    107      1.1  mrg     while True:
    108      1.1  mrg         if os.path.exists(os.path.join(root, folder, 'ChangeLog')):
    109      1.1  mrg             return folder
    110      1.1  mrg         folder = os.path.dirname(folder)
    111      1.1  mrg         if folder == '':
    112      1.1  mrg             return folder
    113      1.1  mrg     raise AssertionError()
    114      1.1  mrg 
    115      1.1  mrg 
    116      1.1  mrg def extract_function_name(line):
    117      1.1  mrg     if comment_regex.match(line):
    118      1.1  mrg         return None
    119      1.1  mrg     m = struct_regex.search(line)
    120      1.1  mrg     if m:
    121      1.1  mrg         # Struct declaration
    122      1.1  mrg         return m.group(1) + ' ' + m.group(3)
    123      1.1  mrg     m = macro_regex.search(line)
    124      1.1  mrg     if m:
    125      1.1  mrg         # Macro definition
    126      1.1  mrg         return m.group(2)
    127      1.1  mrg     m = super_macro_regex.search(line)
    128      1.1  mrg     if m:
    129      1.1  mrg         # Supermacro
    130      1.1  mrg         return m.group(1)
    131      1.1  mrg     m = fn_regex.search(line)
    132      1.1  mrg     if m:
    133      1.1  mrg         # Discard template and function parameters.
    134      1.1  mrg         fn = m.group(1)
    135      1.1  mrg         fn = re.sub(template_and_param_regex, '', fn)
    136      1.1  mrg         return fn.rstrip()
    137      1.1  mrg     return None
    138      1.1  mrg 
    139      1.1  mrg 
    140      1.1  mrg def try_add_function(functions, line):
    141      1.1  mrg     fn = extract_function_name(line)
    142      1.1  mrg     if fn and fn not in functions:
    143      1.1  mrg         functions.append(fn)
    144      1.1  mrg     return bool(fn)
    145      1.1  mrg 
    146      1.1  mrg 
    147      1.1  mrg def sort_changelog_files(changed_file):
    148      1.1  mrg     return (changed_file.is_added_file, changed_file.is_removed_file)
    149      1.1  mrg 
    150      1.1  mrg 
    151      1.1  mrg def get_pr_titles(prs):
    152      1.1  mrg     output = []
    153      1.1  mrg     for idx, pr in enumerate(prs):
    154      1.1  mrg         pr_id = pr.split('/')[-1]
    155      1.1  mrg         r = requests.get(bugzilla_url % pr_id)
    156      1.1  mrg         bugs = r.json()['bugs']
    157      1.1  mrg         if len(bugs) == 1:
    158      1.1  mrg             prs[idx] = 'PR %s/%s' % (bugs[0]['component'], pr_id)
    159      1.1  mrg             out = '%s - %s\n' % (prs[idx], bugs[0]['summary'])
    160      1.1  mrg             if out not in output:
    161      1.1  mrg                 output.append(out)
    162      1.1  mrg     if output:
    163      1.1  mrg         output.append('')
    164      1.1  mrg     return '\n'.join(output)
    165      1.1  mrg 
    166      1.1  mrg 
    167  1.1.1.2  mrg def append_changelog_line(out, relative_path, text):
    168  1.1.1.2  mrg     line = f'\t* {relative_path}:'
    169  1.1.1.2  mrg     if len(line.replace('\t', ' ' * TAB_WIDTH) + ' ' + text) <= LINE_LIMIT:
    170  1.1.1.2  mrg         out += f'{line} {text}\n'
    171  1.1.1.2  mrg     else:
    172  1.1.1.2  mrg         out += f'{line}\n'
    173  1.1.1.2  mrg         out += f'\t{text}\n'
    174  1.1.1.2  mrg     return out
    175  1.1.1.2  mrg 
    176  1.1.1.2  mrg 
    177  1.1.1.2  mrg def get_rel_path_if_prefixed(path, folder):
    178  1.1.1.2  mrg     if path.startswith(folder):
    179  1.1.1.2  mrg         return path[len(folder):].lstrip('/')
    180  1.1.1.2  mrg     else:
    181  1.1.1.2  mrg         return path
    182  1.1.1.2  mrg 
    183  1.1.1.2  mrg 
    184      1.1  mrg def generate_changelog(data, no_functions=False, fill_pr_titles=False,
    185      1.1  mrg                        additional_prs=None):
    186  1.1.1.3  mrg     global prs
    187  1.1.1.3  mrg     prs = []
    188  1.1.1.3  mrg 
    189      1.1  mrg     changelogs = {}
    190      1.1  mrg     changelog_list = []
    191      1.1  mrg     out = ''
    192      1.1  mrg     diff = PatchSet(data)
    193      1.1  mrg 
    194      1.1  mrg     if additional_prs:
    195  1.1.1.2  mrg         for apr in additional_prs:
    196  1.1.1.2  mrg             if not apr.startswith('PR ') and '/' in apr:
    197  1.1.1.2  mrg                 apr = 'PR ' + apr
    198  1.1.1.2  mrg             if apr not in prs:
    199  1.1.1.2  mrg                 prs.append(apr)
    200      1.1  mrg     for file in diff:
    201      1.1  mrg         # skip files that can't be parsed
    202      1.1  mrg         if file.path == '/dev/null':
    203      1.1  mrg             continue
    204      1.1  mrg         changelog = find_changelog(file.path)
    205      1.1  mrg         if changelog not in changelogs:
    206      1.1  mrg             changelogs[changelog] = []
    207      1.1  mrg             changelog_list.append(changelog)
    208      1.1  mrg         changelogs[changelog].append(file)
    209      1.1  mrg 
    210      1.1  mrg         # Extract PR entries from newly added tests
    211      1.1  mrg         if 'testsuite' in file.path and file.is_added_file:
    212      1.1  mrg             # Only search first ten lines as later lines may
    213      1.1  mrg             # contains commented code which a note that it
    214      1.1  mrg             # has not been tested due to a certain PR or DR.
    215      1.1  mrg             this_file_prs = []
    216  1.1.1.3  mrg             hunks = list(file)
    217  1.1.1.3  mrg             if hunks:
    218  1.1.1.3  mrg                 for line in hunks[0][0:10]:
    219  1.1.1.3  mrg                     m = pr_regex.search(line.value)
    220      1.1  mrg                     if m:
    221  1.1.1.3  mrg                         pr = m.group('pr')
    222  1.1.1.3  mrg                         if pr not in prs:
    223  1.1.1.3  mrg                             prs.append(pr)
    224  1.1.1.3  mrg                             this_file_prs.append(pr.split('/')[-1])
    225  1.1.1.3  mrg                     else:
    226  1.1.1.3  mrg                         m = dr_regex.search(line.value)
    227  1.1.1.3  mrg                         if m:
    228  1.1.1.3  mrg                             dr = m.group('dr')
    229  1.1.1.3  mrg                             if dr not in prs:
    230  1.1.1.3  mrg                                 prs.append(dr)
    231  1.1.1.3  mrg                                 this_file_prs.append(dr.split('/')[-1])
    232  1.1.1.3  mrg                         elif dg_regex.search(line.value):
    233  1.1.1.3  mrg                             # Found dg-warning/dg-error line
    234  1.1.1.3  mrg                             break
    235  1.1.1.3  mrg 
    236      1.1  mrg             # PR number in the file name
    237      1.1  mrg             fname = os.path.basename(file.path)
    238      1.1  mrg             m = pr_filename_regex.search(fname)
    239      1.1  mrg             if m:
    240      1.1  mrg                 pr = m.group('pr')
    241      1.1  mrg                 pr2 = 'PR ' + pr
    242      1.1  mrg                 if pr not in this_file_prs and pr2 not in prs:
    243      1.1  mrg                     prs.append(pr2)
    244      1.1  mrg 
    245      1.1  mrg     if fill_pr_titles:
    246      1.1  mrg         out += get_pr_titles(prs)
    247      1.1  mrg 
    248      1.1  mrg     # print list of PR entries before ChangeLog entries
    249      1.1  mrg     if prs:
    250      1.1  mrg         if not out:
    251      1.1  mrg             out += '\n'
    252      1.1  mrg         for pr in prs:
    253      1.1  mrg             out += '\t%s\n' % pr
    254      1.1  mrg         out += '\n'
    255      1.1  mrg 
    256      1.1  mrg     # sort ChangeLog so that 'testsuite' is at the end
    257      1.1  mrg     for changelog in sorted(changelog_list, key=lambda x: 'testsuite' in x):
    258      1.1  mrg         files = changelogs[changelog]
    259      1.1  mrg         out += '%s:\n' % os.path.join(changelog, 'ChangeLog')
    260      1.1  mrg         out += '\n'
    261      1.1  mrg         # new and deleted files should be at the end
    262      1.1  mrg         for file in sorted(files, key=sort_changelog_files):
    263      1.1  mrg             assert file.path.startswith(changelog)
    264      1.1  mrg             in_tests = 'testsuite' in changelog or 'testsuite' in file.path
    265  1.1.1.2  mrg             relative_path = get_rel_path_if_prefixed(file.path, changelog)
    266      1.1  mrg             functions = []
    267      1.1  mrg             if file.is_added_file:
    268  1.1.1.2  mrg                 msg = 'New test.' if in_tests else 'New file.'
    269  1.1.1.2  mrg                 out = append_changelog_line(out, relative_path, msg)
    270      1.1  mrg             elif file.is_removed_file:
    271  1.1.1.2  mrg                 out = append_changelog_line(out, relative_path, 'Removed.')
    272      1.1  mrg             elif hasattr(file, 'is_rename') and file.is_rename:
    273      1.1  mrg                 # A file can be theoretically moved to a location that
    274      1.1  mrg                 # belongs to a different ChangeLog.  Let user fix it.
    275  1.1.1.2  mrg                 #
    276  1.1.1.2  mrg                 # Since unidiff 0.7.0, path.file == path.target_file[2:],
    277  1.1.1.2  mrg                 # it used to be path.source_file[2:]
    278  1.1.1.2  mrg                 relative_path = get_rel_path_if_prefixed(file.source_file[2:],
    279  1.1.1.2  mrg                                                          changelog)
    280  1.1.1.3  mrg                 out = append_changelog_line(out, relative_path, 'Move to...')
    281  1.1.1.2  mrg                 new_path = get_rel_path_if_prefixed(file.target_file[2:],
    282  1.1.1.2  mrg                                                     changelog)
    283  1.1.1.2  mrg                 out += f'\t* {new_path}: ...here.\n'
    284      1.1  mrg             elif os.path.basename(file.path) in generated_files:
    285      1.1  mrg                 out += '\t* %s: Regenerate.\n' % (relative_path)
    286  1.1.1.2  mrg                 append_changelog_line(out, relative_path, 'Regenerate.')
    287      1.1  mrg             else:
    288      1.1  mrg                 if not no_functions:
    289      1.1  mrg                     for hunk in file:
    290      1.1  mrg                         # Do not add function names for testsuite files
    291      1.1  mrg                         extension = os.path.splitext(relative_path)[1]
    292      1.1  mrg                         if not in_tests and extension in function_extensions:
    293      1.1  mrg                             last_fn = None
    294      1.1  mrg                             modified_visited = False
    295      1.1  mrg                             success = False
    296      1.1  mrg                             for line in hunk:
    297      1.1  mrg                                 m = identifier_regex.match(line.value)
    298      1.1  mrg                                 if line.is_added or line.is_removed:
    299      1.1  mrg                                     # special-case definition in .md files
    300      1.1  mrg                                     m2 = md_def_regex.match(line.value)
    301      1.1  mrg                                     if extension == '.md' and m2:
    302      1.1  mrg                                         fn = m2.group(1)
    303      1.1  mrg                                         if fn not in functions:
    304      1.1  mrg                                             functions.append(fn)
    305      1.1  mrg                                             last_fn = None
    306      1.1  mrg                                             success = True
    307      1.1  mrg 
    308      1.1  mrg                                     if not line.value.strip():
    309      1.1  mrg                                         continue
    310      1.1  mrg                                     modified_visited = True
    311      1.1  mrg                                     if m and try_add_function(functions,
    312      1.1  mrg                                                               m.group(1)):
    313      1.1  mrg                                         last_fn = None
    314      1.1  mrg                                         success = True
    315      1.1  mrg                                 elif line.is_context:
    316      1.1  mrg                                     if last_fn and modified_visited:
    317      1.1  mrg                                         try_add_function(functions, last_fn)
    318      1.1  mrg                                         last_fn = None
    319      1.1  mrg                                         modified_visited = False
    320      1.1  mrg                                         success = True
    321      1.1  mrg                                     elif m:
    322      1.1  mrg                                         last_fn = m.group(1)
    323      1.1  mrg                                         modified_visited = False
    324      1.1  mrg                             if not success:
    325      1.1  mrg                                 try_add_function(functions,
    326      1.1  mrg                                                  hunk.section_header)
    327      1.1  mrg                 if functions:
    328      1.1  mrg                     out += '\t* %s (%s):\n' % (relative_path, functions[0])
    329      1.1  mrg                     for fn in functions[1:]:
    330      1.1  mrg                         out += '\t(%s):\n' % fn
    331      1.1  mrg                 else:
    332      1.1  mrg                     out += '\t* %s:\n' % relative_path
    333      1.1  mrg         out += '\n'
    334      1.1  mrg     return out
    335      1.1  mrg 
    336      1.1  mrg 
    337      1.1  mrg def update_copyright(data):
    338      1.1  mrg     current_timestamp = datetime.datetime.now().strftime('%Y-%m-%d')
    339      1.1  mrg     username = subprocess.check_output('git config user.name', shell=True,
    340      1.1  mrg                                        encoding='utf8').strip()
    341      1.1  mrg     email = subprocess.check_output('git config user.email', shell=True,
    342      1.1  mrg                                     encoding='utf8').strip()
    343      1.1  mrg 
    344      1.1  mrg     changelogs = set()
    345      1.1  mrg     diff = PatchSet(data)
    346      1.1  mrg 
    347      1.1  mrg     for file in diff:
    348      1.1  mrg         changelog = os.path.join(find_changelog(file.path), 'ChangeLog')
    349      1.1  mrg         if changelog not in changelogs:
    350      1.1  mrg             changelogs.add(changelog)
    351      1.1  mrg             with open(changelog) as f:
    352      1.1  mrg                 content = f.read()
    353      1.1  mrg             with open(changelog, 'w+') as f:
    354      1.1  mrg                 f.write(f'{current_timestamp}  {username}  <{email}>\n\n')
    355      1.1  mrg                 f.write('\tUpdate copyright years.\n\n')
    356      1.1  mrg                 f.write(content)
    357      1.1  mrg 
    358      1.1  mrg 
    359  1.1.1.2  mrg def skip_line_in_changelog(line):
    360  1.1.1.3  mrg     return FIRST_LINE_OF_END_RE.match(line) is None
    361  1.1.1.2  mrg 
    362  1.1.1.2  mrg 
    363      1.1  mrg if __name__ == '__main__':
    364  1.1.1.3  mrg     extra_args = os.getenv('GCC_MKLOG_ARGS')
    365  1.1.1.3  mrg     if extra_args:
    366  1.1.1.3  mrg         sys.argv += json.loads(extra_args)
    367  1.1.1.3  mrg 
    368      1.1  mrg     parser = argparse.ArgumentParser(description=help_message)
    369      1.1  mrg     parser.add_argument('input', nargs='?',
    370      1.1  mrg                         help='Patch file (or missing, read standard input)')
    371      1.1  mrg     parser.add_argument('-b', '--pr-numbers', action='store',
    372      1.1  mrg                         type=lambda arg: arg.split(','), nargs='?',
    373      1.1  mrg                         help='Add the specified PRs (comma separated)')
    374      1.1  mrg     parser.add_argument('-s', '--no-functions', action='store_true',
    375      1.1  mrg                         help='Do not generate function names in ChangeLogs')
    376      1.1  mrg     parser.add_argument('-p', '--fill-up-bug-titles', action='store_true',
    377      1.1  mrg                         help='Download title of mentioned PRs')
    378      1.1  mrg     parser.add_argument('-d', '--directory',
    379      1.1  mrg                         help='Root directory where to search for ChangeLog '
    380      1.1  mrg                         'files')
    381      1.1  mrg     parser.add_argument('-c', '--changelog',
    382      1.1  mrg                         help='Append the ChangeLog to a git commit message '
    383      1.1  mrg                              'file')
    384      1.1  mrg     parser.add_argument('--update-copyright', action='store_true',
    385      1.1  mrg                         help='Update copyright in ChangeLog files')
    386  1.1.1.3  mrg     parser.add_argument('-a', '--append', action='store_true',
    387  1.1.1.3  mrg                         help='Append the generate ChangeLog to the patch file')
    388      1.1  mrg     args = parser.parse_args()
    389      1.1  mrg     if args.input == '-':
    390      1.1  mrg         args.input = None
    391      1.1  mrg     if args.directory:
    392      1.1  mrg         root = args.directory
    393      1.1  mrg 
    394  1.1.1.3  mrg     data = open(args.input, newline='\n') if args.input else sys.stdin
    395      1.1  mrg     if args.update_copyright:
    396      1.1  mrg         update_copyright(data)
    397      1.1  mrg     else:
    398      1.1  mrg         output = generate_changelog(data, args.no_functions,
    399      1.1  mrg                                     args.fill_up_bug_titles, args.pr_numbers)
    400  1.1.1.3  mrg         if args.append:
    401  1.1.1.3  mrg             if (not args.input):
    402  1.1.1.3  mrg                 raise Exception("`-a or --append` option not support standard "
    403  1.1.1.3  mrg                                 "input")
    404  1.1.1.3  mrg             lines = []
    405  1.1.1.3  mrg             with open(args.input, 'r', newline='\n') as f:
    406  1.1.1.3  mrg                 # 1 -> not find the possible start of diff log
    407  1.1.1.3  mrg                 # 2 -> find the possible start of diff log
    408  1.1.1.3  mrg                 # 3 -> finish add ChangeLog to the patch file
    409  1.1.1.3  mrg                 maybe_diff_log = 1
    410  1.1.1.3  mrg                 for line in f:
    411  1.1.1.3  mrg                     if maybe_diff_log == 1 and line == "---\n":
    412  1.1.1.3  mrg                         maybe_diff_log = 2
    413  1.1.1.3  mrg                     elif (maybe_diff_log == 2 and
    414  1.1.1.3  mrg                           re.match(r"\s[^\s]+\s+\|\s+\d+\s[+\-]+\n", line)):
    415  1.1.1.3  mrg                         lines += [output, "---\n", line]
    416  1.1.1.3  mrg                         maybe_diff_log = 3
    417  1.1.1.3  mrg                     else:
    418  1.1.1.3  mrg                         # the possible start is not the true start.
    419  1.1.1.3  mrg                         if maybe_diff_log == 2:
    420  1.1.1.3  mrg                             lines.append("---\n")
    421  1.1.1.3  mrg                             maybe_diff_log = 1
    422  1.1.1.3  mrg                         lines.append(line)
    423  1.1.1.3  mrg             with open(args.input, "w") as f:
    424  1.1.1.3  mrg                 f.writelines(lines)
    425  1.1.1.3  mrg         elif args.changelog:
    426      1.1  mrg             lines = open(args.changelog).read().split('\n')
    427  1.1.1.2  mrg             start = list(takewhile(skip_line_in_changelog, lines))
    428      1.1  mrg             end = lines[len(start):]
    429      1.1  mrg             with open(args.changelog, 'w') as f:
    430      1.1  mrg                 if not start or not start[0]:
    431  1.1.1.3  mrg                     if len(prs) == 1:
    432  1.1.1.3  mrg                         # initial commit subject line 'component: [PRnnnnn]'
    433  1.1.1.3  mrg                         m = prnum_regex.match(prs[0])
    434  1.1.1.3  mrg                         if m:
    435  1.1.1.3  mrg                             title = f'{m.group("comp")}: [PR{m.group("num")}]'
    436  1.1.1.3  mrg                             start.insert(0, title)
    437      1.1  mrg                 if start:
    438      1.1  mrg                     # append empty line
    439      1.1  mrg                     if start[-1] != '':
    440      1.1  mrg                         start.append('')
    441      1.1  mrg                 else:
    442      1.1  mrg                     # append 2 empty lines
    443      1.1  mrg                     start = 2 * ['']
    444      1.1  mrg                 f.write('\n'.join(start))
    445      1.1  mrg                 f.write('\n')
    446      1.1  mrg                 f.write(output)
    447      1.1  mrg                 f.write('\n'.join(end))
    448      1.1  mrg         else:
    449      1.1  mrg             print(output, end='')
    450