diff --git a/commitizen/changelog.py b/commitizen/changelog.py index 12d52f7b0..32a66c47e 100644 --- a/commitizen/changelog.py +++ b/commitizen/changelog.py @@ -44,6 +44,7 @@ from commitizen import out from commitizen.bump import normalize_tag from commitizen.cz.base import ChangelogReleaseHook +from commitizen.defaults import get_tag_regexes from commitizen.exceptions import InvalidConfigurationError, NoCommitsFoundError from commitizen.git import GitCommit, GitTag from commitizen.version_schemes import ( @@ -93,16 +94,21 @@ def tag_included_in_changelog( return True -def get_version_tags(scheme: type[BaseVersion], tags: list[GitTag]) -> list[GitTag]: +def get_version_tags( + scheme: type[BaseVersion], tags: list[GitTag], tag_format: str +) -> list[GitTag]: valid_tags: list[GitTag] = [] + TAG_FORMAT_REGEXS = get_tag_regexes(scheme.parser.pattern) + tag_format_regex = tag_format + for pattern, regex in TAG_FORMAT_REGEXS.items(): + tag_format_regex = tag_format_regex.replace(pattern, regex) for tag in tags: - try: - scheme(tag.name) - except InvalidVersion: - out.warn(f"InvalidVersion {tag}") - else: + if re.match(tag_format_regex, tag.name): valid_tags.append(tag) - + else: + out.warn( + f"InvalidVersion {tag.name} doesn't match configured tag format {tag_format}" + ) return valid_tags @@ -351,7 +357,6 @@ def get_oldest_and_newest_rev( oldest, newest = version.split("..") except ValueError: newest = version - newest_tag = normalize_tag(newest, tag_format=tag_format, scheme=scheme) oldest_tag = None diff --git a/commitizen/changelog_formats/asciidoc.py b/commitizen/changelog_formats/asciidoc.py index d738926f6..6007a56d1 100644 --- a/commitizen/changelog_formats/asciidoc.py +++ b/commitizen/changelog_formats/asciidoc.py @@ -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_version}-{partial_matches['prerelease']}" + if partial_matches.get("devrelease"): + partial_version = f"{partial_version}{partial_matches['devrelease']}" + return partial_version def parse_title_level(self, line: str) -> int | None: m = self.RE_TITLE.match(line) diff --git a/commitizen/changelog_formats/base.py b/commitizen/changelog_formats/base.py index 7c802d63d..53527a060 100644 --- a/commitizen/changelog_formats/base.py +++ b/commitizen/changelog_formats/base.py @@ -1,12 +1,14 @@ from __future__ import annotations import os +import re from abc import ABCMeta from re import Pattern from typing import IO, Any, ClassVar from commitizen.changelog import Metadata from commitizen.config.base_config import BaseConfig +from commitizen.defaults import get_tag_regexes from commitizen.version_schemes import get_version_scheme from . import ChangelogFormat @@ -25,10 +27,16 @@ def __init__(self, config: BaseConfig): # See: https://bugs.python.org/issue44807 self.config = config self.encoding = self.config.settings["encoding"] + self.tag_format = self.config.settings["tag_format"] @property def version_parser(self) -> Pattern: - return get_version_scheme(self.config).parser + tag_regex: str = self.tag_format + version_regex = get_version_scheme(self.config).parser.pattern + TAG_FORMAT_REGEXS = get_tag_regexes(version_regex) + 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): diff --git a/commitizen/changelog_formats/markdown.py b/commitizen/changelog_formats/markdown.py index a5a0f42de..29c1cce54 100644 --- a/commitizen/changelog_formats/markdown.py +++ b/commitizen/changelog_formats/markdown.py @@ -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"{partial_version}-{matches['prerelease']}" + if matches.get("devrelease"): + partial_version = f"{partial_version}{matches['devrelease']}" + return partial_version def parse_title_level(self, line: str) -> int | None: m = self.RE_TITLE.match(line) diff --git a/commitizen/changelog_formats/restructuredtext.py b/commitizen/changelog_formats/restructuredtext.py index 37acf81ef..09d032400 100644 --- a/commitizen/changelog_formats/restructuredtext.py +++ b/commitizen/changelog_formats/restructuredtext.py @@ -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]) @@ -67,10 +66,29 @@ 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"{partial_version}-{matches['prerelease']}" + ) + if matches.get("devrelease"): + partial_version = ( + f"{partial_version}{matches['devrelease']}" + ) + 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 diff --git a/commitizen/changelog_formats/textile.py b/commitizen/changelog_formats/textile.py index 80118cdb3..8750f0056 100644 --- a/commitizen/changelog_formats/textile.py +++ b/commitizen/changelog_formats/textile.py @@ -16,7 +16,24 @@ 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() + if not all( + [ + version_segment in matches + for version_segment in ("major", "minor", "patch") + ] + ): + return None + + partial_version = f"{matches['major']}.{matches['minor']}.{matches['patch']}" + + if matches.get("prerelease"): + partial_version = f"{partial_version}-{matches['prerelease']}" + if matches.get("devrelease"): + partial_version = f"{partial_version}{matches['devrelease']}" + return partial_version def parse_title_level(self, line: str) -> int | None: m = self.RE_TITLE.match(line) diff --git a/commitizen/commands/changelog.py b/commitizen/commands/changelog.py index bda7a1844..25e644aae 100644 --- a/commitizen/commands/changelog.py +++ b/commitizen/commands/changelog.py @@ -168,8 +168,10 @@ def __call__(self): # Don't continue if no `file_name` specified. assert self.file_name - tags = changelog.get_version_tags(self.scheme, git.get_tags()) or [] - + tags = ( + changelog.get_version_tags(self.scheme, git.get_tags(), self.tag_format) + or [] + ) end_rev = "" if self.incremental: changelog_meta = self.changelog_format.get_metadata(self.file_name) @@ -182,7 +184,6 @@ def __call__(self): start_rev = self._find_incremental_rev( strip_local_version(latest_tag_version), tags ) - if self.rev_range: start_rev, end_rev = changelog.get_oldest_and_newest_rev( tags, @@ -190,13 +191,11 @@ def __call__(self): tag_format=self.tag_format, scheme=self.scheme, ) - commits = git.get_commits(start=start_rev, end=end_rev, args="--topo-order") if not commits and ( self.current_version is None or not self.current_version.is_prerelease ): raise NoCommitsFoundError("No commits found") - tree = changelog.generate_tree_from_commits( commits, tags, diff --git a/commitizen/defaults.py b/commitizen/defaults.py index e4363f4ab..2d092d500 100644 --- a/commitizen/defaults.py +++ b/commitizen/defaults.py @@ -132,3 +132,22 @@ class Settings(TypedDict, total=False): ) change_type_order = ["BREAKING CHANGE", "Feat", "Fix", "Refactor", "Perf"] bump_message = "bump: version $current_version → $new_version" + + +def get_tag_regexes( + version_regex: str, +) -> dict[str, str]: + return { + "$version": version_regex, + "$major": r"(?P\d+)", + "$minor": r"(?P\d+)", + "$patch": r"(?P\d+)", + "$prerelease": r"(?P\w+\d+)?", + "$devrelease": r"(?P\.dev\d+)?", + "${version}": version_regex, + "${major}": r"(?P\d+)", + "${minor}": r"(?P\d+)", + "${patch}": r"(?P\d+)", + "${prerelease}": r"(?P\w+\d+)?", + "${devrelease}": r"(?P\.dev\d+)?", + } diff --git a/commitizen/providers/scm_provider.py b/commitizen/providers/scm_provider.py index 00df3e415..33e470cfc 100644 --- a/commitizen/providers/scm_provider.py +++ b/commitizen/providers/scm_provider.py @@ -3,6 +3,7 @@ import re from typing import Callable +from commitizen.defaults import get_tag_regexes from commitizen.git import get_tags from commitizen.providers.base_provider import VersionProvider from commitizen.version_schemes import ( @@ -22,14 +23,7 @@ class ScmProvider(VersionProvider): It is meant for `setuptools-scm` or any package manager `*-scm` provider. """ - TAG_FORMAT_REGEXS = { - "$version": r"(?P.+)", - "$major": r"(?P\d+)", - "$minor": r"(?P\d+)", - "$patch": r"(?P\d+)", - "$prerelease": r"(?P\w+\d+)?", - "$devrelease": r"(?P\.dev\d+)?", - } + TAG_FORMAT_REGEXS = get_tag_regexes(r"(?P.+)") def _tag_format_matcher(self) -> Callable[[str], VersionProtocol | None]: version_scheme = get_version_scheme(self.config) diff --git a/docs/commands/bump.md b/docs/commands/bump.md index efb5b0881..afb43230e 100644 --- a/docs/commands/bump.md +++ b/docs/commands/bump.md @@ -414,18 +414,18 @@ In your `pyproject.toml` or `.cz.toml` tag_format = "v$major.$minor.$patch$prerelease" ``` -The variables must be preceded by a `$` sign. Default is `$version`. +The variables must be preceded by a `$` sign and optionally can be wrapped in `{}` . Default is `$version`. Supported variables: -| Variable | Description | -| ------------- | ------------------------------------------- | -| `$version` | full generated version | -| `$major` | MAJOR increment | -| `$minor` | MINOR increment | -| `$patch` | PATCH increment | -| `$prerelease` | Prerelease (alpha, beta, release candidate) | -| `$devrelease` | Development release | +| Variable | Description | +|--------------------------------|---------------------------------------------| +| `$version`, `${version}` | full generated version | +| `$major`, `${major}` | MAJOR increment | +| `$minor`, `${minor}` | MINOR increment | +| `$patch`, `${patch}` | PATCH increment | +| `$prerelease`, `${prerelease}` | Prerelease (alpha, beta, release candidate) | +| `$devrelease`, ${devrelease}` | Development release | --- diff --git a/docs/tutorials/monorepo_guidance.md b/docs/tutorials/monorepo_guidance.md new file mode 100644 index 000000000..c4345d6bc --- /dev/null +++ b/docs/tutorials/monorepo_guidance.md @@ -0,0 +1,41 @@ +# Configuring commitizen in a monorepo + +This tutorial assumes the monorepo layout is designed with multiple components that can be released independently of each +other, it also assumes that conventional commits with scopes are in use. Some suggested layouts: + +``` +. +├── library-b +│   └── .cz.toml +└── library-z + └── .cz.toml +``` + +``` +src +├── library-b +│   └── .cz.toml +└── library-z + └── .cz.toml +``` + +Each component will have its own changelog, commits will need to use scopes so only relevant commits are included in the +appropriate change log for a given component. Example config and commit for `library-b` + +```toml +[tool.commitizen] +name = "cz_customize" +version = "0.0.0" +tag_format = "${version}-library-b" # the component name can be a prefix or suffix with or without a separator +update_changelog_on_bump = true + +[tool.commitizen.customize] +changelog_pattern = "^(feat|fix)\\(library-b\\)(!)?:" #the pattern on types can be a wild card or any types you wish to include +``` + +example commit message for the above + +`fix:(library-b) Some awesome message` + +If the above is followed and the `cz bump --changelog` is run in the directory containing the component the changelog +should be generated in the same directory with only commits scoped to the component. diff --git a/tests/commands/test_changelog_command.py b/tests/commands/test_changelog_command.py index 3d9a5c5c4..bc0d6c6a2 100644 --- a/tests/commands/test_changelog_command.py +++ b/tests/commands/test_changelog_command.py @@ -1523,6 +1523,122 @@ def test_changelog_template_extras_precedance( assert changelog.read_text() == "from-command - from-config - from-plugin" +@pytest.mark.usefixtures("tmp_commitizen_project") +@pytest.mark.freeze_time("2021-06-11") +def test_changelog_only_tag_matching_tag_format_included_prefix( + mocker: MockFixture, + changelog_path: Path, + config_path: Path, +): + with open(config_path, "a", encoding="utf-8") as f: + f.write('\ntag_format = "custom${version}"\n') + create_file_and_commit("feat: new file") + git.tag("v0.2.0") + create_file_and_commit("feat: another new file") + git.tag("0.2.0") + git.tag("random0.2.0") + testargs = ["cz", "bump", "--changelog", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + wait_for_tag() + create_file_and_commit("feat: another new file") + cli.main() + with open(changelog_path) as f: + out = f.read() + assert out.startswith("## custom0.3.0 (2021-06-11)") + assert "## v0.2.0 (2021-06-11)" not in out + assert "## 0.2.0 (2021-06-11)" not in out + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_changelog_only_tag_matching_tag_format_included_prefix_sep( + mocker: MockFixture, + changelog_path: Path, + config_path: Path, +): + with open(config_path, "a", encoding="utf-8") as f: + f.write('\ntag_format = "custom-${version}"\n') + create_file_and_commit("feat: new file") + git.tag("v0.2.0") + create_file_and_commit("feat: another new file") + git.tag("0.2.0") + git.tag("random0.2.0") + wait_for_tag() + testargs = ["cz", "bump", "--changelog", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + with open(changelog_path) as f: + out = f.read() + create_file_and_commit("feat: new version another new file") + create_file_and_commit("feat: new version some new file") + testargs = ["cz", "bump", "--changelog"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + with open(changelog_path) as f: + out = f.read() + assert out.startswith("## custom-0.3.0") + assert "## v0.2.0" not in out + assert "## 0.2.0" not in out + + +@pytest.mark.usefixtures("tmp_commitizen_project") +@pytest.mark.freeze_time("2021-06-11") +def test_changelog_only_tag_matching_tag_format_included_suffix( + mocker: MockFixture, + changelog_path: Path, + config_path: Path, +): + with open(config_path, "a", encoding="utf-8") as f: + f.write('\ntag_format = "${version}custom"\n') + create_file_and_commit("feat: new file") + git.tag("v0.2.0") + create_file_and_commit("feat: another new file") + git.tag("0.2.0") + git.tag("random0.2.0") + testargs = ["cz", "bump", "--changelog", "--yes"] + mocker.patch.object(sys, "argv", testargs) + # bump to 0.2.0custom + cli.main() + wait_for_tag() + create_file_and_commit("feat: another new file") + # bump to 0.3.0custom + cli.main() + wait_for_tag() + with open(changelog_path) as f: + out = f.read() + assert out.startswith("## 0.3.0custom (2021-06-11)") + assert "## v0.2.0 (2021-06-11)" not in out + assert "## 0.2.0 (2021-06-11)" not in out + + +@pytest.mark.usefixtures("tmp_commitizen_project") +@pytest.mark.freeze_time("2021-06-11") +def test_changelog_only_tag_matching_tag_format_included_suffix_sep( + mocker: MockFixture, + changelog_path: Path, + config_path: Path, +): + with open(config_path, "a", encoding="utf-8") as f: + f.write('\ntag_format = "${version}-custom"\n') + create_file_and_commit("feat: new file") + git.tag("v0.2.0") + create_file_and_commit("feat: another new file") + git.tag("0.2.0") + git.tag("random0.2.0") + testargs = ["cz", "bump", "--changelog", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + wait_for_tag() + create_file_and_commit("feat: another new file") + cli.main() + wait_for_tag() + with open(changelog_path) as f: + out = f.read() + assert out.startswith("## 0.3.0-custom (2021-06-11)") + assert "## v0.2.0 (2021-06-11)" not in out + assert "## 0.2.0 (2021-06-11)" not in out + + def test_changelog_template_extra_quotes( mocker: MockFixture, tmp_commitizen_project: Path, diff --git a/tests/test_changelog_format_asciidoc.py b/tests/test_changelog_format_asciidoc.py index 89740d214..0c5930df4 100644 --- a/tests/test_changelog_format_asciidoc.py +++ b/tests/test_changelog_format_asciidoc.py @@ -72,12 +72,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"), ( @@ -135,3 +165,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}${devrelease}-example", + "1-0-0-a1.dev1-example", + "1.0.0-a1.dev1", + ), + ), + 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 diff --git a/tests/test_changelog_format_markdown.py b/tests/test_changelog_format_markdown.py index ab7c65453..52612b8e2 100644 --- a/tests/test_changelog_format_markdown.py +++ b/tests/test_changelog_format_markdown.py @@ -72,12 +72,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"), ( @@ -135,3 +165,40 @@ 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", + ), + pytest.param( + "${major}-${minor}-${patch}-${prerelease}${devrelease}-example", + "1-0-0-a1.dev1-example", + "1.0.0-a1.dev1", + ), + ), + 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 diff --git a/tests/test_changelog_format_restructuredtext.py b/tests/test_changelog_format_restructuredtext.py index 46a11ebcd..11356ae28 100644 --- a/tests/test_changelog_format_restructuredtext.py +++ b/tests/test_changelog_format_restructuredtext.py @@ -273,12 +273,39 @@ def case( """, ) +CHANGELOG = """ +Changelog + ######### + + All notable changes to this project will be documented in this file. + + The format is based on `Keep a Changelog `, + and this project adheres to `Semantic Versioning `. + + Unreleased + ========== + * Start using "changelog" over "change log" since it's the common usage. + + {tag_formatted_version} - 2017-06-20 + {underline} + Added + ----- + * New visual identity by `@tylerfortune8 `. + * Version navigation. +""".strip() + @pytest.fixture def format(config: BaseConfig) -> RestructuredText: return RestructuredText(config) +@pytest.fixture +def format_with_tags(config: BaseConfig, request) -> RestructuredText: + config.settings["tag_format"] = request.param + return RestructuredText(config) + + @pytest.mark.parametrize("content, expected", CASES) def test_get_matadata( tmp_path: Path, format: RestructuredText, content: str, expected: Metadata @@ -308,3 +335,42 @@ def test_is_overlined_title(format: RestructuredText, text: str, expected: bool) _, first, second, third = dedent(text).splitlines() assert format.is_overlined_title(first, second, third) is expected + + +@pytest.mark.parametrize( + "format_with_tags, tag_string, expected, ", + ( + pytest.param("${version}-example", "1.0.0-example", "1.0.0"), + pytest.param("${version}", "1.0.0", "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}${devrelease}-example", + "1-0-0-a1.dev1-example", + "1.0.0-a1.dev1", + ), + ), + indirect=["format_with_tags"], +) +def test_get_metadata_custom_tag_format( + tmp_path: Path, + format_with_tags: RestructuredText, + tag_string: str, + expected: Metadata, +): + content = CHANGELOG.format( + tag_formatted_version=tag_string, + underline="=" * len(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 diff --git a/tests/test_changelog_format_textile.py b/tests/test_changelog_format_textile.py index e382e1c74..3fac5c175 100644 --- a/tests/test_changelog_format_textile.py +++ b/tests/test_changelog_format_textile.py @@ -72,12 +72,35 @@ unreleased_start=1, ) +CHANGELOG_E = """ +h1. 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. + +h2. [Unreleased] +- Start using "changelog" over "change log" since it's the common usage. + +h2. [{tag_formatted_version}] - 2017-06-20 +h3. Added +* New visual identity by [@tylerfortune8](https://github.com/tylerfortune8). +* Version navigation. +""".strip() + @pytest.fixture def format(config: BaseConfig) -> Textile: return Textile(config) +@pytest.fixture +def format_with_tags(config: BaseConfig, request) -> Textile: + config.settings["tag_format"] = request.param + return Textile(config) + + VERSIONS_EXAMPLES = [ ("h2. [1.0.0] - 2017-06-20", "1.0.0"), ( @@ -135,3 +158,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}${devrelease}-example", + "1-0-0-a1.dev1-example", + "1.0.0-a1.dev1", + ), + ), + indirect=["format_with_tags"], +) +def test_get_metadata_custom_tag_format( + tmp_path: Path, format_with_tags: Textile, 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