Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 7d74d80

Browse files
committedApr 7, 2024··
fix(changelog): Handle tag format without version pattern
1 parent 0a89510 commit 7d74d80

10 files changed

+333
-30
lines changed
 

Diff for: ‎commitizen/changelog.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -98,13 +98,13 @@ def get_version_tags(
9898
) -> list[GitTag]:
9999
valid_tags: list[GitTag] = []
100100
TAG_FORMAT_REGEXS = {
101-
"$version": str(scheme.parser.pattern),
101+
"$version": scheme.parser.pattern,
102102
"$major": r"(?P<major>\d+)",
103103
"$minor": r"(?P<minor>\d+)",
104104
"$patch": r"(?P<patch>\d+)",
105105
"$prerelease": r"(?P<prerelease>\w+\d+)?",
106106
"$devrelease": r"(?P<devrelease>\.dev\d+)?",
107-
"${version}": str(scheme.parser.pattern),
107+
"${version}": scheme.parser.pattern,
108108
"${major}": r"(?P<major>\d+)",
109109
"${minor}": r"(?P<minor>\d+)",
110110
"${patch}": r"(?P<patch>\d+)",

Diff for: ‎commitizen/changelog_formats/asciidoc.py

+13-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,19 @@ def parse_version_from_title(self, line: str) -> str | None:
1818
matches = list(re.finditer(self.version_parser, m.group("title")))
1919
if not matches:
2020
return None
21-
return matches[-1].group("version")
21+
if "version" in matches[-1].groupdict():
22+
return matches[-1].group("version")
23+
partial_matches = matches[-1].groupdict()
24+
try:
25+
partial_version = f"{partial_matches['major']}.{partial_matches['minor']}.{partial_matches['patch']}"
26+
except KeyError:
27+
return None
28+
29+
if partial_matches.get("prerelease"):
30+
partial_version += f"-{partial_matches['prerelease']}"
31+
if partial_matches.get("devrelease"):
32+
partial_version += f"{partial_matches['devrelease']}"
33+
return partial_version
2234

2335
def parse_title_level(self, line: str) -> int | None:
2436
m = self.RE_TITLE.match(line)

Diff for: ‎commitizen/changelog_formats/base.py

+20-20
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import os
4+
import re
45
from abc import ABCMeta
56
from re import Pattern
67
from typing import IO, Any, ClassVar
@@ -24,30 +25,29 @@ def __init__(self, config: BaseConfig):
2425
# Constructor needs to be redefined because `Protocol` prevent instantiation by default
2526
# See: https://bugs.python.org/issue44807
2627
self.config = config
27-
self.tag_format = self.config.settings.get("tag_format")
28+
self.tag_format = self.config.settings["tag_format"]
2829

2930
@property
3031
def version_parser(self) -> Pattern:
32+
tag_regex: str = self.tag_format
3133
version_regex = get_version_scheme(self.config).parser.pattern
32-
if self.tag_format != "$version":
33-
TAG_FORMAT_REGEXS = {
34-
"$version": version_regex,
35-
"$major": "(?P<major>\d+)",
36-
"$minor": "(?P<minor>\d+)",
37-
"$patch": "(?P<patch>\d+)",
38-
"$prerelease": "(?P<prerelease>\w+\d+)?",
39-
"$devrelease": "(?P<devrelease>\.dev\d+)?",
40-
"${version}": version_regex,
41-
"${major}": "(?P<major>\d+)",
42-
"${minor}": "(?P<minor>\d+)",
43-
"${patch}": "(?P<patch>\d+)",
44-
"${prerelease}": "(?P<prerelease>\w+\d+)?",
45-
"${devrelease}": "(?P<devrelease>\.dev\d+)?",
46-
}
47-
version_regex = self.tag_format
48-
for pattern, regex in TAG_FORMAT_REGEXS.items():
49-
version_regex = version_regex.replace(pattern, regex)
50-
return rf"{version_regex}"
34+
TAG_FORMAT_REGEXS = {
35+
"$version": version_regex,
36+
"$major": r"(?P<major>\d+)",
37+
"$minor": r"(?P<minor>\d+)",
38+
"$patch": r"(?P<patch>\d+)",
39+
"$prerelease": r"(?P<prerelease>\w+\d+)?",
40+
"$devrelease": r"(?P<devrelease>\.dev\d+)?",
41+
"${version}": version_regex,
42+
"${major}": r"(?P<major>\d+)",
43+
"${minor}": r"(?P<minor>\d+)",
44+
"${patch}": r"(?P<patch>\d+)",
45+
"${prerelease}": r"(?P<prerelease>\w+\d+)?",
46+
"${devrelease}": r"(?P<devrelease>\.dev\d+)?",
47+
}
48+
for pattern, regex in TAG_FORMAT_REGEXS.items():
49+
tag_regex = tag_regex.replace(pattern, regex)
50+
return re.compile(tag_regex)
5151

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

Diff for: ‎commitizen/changelog_formats/markdown.py

+15-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,21 @@ def parse_version_from_title(self, line: str) -> str | None:
1919
m = re.search(self.version_parser, m.group("title"))
2020
if not m:
2121
return None
22-
return m.group("version")
22+
if "version" in m.groupdict():
23+
return m.group("version")
24+
matches = m.groupdict()
25+
try:
26+
partial_version = (
27+
f"{matches['major']}.{matches['minor']}.{matches['patch']}"
28+
)
29+
except KeyError:
30+
return None
31+
32+
if matches.get("prerelease"):
33+
partial_version += f"-{matches['prerelease']}"
34+
if matches.get("devrelease"):
35+
partial_version += f"{matches['devrelease']}"
36+
return partial_version
2337

2438
def parse_title_level(self, line: str) -> int | None:
2539
m = self.RE_TITLE.match(line)

Diff for: ‎commitizen/changelog_formats/restructuredtext.py

+19-5
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ def get_metadata_from_file(self, file: IO[Any]) -> Metadata:
4646
third = third.strip().lower()
4747
title: str | None = None
4848
kind: TitleKind | None = None
49-
5049
if self.is_overlined_title(first, second, third):
5150
title = second
5251
kind = (first[0], third[0])
@@ -67,10 +66,25 @@ def get_metadata_from_file(self, file: IO[Any]) -> Metadata:
6766
# Try to find the latest release done
6867
m = re.search(self.version_parser, title)
6968
if m:
70-
version = m.group("version")
71-
meta.latest_version = version
72-
meta.latest_version_position = index
73-
break # there's no need for more info
69+
matches = m.groupdict()
70+
if "version" in matches:
71+
version = m.group("version")
72+
meta.latest_version = version
73+
meta.latest_version_position = index
74+
break # there's no need for more info
75+
try:
76+
partial_version = (
77+
f"{matches['major']}.{matches['minor']}.{matches['patch']}"
78+
)
79+
if matches.get("prerelease"):
80+
partial_version += f"-{matches['prerelease']}"
81+
if matches.get("devrelease"):
82+
partial_version += f"{matches['devrelease']}"
83+
meta.latest_version = partial_version
84+
meta.latest_version_position = index
85+
break
86+
except KeyError:
87+
pass
7488
if meta.unreleased_start is not None and meta.unreleased_end is None:
7589
meta.unreleased_end = (
7690
meta.latest_version_position if meta.latest_version else index + 1

Diff for: ‎commitizen/changelog_formats/textile.py

+15-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,21 @@ def parse_version_from_title(self, line: str) -> str | None:
1616
m = re.search(self.version_parser, line)
1717
if not m:
1818
return None
19-
return m.group("version")
19+
if "version" in m.groupdict():
20+
return m.group("version")
21+
matches = m.groupdict()
22+
try:
23+
partial_version = (
24+
f"{matches['major']}.{matches['minor']}.{matches['patch']}"
25+
)
26+
except KeyError:
27+
return None
28+
29+
if matches.get("prerelease"):
30+
partial_version += f"-{matches['prerelease']}"
31+
if matches.get("devrelease"):
32+
partial_version += f"{matches['devrelease']}"
33+
return partial_version
2034

2135
def parse_title_level(self, line: str) -> int | None:
2236
m = self.RE_TITLE.match(line)

Diff for: ‎tests/test_changelog_format_asciidoc.py

+61
Original file line numberDiff line numberDiff line change
@@ -73,12 +73,42 @@
7373
unreleased_start=1,
7474
)
7575

76+
CHANGELOG_E = """
77+
= Changelog
78+
79+
All notable changes to this project will be documented in this file.
80+
81+
The format is based on https://keepachangelog.com/en/1.0.0/[Keep a Changelog],
82+
and this project adheres to https://semver.org/spec/v2.0.0.html[Semantic Versioning].
83+
84+
== [Unreleased]
85+
* Start using "changelog" over "change log" since it's the common usage.
86+
87+
== [{tag_formatted_version}] - 2017-06-20
88+
=== Added
89+
* New visual identity by https://github.com/tylerfortune8[@tylerfortune8].
90+
* Version navigation.
91+
""".strip()
92+
93+
EXPECTED_E = Metadata(
94+
latest_version="1.0.0",
95+
latest_version_position=10,
96+
unreleased_end=10,
97+
unreleased_start=7,
98+
)
99+
76100

77101
@pytest.fixture
78102
def format(config: BaseConfig) -> AsciiDoc:
79103
return AsciiDoc(config)
80104

81105

106+
@pytest.fixture
107+
def format_with_tags(config: BaseConfig, request) -> AsciiDoc:
108+
config.settings["tag_format"] = request.param
109+
return AsciiDoc(config)
110+
111+
82112
VERSIONS_EXAMPLES = [
83113
("== [1.0.0] - 2017-06-20", "1.0.0"),
84114
(
@@ -136,3 +166,34 @@ def test_get_matadata(
136166
changelog.write_text(content)
137167

138168
assert format.get_metadata(str(changelog)) == expected
169+
170+
171+
@pytest.mark.parametrize(
172+
"format_with_tags, tag_string, expected, ",
173+
(
174+
pytest.param("${version}-example", "1.0.0-example", "1.0.0"),
175+
pytest.param("${version}example", "1.0.0example", "1.0.0"),
176+
pytest.param("example${version}", "example1.0.0", "1.0.0"),
177+
pytest.param("example-${version}", "example-1.0.0", "1.0.0"),
178+
pytest.param("example-${major}-${minor}-${patch}", "example-1-0-0", "1.0.0"),
179+
pytest.param("example-${major}-${minor}", "example-1-0-0", None),
180+
pytest.param(
181+
"${major}-${minor}-${patch}-${prerelease}-example",
182+
"1-0-0-rc1-example",
183+
"1.0.0-rc1",
184+
),
185+
pytest.param(
186+
"${major}-${minor}-${patch}-${prerelease}${devrelease}-example",
187+
"1-0-0-a1.dev1-example",
188+
"1.0.0-a1.dev1",
189+
),
190+
),
191+
indirect=["format_with_tags"],
192+
)
193+
def test_get_metadata_custom_tag_format(
194+
tmp_path: Path, format_with_tags: AsciiDoc, tag_string: str, expected: Metadata
195+
):
196+
content = CHANGELOG_E.format(tag_formatted_version=tag_string)
197+
changelog = tmp_path / format_with_tags.default_changelog_file
198+
changelog.write_text(content)
199+
assert format_with_tags.get_metadata(str(changelog)).latest_version == expected

Diff for: ‎tests/test_changelog_format_markdown.py

+67
Original file line numberDiff line numberDiff line change
@@ -73,12 +73,42 @@
7373
unreleased_start=1,
7474
)
7575

76+
CHANGELOG_E = """
77+
# Changelog
78+
79+
All notable changes to this project will be documented in this file.
80+
81+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
82+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
83+
84+
## [Unreleased]
85+
- Start using "changelog" over "change log" since it's the common usage.
86+
87+
## {tag_formatted_version} - 2017-06-20
88+
### Added
89+
- New visual identity by [@tylerfortune8](https://github.com/tylerfortune8).
90+
- Version navigation.
91+
""".strip()
92+
93+
EXPECTED_E = Metadata(
94+
latest_version="1.0.0",
95+
latest_version_position=10,
96+
unreleased_end=10,
97+
unreleased_start=7,
98+
)
99+
76100

77101
@pytest.fixture
78102
def format(config: BaseConfig) -> Markdown:
79103
return Markdown(config)
80104

81105

106+
@pytest.fixture
107+
def format_with_tags(config: BaseConfig, request) -> Markdown:
108+
config.settings["tag_format"] = request.param
109+
return Markdown(config)
110+
111+
82112
VERSIONS_EXAMPLES = [
83113
("## [1.0.0] - 2017-06-20", "1.0.0"),
84114
(
@@ -136,3 +166,40 @@ def test_get_matadata(
136166
changelog.write_text(content)
137167

138168
assert format.get_metadata(str(changelog)) == expected
169+
170+
171+
@pytest.mark.parametrize(
172+
"format_with_tags, tag_string, expected, ",
173+
(
174+
pytest.param("${version}-example", "1.0.0-example", "1.0.0"),
175+
pytest.param("${version}example", "1.0.0example", "1.0.0"),
176+
pytest.param("example${version}", "example1.0.0", "1.0.0"),
177+
pytest.param("example-${version}", "example-1.0.0", "1.0.0"),
178+
pytest.param("example-${major}-${minor}-${patch}", "example-1-0-0", "1.0.0"),
179+
pytest.param("example-${major}-${minor}", "example-1-0-0", None),
180+
pytest.param(
181+
"${major}-${minor}-${patch}-${prerelease}-example",
182+
"1-0-0-rc1-example",
183+
"1.0.0-rc1",
184+
),
185+
pytest.param(
186+
"${major}-${minor}-${patch}-${prerelease}-example",
187+
"1-0-0-a1-example",
188+
"1.0.0-a1",
189+
),
190+
pytest.param(
191+
"${major}-${minor}-${patch}-${prerelease}${devrelease}-example",
192+
"1-0-0-a1.dev1-example",
193+
"1.0.0-a1.dev1",
194+
),
195+
),
196+
indirect=["format_with_tags"],
197+
)
198+
def test_get_metadata_custom_tag_format(
199+
tmp_path: Path, format_with_tags: Markdown, tag_string: str, expected: Metadata
200+
):
201+
content = CHANGELOG_E.format(tag_formatted_version=tag_string)
202+
changelog = tmp_path / format_with_tags.default_changelog_file
203+
changelog.write_text(content)
204+
205+
assert format_with_tags.get_metadata(str(changelog)).latest_version == expected

Diff for: ‎tests/test_changelog_format_restructuredtext.py

+66
Original file line numberDiff line numberDiff line change
@@ -273,12 +273,39 @@ def case(
273273
""",
274274
)
275275

276+
CHANGELOG = """
277+
Changelog
278+
#########
279+
280+
All notable changes to this project will be documented in this file.
281+
282+
The format is based on `Keep a Changelog <https://keepachangelog.com/en/1.0.0/>`,
283+
and this project adheres to `Semantic Versioning <https://semver.org/spec/v2.0.0.html>`.
284+
285+
Unreleased
286+
==========
287+
* Start using "changelog" over "change log" since it's the common usage.
288+
289+
{tag_formatted_version} - 2017-06-20
290+
{underline}
291+
Added
292+
-----
293+
* New visual identity by `@tylerfortune8 <https://github.com/tylerfortune8>`.
294+
* Version navigation.
295+
""".strip()
296+
276297

277298
@pytest.fixture
278299
def format(config: BaseConfig) -> RestructuredText:
279300
return RestructuredText(config)
280301

281302

303+
@pytest.fixture
304+
def format_with_tags(config: BaseConfig, request) -> RestructuredText:
305+
config.settings["tag_format"] = request.param
306+
return RestructuredText(config)
307+
308+
282309
@pytest.mark.parametrize("content, expected", CASES)
283310
def test_get_matadata(
284311
tmp_path: Path, format: RestructuredText, content: str, expected: Metadata
@@ -308,3 +335,42 @@ def test_is_overlined_title(format: RestructuredText, text: str, expected: bool)
308335
_, first, second, third = dedent(text).splitlines()
309336

310337
assert format.is_overlined_title(first, second, third) is expected
338+
339+
340+
@pytest.mark.parametrize(
341+
"format_with_tags, tag_string, expected, ",
342+
(
343+
pytest.param("${version}-example", "1.0.0-example", "1.0.0"),
344+
pytest.param("${version}", "1.0.0", "1.0.0"),
345+
pytest.param("${version}example", "1.0.0example", "1.0.0"),
346+
pytest.param("example${version}", "example1.0.0", "1.0.0"),
347+
pytest.param("example-${version}", "example-1.0.0", "1.0.0"),
348+
pytest.param("example-${major}-${minor}-${patch}", "example-1-0-0", "1.0.0"),
349+
pytest.param("example-${major}-${minor}", "example-1-0-0", None),
350+
pytest.param(
351+
"${major}-${minor}-${patch}-${prerelease}-example",
352+
"1-0-0-rc1-example",
353+
"1.0.0-rc1",
354+
),
355+
pytest.param(
356+
"${major}-${minor}-${patch}-${prerelease}${devrelease}-example",
357+
"1-0-0-a1.dev1-example",
358+
"1.0.0-a1.dev1",
359+
),
360+
),
361+
indirect=["format_with_tags"],
362+
)
363+
def test_get_metadata_custom_tag_format(
364+
tmp_path: Path,
365+
format_with_tags: RestructuredText,
366+
tag_string: str,
367+
expected: Metadata,
368+
):
369+
content = CHANGELOG.format(
370+
tag_formatted_version=tag_string,
371+
underline="=" * len(tag_string) + "=============",
372+
)
373+
changelog = tmp_path / format_with_tags.default_changelog_file
374+
changelog.write_text(content)
375+
376+
assert format_with_tags.get_metadata(str(changelog)).latest_version == expected

Diff for: ‎tests/test_changelog_format_textile.py

+55
Original file line numberDiff line numberDiff line change
@@ -73,12 +73,35 @@
7373
unreleased_start=1,
7474
)
7575

76+
CHANGELOG_E = """
77+
h1. Changelog
78+
79+
All notable changes to this project will be documented in this file.
80+
81+
The format is based on "Keep a Changelog":https://keepachangelog.com/en/1.0.0/,
82+
and this project adheres to "Semantic Versioning":https://semver.org/spec/v2.0.0.html.
83+
84+
h2. [Unreleased]
85+
- Start using "changelog" over "change log" since it's the common usage.
86+
87+
h2. [{tag_formatted_version}] - 2017-06-20
88+
h3. Added
89+
* New visual identity by [@tylerfortune8](https://github.com/tylerfortune8).
90+
* Version navigation.
91+
""".strip()
92+
7693

7794
@pytest.fixture
7895
def format(config: BaseConfig) -> Textile:
7996
return Textile(config)
8097

8198

99+
@pytest.fixture
100+
def format_with_tags(config: BaseConfig, request) -> Textile:
101+
config.settings["tag_format"] = request.param
102+
return Textile(config)
103+
104+
82105
VERSIONS_EXAMPLES = [
83106
("h2. [1.0.0] - 2017-06-20", "1.0.0"),
84107
(
@@ -136,3 +159,35 @@ def test_get_matadata(
136159
changelog.write_text(content)
137160

138161
assert format.get_metadata(str(changelog)) == expected
162+
163+
164+
@pytest.mark.parametrize(
165+
"format_with_tags, tag_string, expected, ",
166+
(
167+
pytest.param("${version}-example", "1.0.0-example", "1.0.0"),
168+
pytest.param("${version}example", "1.0.0example", "1.0.0"),
169+
pytest.param("example${version}", "example1.0.0", "1.0.0"),
170+
pytest.param("example-${version}", "example-1.0.0", "1.0.0"),
171+
pytest.param("example-${major}-${minor}-${patch}", "example-1-0-0", "1.0.0"),
172+
pytest.param("example-${major}-${minor}", "example-1-0-0", None),
173+
pytest.param(
174+
"${major}-${minor}-${patch}-${prerelease}-example",
175+
"1-0-0-rc1-example",
176+
"1.0.0-rc1",
177+
),
178+
pytest.param(
179+
"${major}-${minor}-${patch}-${prerelease}${devrelease}-example",
180+
"1-0-0-a1.dev1-example",
181+
"1.0.0-a1.dev1",
182+
),
183+
),
184+
indirect=["format_with_tags"],
185+
)
186+
def test_get_metadata_custom_tag_format(
187+
tmp_path: Path, format_with_tags: Textile, tag_string: str, expected: Metadata
188+
):
189+
content = CHANGELOG_E.format(tag_formatted_version=tag_string)
190+
changelog = tmp_path / format_with_tags.default_changelog_file
191+
changelog.write_text(content)
192+
193+
assert format_with_tags.get_metadata(str(changelog)).latest_version == expected

0 commit comments

Comments
 (0)
Please sign in to comment.