diff --git a/commitizen/changelog.py b/commitizen/changelog.py index bdf11326b..86b7f96f8 100644 --- a/commitizen/changelog.py +++ b/commitizen/changelog.py @@ -72,6 +72,17 @@ def __post_init__(self) -> None: self.latest_version_tag = self.latest_version +@dataclass +class IncrementalMergeInfo: + """ + Information regarding the last non-pre-release, parsed from the changelog. Required to merge pre-releases on bump. + Separate from Metadata to not mess with the interface. + """ + + name: str | None = None + index: int | None = None + + def get_commit_tag(commit: GitCommit, tags: list[GitTag]) -> GitTag | None: return next((tag for tag in tags if tag.rev == commit.rev), None) @@ -86,6 +97,7 @@ def generate_tree_from_commits( changelog_message_builder_hook: MessageBuilderHook | None = None, changelog_release_hook: ChangelogReleaseHook | None = None, rules: TagRules | None = None, + during_version_bump: bool = False, ) -> Generator[dict[str, Any], None, None]: pat = re.compile(changelog_pattern) map_pat = re.compile(commit_parser, re.MULTILINE) @@ -93,8 +105,10 @@ def generate_tree_from_commits( rules = rules or TagRules() # Check if the latest commit is not tagged - - current_tag = get_commit_tag(commits[0], tags) if commits else None + if during_version_bump and rules.merge_prereleases: + current_tag = None + else: + current_tag = get_commit_tag(commits[0], tags) if commits else None current_tag_name = unreleased_version or "Unreleased" current_tag_date = ( date.today().isoformat() if unreleased_version is not None else "" diff --git a/commitizen/changelog_formats/__init__.py b/commitizen/changelog_formats/__init__.py index 9a5eea7ab..453b63945 100644 --- a/commitizen/changelog_formats/__init__.py +++ b/commitizen/changelog_formats/__init__.py @@ -8,7 +8,7 @@ else: import importlib_metadata as metadata -from commitizen.changelog import Metadata +from commitizen.changelog import IncrementalMergeInfo, Metadata from commitizen.config.base_config import BaseConfig from commitizen.exceptions import ChangelogFormatUnknown @@ -48,6 +48,12 @@ def get_metadata(self, filepath: str) -> Metadata: """ raise NotImplementedError + def get_latest_full_release(self, filepath: str) -> IncrementalMergeInfo: + """ + Extract metadata for the last non-pre-release. + """ + raise NotImplementedError + KNOWN_CHANGELOG_FORMATS: dict[str, type[ChangelogFormat]] = { ep.name: ep.load() diff --git a/commitizen/changelog_formats/base.py b/commitizen/changelog_formats/base.py index 64a795207..e9476c79e 100644 --- a/commitizen/changelog_formats/base.py +++ b/commitizen/changelog_formats/base.py @@ -4,8 +4,9 @@ from abc import ABCMeta from typing import IO, Any, ClassVar -from commitizen.changelog import Metadata +from commitizen.changelog import IncrementalMergeInfo, Metadata from commitizen.config.base_config import BaseConfig +from commitizen.git import GitTag from commitizen.tags import TagRules, VersionTag from commitizen.version_schemes import get_version_scheme @@ -69,6 +70,27 @@ def get_metadata_from_file(self, file: IO[Any]) -> Metadata: return meta + def get_latest_full_release(self, filepath: str) -> IncrementalMergeInfo: + if not os.path.isfile(filepath): + return IncrementalMergeInfo() + + with open( + filepath, encoding=self.config.settings["encoding"] + ) as changelog_file: + return self.get_latest_full_release_from_file(changelog_file) + + def get_latest_full_release_from_file(self, file: IO[Any]) -> IncrementalMergeInfo: + for index, line in enumerate(file): + line = line.strip().lower() + + parsed = self.parse_version_from_title(line) + if parsed: + if not self.tag_rules.extract_version( + GitTag(parsed.tag, "", "") + ).is_prerelease: + return IncrementalMergeInfo(name=parsed.tag, index=index) + return IncrementalMergeInfo() + def parse_version_from_title(self, line: str) -> VersionTag | None: """ Extract the version from a title line if any diff --git a/commitizen/commands/bump.py b/commitizen/commands/bump.py index 3dc678920..128a45d61 100644 --- a/commitizen/commands/bump.py +++ b/commitizen/commands/bump.py @@ -311,6 +311,8 @@ def __call__(self) -> None: "extras": self.extras, "incremental": True, "dry_run": dry_run, + "during_version_bump": prerelease + is None, # We let the changelog implementation know that we want to replace prereleases while staying incremental AND the new tag does not exist already } if self.changelog_to_stdout: changelog_cmd = Changelog(self.config, {**args, "dry_run": True}) # type: ignore[typeddict-item] diff --git a/commitizen/commands/changelog.py b/commitizen/commands/changelog.py index 27b8ccb25..b5d5af25b 100644 --- a/commitizen/commands/changelog.py +++ b/commitizen/commands/changelog.py @@ -41,6 +41,7 @@ class ChangelogArgs(TypedDict, total=False): template: str extras: dict[str, Any] export_template: str + during_version_bump: bool | None class Changelog: @@ -121,6 +122,8 @@ def __init__(self, config: BaseConfig, arguments: ChangelogArgs) -> None: self.extras = arguments.get("extras") or {} self.export_template_to = arguments.get("export_template") + self.during_version_bump: bool = arguments.get("during_version_bump") or False + def _find_incremental_rev(self, latest_version: str, tags: Iterable[GitTag]) -> str: """Try to find the 'start_rev'. @@ -218,6 +221,16 @@ def __call__(self) -> None: self.tag_rules, ) + if self.during_version_bump and self.tag_rules.merge_prereleases: + latest_full_release_info = self.changelog_format.get_latest_full_release( + self.file_name + ) + start_rev = latest_full_release_info.name or "" + if latest_full_release_info.index: + changelog_meta.unreleased_start = 0 + changelog_meta.latest_version_position = latest_full_release_info.index + changelog_meta.unreleased_end = latest_full_release_info.index - 1 + 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 @@ -234,6 +247,7 @@ def __call__(self) -> None: changelog_message_builder_hook=self.cz.changelog_message_builder_hook, changelog_release_hook=self.cz.changelog_release_hook, rules=self.tag_rules, + during_version_bump=self.during_version_bump, ) if self.change_type_order: tree = changelog.generate_ordered_changelog_tree( diff --git a/tests/commands/test_bump_command.py b/tests/commands/test_bump_command.py index 59297b172..8ed83940b 100644 --- a/tests/commands/test_bump_command.py +++ b/tests/commands/test_bump_command.py @@ -1737,3 +1737,74 @@ def test_is_initial_tag(mocker: MockFixture, tmp_commitizen_project): # Test case 4: No current tag, user denies mocker.patch("questionary.confirm", return_value=mocker.Mock(ask=lambda: False)) assert bump_cmd._is_initial_tag(None, is_yes=False) is False + + +@pytest.mark.parametrize("test_input", ["rc", "alpha", "beta"]) +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_changelog_config_flag_merge_prerelease( + mocker: MockFixture, changelog_path, config_path, file_regression, test_input +): + with open(config_path, "a") as f: + f.write("changelog_merge_prerelease = true\n") + f.write("update_changelog_on_bump = true\n") + f.write("annotated_tag = true\n") + + create_file_and_commit("irrelevant commit") + mocker.patch("commitizen.git.GitTag.date", "1970-01-01") + git.tag("0.1.0") + + create_file_and_commit("feat: add new output") + create_file_and_commit("fix: output glitch") + testargs = ["cz", "bump", "--prerelease", test_input, "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + testargs = ["cz", "bump", "--changelog"] + + mocker.patch.object(sys, "argv", testargs) + cli.main() + + with open(changelog_path) as f: + out = f.read() + out = re.sub( + r"\([^)]*\)", "", out + ) # remove date from release, since I have no idea how to mock that + print(out) + + file_regression.check(out, extension=".md") + + +@pytest.mark.parametrize("test_input", ["rc", "alpha", "beta"]) +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_changelog_config_flag_merge_prerelease_more_commits( + mocker: MockFixture, changelog_path, config_path, file_regression, test_input +): + # supposed to verify that logic regarding indexes is generic + with open(config_path, "a") as f: + f.write("changelog_merge_prerelease = true\n") + f.write("update_changelog_on_bump = true\n") + f.write("annotated_tag = true\n") + + create_file_and_commit("feat: more relevant commit") + mocker.patch("commitizen.git.GitTag.date", "1970-01-01") + git.tag("0.1.0") + + create_file_and_commit("feat: add new output") + create_file_and_commit("fix: output glitch") + testargs = ["cz", "bump", "--prerelease", test_input, "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + testargs = ["cz", "bump", "--changelog"] + + mocker.patch.object(sys, "argv", testargs) + cli.main() + + with open(changelog_path) as f: + out = f.read() + out = re.sub( + r"\([^)]*\)", "", out + ) # remove date from release, since I have no idea how to mock that + print(out) + + file_regression.check(out, extension=".md") diff --git a/tests/commands/test_bump_command/test_changelog_config_flag_merge_prerelease_alpha_.md b/tests/commands/test_bump_command/test_changelog_config_flag_merge_prerelease_alpha_.md new file mode 100644 index 000000000..ce5c68708 --- /dev/null +++ b/tests/commands/test_bump_command/test_changelog_config_flag_merge_prerelease_alpha_.md @@ -0,0 +1,11 @@ +## 0.2.0 + +### Feat + +- add new output + +### Fix + +- output glitch + +## 0.1.0 diff --git a/tests/commands/test_bump_command/test_changelog_config_flag_merge_prerelease_beta_.md b/tests/commands/test_bump_command/test_changelog_config_flag_merge_prerelease_beta_.md new file mode 100644 index 000000000..ce5c68708 --- /dev/null +++ b/tests/commands/test_bump_command/test_changelog_config_flag_merge_prerelease_beta_.md @@ -0,0 +1,11 @@ +## 0.2.0 + +### Feat + +- add new output + +### Fix + +- output glitch + +## 0.1.0 diff --git a/tests/commands/test_bump_command/test_changelog_config_flag_merge_prerelease_more_commits_alpha_.md b/tests/commands/test_bump_command/test_changelog_config_flag_merge_prerelease_more_commits_alpha_.md new file mode 100644 index 000000000..29013670a --- /dev/null +++ b/tests/commands/test_bump_command/test_changelog_config_flag_merge_prerelease_more_commits_alpha_.md @@ -0,0 +1,15 @@ +## 0.2.0 + +### Feat + +- add new output + +### Fix + +- output glitch + +## 0.1.0 + +### Feat + +- more relevant commit diff --git a/tests/commands/test_bump_command/test_changelog_config_flag_merge_prerelease_more_commits_beta_.md b/tests/commands/test_bump_command/test_changelog_config_flag_merge_prerelease_more_commits_beta_.md new file mode 100644 index 000000000..29013670a --- /dev/null +++ b/tests/commands/test_bump_command/test_changelog_config_flag_merge_prerelease_more_commits_beta_.md @@ -0,0 +1,15 @@ +## 0.2.0 + +### Feat + +- add new output + +### Fix + +- output glitch + +## 0.1.0 + +### Feat + +- more relevant commit diff --git a/tests/commands/test_bump_command/test_changelog_config_flag_merge_prerelease_more_commits_rc_.md b/tests/commands/test_bump_command/test_changelog_config_flag_merge_prerelease_more_commits_rc_.md new file mode 100644 index 000000000..29013670a --- /dev/null +++ b/tests/commands/test_bump_command/test_changelog_config_flag_merge_prerelease_more_commits_rc_.md @@ -0,0 +1,15 @@ +## 0.2.0 + +### Feat + +- add new output + +### Fix + +- output glitch + +## 0.1.0 + +### Feat + +- more relevant commit diff --git a/tests/commands/test_bump_command/test_changelog_config_flag_merge_prerelease_rc_.md b/tests/commands/test_bump_command/test_changelog_config_flag_merge_prerelease_rc_.md new file mode 100644 index 000000000..ce5c68708 --- /dev/null +++ b/tests/commands/test_bump_command/test_changelog_config_flag_merge_prerelease_rc_.md @@ -0,0 +1,11 @@ +## 0.2.0 + +### Feat + +- add new output + +### Fix + +- output glitch + +## 0.1.0