Commit 112c0192 authored by Simon Chabot's avatar Simon Chabot
Browse files

feat: autogenerate changelog with hg using conventionnal commits

parents
#!/usr/bin/env python3
# coding: utf-8
import re
import pathlib
import sys
from enum import Enum
from dataclasses import dataclass
from collections import defaultdict
from jinja2 import Template
from mercurial import registrar
from mercurial.i18n import _
cmdtable = {}
command = registrar.command(cmdtable)
ROOT = pathlib.Path(__file__).parent
with open(ROOT / "templates" / "changelog.md") as fobj:
CHANGE_LOG_TEMPLATE = Template(fobj.read())
class ChangeType(Enum):
chore = "chore"
ci = "ci"
docs = "docs"
feat = "feat"
fix = "fix"
perf = "perf"
refactor = "refactor"
style = "style"
test = "test"
revert = "revert"
unknown = "unknown"
@dataclass
class Change:
change_type: ChangeType
title: str
issue: str
scope: str
breaking_change: str
BLACKLISTED_TITLES = ("Added tag",)
RE_PARSE_CONVENTIONAL_TITLE = re.compile(
r"^(?P<type>\w+)(\((?P<scope>.+?)\))? *: *(?P<title>.*)$"
)
RE_PARSE_TITLE = re.compile(r"^(\[(?P<scope>.+?)\])? *:? *(?P<title>.*)$")
RE_PARSE_ISSUE = re.compile(r"related *: *(?P<issue>.+)", re.IGNORECASE)
RE_PARSE_BREAKING_CHANGE = re.compile(
r"BREAKING CHANGE *: *(?P<breaking_change>.+)", re.IGNORECASE
)
@classmethod
def from_commit(cls, commit):
description = commit.description().decode("utf-8")
title, *body = description.splitlines()
match_title = cls.RE_PARSE_CONVENTIONAL_TITLE.match(title)
for blacklisted in cls.BLACKLISTED_TITLES:
if blacklisted in title:
return None
if not match_title:
match_title = cls.RE_PARSE_TITLE.match(title)
if not match_title:
print("Unable to parse the title commit {commit.rev()}", file=sys.stderr)
return None
try:
change_type = ChangeType(match_title["type"])
except (IndexError, ValueError):
change_type = ChangeType.unknown
breaking_change = ""
issue = ""
for line in body:
match_issue = cls.RE_PARSE_ISSUE.match(line)
if match_issue:
issue = match_issue["issue"]
continue
match_breaking_change = cls.RE_PARSE_BREAKING_CHANGE.match(line)
if match_breaking_change:
breaking_change += match_breaking_change["breaking_change"]
return Change(
change_type=change_type,
title=match_title["title"],
scope=match_title["scope"],
issue=issue,
breaking_change=breaking_change,
)
class Changelog:
def __init__(self):
self.changes_by_version = defaultdict(lambda: defaultdict(list))
def add_commit(self, commit, version=None):
change = self._change_from_commit(commit)
if not change:
return
self.changes_by_version[version][change.change_type].append(change)
def _change_from_commit(self, commit):
return Change.from_commit(commit)
def render(self):
return CHANGE_LOG_TEMPLATE.render(changelog=self, ChangeType=ChangeType,)
@command(
b"autochangelog",
[
(
b"r",
b"revs",
b"ancestors(last(public()))",
_("The revisions used to generate the changelog"),
),
],
_("[options]"),
)
def autochangelog(ui, repo, **opts):
changelog = Changelog()
version = "upcoming"
user_revs = opts["revs"].decode("utf-8")
for rev in repo.revs(f"sort({user_revs}, -date)".encode("utf-8")):
commit = repo[rev]
if len(commit.parents()) > 1:
# ignore merge
continue
tags = [t for t in commit.tags() if t != b"tip"]
if tags:
version = tags[0].decode("utf-8")
changelog.add_commit(commit, version)
print(changelog.render())
{% macro new_section(changes, changetype, title) -%}
{%- if changes.get(changetype) %}
### {{ title }}
{% for change in changes[changetype] -%}
- {%if change.scope %}{{change.scope}}: {% endif %}{{ change.title }}{% if change.issue %} ({{ change.issue }}){% endif %}
{%- if change.breaking_change %}
*BREAKING CHANGE*: {{ change.breaking_change }}
{%- endif %}
{% endfor -%}
{%- endif %}
{%- endmacro %}
# CHANGELOG
{% for version, changes in changelog.changes_by_version.items() %}
{%- if version == "upcoming" %}
## Upcoming version
{% else %}
## Version {{ version }}
{%- endif %}
{{- new_section(changes, ChangeType.feat, "New features") }}
{{- new_section(changes, ChangeType.perf, "Performance improvements") }}
{{- new_section(changes, ChangeType.fix, "Bug fixes") }}
{{- new_section(changes, ChangeType.docs, "Documentation") }}
{{- new_section(changes, ChangeType.ci, "Continuous integration") }}
{{- new_section(changes, ChangeType.unknown, "Various changes") }}
{% endfor %}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment