Browse Source

update "shore changelog" cli and add --markdown option

master
Niklas Rosenstein 5 months ago
parent
commit
edba080ae0
Signed by: NiklasRosenstein <rosensteinniklas@gmail.com> GPG Key ID: 06D269B33D25F6C6
3 changed files with 124 additions and 63 deletions
  1. +8
    -0
      .changelog/_unreleased.yml
  2. +18
    -62
      src/shore/__main__.py
  3. +98
    -1
      src/shore/util/changelog.py

+ 8
- 0
.changelog/_unreleased.yml View File

@@ -0,0 +1,8 @@
- types:
- change
issues: []
components:
- cli
- changelog
description: Updates the `shore changelog` CLI and adds a --markdown option for
formatting

+ 18
- 62
src/shore/__main__.py View File

@@ -82,16 +82,6 @@ def _commit_distance_version(subject: [Monorepo, Package]) -> Version:
subject.get_tag(subject.version)) or subject.version


def _md_term_stylize(text: str) -> str:
def _code(m):
return colored(m.group(1), 'cyan')
def _issue_ref(m):
return colored(m.group(0), 'yellow', attrs=['bold'])
text = re.sub(r'`([^`]+)`', _code, text)
text = re.sub(r'#\d+', _issue_ref, text)
return text


def _editor_open(filename: str):
editor = shlex.split(os.getenv('EDITOR', 'vim'))
return subprocess.call(editor + [filename])
@@ -790,22 +780,19 @@ def publish(**args):

@cli.command()
@click.argument('version', type=parse_version, required=False)
@click.option('--reformat', is_flag=True, help='Reformat the changelog.')
@click.option('-n', '--new', metavar='type,…',
help='Create a new entry. The argument for this option is the changelog type(s). '
'(usually a subset of {}).'.format(', '.join(ChangelogManager.TYPES)))
@click.option('-m', '--message', metavar='text',
help='The changelog entry description. Only with --new. If this is not provided, the EDITOR '
'will be opened to allow editing the changelog entry.')
@click.option('-c', '--components', metavar='name', help='The component for the changelog entry.')
@click.option('-i', '--issues', metavar='issue,…', help='Issues related to this changelog.')
@click.option('-e', '--edit', is_flag=True, help='Edit the staged changelog file in EDITOR.')
@click.option('--reformat', is_flag=True, help='reformat the changelog')
@click.option('--add', metavar='type,…', help='create a new changelog entry')
@click.option('--for', metavar='component,…', help='components for the new changelog entry')
@click.option('--fixes', metavar='issue,…', help='issues that this changelog entry fixes')
@click.option('-m', '--message', metavar='text', help='changelog entry description')
@click.option('-e', '--edit', is_flag=True, help='edit the changelog entry or file')
@click.option('--markdown', is_flag=True, help='render the changelog as markdown')
def changelog(**args):
"""
Show or create changelog entries.
Show changelogs or create new entries.
"""

if (args['version'] or args['reformat']) and args['new']:
if (args['version'] or args['reformat']) and args['add']:
logger.error('unsupported combination of arguments')
sys.exit(1)

@@ -818,17 +805,17 @@ def changelog(**args):
def _split(s: Optional[str]) -> List[str]:
return list(filter(bool, map(str.strip, (s or '').split(','))))

if args['new']:
if args['add']:

# Warn about bad changelog types.
for entry_type in _split(args['new']):
for entry_type in _split(args['add']):
if entry_type not in manager.TYPES:
logger.warning('"%s" is not a well-known changelog entry type.', entry_type)

entry = ChangelogEntry(
_split(args['new']),
_split(args['issues']),
_split(args['components']),
_split(args['add']),
_split(args['fixes']),
_split(args['for']),
args['message'] or '')

# Allow the user to edit the entry if no description is provided or the
@@ -866,42 +853,11 @@ def changelog(**args):
changelog.save()
sys.exit(0)

def _fmt_issue(i):
if str(i).isdigit():
return '#' + str(i)
return i

def _fmt_issues(entry):
if not entry.issues:
return None
return '(' + ', '.join(colored(_fmt_issue(i), 'yellow', attrs=['underline']) for i in entry.issues) + ')'

def _fmt_types(entry):
return ', '.join(colored(f, attrs=['bold']) for f in entry.types)

def _fmt_components(entry):
if len(entry.components) <= 1:
return None
return '(' + ', '.join(colored(f, 'red', attrs=['bold', 'underline']) for f in entry.components[1:]) + ')'

if hasattr(shutil, 'get_terminal_size'):
width = shutil.get_terminal_size((80, 23))[0]
if args['markdown']:
changelog_format = 'markdown'
else:
width = 80

# Explode entries by component.
for component, entries in Stream.groupby(changelog.entries, lambda x: x.components[0], collect=list):

maxw = max(len(', '.join(x.types)) for x in entries)
print(colored(component or 'No Component', 'red', attrs=['bold', 'underline']))
for entry in entries:
lines = textwrap.wrap(entry.description, width - (maxw + 4))
suffix_fmt = ' '.join(filter(bool, (_fmt_issues(entry), _fmt_components(entry))))
lines[-1] += ' ' + suffix_fmt
delta = maxw - len(', '.join(entry.types))
print(' {}'.format(colored((_fmt_types(entry) + ':') + ' ' * delta, attrs=['bold'])), _md_term_stylize(lines[0]))
for line in lines[1:]:
print(' {}{}'.format(' ' * (maxw+2), _md_term_stylize(line)))
changelog_format = 'terminal'
changelog.render_as(sys.stdout, changelog_format)


_entry_point = lambda: sys.exit(cli())


+ 98
- 1
src/shore/util/changelog.py View File

@@ -20,9 +20,14 @@
# IN THE SOFTWARE.

from nr.databind.core import Field, ObjectMapper, Struct
from nr.stream import Stream
from shore.util.version import Version
from typing import Optional
from termcolor import colored
from typing import Optional, TextIO
import os
import re
import shutil
import textwrap
import yaml


@@ -35,6 +40,8 @@ class ChangelogEntry(Struct):

class Changelog:

RENDERERS = {}

def __init__(self, filename: str, version: Optional[Version], mapper: ObjectMapper) -> None:
self.filename = filename
self.version = version
@@ -59,6 +66,9 @@ class Changelog:
def add_entry(self, entry: ChangelogEntry) -> None:
self.entries.append(entry)

def render_as(self, fp: TextIO, format: str) -> None:
self.RENDERERS[format](fp, self)


class ChangelogManager:

@@ -95,3 +105,90 @@ class ChangelogManager:
os.rename(unreleased.filename, self.version(version).filename)
self._cache.clear()
return self.version(version)


def _group_entries_by_component(entries):
key = lambda x: x.components[0]
return list(Stream.sortby(entries, key).groupby(key, collect=list))


def render_changelog_for_terminal(fp: TextIO, changelog: Changelog) -> None:
"""
Renders a #Changelog for the terminal to *fp*.
"""

def _md_term_stylize(text: str) -> str:
def _code(m):
return colored(m.group(1), 'cyan')
def _issue_ref(m):
return colored(m.group(0), 'yellow', attrs=['bold'])
text = re.sub(r'`([^`]+)`', _code, text)
text = re.sub(r'#\d+', _issue_ref, text)
return text

def _fmt_issue(i):
if str(i).isdigit():
return '#' + str(i)
return i

def _fmt_issues(entry):
if not entry.issues:
return None
return '(' + ', '.join(colored(_fmt_issue(i), 'yellow', attrs=['underline']) for i in entry.issues) + ')'

def _fmt_types(entry):
return ', '.join(colored(f, attrs=['bold']) for f in entry.types)

def _fmt_components(entry):
if len(entry.components) <= 1:
return None
return '(' + ', '.join(colored(f, 'red', attrs=['bold', 'underline']) for f in entry.components[1:]) + ')'

if hasattr(shutil, 'get_terminal_size'):
width = shutil.get_terminal_size((80, 23))[0]
else:
width = 80

# Explode entries by component.
for component, entries in _group_entries_by_component(changelog.entries):
maxw = max(len(', '.join(x.types)) for x in entries)
fp.write(colored(component or 'No Component', 'red', attrs=['bold', 'underline']) + '\n')
for entry in entries:
lines = textwrap.wrap(entry.description, width - (maxw + 4))
suffix_fmt = ' '.join(filter(bool, (_fmt_issues(entry), _fmt_components(entry))))
lines[-1] += ' ' + suffix_fmt
delta = maxw - len(', '.join(entry.types))
fp.write(' {} {}\n'.format(colored((_fmt_types(entry) + ':') + ' ' * delta, attrs=['bold']), _md_term_stylize(lines[0])))
for line in lines[1:]:
fp.write(' {}{}\n'.format(' ' * (maxw+2), _md_term_stylize(line)))


def render_changelog_as_markdown(fp: TextIO, changelog: Changelog) -> None:

def _fmt_issue(i):
if str(i).isdigit():
return '#' + str(i)
return i

def _fmt_issues(entry):
if not entry.issues:
return None
return '(' + ', '.join(_fmt_issue(i) for i in entry.issues) + ')'

fp.write('## {}\n\n'.format(changelog.version or 'unreleased'))
for component, entries in _group_entries_by_component(changelog.entries):
fp.write('* __{}__\n'.format(component))
for entry in entries:
description ='**' + ', '.join(entry.types) + '**: ' + entry.description
if entry.issues:
description += ' ' + _fmt_issues(entry)
lines = textwrap.wrap(description, 80)
fp.write(' * {}\n'.format(lines[0]))
for line in lines[1:]:
fp.write(' {}\n'.format(line))


Changelog.RENDERERS.update({
'terminal': render_changelog_for_terminal,
'markdown': render_changelog_as_markdown,
})

Loading…
Cancel
Save