Skip to content

Commit

Permalink
fix(changelog): Handle tag format without version pattern
Browse files Browse the repository at this point in the history
  • Loading branch information
grahamhar committed Apr 7, 2024
1 parent 0a89510 commit 0cc5cef
Show file tree
Hide file tree
Showing 10 changed files with 328 additions and 30 deletions.
4 changes: 2 additions & 2 deletions commitizen/changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,13 +98,13 @@ def get_version_tags(
) -> list[GitTag]:
valid_tags: list[GitTag] = []
TAG_FORMAT_REGEXS = {
"$version": str(scheme.parser.pattern),
"$version": scheme.parser.pattern,
"$major": r"(?P<major>\d+)",
"$minor": r"(?P<minor>\d+)",
"$patch": r"(?P<patch>\d+)",
"$prerelease": r"(?P<prerelease>\w+\d+)?",
"$devrelease": r"(?P<devrelease>\.dev\d+)?",
"${version}": str(scheme.parser.pattern),
"${version}": scheme.parser.pattern,
"${major}": r"(?P<major>\d+)",
"${minor}": r"(?P<minor>\d+)",
"${patch}": r"(?P<patch>\d+)",
Expand Down
14 changes: 13 additions & 1 deletion commitizen/changelog_formats/asciidoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,19 @@ def parse_version_from_title(self, line: str) -> str | None:
matches = list(re.finditer(self.version_parser, m.group("title")))
if not matches:
return None
return matches[-1].group("version")
if "version" in matches[-1].groupdict():
return matches[-1].group("version")
partial_matches = matches[-1].groupdict()
try:
partial_version = f"{partial_matches['major']}.{partial_matches['minor']}.{partial_matches['patch']}"
except KeyError:
return None

if partial_matches.get("prerelease"):
partial_version += f"-{partial_matches['prerelease']}"
if partial_matches.get("devrelease"):
partial_version += f"+{partial_matches['devrelease']}"

Check warning on line 32 in commitizen/changelog_formats/asciidoc.py

View check run for this annotation

Codecov / codecov/patch

commitizen/changelog_formats/asciidoc.py#L32

Added line #L32 was not covered by tests
return partial_version

def parse_title_level(self, line: str) -> int | None:
m = self.RE_TITLE.match(line)
Expand Down
40 changes: 20 additions & 20 deletions commitizen/changelog_formats/base.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import os
import re
from abc import ABCMeta
from re import Pattern
from typing import IO, Any, ClassVar
Expand All @@ -24,30 +25,29 @@ 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.tag_format = self.config.settings.get("tag_format")
self.tag_format = self.config.settings["tag_format"]

@property
def version_parser(self) -> Pattern:
tag_regex: str = self.tag_format
version_regex = get_version_scheme(self.config).parser.pattern
if self.tag_format != "$version":
TAG_FORMAT_REGEXS = {
"$version": version_regex,
"$major": "(?P<major>\d+)",
"$minor": "(?P<minor>\d+)",
"$patch": "(?P<patch>\d+)",
"$prerelease": "(?P<prerelease>\w+\d+)?",
"$devrelease": "(?P<devrelease>\.dev\d+)?",
"${version}": version_regex,
"${major}": "(?P<major>\d+)",
"${minor}": "(?P<minor>\d+)",
"${patch}": "(?P<patch>\d+)",
"${prerelease}": "(?P<prerelease>\w+\d+)?",
"${devrelease}": "(?P<devrelease>\.dev\d+)?",
}
version_regex = self.tag_format
for pattern, regex in TAG_FORMAT_REGEXS.items():
version_regex = version_regex.replace(pattern, regex)
return rf"{version_regex}"
TAG_FORMAT_REGEXS = {
"$version": version_regex,
"$major": r"(?P<major>\d+)",
"$minor": r"(?P<minor>\d+)",
"$patch": r"(?P<patch>\d+)",
"$prerelease": r"(?P<prerelease>\w+\d+)?",
"$devrelease": r"(?P<devrelease>\.dev\d+)?",
"${version}": version_regex,
"${major}": r"(?P<major>\d+)",
"${minor}": r"(?P<minor>\d+)",
"${patch}": r"(?P<patch>\d+)",
"${prerelease}": r"(?P<prerelease>\w+\d+)?",
"${devrelease}": r"(?P<devrelease>\.dev\d+)?",
}
for pattern, regex in TAG_FORMAT_REGEXS.items():
tag_regex = tag_regex.replace(pattern, regex)
return re.compile(tag_regex)

def get_metadata(self, filepath: str) -> Metadata:
if not os.path.isfile(filepath):
Expand Down
16 changes: 15 additions & 1 deletion commitizen/changelog_formats/markdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,21 @@ def parse_version_from_title(self, line: str) -> str | None:
m = re.search(self.version_parser, m.group("title"))
if not m:
return None
return m.group("version")
if "version" in m.groupdict():
return m.group("version")
matches = m.groupdict()
try:
partial_version = (
f"{matches['major']}.{matches['minor']}.{matches['patch']}"
)
except KeyError:
return None

if matches.get("prerelease"):
partial_version += f"-{matches['prerelease']}"
if matches.get("devrelease"):
partial_version += f"+{matches['devrelease']}"

Check warning on line 35 in commitizen/changelog_formats/markdown.py

View check run for this annotation

Codecov / codecov/patch

commitizen/changelog_formats/markdown.py#L35

Added line #L35 was not covered by tests
return partial_version

def parse_title_level(self, line: str) -> int | None:
m = self.RE_TITLE.match(line)
Expand Down
24 changes: 19 additions & 5 deletions commitizen/changelog_formats/restructuredtext.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ def get_metadata_from_file(self, file: IO[Any]) -> Metadata:
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])
Expand All @@ -67,10 +66,25 @@ def get_metadata_from_file(self, file: IO[Any]) -> Metadata:
# Try to find the latest release done
m = re.search(self.version_parser, title)
if m:
version = m.group("version")
meta.latest_version = version
meta.latest_version_position = index
break # there's no need for more info
matches = m.groupdict()
if "version" in matches:
version = m.group("version")
meta.latest_version = version
meta.latest_version_position = index
break # there's no need for more info
try:
partial_version = (
f"{matches['major']}.{matches['minor']}.{matches['patch']}"
)
if matches.get("prerelease"):
partial_version += f"-{matches['prerelease']}"
if matches.get("devrelease"):
partial_version += f"+{matches['devrelease']}"

Check warning on line 82 in commitizen/changelog_formats/restructuredtext.py

View check run for this annotation

Codecov / codecov/patch

commitizen/changelog_formats/restructuredtext.py#L82

Added line #L82 was not covered by tests
meta.latest_version = partial_version
meta.latest_version_position = index
break
except KeyError:
pass
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
Expand Down
16 changes: 15 additions & 1 deletion commitizen/changelog_formats/textile.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,21 @@ def parse_version_from_title(self, line: str) -> str | None:
m = re.search(self.version_parser, line)
if not m:
return None
return m.group("version")
if "version" in m.groupdict():
return m.group("version")
matches = m.groupdict()
try:
partial_version = (
f"{matches['major']}.{matches['minor']}.{matches['patch']}"
)
except KeyError:
return None

if matches.get("prerelease"):
partial_version += f"-{matches['prerelease']}"
if matches.get("devrelease"):
partial_version += f"+{matches['devrelease']}"

Check warning on line 32 in commitizen/changelog_formats/textile.py

View check run for this annotation

Codecov / codecov/patch

commitizen/changelog_formats/textile.py#L32

Added line #L32 was not covered by tests
return partial_version

def parse_title_level(self, line: str) -> int | None:
m = self.RE_TITLE.match(line)
Expand Down
61 changes: 61 additions & 0 deletions tests/test_changelog_format_asciidoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,42 @@
unreleased_start=1,
)

CHANGELOG_E = """
= Changelog
All notable changes to this project will be documented in this file.
The format is based on https://keepachangelog.com/en/1.0.0/[Keep a Changelog],
and this project adheres to https://semver.org/spec/v2.0.0.html[Semantic Versioning].
== [Unreleased]
* Start using "changelog" over "change log" since it's the common usage.
== [{tag_formatted_version}] - 2017-06-20
=== Added
* New visual identity by https://github.com/tylerfortune8[@tylerfortune8].
* Version navigation.
""".strip()

EXPECTED_E = Metadata(
latest_version="1.0.0",
latest_version_position=10,
unreleased_end=10,
unreleased_start=7,
)


@pytest.fixture
def format(config: BaseConfig) -> AsciiDoc:
return AsciiDoc(config)


@pytest.fixture
def format_with_tags(config: BaseConfig, request) -> AsciiDoc:
config.settings["tag_format"] = request.param
return AsciiDoc(config)


VERSIONS_EXAMPLES = [
("== [1.0.0] - 2017-06-20", "1.0.0"),
(
Expand Down Expand Up @@ -136,3 +166,34 @@ def test_get_matadata(
changelog.write_text(content)

assert format.get_metadata(str(changelog)) == expected


@pytest.mark.parametrize(
"format_with_tags, tag_string, expected, ",
(
pytest.param("${version}-example", "1.0.0-example", "1.0.0"),
pytest.param("${version}example", "1.0.0example", "1.0.0"),
pytest.param("example${version}", "example1.0.0", "1.0.0"),
pytest.param("example-${version}", "example-1.0.0", "1.0.0"),
pytest.param("example-${major}-${minor}-${patch}", "example-1-0-0", "1.0.0"),
pytest.param("example-${major}-${minor}", "example-1-0-0", None),
pytest.param(
"${major}-${minor}-${patch}-${prerelease}-example",
"1-0-0-rc1-example",
"1.0.0-rc1",
),
pytest.param(
"${major}-${minor}-${patch}-${prerelease}-example",
"1-0-0-a1-example",
"1.0.0-a1",
),
),
indirect=["format_with_tags"],
)
def test_get_metadata_custom_tag_format(
tmp_path: Path, format_with_tags: AsciiDoc, tag_string: str, expected: Metadata
):
content = CHANGELOG_E.format(tag_formatted_version=tag_string)
changelog = tmp_path / format_with_tags.default_changelog_file
changelog.write_text(content)
assert format_with_tags.get_metadata(str(changelog)).latest_version == expected
62 changes: 62 additions & 0 deletions tests/test_changelog_format_markdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,42 @@
unreleased_start=1,
)

CHANGELOG_E = """
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
- Start using "changelog" over "change log" since it's the common usage.
## {tag_formatted_version} - 2017-06-20
### Added
- New visual identity by [@tylerfortune8](https://github.com/tylerfortune8).
- Version navigation.
""".strip()

EXPECTED_E = Metadata(
latest_version="1.0.0",
latest_version_position=10,
unreleased_end=10,
unreleased_start=7,
)


@pytest.fixture
def format(config: BaseConfig) -> Markdown:
return Markdown(config)


@pytest.fixture
def format_with_tags(config: BaseConfig, request) -> Markdown:
config.settings["tag_format"] = request.param
return Markdown(config)


VERSIONS_EXAMPLES = [
("## [1.0.0] - 2017-06-20", "1.0.0"),
(
Expand Down Expand Up @@ -136,3 +166,35 @@ def test_get_matadata(
changelog.write_text(content)

assert format.get_metadata(str(changelog)) == expected


@pytest.mark.parametrize(
"format_with_tags, tag_string, expected, ",
(
pytest.param("${version}-example", "1.0.0-example", "1.0.0"),
pytest.param("${version}example", "1.0.0example", "1.0.0"),
pytest.param("example${version}", "example1.0.0", "1.0.0"),
pytest.param("example-${version}", "example-1.0.0", "1.0.0"),
pytest.param("example-${major}-${minor}-${patch}", "example-1-0-0", "1.0.0"),
pytest.param("example-${major}-${minor}", "example-1-0-0", None),
pytest.param(
"${major}-${minor}-${patch}-${prerelease}-example",
"1-0-0-rc1-example",
"1.0.0-rc1",
),
pytest.param(
"${major}-${minor}-${patch}-${prerelease}-example",
"1-0-0-a1-example",
"1.0.0-a1",
),
),
indirect=["format_with_tags"],
)
def test_get_metadata_custom_tag_format(
tmp_path: Path, format_with_tags: Markdown, tag_string: str, expected: Metadata
):
content = CHANGELOG_E.format(tag_formatted_version=tag_string)
changelog = tmp_path / format_with_tags.default_changelog_file
changelog.write_text(content)

assert format_with_tags.get_metadata(str(changelog)).latest_version == expected
Loading

0 comments on commit 0cc5cef

Please sign in to comment.