Skip to content

Commit c1f2e4f

Browse files
committed
feat(tags): adds legacy_tag_formats and ignored_tag_formats settings
1 parent b110988 commit c1f2e4f

30 files changed

+781
-373
lines changed

commitizen/bump.py

+1-29
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from commitizen.defaults import MAJOR, MINOR, PATCH, bump_message, encoding
1111
from commitizen.exceptions import CurrentVersionNotFoundError
1212
from commitizen.git import GitCommit, smart_open
13-
from commitizen.version_schemes import DEFAULT_SCHEME, Increment, Version, VersionScheme
13+
from commitizen.version_schemes import Increment, Version
1414

1515
VERSION_TYPES = [None, PATCH, MINOR, MAJOR]
1616

@@ -131,34 +131,6 @@ def _version_to_regex(version: str) -> str:
131131
return version.replace(".", r"\.").replace("+", r"\+")
132132

133133

134-
def normalize_tag(
135-
version: Version | str,
136-
tag_format: str,
137-
scheme: VersionScheme | None = None,
138-
) -> str:
139-
"""The tag and the software version might be different.
140-
141-
That's why this function exists.
142-
143-
Example:
144-
| tag | version (PEP 0440) |
145-
| --- | ------- |
146-
| v0.9.0 | 0.9.0 |
147-
| ver1.0.0 | 1.0.0 |
148-
| ver1.0.0.a0 | 1.0.0a0 |
149-
"""
150-
scheme = scheme or DEFAULT_SCHEME
151-
version = scheme(version) if isinstance(version, str) else version
152-
153-
major, minor, patch = version.release
154-
prerelease = version.prerelease or ""
155-
156-
t = Template(tag_format)
157-
return t.safe_substitute(
158-
version=version, major=major, minor=minor, patch=patch, prerelease=prerelease
159-
)
160-
161-
162134
def create_commit_message(
163135
current_version: Version | str,
164136
new_version: Version | str,

commitizen/changelog.py

+26-58
Original file line numberDiff line numberDiff line change
@@ -42,21 +42,13 @@
4242
Template,
4343
)
4444

45-
from commitizen import out
46-
from commitizen.bump import normalize_tag
4745
from commitizen.cz.base import ChangelogReleaseHook
48-
from commitizen.defaults import get_tag_regexes
4946
from commitizen.exceptions import InvalidConfigurationError, NoCommitsFoundError
5047
from commitizen.git import GitCommit, GitTag
51-
from commitizen.version_schemes import (
52-
DEFAULT_SCHEME,
53-
BaseVersion,
54-
InvalidVersion,
55-
)
48+
from commitizen.tags import TagRules
5649

5750
if TYPE_CHECKING:
5851
from commitizen.cz.base import MessageBuilderHook
59-
from commitizen.version_schemes import VersionScheme
6052

6153

6254
@dataclass
@@ -69,50 +61,19 @@ class Metadata:
6961
unreleased_end: int | None = None
7062
latest_version: str | None = None
7163
latest_version_position: int | None = None
64+
latest_version_tag: str | None = None
65+
66+
def __post_init__(self):
67+
if self.latest_version and not self.latest_version_tag:
68+
# Test syntactic sugar
69+
# latest version tag is optional if same as latest version
70+
self.latest_version_tag = self.latest_version
7271

7372

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

7776

78-
def tag_included_in_changelog(
79-
tag: GitTag,
80-
used_tags: list,
81-
merge_prerelease: bool,
82-
scheme: VersionScheme = DEFAULT_SCHEME,
83-
) -> bool:
84-
if tag in used_tags:
85-
return False
86-
87-
try:
88-
version = scheme(tag.name)
89-
except InvalidVersion:
90-
return False
91-
92-
if merge_prerelease and version.is_prerelease:
93-
return False
94-
95-
return True
96-
97-
98-
def get_version_tags(
99-
scheme: type[BaseVersion], tags: list[GitTag], tag_format: str
100-
) -> list[GitTag]:
101-
valid_tags: list[GitTag] = []
102-
TAG_FORMAT_REGEXS = get_tag_regexes(scheme.parser.pattern)
103-
tag_format_regex = tag_format
104-
for pattern, regex in TAG_FORMAT_REGEXS.items():
105-
tag_format_regex = tag_format_regex.replace(pattern, regex)
106-
for tag in tags:
107-
if re.match(tag_format_regex, tag.name):
108-
valid_tags.append(tag)
109-
else:
110-
out.warn(
111-
f"InvalidVersion {tag.name} doesn't match configured tag format {tag_format}"
112-
)
113-
return valid_tags
114-
115-
11677
def generate_tree_from_commits(
11778
commits: list[GitCommit],
11879
tags: list[GitTag],
@@ -122,13 +83,13 @@ def generate_tree_from_commits(
12283
change_type_map: dict[str, str] | None = None,
12384
changelog_message_builder_hook: MessageBuilderHook | None = None,
12485
changelog_release_hook: ChangelogReleaseHook | None = None,
125-
merge_prerelease: bool = False,
126-
scheme: VersionScheme = DEFAULT_SCHEME,
86+
rules: TagRules | None = None,
12787
) -> Iterable[dict]:
12888
pat = re.compile(changelog_pattern)
12989
map_pat = re.compile(commit_parser, re.MULTILINE)
13090
body_map_pat = re.compile(commit_parser, re.MULTILINE | re.DOTALL)
13191
current_tag: GitTag | None = None
92+
rules = rules or TagRules()
13293

13394
# Check if the latest commit is not tagged
13495
if commits:
@@ -148,8 +109,10 @@ def generate_tree_from_commits(
148109
for commit in commits:
149110
commit_tag = get_commit_tag(commit, tags)
150111

151-
if commit_tag is not None and tag_included_in_changelog(
152-
commit_tag, used_tags, merge_prerelease, scheme=scheme
112+
if (
113+
commit_tag
114+
and commit_tag not in used_tags
115+
and rules.include_in_changelog(commit_tag)
153116
):
154117
used_tags.append(commit_tag)
155118
release = {
@@ -343,8 +306,7 @@ def get_smart_tag_range(
343306
def get_oldest_and_newest_rev(
344307
tags: list[GitTag],
345308
version: str,
346-
tag_format: str,
347-
scheme: VersionScheme | None = None,
309+
rules: TagRules,
348310
) -> tuple[str | None, str | None]:
349311
"""Find the tags for the given version.
350312
@@ -358,22 +320,28 @@ def get_oldest_and_newest_rev(
358320
oldest, newest = version.split("..")
359321
except ValueError:
360322
newest = version
361-
newest_tag = normalize_tag(newest, tag_format=tag_format, scheme=scheme)
323+
if not (newest_tag := rules.find_tag_for(tags, newest)):
324+
raise NoCommitsFoundError("Could not find a valid revision range.")
362325

363326
oldest_tag = None
327+
oldest_tag_name = None
364328
if oldest:
365-
oldest_tag = normalize_tag(oldest, tag_format=tag_format, scheme=scheme)
329+
if not (oldest_tag := rules.find_tag_for(tags, oldest)):
330+
raise NoCommitsFoundError("Could not find a valid revision range.")
331+
oldest_tag_name = oldest_tag.name
366332

367-
tags_range = get_smart_tag_range(tags, newest=newest_tag, oldest=oldest_tag)
333+
tags_range = get_smart_tag_range(
334+
tags, newest=newest_tag.name, oldest=oldest_tag_name
335+
)
368336
if not tags_range:
369337
raise NoCommitsFoundError("Could not find a valid revision range.")
370338

371339
oldest_rev: str | None = tags_range[-1].name
372-
newest_rev = newest_tag
340+
newest_rev = newest_tag.name
373341

374342
# check if it's the first tag created
375343
# and it's also being requested as part of the range
376-
if oldest_rev == tags[-1].name and oldest_rev == oldest_tag:
344+
if oldest_rev == tags[-1].name and oldest_rev == oldest_tag_name:
377345
return None, newest_rev
378346

379347
# when they are the same, and it's also the

commitizen/changelog_formats/asciidoc.py

+2-17
Original file line numberDiff line numberDiff line change
@@ -10,27 +10,12 @@ class AsciiDoc(BaseFormat):
1010

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

13-
def parse_version_from_title(self, line: str) -> str | None:
13+
def parse_version_from_title(self, line: str) -> tuple[str, str] | None:
1414
m = self.RE_TITLE.match(line)
1515
if not m:
1616
return None
1717
# Capture last match as AsciiDoc use postfixed URL labels
18-
matches = list(re.finditer(self.version_parser, m.group("title")))
19-
if not matches:
20-
return None
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_version}-{partial_matches['prerelease']}"
31-
if partial_matches.get("devrelease"):
32-
partial_version = f"{partial_version}{partial_matches['devrelease']}"
33-
return partial_version
18+
return self.tag_rules.search_version(m.group("title"), last=True)
3419

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

commitizen/changelog_formats/base.py

+10-15
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
from __future__ import annotations
22

33
import os
4-
import re
54
from abc import ABCMeta
6-
from re import Pattern
75
from typing import IO, Any, ClassVar
86

9-
from commitizen.changelog import Metadata
7+
from commitizen.changelog import Metadata, TagRules
108
from commitizen.config.base_config import BaseConfig
11-
from commitizen.defaults import get_tag_regexes
129
from commitizen.version_schemes import get_version_scheme
1310

1411
from . import ChangelogFormat
@@ -28,15 +25,12 @@ def __init__(self, config: BaseConfig):
2825
self.config = config
2926
self.encoding = self.config.settings["encoding"]
3027
self.tag_format = self.config.settings["tag_format"]
31-
32-
@property
33-
def version_parser(self) -> Pattern:
34-
tag_regex: str = self.tag_format
35-
version_regex = get_version_scheme(self.config).parser.pattern
36-
TAG_FORMAT_REGEXS = get_tag_regexes(version_regex)
37-
for pattern, regex in TAG_FORMAT_REGEXS.items():
38-
tag_regex = tag_regex.replace(pattern, regex)
39-
return re.compile(tag_regex)
28+
self.tag_rules = TagRules(
29+
scheme=get_version_scheme(self.config.settings),
30+
tag_format=self.tag_format,
31+
legacy_tag_formats=self.config.settings["legacy_tag_formats"],
32+
ignored_tag_formats=self.config.settings["ignored_tag_formats"],
33+
)
4034

4135
def get_metadata(self, filepath: str) -> Metadata:
4236
if not os.path.isfile(filepath):
@@ -65,15 +59,16 @@ def get_metadata_from_file(self, file: IO[Any]) -> Metadata:
6559
# Try to find the latest release done
6660
version = self.parse_version_from_title(line)
6761
if version:
68-
meta.latest_version = version
62+
meta.latest_version = version[0]
63+
meta.latest_version_tag = version[1]
6964
meta.latest_version_position = index
7065
break # there's no need for more info
7166
if meta.unreleased_start is not None and meta.unreleased_end is None:
7267
meta.unreleased_end = index
7368

7469
return meta
7570

76-
def parse_version_from_title(self, line: str) -> str | None:
71+
def parse_version_from_title(self, line: str) -> tuple[str, str] | None:
7772
"""
7873
Extract the version from a title line if any
7974
"""

commitizen/changelog_formats/markdown.py

+2-19
Original file line numberDiff line numberDiff line change
@@ -12,28 +12,11 @@ class Markdown(BaseFormat):
1212

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

15-
def parse_version_from_title(self, line: str) -> str | None:
15+
def parse_version_from_title(self, line: str) -> tuple[str, str] | None:
1616
m = self.RE_TITLE.match(line)
1717
if not m:
1818
return None
19-
m = re.search(self.version_parser, m.group("title"))
20-
if not m:
21-
return None
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"{partial_version}-{matches['prerelease']}"
34-
if matches.get("devrelease"):
35-
partial_version = f"{partial_version}{matches['devrelease']}"
36-
return partial_version
19+
return self.tag_rules.search_version(m.group("title"))
3720

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

commitizen/changelog_formats/restructuredtext.py

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

3-
import re
43
import sys
54
from itertools import zip_longest
65
from typing import IO, TYPE_CHECKING, Any, Union
@@ -64,31 +63,11 @@ def get_metadata_from_file(self, file: IO[Any]) -> Metadata:
6463
elif unreleased_title_kind and unreleased_title_kind == kind:
6564
meta.unreleased_end = index
6665
# Try to find the latest release done
67-
m = re.search(self.version_parser, title)
68-
if m:
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 = (
81-
f"{partial_version}-{matches['prerelease']}"
82-
)
83-
if matches.get("devrelease"):
84-
partial_version = (
85-
f"{partial_version}{matches['devrelease']}"
86-
)
87-
meta.latest_version = partial_version
88-
meta.latest_version_position = index
89-
break
90-
except KeyError:
91-
pass
66+
if version := self.tag_rules.search_version(title):
67+
meta.latest_version = version[0]
68+
meta.latest_version_tag = version[1]
69+
meta.latest_version_position = index
70+
break
9271
if meta.unreleased_start is not None and meta.unreleased_end is None:
9372
meta.unreleased_end = (
9473
meta.latest_version_position if meta.latest_version else index + 1

commitizen/changelog_formats/textile.py

+2-22
Original file line numberDiff line numberDiff line change
@@ -10,30 +10,10 @@ class Textile(BaseFormat):
1010

1111
RE_TITLE = re.compile(r"^h(?P<level>\d)\. (?P<title>.*)$")
1212

13-
def parse_version_from_title(self, line: str) -> str | None:
13+
def parse_version_from_title(self, line: str) -> tuple[str, str] | None:
1414
if not self.RE_TITLE.match(line):
1515
return None
16-
m = re.search(self.version_parser, line)
17-
if not m:
18-
return None
19-
if "version" in m.groupdict():
20-
return m.group("version")
21-
matches = m.groupdict()
22-
if not all(
23-
[
24-
version_segment in matches
25-
for version_segment in ("major", "minor", "patch")
26-
]
27-
):
28-
return None
29-
30-
partial_version = f"{matches['major']}.{matches['minor']}.{matches['patch']}"
31-
32-
if matches.get("prerelease"):
33-
partial_version = f"{partial_version}-{matches['prerelease']}"
34-
if matches.get("devrelease"):
35-
partial_version = f"{partial_version}{matches['devrelease']}"
36-
return partial_version
16+
return self.tag_rules.search_version(line)
3717

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

0 commit comments

Comments
 (0)