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)