Skip to content

Commit

Permalink
feat(formats): expose customizable changelog formats on the `commitiz…
Browse files Browse the repository at this point in the history
…en.formats` endpoint
  • Loading branch information
noirbizarre authored and Skeen committed Aug 5, 2023
1 parent cce76df commit a8f23a9
Show file tree
Hide file tree
Showing 37 changed files with 1,629 additions and 661 deletions.
2 changes: 1 addition & 1 deletion commitizen/changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
@dataclass
class Metadata:
"""
Metadata extracted from the changelog pproduced by a plugin
Metadata extracted from the changelog produced by a plugin
"""

unreleased_start: int | None = None
Expand Down
20 changes: 19 additions & 1 deletion commitizen/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from functools import partial
from pathlib import Path
from types import TracebackType
from typing import Any, Sequence

import argcomplete
from decli import cli
Expand All @@ -16,6 +17,7 @@
CommitizenException,
ExitCode,
ExpectedExit,
InvalidCommandArgumentError,
NoCommandFoundError,
)

Expand All @@ -41,9 +43,25 @@ class ParseKwargs(argparse.Action):
}
"""

def __call__(self, parser, namespace, kwarg, option_string=None):
def __call__(
self,
parser: argparse.ArgumentParser,
namespace: argparse.Namespace,
kwarg: str | Sequence[Any] | None,
option_string: str | None = None,
):
if not isinstance(kwarg, str):
return
if "=" not in kwarg:
raise InvalidCommandArgumentError(
f"Option {option_string} expect a key=value format"
)
kwargs = getattr(namespace, self.dest, None) or {}
key, value = kwarg.split("=", 1)
if not key:
raise InvalidCommandArgumentError(
f"Option {option_string} expect a key=value format"
)
kwargs[key] = value.strip("'\"")
setattr(namespace, self.dest, kwargs)

Expand Down
13 changes: 7 additions & 6 deletions commitizen/commands/bump.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
NotAllowed,
NoVersionSpecifiedError,
)
from commitizen.formats import get_format
from commitizen.providers import get_provider
from commitizen.version_schemes import InvalidVersion, get_version_scheme

Expand Down Expand Up @@ -79,15 +80,15 @@ def __init__(self, config: BaseConfig, arguments: dict):
self.scheme = get_version_scheme(
self.config, arguments["version_scheme"] or deprecated_version_type
)
self.file_name = arguments["file_name"] or self.config.settings.get(
"changelog_file"
)
self.format = get_format(self.config, self.file_name)

self.template = (
arguments["template"]
or self.config.settings.get("template")
or self.cz.template
)
self.file_name = (
arguments["file_name"]
or self.config.settings.get("changelog_file")
or self.cz.changelog_file
or self.format.template
)
self.extras = arguments["extras"]

Expand Down
26 changes: 13 additions & 13 deletions commitizen/commands/changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
NotAGitProjectError,
NotAllowed,
)
from commitizen.formats import get_format
from commitizen.git import GitTag, smart_open
from commitizen.version_schemes import get_version_scheme

Expand All @@ -35,11 +36,17 @@ def __init__(self, config: BaseConfig, args):
self.start_rev = args.get("start_rev") or self.config.settings.get(
"changelog_start_rev"
)
self.file_name = (
args.get("file_name")
or self.config.settings.get("changelog_file")
or self.cz.changelog_file
self.file_name = args.get("file_name") or self.config.settings.get(
"changelog_file"
)
if not isinstance(self.file_name, str):
raise NotAllowed(
"Changelog file name is broken.\n"
"Check the flag `--file-name` in the terminal "
f"or the setting `changelog_file` in {self.config.path}"
)
self.format = get_format(self.config, self.file_name)

self.incremental = args["incremental"] or self.config.settings.get(
"changelog_incremental"
)
Expand Down Expand Up @@ -72,7 +79,7 @@ def __init__(self, config: BaseConfig, args):
self.template = (
args.get("template")
or self.config.settings.get("template")
or self.cz.template
or self.format.template
)
self.extras = args.get("extras") or {}
self.export_template_to = args.get("export_template")
Expand Down Expand Up @@ -106,13 +113,6 @@ def _find_incremental_rev(self, latest_version: str, tags: list[GitTag]) -> str:
def write_changelog(
self, changelog_out: str, lines: list[str], changelog_meta: changelog.Metadata
):
if not isinstance(self.file_name, str):
raise NotAllowed(
"Changelog file name is broken.\n"
"Check the flag `--file-name` in the terminal "
f"or the setting `changelog_file` in {self.config.path}"
)

changelog_hook: Callable | None = self.cz.changelog_hook
with smart_open(self.file_name, "w", encoding=self.encoding) as changelog_file:
partial_changelog: str | None = None
Expand Down Expand Up @@ -162,7 +162,7 @@ def __call__(self):

end_rev = ""
if self.incremental:
changelog_meta = self.cz.get_metadata(self.file_name)
changelog_meta = self.format.get_metadata(self.file_name)
if changelog_meta.latest_version:
latest_tag_version: str = bump.normalize_tag(
changelog_meta.latest_version,
Expand Down
67 changes: 2 additions & 65 deletions commitizen/cz/base.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
from __future__ import annotations

from abc import ABCMeta, abstractmethod
import os
import re
from typing import IO, Any, Callable
from typing import Any, Callable

from jinja2 import BaseLoader, PackageLoader
from prompt_toolkit.styles import Style, merge_styles

from commitizen import git
from commitizen import defaults
from commitizen.changelog import Metadata
from commitizen.config.base_config import BaseConfig
from commitizen.defaults import Questions

Expand Down Expand Up @@ -48,9 +44,7 @@ class BaseCommitizen(metaclass=ABCMeta):
# Executed only at the end of the changelog generation
changelog_hook: Callable[[str, str | None], str] | None = None

changelog_file: str = "CHANGELOG.md"

# template: str = DEFAULT_TEMPLATE
# Plugins can override templates and provide extra template data
template_loader: BaseLoader = PackageLoader("commitizen", "templates")
template_extras: dict[str, Any] = {}

Expand All @@ -59,10 +53,6 @@ def __init__(self, config: BaseConfig):
if not self.config.settings.get("style"):
self.config.settings.update({"style": BaseCommitizen.default_style_config})

@property
def template(self) -> str:
return f"{self.changelog_file}.j2"

@abstractmethod
def questions(self) -> Questions:
"""Questions regarding the commit message."""
Expand Down Expand Up @@ -102,56 +92,3 @@ def process_commit(self, commit: str) -> str:
If not overwritten, it returns the first line of commit.
"""
return commit.split("\n")[0]

def get_metadata(self, filepath: str) -> Metadata:
if not os.path.isfile(filepath):
return Metadata()

with open(filepath, "r") as changelog_file:
return self.get_metadata_from_file(changelog_file)

def get_metadata_from_file(self, file: IO[Any]) -> Metadata:
meta = Metadata()
unreleased_title: str | None = None
for index, line in enumerate(file):
line = line.strip().lower()

unreleased: str | None = None
if "unreleased" in line:
unreleased = self.parse_title_type_of_line(line)
# Try to find beginning and end lines of the unreleased block
if unreleased:
meta.unreleased_start = index
unreleased_title = unreleased
continue
elif (
isinstance(unreleased_title, str)
and self.parse_title_type_of_line(line) == unreleased_title
):
meta.unreleased_end = index

# Try to find the latest release done
version = self.parse_version_from_changelog(line)
if version:
meta.latest_version = version
meta.latest_version_position = index
break # there's no need for more info
if meta.unreleased_start is not None and meta.unreleased_end is None:
meta.unreleased_end = index

return meta

def parse_version_from_changelog(self, line: str) -> str | None:
if not line.startswith("#"):
return None
m = re.search(defaults.version_parser, line)
if not m:
return None
return m.groupdict().get("version")

def parse_title_type_of_line(self, line: str) -> str | None:
md_title_parser = r"^(?P<title>#+)"
m = re.search(md_title_parser, line)
if not m:
return None
return m.groupdict().get("title")
7 changes: 1 addition & 6 deletions commitizen/cz/conventional_commits/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1 @@
from .conventional_commits import (
ConventionalCommitsCz, # noqa: F401
ConventionalCommitsTextile, # noqa: F401
ConventionalCommitsAsciiDoc, # noqa: F401
ConventionalCommitsRst, # noqa: F401
)
from .conventional_commits import ConventionalCommitsCz # noqa: F401
103 changes: 0 additions & 103 deletions commitizen/cz/conventional_commits/conventional_commits.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import os
import re
from typing import IO, Any, Optional

from commitizen import defaults
from commitizen.changelog import Metadata
from commitizen.cz.base import BaseCommitizen
from commitizen.cz.utils import multiple_line_breaker, required_validator
from commitizen.defaults import Questions
Expand Down Expand Up @@ -214,104 +212,3 @@ def process_commit(self, commit: str) -> str:
if m is None:
return ""
return m.group(3).strip()


class ConventionalCommitsTextile(ConventionalCommitsCz):
changelog_file = "CHANGELOG.textile"

def parse_version_from_changelog(self, line: str) -> Optional[str]:
if not line.startswith("h2. "):
return None
m = re.search(defaults.version_parser, line)
if not m:
return None
return m.groupdict().get("version")

def parse_title_type_of_line(self, line: str) -> Optional[str]:
md_title_parser = r"^(?P<title>h\d\. )"
m = re.search(md_title_parser, line)
if not m:
return None
return m.groupdict().get("title")


class ConventionalCommitsAsciiDoc(ConventionalCommitsCz):
changelog_file = "CHANGELOG.adoc"

def parse_version_from_changelog(self, line: str) -> Optional[str]:
if not line.startswith("== "):
return None
m = re.search(defaults.version_parser, line)
if not m:
return None
return m.groupdict().get("version")

def parse_title_type_of_line(self, line: str) -> Optional[str]:
md_title_parser = r"^(?P<title>=+)"
m = re.search(md_title_parser, line)
if not m:
return None
return m.groupdict().get("title")


class ConventionalCommitsRst(ConventionalCommitsCz):
changelog_file = "CHANGELOG.rst"

def get_metadata_from_file(self, file: IO[Any]) -> Metadata:
"""
RestructuredText section titles are not one-line-based,
it requires its own algorithm.
This method does not extract metadata from any restructuredtext file
but only from the one generated by this plugin with its template.
For a more generic approach, you need to rely on `docutils`.
"""
meta = Metadata()
unreleased_title: Optional[str] = None
lines = file.readlines()
for index, (line, next_line) in enumerate(zip(lines, lines[1:])):
line = line.strip().lower()
next_line = next_line.strip()

unreleased: Optional[str] = None
if "unreleased" in line:
unreleased = self.title_type_of_line(line, next_line)
# Try to find beginning and end lines of the unreleased block
if unreleased:
meta.unreleased_start = index
unreleased_title = unreleased
continue
elif (
isinstance(unreleased_title, str)
and self.title_type_of_line(line, next_line) == unreleased_title
):
meta.unreleased_end = index

# Try to find the latest release done
version = self.version_from_changelog(line, next_line)
if version:
meta.latest_version = version
meta.latest_version_position = index
break # there's no need for more info
if meta.unreleased_start is not None and meta.unreleased_end is None:
meta.unreleased_end = index + 1

return meta

def version_from_changelog(self, line: str, next_line: str) -> Optional[str]:
if not len(next_line) >= len(line):
return None
elif not next_line == "=" * len(next_line):
return None
m = re.search(defaults.version_parser, line)
if not m:
return None
return m.groupdict().get("version")

def title_type_of_line(self, line: str, next_line: str) -> Optional[str]:
if not len(next_line) >= len(line):
return None
elif not next_line == "=" * len(next_line):
return None
return next_line[0]
Loading

0 comments on commit a8f23a9

Please sign in to comment.