diff --git a/setup.cfg b/setup.cfg index 0ce60dd..20d585a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,6 +27,9 @@ project_urls = Changelog = https://github.com/drewcassidy/yaclog/blob/main/CHANGELOG.md [options] -install_requires = Click; GitPython +install_requires = + Click ~= 7.0 + GitPython >= 3 + packaging >= 20 python_requires = >= 3.8 packages = find: diff --git a/yaclog/changelog.py b/yaclog/changelog.py index 941b8dd..e9ea20d 100644 --- a/yaclog/changelog.py +++ b/yaclog/changelog.py @@ -63,7 +63,7 @@ def _join_markdown(segments: List[str]) -> str: class VersionEntry: def __init__(self): self.sections = {'': []} - self.name: str = '' + self.name: str = 'Unreleased' self.date: Optional[datetime.date] = None self.tags: List[str] = [] self.link: str = '' diff --git a/yaclog/cli/__main__.py b/yaclog/cli/__main__.py index b3a3c81..b322c8d 100644 --- a/yaclog/cli/__main__.py +++ b/yaclog/cli/__main__.py @@ -15,8 +15,9 @@ # along with this program. If not, see . import click -import yaclog import os.path +import datetime +import yaclog.cli.version_util from yaclog import Changelog @@ -112,17 +113,23 @@ def tag(obj: Changelog, add, tag_name: str, version_name: str): obj.write() -@cli.command() +@cli.command(short_help='Add entries to the changelog.') @click.option('--bullet', '-b', 'bullets', multiple=True, type=str, help='Bullet points to add. ' 'When multiple bullet points are provided, additional points are added as sub-points.') @click.option('--paragraph', '-p', 'paragraphs', multiple=True, type=str, help='Paragraphs to add') -@click.argument('section_name', metavar='section', type=str) -@click.argument('version_name', metavar='version', type=str, required=False) +@click.argument('section_name', metavar='SECTION', type=str, default='', required=False) +@click.argument('version_name', metavar='VERSION', type=str, default=None, required=False) @click.pass_obj def entry(obj: Changelog, bullets, paragraphs, section_name, version_name): - """Add entries to the changelog""" + """Add entries to SECTION in VERSION + + SECTION is the name of the section to append to. If not given, entries will be uncategorized. + + VERSION is the name of the version to append to. If not given, the most recent version will be used, + or a new 'Unreleased' version will be added if the most recent version has been released. + """ section_name = section_name.title() if version_name: @@ -132,6 +139,9 @@ def entry(obj: Changelog, bullets, paragraphs, section_name, version_name): version = matches[0] else: version = obj.versions[0] + if version.name.lower() != 'unreleased': + version = yaclog.changelog.VersionEntry() + obj.versions.insert(0, version) if section_name not in version.sections.keys(): version.sections[section_name] = [] @@ -140,17 +150,45 @@ def entry(obj: Changelog, bullets, paragraphs, section_name, version_name): section += paragraphs sub_bullet = False + bullet_str = '' for bullet in bullets: bullet = bullet.strip() if bullet[0] not in ['-+*']: bullet = '- ' + bullet if sub_bullet: - bullet = ' ' + bullet - - section.append(bullet) + bullet = '\n ' + bullet + bullet_str += bullet sub_bullet = True + section.append(bullet_str) + + obj.write() + + +@cli.command() +@click.option('-v', '--version', 'v_flag', type=str, help='The full version string to release.') +@click.option('-M', '--major', 'v_flag', flag_value='+M', help='Increment major version number.') +@click.option('-m', '--minor', 'v_flag', flag_value='+m', help='Increment minor version number.') +@click.option('-p', '--patch', 'v_flag', flag_value='+p', help='Increment patch number.') +@click.option('-a', '--alpha', 'v_flag', flag_value='+a', help='Increment alpha version number.') +@click.option('-b', '--beta', 'v_flag', flag_value='+b', help='Increment beta version number.') +@click.option('-r', '--rc', 'v_flag', flag_value='+rc', help='Increment release candidate version number.') +@click.option('-c', '--commit', help='Create a git commit tagged with the new version number.') +@click.pass_obj +def release(obj: Changelog, v_flag, commit): + version = [v for v in obj.versions if v.name.lower() != 'unreleased'][0] + cur_version = obj.versions[0] + + if v_flag[0] == '+': + new_name = yaclog.cli.version_util.increment_version(version.name, v_flag) + else: + new_name = v_flag + + if yaclog.cli.version_util.is_release(cur_version.name): + click.confirm(f'Rename release version "{cur_version.name}" to "{new_name}"?', abort=True) + + cur_version.date = datetime.datetime.utcnow().date() obj.write() diff --git a/yaclog/cli/version_util.py b/yaclog/cli/version_util.py new file mode 100644 index 0000000..e729840 --- /dev/null +++ b/yaclog/cli/version_util.py @@ -0,0 +1,78 @@ +# yaclog: yet another changelog tool +# Copyright (c) 2021. Andrew Cassidy +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from packaging.version import Version, parse + + +def is_release(version: str) -> bool: + v = parse(version) + + return not (v.is_devrelease or v.is_prerelease) + + +def increment_version(version: str, mode: str) -> str: + v = parse(version) + epoch = v.epoch + release = v.release + pre = v.pre + post = v.post + dev = v.dev + local = v.local + + if mode == '+M': + release = (release[0] + 1,) + release[1:] + elif mode == '+m': + release = (release[0], release[1] + 1) + release[2:] + elif mode == '+p': + release = (release[0], release[1], release[2] + 1) + release[3:] + elif mode in ['+a', '+b', '+rc']: + if pre[0] == mode[1:]: + pre = (mode[1:], pre[1] + 1) + else: + pre = (mode[1:], 0) + else: + raise IndexError(f'Unknown mode {mode}') + + return join_version(epoch, release, pre, post, dev, local) + + +def join_version(epoch, release, pre, post, dev, local) -> str: + parts = [] + + # Epoch + if epoch != 0: + parts.append(f"{epoch}!") + + # Release segment + parts.append(".".join(str(x) for x in release)) + + # Pre-release + if pre is not None: + parts.append("".join(str(x) for x in pre)) + + # Post-release + if post is not None: + parts.append(f".post{post}") + + # Development release + if dev is not None: + parts.append(f".dev{dev}") + + # Local version segment + if local is not None: + parts.append(f"+{local}") + + return "".join(parts)