Skip to content
Draft
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
18 changes: 16 additions & 2 deletions commitizen/changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -86,15 +97,18 @@ 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)
body_map_pat = re.compile(commit_parser, re.MULTILINE | re.DOTALL)
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 ""
Expand Down
8 changes: 7 additions & 1 deletion commitizen/changelog_formats/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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()
Expand Down
24 changes: 23 additions & 1 deletion commitizen/changelog_formats/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions commitizen/commands/bump.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
14 changes: 14 additions & 0 deletions commitizen/commands/changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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'.

Expand Down Expand Up @@ -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
Expand All @@ -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(
Expand Down
71 changes: 71 additions & 0 deletions tests/commands/test_bump_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
## 0.2.0

### Feat

- add new output

### Fix

- output glitch

## 0.1.0
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
## 0.2.0

### Feat

- add new output

### Fix

- output glitch

## 0.1.0
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
## 0.2.0

### Feat

- add new output

### Fix

- output glitch

## 0.1.0

### Feat

- more relevant commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
## 0.2.0

### Feat

- add new output

### Fix

- output glitch

## 0.1.0

### Feat

- more relevant commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
## 0.2.0

### Feat

- add new output

### Fix

- output glitch

## 0.1.0

### Feat

- more relevant commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
## 0.2.0

### Feat

- add new output

### Fix

- output glitch

## 0.1.0
Loading