Skip to content

Commit e9f5ba9

Browse files
fix(changelog_merge_prerelease): changelog not merged during cz bump
1 parent 2072f8e commit e9f5ba9

13 files changed

+208
-9
lines changed

commitizen/changelog.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,17 @@ def __post_init__(self) -> None:
7272
self.latest_version_tag = self.latest_version
7373

7474

75+
@dataclass
76+
class IncrementalMergeInfo:
77+
"""
78+
Information regarding the last non-pre-release, parsed from the changelog. Required to merge pre-releases on bump.
79+
Separate from Metadata to not mess with the interface.
80+
"""
81+
82+
name: str | None = None
83+
index: int | None = None
84+
85+
7586
def get_commit_tag(commit: GitCommit, tags: list[GitTag]) -> GitTag | None:
7687
return next((tag for tag in tags if tag.rev == commit.rev), None)
7788

@@ -86,15 +97,18 @@ def generate_tree_from_commits(
8697
changelog_message_builder_hook: MessageBuilderHook | None = None,
8798
changelog_release_hook: ChangelogReleaseHook | None = None,
8899
rules: TagRules | None = None,
100+
during_version_bump: bool = False,
89101
) -> Generator[dict[str, Any], None, None]:
90102
pat = re.compile(changelog_pattern)
91103
map_pat = re.compile(commit_parser, re.MULTILINE)
92104
body_map_pat = re.compile(commit_parser, re.MULTILINE | re.DOTALL)
93105
rules = rules or TagRules()
94106

95107
# Check if the latest commit is not tagged
96-
97-
current_tag = get_commit_tag(commits[0], tags) if commits else None
108+
if during_version_bump and rules.merge_prereleases:
109+
current_tag = None
110+
else:
111+
current_tag = get_commit_tag(commits[0], tags) if commits else None
98112
current_tag_name = unreleased_version or "Unreleased"
99113
current_tag_date = (
100114
date.today().isoformat() if unreleased_version is not None else ""

commitizen/changelog_formats/__init__.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
if TYPE_CHECKING:
99
from collections.abc import Callable
1010

11-
from commitizen.changelog import Metadata
11+
from commitizen.changelog import IncrementalMergeInfo, Metadata
1212
from commitizen.config.base_config import BaseConfig
1313

1414
CHANGELOG_FORMAT_ENTRYPOINT = "commitizen.changelog_format"
@@ -47,6 +47,12 @@ def get_metadata(self, filepath: str) -> Metadata:
4747
"""
4848
raise NotImplementedError
4949

50+
def get_latest_full_release(self, filepath: str) -> IncrementalMergeInfo:
51+
"""
52+
Extract metadata for the last non-pre-release.
53+
"""
54+
raise NotImplementedError
55+
5056

5157
KNOWN_CHANGELOG_FORMATS: dict[str, type[ChangelogFormat]] = {
5258
ep.name: ep.load()

commitizen/changelog_formats/base.py

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
from abc import ABCMeta
55
from typing import IO, TYPE_CHECKING, Any, ClassVar
66

7-
from commitizen.changelog import Metadata
7+
from commitizen.changelog import IncrementalMergeInfo, Metadata
8+
from commitizen.config.base_config import BaseConfig
9+
from commitizen.git import GitTag
810
from commitizen.tags import TagRules, VersionTag
911
from commitizen.version_schemes import get_version_scheme
1012

@@ -60,17 +62,42 @@ def get_metadata_from_file(self, file: IO[Any]) -> Metadata:
6062
meta.unreleased_end = index
6163

6264
# Try to find the latest release done
63-
parsed = self.parse_version_from_title(line)
64-
if parsed:
65-
meta.latest_version = parsed.version
66-
meta.latest_version_tag = parsed.tag
65+
parsed_version = self.parse_version_from_title(line)
66+
if parsed_version:
67+
meta.latest_version = parsed_version.version
68+
meta.latest_version_tag = parsed_version.tag
6769
meta.latest_version_position = index
6870
break # there's no need for more info
6971
if meta.unreleased_start is not None and meta.unreleased_end is None:
7072
meta.unreleased_end = index
7173

7274
return meta
7375

76+
def get_latest_full_release(self, filepath: str) -> IncrementalMergeInfo:
77+
if not os.path.isfile(filepath):
78+
return IncrementalMergeInfo()
79+
80+
with open(
81+
filepath, encoding=self.config.settings["encoding"]
82+
) as changelog_file:
83+
return self.get_latest_full_release_from_file(changelog_file)
84+
85+
def get_latest_full_release_from_file(self, file: IO[Any]) -> IncrementalMergeInfo:
86+
latest_version_index: int | None = None
87+
for index, line in enumerate(file):
88+
latest_version_index = index
89+
line = line.strip().lower()
90+
91+
parsed_version = self.parse_version_from_title(line)
92+
if (
93+
parsed_version
94+
and not self.tag_rules.extract_version(
95+
GitTag(parsed_version.tag, "", "")
96+
).is_prerelease
97+
):
98+
return IncrementalMergeInfo(name=parsed_version.tag, index=index)
99+
return IncrementalMergeInfo(index=latest_version_index)
100+
74101
def parse_version_from_title(self, line: str) -> VersionTag | None:
75102
"""
76103
Extract the version from a title line if any

commitizen/commands/bump.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,8 @@ def __call__(self) -> None:
314314
"extras": self.extras,
315315
"incremental": True,
316316
"dry_run": dry_run,
317+
"during_version_bump": self.arguments["prerelease"]
318+
is None, # governs logic for merge_prerelease
317319
}
318320
if self.changelog_to_stdout:
319321
changelog_cmd = Changelog(

commitizen/commands/changelog.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ class ChangelogArgs(TypedDict, total=False):
4444
template: str
4545
extras: dict[str, Any]
4646
export_template: str
47+
during_version_bump: bool | None
4748

4849

4950
class Changelog:
@@ -124,6 +125,8 @@ def __init__(self, config: BaseConfig, arguments: ChangelogArgs) -> None:
124125
self.extras = arguments.get("extras") or {}
125126
self.export_template_to = arguments.get("export_template")
126127

128+
self.during_version_bump: bool = arguments.get("during_version_bump") or False
129+
127130
def _find_incremental_rev(self, latest_version: str, tags: Iterable[GitTag]) -> str:
128131
"""Try to find the 'start_rev'.
129132
@@ -222,6 +225,21 @@ def __call__(self) -> None:
222225
self.tag_rules,
223226
)
224227

228+
if self.during_version_bump and self.tag_rules.merge_prereleases:
229+
latest_full_release_info = self.changelog_format.get_latest_full_release(
230+
self.file_name
231+
)
232+
if latest_full_release_info.index:
233+
changelog_meta.unreleased_start = 0
234+
changelog_meta.latest_version_position = latest_full_release_info.index
235+
changelog_meta.unreleased_end = latest_full_release_info.index - 1
236+
237+
start_rev = latest_full_release_info.name or ""
238+
if not start_rev and latest_full_release_info.index:
239+
# Only pre-releases in changelog
240+
changelog_meta.latest_version_position = None
241+
changelog_meta.unreleased_end = latest_full_release_info.index + 1
242+
225243
commits = git.get_commits(start=start_rev, end=end_rev, args="--topo-order")
226244
if not commits and (
227245
self.current_version is None or not self.current_version.is_prerelease
@@ -238,6 +256,7 @@ def __call__(self) -> None:
238256
changelog_message_builder_hook=self.cz.changelog_message_builder_hook,
239257
changelog_release_hook=self.cz.changelog_release_hook,
240258
rules=self.tag_rules,
259+
during_version_bump=self.during_version_bump,
241260
)
242261
if self.change_type_order:
243262
tree = changelog.generate_ordered_changelog_tree(

tests/commands/test_bump_command.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from unittest.mock import MagicMock, call
1010

1111
import pytest
12+
from freezegun import freeze_time
1213

1314
import commitizen.commands.bump as bump
1415
from commitizen import cli, cmd, defaults, git, hooks
@@ -1705,3 +1706,66 @@ def test_is_initial_tag(mocker: MockFixture, tmp_commitizen_project):
17051706
# Test case 4: No current tag, user denies
17061707
mocker.patch("questionary.confirm", return_value=mocker.Mock(ask=lambda: False))
17071708
assert bump_cmd._is_initial_tag(None, is_yes=False) is False
1709+
1710+
1711+
@pytest.mark.parametrize("test_input", ["rc", "alpha", "beta"])
1712+
@pytest.mark.usefixtures("tmp_commitizen_project")
1713+
@freeze_time("2025-01-01")
1714+
def test_changelog_config_flag_merge_prerelease(
1715+
mocker: MockFixture, changelog_path, config_path, file_regression, test_input
1716+
):
1717+
with open(config_path, "a") as f:
1718+
f.write("changelog_merge_prerelease = true\n")
1719+
f.write("update_changelog_on_bump = true\n")
1720+
f.write("annotated_tag = true\n")
1721+
1722+
create_file_and_commit("irrelevant commit")
1723+
mocker.patch("commitizen.git.GitTag.date", "1970-01-01")
1724+
git.tag("0.1.0")
1725+
1726+
create_file_and_commit("feat: add new output")
1727+
create_file_and_commit("fix: output glitch")
1728+
testargs = ["cz", "bump", "--prerelease", test_input, "--yes"]
1729+
mocker.patch.object(sys, "argv", testargs)
1730+
cli.main()
1731+
1732+
testargs = ["cz", "bump", "--changelog"]
1733+
mocker.patch.object(sys, "argv", testargs)
1734+
cli.main()
1735+
1736+
with open(changelog_path) as f:
1737+
out = f.read()
1738+
1739+
file_regression.check(out, extension=".md")
1740+
1741+
1742+
@pytest.mark.parametrize("test_input", ["rc", "alpha", "beta"])
1743+
@pytest.mark.usefixtures("tmp_commitizen_project")
1744+
@freeze_time("2025-01-01")
1745+
def test_changelog_config_flag_merge_prerelease_only_prerelease_present(
1746+
mocker: MockFixture, changelog_path, config_path, file_regression, test_input
1747+
):
1748+
with open(config_path, "a") as f:
1749+
f.write("changelog_merge_prerelease = true\n")
1750+
f.write("update_changelog_on_bump = true\n")
1751+
f.write("annotated_tag = true\n")
1752+
1753+
create_file_and_commit("feat: more relevant commit")
1754+
testargs = ["cz", "bump", "--prerelease", test_input, "--yes"]
1755+
mocker.patch.object(sys, "argv", testargs)
1756+
cli.main()
1757+
1758+
create_file_and_commit("feat: add new output")
1759+
create_file_and_commit("fix: output glitch")
1760+
testargs = ["cz", "bump", "--prerelease", test_input, "--yes"]
1761+
mocker.patch.object(sys, "argv", testargs)
1762+
cli.main()
1763+
1764+
testargs = ["cz", "bump", "--changelog"]
1765+
mocker.patch.object(sys, "argv", testargs)
1766+
cli.main()
1767+
1768+
with open(changelog_path) as f:
1769+
out = f.read()
1770+
1771+
file_regression.check(out, extension=".md")
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
## 0.2.0 (2025-01-01)
2+
3+
### Feat
4+
5+
- add new output
6+
7+
### Fix
8+
9+
- output glitch
10+
11+
## 0.1.0 (1970-01-01)
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
## 0.2.0 (2025-01-01)
2+
3+
### Feat
4+
5+
- add new output
6+
7+
### Fix
8+
9+
- output glitch
10+
11+
## 0.1.0 (1970-01-01)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
## 0.2.0 (2025-01-01)
2+
3+
### Feat
4+
5+
- add new output
6+
- more relevant commit
7+
8+
### Fix
9+
10+
- output glitch
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
## 0.2.0 (2025-01-01)
2+
3+
### Feat
4+
5+
- add new output
6+
- more relevant commit
7+
8+
### Fix
9+
10+
- output glitch

0 commit comments

Comments
 (0)