Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 25 additions & 7 deletions commitizen/commands/changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,16 +227,34 @@ 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)
has_prereleases_to_merge = 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
)
)

if has_prereleases_to_merge and latest_full_release_info.index is not None:
Comment on lines +233 to +242
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
has_prereleases_to_merge = 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
)
)
if has_prereleases_to_merge and latest_full_release_info.index is not None:
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
)
):

While has_prereleases_to_merge is descriptive, the condition latest_full_release_info.index is not None is duplicated and removing it will make mypy unhappy. The above comment is enough to help reader understand the intention behind this long expression.

# 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
85 changes: 85 additions & 0 deletions tests/commands/test_bump_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -1494,6 +1494,91 @@ 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()

# The header should be preserved
assert out.startswith("# Changelog\n")
assert "All notable changes to this project will be documented here." in out
Comment on lines +1532 to +1533
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These assertions aren't needed

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()

# The header and existing content should be preserved
assert out.startswith("# Changelog\n")
assert "All notable changes." in out
assert "## 0.1.0 (1970-01-01)" in out
Comment on lines +1576 to +1578
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

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)