1
0
Fork 0

Adding upstream version 4.6.0.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-04-21 10:42:01 +02:00
parent f3ad83a1a5
commit a7fbe822ec
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
278 changed files with 30423 additions and 0 deletions

View file

@ -0,0 +1,93 @@
from __future__ import annotations
import sys
from typing import ClassVar, Protocol
if sys.version_info >= (3, 10):
from importlib import metadata
else:
import importlib_metadata as metadata
from commitizen.changelog import Metadata
from commitizen.config.base_config import BaseConfig
from commitizen.exceptions import ChangelogFormatUnknown
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
@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

View file

@ -0,0 +1,28 @@
from __future__ import annotations
import re
from typing import TYPE_CHECKING
from .base import BaseFormat
if TYPE_CHECKING:
from commitizen.tags import VersionTag
class AsciiDoc(BaseFormat):
extension = "adoc"
RE_TITLE = re.compile(r"^(?P<level>=+) (?P<title>.*)$")
def parse_version_from_title(self, line: str) -> VersionTag | None:
m = self.RE_TITLE.match(line)
if not m:
return None
# Capture last match as AsciiDoc use postfixed URL labels
return self.tag_rules.search_version(m.group("title"), last=True)
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"))

View file

@ -0,0 +1,86 @@
from __future__ import annotations
import os
from abc import ABCMeta
from typing import IO, Any, ClassVar
from commitizen.changelog import Metadata
from commitizen.config.base_config import BaseConfig
from commitizen.tags import TagRules, VersionTag
from commitizen.version_schemes import get_version_scheme
from . import ChangelogFormat
class BaseFormat(ChangelogFormat, metaclass=ABCMeta):
"""
Base class to extend to implement a changelog file format.
"""
extension: ClassVar[str] = ""
alternative_extensions: ClassVar[set[str]] = set()
def __init__(self, config: BaseConfig):
# Constructor needs to be redefined because `Protocol` prevent instantiation by default
# See: https://bugs.python.org/issue44807
self.config = config
self.encoding = self.config.settings["encoding"]
self.tag_format = self.config.settings["tag_format"]
self.tag_rules = TagRules(
scheme=get_version_scheme(self.config.settings),
tag_format=self.tag_format,
legacy_tag_formats=self.config.settings["legacy_tag_formats"],
ignored_tag_formats=self.config.settings["ignored_tag_formats"],
)
def get_metadata(self, filepath: str) -> Metadata:
if not os.path.isfile(filepath):
return Metadata()
with open(filepath, encoding=self.encoding) as changelog_file:
return self.get_metadata_from_file(changelog_file)
def get_metadata_from_file(self, file: IO[Any]) -> Metadata:
meta = Metadata()
unreleased_level: int | None = None
for index, line in enumerate(file):
line = line.strip().lower()
unreleased: int | None = None
if "unreleased" in line:
unreleased = self.parse_title_level(line)
# Try to find beginning and end lines of the unreleased block
if unreleased:
meta.unreleased_start = index
unreleased_level = unreleased
continue
elif unreleased_level and self.parse_title_level(line) == unreleased_level:
meta.unreleased_end = index
# Try to find the latest release done
parsed = self.parse_version_from_title(line)
if parsed:
meta.latest_version = parsed.version
meta.latest_version_tag = parsed.tag
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_title(self, line: str) -> VersionTag | None:
"""
Extract the version from a title line if any
"""
raise NotImplementedError(
"Default `get_metadata_from_file` requires `parse_version_from_changelog` to be implemented"
)
def parse_title_level(self, line: str) -> int | None:
"""
Get the title level/type of a line if any
"""
raise NotImplementedError(
"Default `get_metadata_from_file` requires `parse_title_type_of_line` to be implemented"
)

View file

@ -0,0 +1,29 @@
from __future__ import annotations
import re
from typing import TYPE_CHECKING
from .base import BaseFormat
if TYPE_CHECKING:
from commitizen.tags import VersionTag
class Markdown(BaseFormat):
extension = "md"
alternative_extensions = {"markdown", "mkd"}
RE_TITLE = re.compile(r"^(?P<level>#+) (?P<title>.*)$")
def parse_version_from_title(self, line: str) -> VersionTag | None:
m = self.RE_TITLE.match(line)
if not m:
return None
return self.tag_rules.search_version(m.group("title"))
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"))

View file

@ -0,0 +1,92 @@
from __future__ import annotations
import sys
from itertools import zip_longest
from typing import IO, TYPE_CHECKING, Any, Union
from commitizen.changelog import Metadata
from .base import BaseFormat
if TYPE_CHECKING:
# TypeAlias is Python 3.10+ but backported in typing-extensions
if sys.version_info >= (3, 10):
from typing import TypeAlias
else:
from typing_extensions import TypeAlias
# Can't use `|` operator and native type because of https://bugs.python.org/issue42233 only fixed in 3.10
TitleKind: TypeAlias = Union[str, tuple[str, str]]
class RestructuredText(BaseFormat):
extension = "rst"
def get_metadata_from_file(self, file: IO[Any]) -> Metadata:
"""
RestructuredText section titles are not one-line-based,
they spread on 2 or 3 lines and levels are not predefined
but determined byt their occurrence order.
It requires its own algorithm.
For a more generic approach, you need to rely on `docutils`.
"""
meta = Metadata()
unreleased_title_kind: TitleKind | None = None
in_overlined_title = False
lines = file.readlines()
for index, (first, second, third) in enumerate(
zip_longest(lines, lines[1:], lines[2:], fillvalue="")
):
first = first.strip().lower()
second = second.strip().lower()
third = third.strip().lower()
title: str | None = None
kind: TitleKind | None = None
if self.is_overlined_title(first, second, third):
title = second
kind = (first[0], third[0])
in_overlined_title = True
elif not in_overlined_title and self.is_underlined_title(first, second):
title = first
kind = second[0]
else:
in_overlined_title = False
if title:
if "unreleased" in title:
unreleased_title_kind = kind
meta.unreleased_start = index
continue
elif unreleased_title_kind and unreleased_title_kind == kind:
meta.unreleased_end = index
# Try to find the latest release done
if version := self.tag_rules.search_version(title):
meta.latest_version = version[0]
meta.latest_version_tag = version[1]
meta.latest_version_position = index
break
if meta.unreleased_start is not None and meta.unreleased_end is None:
meta.unreleased_end = (
meta.latest_version_position if meta.latest_version else index + 1
)
return meta
def is_overlined_title(self, first: str, second: str, third: str) -> bool:
return (
len(first) >= len(second)
and len(first) == len(third)
and all(char == first[0] for char in first[1:])
and first[0] == third[0]
and self.is_underlined_title(second, third)
)
def is_underlined_title(self, first: str, second: str) -> bool:
return (
len(second) >= len(first)
and not second.isalnum()
and all(char == second[0] for char in second[1:])
)

View file

@ -0,0 +1,26 @@
from __future__ import annotations
import re
from typing import TYPE_CHECKING
from .base import BaseFormat
if TYPE_CHECKING:
from commitizen.tags import VersionTag
class Textile(BaseFormat):
extension = "textile"
RE_TITLE = re.compile(r"^h(?P<level>\d)\. (?P<title>.*)$")
def parse_version_from_title(self, line: str) -> VersionTag | None:
if not self.RE_TITLE.match(line):
return None
return self.tag_rules.search_version(line)
def parse_title_level(self, line: str) -> int | None:
m = self.RE_TITLE.match(line)
if not m:
return None
return int(m.group("level"))