diff --git a/commitizen/commands/changelog.py b/commitizen/commands/changelog.py index 21a5abfe15..5093ed9f29 100644 --- a/commitizen/commands/changelog.py +++ b/commitizen/commands/changelog.py @@ -227,16 +227,32 @@ def __call__(self) -> None: latest_full_release_info = self.changelog_format.get_latest_full_release( self.file_name ) - if latest_full_release_info.index: - changelog_meta.unreleased_start = 0 + # Determine if there are prereleases to merge: + # - Only prereleases in changelog (no full release found), OR + # - First version in changelog is before first full release (prereleases exist) + if latest_full_release_info.index is not None and ( + latest_full_release_info.name is None + or ( + changelog_meta.latest_version_position is not None + and changelog_meta.latest_version_position + < latest_full_release_info.index + ) + ): + # Use the existing unreleased_start if available (from get_metadata()). + # Otherwise, use the position of the first version entry (prerelease) + # to preserve the changelog header. + if changelog_meta.unreleased_start is None: + changelog_meta.unreleased_start = ( + changelog_meta.latest_version_position + ) changelog_meta.latest_version_position = latest_full_release_info.index changelog_meta.unreleased_end = latest_full_release_info.index - 1 - start_rev = latest_full_release_info.name or "" - if not start_rev and latest_full_release_info.index: - # Only pre-releases in changelog - changelog_meta.latest_version_position = None - changelog_meta.unreleased_end = latest_full_release_info.index + 1 + start_rev = latest_full_release_info.name or "" + if not start_rev: + # Only pre-releases in changelog + changelog_meta.latest_version_position = None + 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 ( diff --git a/tests/commands/test_bump_command.py b/tests/commands/test_bump_command.py index da7a12c0d7..ef950e7ca7 100644 --- a/tests/commands/test_bump_command.py +++ b/tests/commands/test_bump_command.py @@ -1494,11 +1494,57 @@ def test_changelog_config_flag_merge_prerelease_only_prerelease_present( file_regression.check(out, extension=".md") +@pytest.mark.parametrize( + ("prerelease", "merge"), + [ + pytest.param(True, "true", id="with_prerelease_merge"), + pytest.param(True, "false", id="with_prerelease_no_merge"), + pytest.param(False, "true", id="without_prerelease"), + ], +) @pytest.mark.usefixtures("tmp_commitizen_project") -def test_bump_deprecate_files_only(util: UtilFixture): - util.create_file_and_commit("feat: new file") - with ( - pytest.warns(DeprecationWarning, match=r".*--files-only.*deprecated"), - pytest.raises(ExpectedExit), - ): - util.run_cli("bump", "--yes", "--files-only") +@pytest.mark.freeze_time("2025-01-01") +def test_changelog_merge_preserves_header( + mocker: MockFixture, + util: UtilFixture, + changelog_path: Path, + config_path: Path, + file_regression: FileRegressionFixture, + prerelease: bool, + merge: str, +): + """Test that merge_prerelease preserves existing changelog header.""" + with config_path.open("a") as f: + f.write(f"changelog_merge_prerelease = {merge}\n") + f.write("update_changelog_on_bump = true\n") + f.write("annotated_tag = true\n") + + # Create initial version with changelog that has a header + util.create_file_and_commit("irrelevant commit") + mocker.patch("commitizen.git.GitTag.date", "1970-01-01") + git.tag("0.1.0") + + # Create a changelog with a header manually + changelog_path.write_text( + dedent("""\ + # Changelog + + All notable changes to this project will be documented here. + + ## 0.1.0 (1970-01-01) + """) + ) + + util.create_file_and_commit("feat: add new output") + util.create_file_and_commit("fix: output glitch") + + if prerelease: + util.run_cli("bump", "--prerelease", "alpha", "--yes") + + util.create_file_and_commit("feat: new feature right before the bump") + util.run_cli("bump", "--changelog") + + with changelog_path.open() as f: + out = f.read() + + file_regression.check(out, extension=".md") diff --git a/tests/commands/test_bump_command/test_changelog_merge_preserves_header_with_prerelease_merge_.md b/tests/commands/test_bump_command/test_changelog_merge_preserves_header_with_prerelease_merge_.md new file mode 100644 index 0000000000..c0ac9c5c9c --- /dev/null +++ b/tests/commands/test_bump_command/test_changelog_merge_preserves_header_with_prerelease_merge_.md @@ -0,0 +1,16 @@ +# Changelog + +All notable changes to this project will be documented here. + +## 0.2.0 (2025-01-01) + +### Feat + +- new feature right before the bump +- add new output + +### Fix + +- output glitch + +## 0.1.0 (1970-01-01) diff --git a/tests/commands/test_bump_command/test_changelog_merge_preserves_header_with_prerelease_no_merge_.md b/tests/commands/test_bump_command/test_changelog_merge_preserves_header_with_prerelease_no_merge_.md new file mode 100644 index 0000000000..6058182503 --- /dev/null +++ b/tests/commands/test_bump_command/test_changelog_merge_preserves_header_with_prerelease_no_merge_.md @@ -0,0 +1,21 @@ +# Changelog + +All notable changes to this project will be documented here. + +## 0.2.0 (2025-01-01) + +### Feat + +- new feature right before the bump + +## 0.2.0a0 (2025-01-01) + +### Feat + +- add new output + +### Fix + +- output glitch + +## 0.1.0 (1970-01-01) diff --git a/tests/commands/test_bump_command/test_changelog_merge_preserves_header_without_prerelease_.md b/tests/commands/test_bump_command/test_changelog_merge_preserves_header_without_prerelease_.md new file mode 100644 index 0000000000..c0ac9c5c9c --- /dev/null +++ b/tests/commands/test_bump_command/test_changelog_merge_preserves_header_without_prerelease_.md @@ -0,0 +1,16 @@ +# Changelog + +All notable changes to this project will be documented here. + +## 0.2.0 (2025-01-01) + +### Feat + +- new feature right before the bump +- add new output + +### Fix + +- output glitch + +## 0.1.0 (1970-01-01)