Skip to content
30 changes: 23 additions & 7 deletions commitizen/commands/changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
78 changes: 78 additions & 0 deletions tests/commands/test_bump_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -1494,6 +1494,84 @@ def test_changelog_config_flag_merge_prerelease_only_prerelease_present(
file_regression.check(out, extension=".md")


@pytest.mark.usefixtures("tmp_commitizen_project")
@pytest.mark.freeze_time("2025-01-01")
def test_changelog_merge_prerelease_preserves_header(
mocker: MockFixture,
util: UtilFixture,
changelog_path: Path,
config_path: Path,
file_regression: FileRegressionFixture,
):
"""Test that merge_prerelease preserves existing changelog header."""
with config_path.open("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 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(
"# Changelog\n\nAll notable changes to this project will be documented here.\n\n## 0.1.0 (1970-01-01)\n"
)

util.create_file_and_commit("feat: add new output")
util.create_file_and_commit("fix: output glitch")
util.run_cli("bump", "--prerelease", "alpha", "--yes")

util.run_cli("bump", "--changelog")

with changelog_path.open() as f:
out = f.read()

file_regression.check(out, extension=".md")


@pytest.mark.usefixtures("tmp_commitizen_project")
@pytest.mark.freeze_time("2025-01-01")
def test_changelog_merge_prerelease_no_prereleases_to_merge(
mocker: MockFixture,
util: UtilFixture,
changelog_path: Path,
config_path: Path,
file_regression: FileRegressionFixture,
):
"""Test that merge_prerelease works correctly when there are no prereleases.

When changelog_merge_prerelease is enabled but there are no prereleases to merge,
the normal incremental changelog behavior should apply and the existing changelog
content should be preserved.
"""
with config_path.open("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 initial version with changelog that has a header
util.create_file_and_commit("feat: initial feature")
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(
"# Changelog\n\nAll notable changes.\n\n## 0.1.0 (1970-01-01)\n\n### Feat\n\n- initial feature\n"
)

# Add new commits and do a regular bump (no prerelease)
util.create_file_and_commit("feat: add new output")
util.create_file_and_commit("fix: output glitch")
util.run_cli("bump", "--changelog")

with changelog_path.open() as f:
out = f.read()

file_regression.check(out, extension=".md")


@pytest.mark.usefixtures("tmp_commitizen_project")
def test_bump_deprecate_files_only(util: UtilFixture):
util.create_file_and_commit("feat: new file")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Changelog

All notable changes.

## 0.2.0 (2025-01-01)

### Feat

- add new output

### Fix

- output glitch

## 0.1.0 (1970-01-01)

### Feat

- initial feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Changelog

All notable changes to this project will be documented here.

## 0.2.0 (2025-01-01)

### Feat

- add new output

### Fix

- output glitch

## 0.1.0 (1970-01-01)