Skip to content

Commit

Permalink
Merge branch 'master' into dependabot/pip/urllib3-2.0.7
Browse files Browse the repository at this point in the history
  • Loading branch information
Lee-W authored Oct 18, 2023
2 parents 0023789 + e9647c7 commit a7125db
Show file tree
Hide file tree
Showing 49 changed files with 2,240 additions and 646 deletions.
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ repos:
exclude: "poetry.lock"

- repo: https://github.com/commitizen-tools/commitizen
rev: v3.11.0 # automatically updated by Commitizen
rev: v3.12.0 # automatically updated by Commitizen
hooks:
- id: commitizen
- id: commitizen-branch
Expand Down
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,18 @@

## v3.12.0 (2023-10-18)

### Feat

- **formats**: expose some new customizable changelog formats on the `commitizen.changelog_format` endpoint (Textile, AsciiDoc and RestructuredText)
- **template**: add `changelog --export-template` command
- **template**: allow to override the template from cli, configuration and plugins

### Fix

- **filename**: ensure `file_name` can be passed to `changelog` from `bump` command

## 3.11.0 (2023-10-17)

## v3.11.0 (2023-10-17)

### Feat
Expand Down
2 changes: 1 addition & 1 deletion commitizen/__version__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "3.11.0"
__version__ = "3.12.0"
127 changes: 45 additions & 82 deletions commitizen/changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,30 +26,46 @@
"""
from __future__ import annotations

import os
import re
from collections import OrderedDict, defaultdict
from dataclasses import dataclass
from datetime import date
from typing import TYPE_CHECKING, Callable, Iterable, cast

from jinja2 import Environment, PackageLoader
from typing import TYPE_CHECKING, Callable, Iterable

from jinja2 import (
BaseLoader,
ChoiceLoader,
Environment,
FileSystemLoader,
Template,
)

from commitizen import out
from commitizen.bump import normalize_tag
from commitizen.defaults import encoding
from commitizen.exceptions import InvalidConfigurationError, NoCommitsFoundError
from commitizen.git import GitCommit, GitTag
from commitizen.version_schemes import (
DEFAULT_SCHEME,
BaseVersion,
InvalidVersion,
Pep440,
)

if TYPE_CHECKING:
from commitizen.version_schemes import VersionScheme


@dataclass
class Metadata:
"""
Metadata extracted from the changelog produced by a plugin
"""

unreleased_start: int | None = None
unreleased_end: int | None = None
latest_version: str | None = None
latest_version_position: int | None = None


def get_commit_tag(commit: GitCommit, tags: list[GitTag]) -> GitTag | None:
return next((tag for tag in tags if tag.rev == commit.rev), None)

Expand Down Expand Up @@ -187,84 +203,31 @@ def order_changelog_tree(tree: Iterable, change_type_order: list[str]) -> Iterab
return sorted_tree


def render_changelog(tree: Iterable) -> str:
loader = PackageLoader("commitizen", "templates")
def get_changelog_template(loader: BaseLoader, template: str) -> Template:
loader = ChoiceLoader(
[
FileSystemLoader("."),
loader,
]
)
env = Environment(loader=loader, trim_blocks=True)
jinja_template = env.get_template("keep_a_changelog_template.j2")
changelog: str = jinja_template.render(tree=tree)
return changelog

return env.get_template(template)

def parse_version_from_markdown(
value: str, scheme: VersionScheme = Pep440
) -> str | None:
if not value.startswith("#"):
return None
m = scheme.parser.search(value)
if not m:
return None
return cast(str, m.group("version"))


def parse_title_type_of_line(value: str) -> str | None:
md_title_parser = r"^(?P<title>#+)"
m = re.search(md_title_parser, value)
if not m:
return None
return m.groupdict().get("title")
def render_changelog(
tree: Iterable,
loader: BaseLoader,
template: str,
**kwargs,
) -> str:
jinja_template = get_changelog_template(loader, template)
changelog: str = jinja_template.render(tree=tree, **kwargs)
return changelog


def get_metadata(
filepath: str, scheme: VersionScheme = Pep440, encoding: str = encoding
) -> dict:
unreleased_start: int | None = None
unreleased_end: int | None = None
unreleased_title: str | None = None
latest_version: str | None = None
latest_version_position: int | None = None
if not os.path.isfile(filepath):
return {
"unreleased_start": None,
"unreleased_end": None,
"latest_version": None,
"latest_version_position": None,
}

with open(filepath, encoding=encoding) as changelog_file:
for index, line in enumerate(changelog_file):
line = line.strip().lower()

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

# Try to find the latest release done
version = parse_version_from_markdown(line, scheme)
if version:
latest_version = version
latest_version_position = index
break # there's no need for more info
if unreleased_start is not None and unreleased_end is None:
unreleased_end = index
return {
"unreleased_start": unreleased_start,
"unreleased_end": unreleased_end,
"latest_version": latest_version,
"latest_version_position": latest_version_position,
}


def incremental_build(new_content: str, lines: list[str], metadata: dict) -> list[str]:
def incremental_build(
new_content: str, lines: list[str], metadata: Metadata
) -> list[str]:
"""Takes the original lines and updates with new_content.
The metadata governs how to remove the old unreleased section and where to place the
Expand All @@ -278,9 +241,9 @@ def incremental_build(new_content: str, lines: list[str], metadata: dict) -> lis
Returns:
Updated lines
"""
unreleased_start = metadata.get("unreleased_start")
unreleased_end = metadata.get("unreleased_end")
latest_version_position = metadata.get("latest_version_position")
unreleased_start = metadata.unreleased_start
unreleased_end = metadata.unreleased_end
latest_version_position = metadata.latest_version_position
skip = False
output_lines: list[str] = []
for index, line in enumerate(lines):
Expand Down
90 changes: 90 additions & 0 deletions commitizen/changelog_formats/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
from __future__ import annotations

from typing import ClassVar, Protocol

import importlib_metadata as metadata

from commitizen.changelog import Metadata
from commitizen.exceptions import ChangelogFormatUnknown
from commitizen.config.base_config import BaseConfig


CHANGELOG_FORMAT_ENTRYPOINT = "commitizen.changelog_format"
TEMPLATE_EXTENSION = "j2"


class ChangelogFormat(Protocol):
extension: ClassVar[str]
"""Standard known extension associated with this format"""

alternative_extensions: ClassVar[set[str]]
"""Known alternatives extensions for this format"""

config: BaseConfig

def __init__(self, config: BaseConfig):
self.config = config

Check warning on line 26 in commitizen/changelog_formats/__init__.py

View check run for this annotation

Codecov / codecov/patch

commitizen/changelog_formats/__init__.py#L26

Added line #L26 was not covered by tests

@property
def ext(self) -> str:
"""Dotted version of extensions, as in `pathlib` and `os` modules"""
return f".{self.extension}"

@property
def template(self) -> str:
"""Expected template name for this format"""
return f"CHANGELOG.{self.extension}.{TEMPLATE_EXTENSION}"

@property
def default_changelog_file(self) -> str:
return f"CHANGELOG.{self.extension}"

def get_metadata(self, filepath: str) -> Metadata:
"""
Extract the changelog metadata.
"""
raise NotImplementedError


KNOWN_CHANGELOG_FORMATS: dict[str, type[ChangelogFormat]] = {
ep.name: ep.load()
for ep in metadata.entry_points(group=CHANGELOG_FORMAT_ENTRYPOINT)
}


def get_changelog_format(
config: BaseConfig, filename: str | None = None
) -> ChangelogFormat:
"""
Get a format from its name
:raises FormatUnknown: if a non-empty name is provided but cannot be found in the known formats
"""
name: str | None = config.settings.get("changelog_format")
format: type[ChangelogFormat] | None = guess_changelog_format(filename)

if name and name in KNOWN_CHANGELOG_FORMATS:
format = KNOWN_CHANGELOG_FORMATS[name]

if not format:
raise ChangelogFormatUnknown(f"Unknown changelog format '{name}'")

return format(config)


def guess_changelog_format(filename: str | None) -> type[ChangelogFormat] | None:
"""
Try guessing the file format from the filename.
Algorithm is basic, extension-based, and won't work
for extension-less file names like `CHANGELOG` or `NEWS`.
"""
if not filename or not isinstance(filename, str):
return None
for format in KNOWN_CHANGELOG_FORMATS.values():
if filename.endswith(f".{format.extension}"):
return format
for alt_extension in format.alternative_extensions:
if filename.endswith(f".{alt_extension}"):
return format
return None
27 changes: 27 additions & 0 deletions commitizen/changelog_formats/asciidoc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from __future__ import annotations

import re

from .base import BaseFormat


class AsciiDoc(BaseFormat):
extension = "adoc"

RE_TITLE = re.compile(r"^(?P<level>=+) (?P<title>.*)$")

def parse_version_from_title(self, line: str) -> str | None:
m = self.RE_TITLE.match(line)
if not m:
return None
# Capture last match as AsciiDoc use postfixed URL labels
matches = list(re.finditer(self.version_parser, m.group("title")))
if not matches:
return None
return matches[-1].group("version")

def parse_title_level(self, line: str) -> int | None:
m = self.RE_TITLE.match(line)
if not m:
return None
return len(m.group("level"))
Loading

0 comments on commit a7125db

Please sign in to comment.