Adding upstream version 4.6.0.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
f3ad83a1a5
commit
a7fbe822ec
278 changed files with 30423 additions and 0 deletions
93
commitizen/changelog_formats/__init__.py
Normal file
93
commitizen/changelog_formats/__init__.py
Normal 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
|
28
commitizen/changelog_formats/asciidoc.py
Normal file
28
commitizen/changelog_formats/asciidoc.py
Normal 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"))
|
86
commitizen/changelog_formats/base.py
Normal file
86
commitizen/changelog_formats/base.py
Normal 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"
|
||||
)
|
29
commitizen/changelog_formats/markdown.py
Normal file
29
commitizen/changelog_formats/markdown.py
Normal 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"))
|
92
commitizen/changelog_formats/restructuredtext.py
Normal file
92
commitizen/changelog_formats/restructuredtext.py
Normal 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:])
|
||||
)
|
26
commitizen/changelog_formats/textile.py
Normal file
26
commitizen/changelog_formats/textile.py
Normal 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"))
|
Loading…
Add table
Add a link
Reference in a new issue