1"""This script renames the forthcoming section in changelog files with the upcoming version and the current date."""
2
3from __future__ import print_function
4
5import argparse
6import datetime
7import os
8import re
9import sys
10
11from catkin_pkg.changelog import CHANGELOG_FILENAME, get_changelog_from_path
12from catkin_pkg.changelog_generator import FORTHCOMING_LABEL
13from catkin_pkg.package_version import bump_version
14from catkin_pkg.packages import find_packages, verify_equal_package_versions
15
16import docutils.core
17
18
19def get_forthcoming_label(rst):
20    document = docutils.core.publish_doctree(rst)
21    forthcoming_label = None
22    for child in document.children:
23        title = None
24        if isinstance(child, docutils.nodes.subtitle):
25            title = child
26        elif isinstance(child, docutils.nodes.section):
27            section = child
28            if len(section.children) > 0 and isinstance(section.children[0], docutils.nodes.title):
29                title = section.children[0]
30        if title and len(title.children) > 0 and isinstance(title.children[0], docutils.nodes.Text):
31            title_text = title.children[0].rawsource
32            if FORTHCOMING_LABEL.lower() in title_text.lower():
33                if forthcoming_label:
34                    raise RuntimeError('Found multiple forthcoming sections')
35                forthcoming_label = title_text
36    return forthcoming_label
37
38
39def rename_section(data, old_label, new_label):
40    valid_section_characters = '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~'
41
42    def replace_section(match):
43        section_char = match.group(2)[0]
44        return new_label + '\n' + section_char * len(new_label)
45    pattern = '^(' + re.escape(old_label) + ')\n([' + re.escape(valid_section_characters) + ']+)$'
46    data, count = re.subn(pattern, replace_section, data, flags=re.MULTILINE)
47    if count == 0:
48        raise RuntimeError('Could not find section')
49    if count > 1:
50        raise RuntimeError('Found multiple matching sections')
51    return data
52
53
54def main(sysargs=None):
55    parser = argparse.ArgumentParser(description='Tag the forthcoming section in the changelog files with an upcoming version number')
56    parser.add_argument('--bump', choices=('major', 'minor', 'patch'), default='patch', help='Which part of the version number to bump? (default: %(default)s)')
57    args = parser.parse_args(sysargs)
58
59    base_path = '.'
60
61    # find packages
62    packages = find_packages(base_path)
63    if not packages:
64        raise RuntimeError('No packages found')
65    print('Found packages: %s' % ', '.join([p.name for p in packages.values()]))
66
67    # fetch current version and verify that all packages have same version number
68    old_version = verify_equal_package_versions(packages.values())
69    new_version = bump_version(old_version, args.bump)
70    print('Tag version %s' % new_version)
71
72    # check for changelog entries
73    changelogs = []
74    missing_forthcoming = []
75    already_tagged = []
76    for pkg_path, package in packages.items():
77        changelog_path = os.path.join(base_path, pkg_path, CHANGELOG_FILENAME)
78        if not os.path.exists(changelog_path):
79            missing_forthcoming.append(package.name)
80            continue
81        changelog = get_changelog_from_path(changelog_path, package.name)
82        if not changelog:
83            missing_forthcoming.append(package.name)
84            continue
85        # check that forthcoming section exists
86        forthcoming_label = get_forthcoming_label(changelog.rst)
87        if not forthcoming_label:
88            missing_forthcoming.append(package.name)
89            continue
90        # check that new_version section does not exist yet
91        try:
92            changelog.get_content_of_version(new_version)
93            already_tagged.append(package.name)
94            continue
95        except KeyError:
96            pass
97        changelogs.append((package.name, changelog_path, changelog, forthcoming_label))
98    if missing_forthcoming:
99        print('The following packages do not have a forthcoming section in their changelog file: %s' % ', '.join(sorted(missing_forthcoming)), file=sys.stderr)
100    if already_tagged:
101        print("The following packages do already have a section '%s' in their changelog file: %s" % (new_version, ', '.join(sorted(already_tagged))), file=sys.stderr)
102
103    # rename forthcoming sections to new_version including current date
104    new_changelog_data = []
105    new_label = '%s (%s)' % (new_version, datetime.date.today().isoformat())
106    for (pkg_name, changelog_path, changelog, forthcoming_label) in changelogs:
107        print("Renaming section '%s' to '%s' in package '%s'..." % (forthcoming_label, new_label, pkg_name))
108        data = rename_section(changelog.rst, forthcoming_label, new_label)
109        new_changelog_data.append((changelog_path, data))
110
111    print('Writing updated changelog files...')
112    for (changelog_path, data) in new_changelog_data:
113        with open(changelog_path, 'wb') as f:
114            f.write(data.encode('utf-8'))
115