Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Markdown format in prepare and generate changelog commands #347

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 98 additions & 17 deletions src/catkin_pkg/changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@

log = logging.getLogger('changelog')

CHANGELOG_FILENAME = 'CHANGELOG.rst'
CHANGELOG_FILE_TYPES = ['rst', 'md']
CHANGELOG_EXTENSIONS = {file_type: ".%s" % file_type for file_type in CHANGELOG_FILE_TYPES}
CHANGELOG_FILENAME = 'CHANGELOG'

example_rst = """\
^^^^^^^^^^^^^^^^^^^^^^^^^
Expand Down Expand Up @@ -116,6 +118,54 @@
0.0
===

0.0.1 (2012-01-31)
------------------

1. Initial release
2. Initial bugs
"""

example_md = """\
## Changelog for package foo

0.1.27 (forthcoming)
--------------------
* Great new feature

0.1.26 (2012-12-26)
-------------------
* Utilizes caching to improve query performance (fix https://github.com/ros/ros_comm/pull/2)
* Simplified API calls based on (https://github.com/ros/robot_model):

* Note that these changes are based on REP 192
* Also they fix a problem related to initialization

* Fixed synchronization issue on startup

.. not mentioning secret feature on purpose

0.1.25 (2012-11-25)
-------------------

- Added thread safety
- Replaced custom XML parser with `TinyXML <http://www.grinninglizard.com/tinyxml/>`_.
- Fixed regression introduced in 0.1.22
- New syntax for foo::

foo('bar')

- Added a safety check for XML parsing

----

The library should now compile under ``Win32``

0.1.0 (2012-10-01)
------------------

*First* public **stable** release


0.0.1 (2012-01-31)
------------------

Expand Down Expand Up @@ -191,30 +241,39 @@ def get_changelog_from_path(path, package_name=None):
:returns: ``Changelog`` changelog class or None if file was not readable
"""
changelog = Changelog(package_name)
if os.path.isdir(path):
path = os.path.join(path, CHANGELOG_FILENAME)
if os.path.isfile(path):
changelog.file_path = path
else:
for changelog_filename in [CHANGELOG_FILENAME+extension for extension in CHANGELOG_EXTENSIONS.values()]:
changelog_path = os.path.join(path, changelog_filename)
if os.path.isfile(changelog_path):
changelog.file_path = changelog_path
try:
with open(path, 'rb') as f:
populate_changelog_from_rst(changelog, f.read().decode('utf-8'))
except IOError:
with open(changelog.file_path, 'rb') as f:
populate_changelog_from_file_content(changelog, f.read().decode('utf-8'))
except (IOError, TypeError):
return None
return changelog


def populate_changelog_from_rst(changelog, rst):
def populate_changelog_from_file_content(changelog, file_content):
"""
Changelog factory, which converts the raw ReST into a class.

:param changelog: ``Changelog`` changelog to be populated
:param rst: ``str`` raw ReST changelog
:param file_content: ``str`` raw changelog
:returns: ``Changelog`` changelog that was populated
"""
document = docutils.core.publish_doctree(rst)
document = docutils.core.publish_doctree(file_content)
processes_changelog_children(changelog, document.children)
changelog.rst = rst
changelog.content = file_content
return changelog


# Backwards compatibility
populate_changelog_from_rst = populate_changelog_from_file_content


def processes_changelog_children(changelog, children):
"""
Process docutils children into a REP-0132 changelog instance.
Expand Down Expand Up @@ -362,15 +421,17 @@ def bullet_generator(self, bullet):


class Changelog(object):
"""Represents a REP-0132 changelog."""
"""Represents a changelog."""

def __init__(self, package_name=None):
self.__file_path = None
self.__package_name = package_name
self.__versions = []
self.__parsed_versions = []
self.__dates = {}
self.__content = {}
self.__rst = ''
self.__extension = None
self.__data = ''

def __str__(self):
value = self.__unicode__()
Expand All @@ -388,6 +449,20 @@ def __unicode__(self):
msg.extend([' ' + i for i in _unicode(item).splitlines()])
return '\n'.join(msg)

@property
def file_path(self):
return self.__file_path

@file_path.setter
def file_path(self, file_path):
self.__file_path = file_path
self.__extension = os.path.splitext(file_path)[1]
assert self.__extension in CHANGELOG_EXTENSIONS.values(), "Invalid extension: '{0}'".format(self.__extension)

@property
def extension(self):
return self.__extension

@property
def package_name(self):
return self.__package_name
Expand All @@ -397,12 +472,12 @@ def package_name(self, package_name):
self.__package_name = package_name

@property
def rst(self):
return self.__rst
def data(self):
return self.__data

@rst.setter
def rst(self, rst):
self.__rst = rst
@data.setter
def data(self, rst):
self.__data = rst

def add_version_section(self, version, date, contents):
"""
Expand Down Expand Up @@ -531,6 +606,12 @@ def as_rst(self):
return _unicode(self.link)
return '`{0} <{1}>`_'.format(self.text, self.link)

def as_md(self):
"""Self as markdown (unicode)."""
if self.text is None:
return _unicode(self.link)
return '[{0}]({1})'.format(self.text, self.link)

def as_txt(self):
"""Self formatted for plain text (unicode)."""
if self.text is None:
Expand Down
37 changes: 22 additions & 15 deletions src/catkin_pkg/changelog_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
import os
import re

from catkin_pkg.changelog import CHANGELOG_FILENAME
from catkin_pkg.changelog import CHANGELOG_FILENAME, CHANGELOG_EXTENSIONS
from catkin_pkg.changelog_generator_vcs import Tag

FORTHCOMING_LABEL = 'Forthcoming'
Expand Down Expand Up @@ -100,26 +100,27 @@ def _get_latest_version_tag_name(vcs_client):
return tag_name


def generate_changelogs(base_path, packages, tag2log_entries, logger=None, vcs_client=None, skip_contributors=False):
def generate_changelogs(base_path, packages, tag2log_entries, logger=None, vcs_client=None, skip_contributors=False, file_type='rst'):
for pkg_path, package in packages.items():
changelog_path = os.path.join(base_path, pkg_path, CHANGELOG_FILENAME)
changelog_path = os.path.join(base_path, pkg_path, CHANGELOG_FILENAME+CHANGELOG_EXTENSIONS[file_type])
if os.path.exists(changelog_path):
continue
# generate package specific changelog file
if logger:
logger.debug("- creating '%s'" % os.path.join(pkg_path, CHANGELOG_FILENAME))
logger.debug("- creating '%s'" % changelog_path)
pkg_tag2log_entries = filter_package_changes(tag2log_entries, pkg_path)
data = generate_changelog_file(package.name, pkg_tag2log_entries, vcs_client=vcs_client, skip_contributors=skip_contributors)
data = generate_changelog_file(package.name, pkg_tag2log_entries, vcs_client=vcs_client, skip_contributors=skip_contributors, file_type=file_type)
with open(changelog_path, 'wb') as f:
f.write(data.encode('utf-8'))


def update_changelogs(base_path, packages, tag2log_entries, logger=None, vcs_client=None, skip_contributors=False):
for pkg_path in packages.keys():
# update package specific changelog file
def update_changelogs(changelogs, tag2log_entries, logger=None, vcs_client=None, skip_contributors=False):
for pkg_name, changelog in changelogs.items():
changelog_path = changelog.file_path
if logger:
logger.debug("- updating '%s'" % os.path.join(pkg_path, CHANGELOG_FILENAME))
pkg_tag2log_entries = filter_package_changes(tag2log_entries, pkg_path)
logger.debug("- updating '%s'" % changelog_path)
pkg_tag2log_entries = filter_package_changes(tag2log_entries, pkg_name)
changelog_path = os.path.join(base_path, pkg_path, CHANGELOG_FILENAME)
with open(changelog_path, 'rb') as f:
data = f.read().decode('utf-8')
Expand All @@ -143,9 +144,9 @@ def filter_package_changes(tag2log_entries, pkg_path):
return pkg_tag2log_entries


def generate_changelog_file(pkg_name, tag2log_entries, vcs_client=None, skip_contributors=False):
def generate_changelog_file(pkg_name, tag2log_entries, vcs_client=None, skip_contributors=False, file_type='rst'):
blocks = []
blocks.append(generate_package_headline(pkg_name))
blocks.append(generate_package_headline(pkg_name, file_type=file_type))

for tag in sorted_tags(tag2log_entries.keys()):
log_entries = tag2log_entries[tag]
Expand Down Expand Up @@ -239,10 +240,16 @@ def sorted_tags(tags):
yield tag


def generate_package_headline(pkg_name):
headline = 'Changelog for package %s' % pkg_name
section_marker = '^' * len(headline)
return '%s\n%s\n%s\n' % (section_marker, headline, section_marker)
def generate_package_headline(pkg_name, file_type='rst'):
if file_type == 'rst':
headline = 'Changelog for package %s' % pkg_name
section_marker = '^' * len(headline)
headline = '%s\n%s\n%s\n' % (section_marker, headline, section_marker)
elif file_type == 'md':
headline = '## Changelog for package %s\n\n' % pkg_name
else:
raise RuntimeError('Unknown file format: %s' % file_type)
return headline


def generate_version_block(version, timestamp, log_entries, vcs_client=None, skip_contributors=False):
Expand Down
19 changes: 11 additions & 8 deletions src/catkin_pkg/cli/generate_changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import os
import sys

from catkin_pkg.changelog import CHANGELOG_FILENAME
from catkin_pkg.changelog import CHANGELOG_FILENAME, CHANGELOG_FILE_TYPES, get_changelog_from_path
from catkin_pkg.changelog_generator import generate_changelog_file, generate_changelogs, get_all_changes, get_forthcoming_changes, update_changelogs
from catkin_pkg.changelog_generator_vcs import get_vcs_client
from catkin_pkg.packages import find_packages
Expand Down Expand Up @@ -59,6 +59,9 @@ def main(sysargs=None):
parser.add_argument(
'-y', '--non-interactive', action='store_true', default=False,
help="Run without user interaction, confirming all questions with 'yes'")
parser.add_argument(
'-f', '--format', default=CHANGELOG_FILE_TYPES[0], choices=CHANGELOG_FILE_TYPES,
help='The format of the changelog file')
args = parser.parse_args(sysargs)

base_path = '.'
Expand Down Expand Up @@ -88,11 +91,10 @@ def main(sysargs=None):
print('Found packages: %s' % ', '.join(sorted(p.name for p in packages.values())))

# check for missing changelogs
missing_changelogs = []
changelogs = {}
for pkg_path, package in packages.items():
changelog_path = os.path.join(base_path, pkg_path, CHANGELOG_FILENAME)
if not os.path.exists(changelog_path):
missing_changelogs.append(package.name)
changelogs[package.name] = get_changelog_from_path(os.path.join(base_path, pkg_path), package.name)
missing_changelogs = [package.name for package in packages.values() if package.name not in changelogs or changelogs[package.name] is None]

if args.all and not missing_changelogs:
raise RuntimeError('All packages already have a changelog. Either remove (some of) them before using --all or invoke the script without --all.')
Expand All @@ -112,19 +114,20 @@ def main(sysargs=None):
print('Querying all tags and commit information...')
tag2log_entries = get_all_changes(vcs_client, skip_merges=args.skip_merges, only_merges=args.only_merges)
print('Generating changelog files with all versions...')
generate_changelogs(base_path, packages, tag2log_entries, logger=logging, vcs_client=vcs_client, skip_contributors=args.skip_contributors)
generate_changelogs(base_path, packages, tag2log_entries, logger=logging, vcs_client=vcs_client, skip_contributors=args.skip_contributors, file_type=args.format)
else:
print('Querying commit information since latest tag...')
tag2log_entries = get_forthcoming_changes(vcs_client, skip_merges=args.skip_merges, only_merges=args.only_merges)
# separate packages with/without a changelog file
packages_without = {pkg_path: package for pkg_path, package in packages.items() if package.name in missing_changelogs}
if packages_without:
print('Generating changelog files with forthcoming version...')
generate_changelogs(base_path, packages_without, tag2log_entries, logger=logging, vcs_client=vcs_client, skip_contributors=args.skip_contributors)
generate_changelogs(base_path, packages_without, tag2log_entries, logger=logging, vcs_client=vcs_client, skip_contributors=args.skip_contributors, file_type=args.format)
packages_with = {pkg_path: package for pkg_path, package in packages.items() if package.name not in missing_changelogs}
if packages_with:
print('Updating forthcoming section of changelog files...')
update_changelogs(base_path, packages_with, tag2log_entries, logger=logging, vcs_client=vcs_client, skip_contributors=args.skip_contributors)
# TODO: it only updates from the last tag in the repo. It should sync from the last tag in the changelog file.
update_changelogs(changelogs, tag2log_entries, logger=logging, vcs_client=vcs_client, skip_contributors=args.skip_contributors)
print('Done.')
print('Please review the extracted commit messages and consolidate the changelog entries before committing the files!')

Expand Down
Loading