Skip to content

Refactor changelog 6 #1460

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
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
159 changes: 89 additions & 70 deletions commitizen/changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
from collections.abc import Iterable
from dataclasses import dataclass
from datetime import date
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any

from jinja2 import (
BaseLoader,
Expand Down Expand Up @@ -74,6 +74,23 @@ def get_commit_tag(commit: GitCommit, tags: list[GitTag]) -> GitTag | None:
return next((tag for tag in tags if tag.rev == commit.rev), None)


def _get_release_info(
prev_tag_name: str,
prev_tag_date: str,
changes: dict[str | None, list],
changelog_release_hook: ChangelogReleaseHook | None,
commit_tag: GitTag | None,
) -> dict[str, Any]:
release = {
"version": prev_tag_name,
"date": prev_tag_date,
"changes": changes,
}
if changelog_release_hook:
return changelog_release_hook(release, commit_tag)
return release


def generate_tree_from_commits(
commits: list[GitCommit],
tags: list[GitTag],
Expand All @@ -88,47 +105,44 @@ def generate_tree_from_commits(
pat = re.compile(changelog_pattern)
map_pat = re.compile(commit_parser, re.MULTILINE)
body_map_pat = re.compile(commit_parser, re.MULTILINE | re.DOTALL)
current_tag: GitTag | None = None
rules = rules or TagRules()

used_tags: set[GitTag] = set()
prev_tag_name = unreleased_version or "Unreleased"
prev_tag_date = date.today().isoformat() if unreleased_version is not None else ""

# Check if the latest commit is not tagged
if commits:
latest_commit = commits[0]
current_tag = get_commit_tag(latest_commit, tags)

current_tag_name: str = unreleased_version or "Unreleased"
current_tag_date: str = ""
if unreleased_version is not None:
current_tag_date = date.today().isoformat()
if current_tag is not None and current_tag.name:
current_tag_name = current_tag.name
current_tag_date = current_tag.date

changes: dict = defaultdict(list)
used_tags: list = [current_tag]
current_tag = get_commit_tag(commits[0], tags) if commits else None
if current_tag is not None:
used_tags.add(current_tag)
if current_tag.name:
prev_tag_name = current_tag.name
prev_tag_date = current_tag.date

changes: defaultdict[str | None, list] = defaultdict(list)
for commit in commits:
commit_tag = get_commit_tag(commit, tags)
current_tag = get_commit_tag(commit, tags)

if (
commit_tag
and commit_tag not in used_tags
and rules.include_in_changelog(commit_tag)
current_tag
and current_tag not in used_tags
and rules.include_in_changelog(current_tag)
):
used_tags.append(commit_tag)
release = {
"version": current_tag_name,
"date": current_tag_date,
"changes": changes,
}
if changelog_release_hook:
release = changelog_release_hook(release, commit_tag)
yield release
current_tag_name = commit_tag.name
current_tag_date = commit_tag.date
used_tags.add(current_tag)

yield _get_release_info(
prev_tag_name,
prev_tag_date,
changes,
changelog_release_hook,
current_tag,
)

prev_tag_name = current_tag.name
prev_tag_date = current_tag.date
changes = defaultdict(list)

matches = pat.match(commit.message)
if not matches:
if not pat.match(commit.message):
continue

# Process subject from commit message
Expand All @@ -153,14 +167,13 @@ def generate_tree_from_commits(
change_type_map,
)

release = {
"version": current_tag_name,
"date": current_tag_date,
"changes": changes,
}
if changelog_release_hook:
release = changelog_release_hook(release, commit_tag)
yield release
yield _get_release_info(
prev_tag_name,
prev_tag_date,
changes,
changelog_release_hook,
current_tag,
)


def process_commit_message(
Expand All @@ -170,21 +183,23 @@ def process_commit_message(
changes: dict[str | None, list],
change_type_map: dict[str, str] | None = None,
):
message: dict = {
message: dict[str, str | list[str] | Any] = {
"sha1": commit.rev,
"parents": commit.parents,
"author": commit.author,
"author_email": commit.author_email,
**parsed.groupdict(),
}

if processed := hook(message, commit) if hook else message:
messages = [processed] if isinstance(processed, dict) else processed
for msg in messages:
change_type = msg.pop("change_type", None)
if change_type_map:
change_type = change_type_map.get(change_type, change_type)
changes[change_type].append(msg)
if not (processed := hook(message, commit) if hook else message):
return

processed_messages = [processed] if isinstance(processed, dict) else processed
for msg in processed_messages:
change_type = msg.pop("change_type", None)
if change_type_map:
change_type = change_type_map.get(change_type, change_type)
changes[change_type].append(msg)


def order_changelog_tree(tree: Iterable, change_type_order: list[str]) -> Iterable:
Expand Down Expand Up @@ -225,8 +240,7 @@ def render_changelog(
**kwargs,
) -> str:
jinja_template = get_changelog_template(loader, template)
changelog: str = jinja_template.render(tree=tree, **kwargs)
return changelog
return jinja_template.render(tree=tree, **kwargs)


def incremental_build(
Expand All @@ -253,7 +267,9 @@ def incremental_build(
for index, line in enumerate(lines):
if index == unreleased_start:
skip = True
elif index == unreleased_end:
continue

if index == unreleased_end:
skip = False
if (
latest_version_position is None
Expand All @@ -268,13 +284,15 @@ def incremental_build(

if index == latest_version_position:
output_lines.extend([new_content, "\n"])

output_lines.append(line)
if not isinstance(latest_version_position, int):
if output_lines and output_lines[-1].strip():
# Ensure at least one blank line between existing and new content.
output_lines.append("\n")
output_lines.append(new_content)

if isinstance(latest_version_position, int):
return output_lines

if output_lines and output_lines[-1].strip():
# Ensure at least one blank line between existing and new content.
output_lines.append("\n")
output_lines.append(new_content)
return output_lines


Expand Down Expand Up @@ -324,8 +342,7 @@ def get_oldest_and_newest_rev(
if not (newest_tag := rules.find_tag_for(tags, newest)):
raise NoCommitsFoundError("Could not find a valid revision range.")

oldest_tag = None
oldest_tag_name = None
oldest_tag_name: str | None = None
if oldest:
if not (oldest_tag := rules.find_tag_for(tags, oldest)):
raise NoCommitsFoundError("Could not find a valid revision range.")
Expand All @@ -337,17 +354,19 @@ def get_oldest_and_newest_rev(
if not tags_range:
raise NoCommitsFoundError("Could not find a valid revision range.")

oldest_rev: str | None = tags_range[-1].name
newest_rev = newest_tag.name

# check if it's the first tag created
# and it's also being requested as part of the range
if oldest_rev == tags[-1].name and oldest_rev == oldest_tag_name:
return None, newest_rev

# when they are the same, and it's also the
# first tag created
if oldest_rev == newest_rev:
return None, newest_rev
# Return None for oldest_rev if:
# 1. The oldest tag is the last tag in the list and matches the requested oldest tag, or
# 2. The oldest and newest tags are the same
oldest_rev: str | None = (
None
if (
tags_range[-1].name == tags[-1].name
and tags_range[-1].name == oldest_tag_name
or tags_range[-1].name == newest_rev
)
else tags_range[-1].name
)

return oldest_rev, newest_rev
3 changes: 3 additions & 0 deletions commitizen/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ def __eq__(self, other) -> bool:
return False
return self.rev == other.rev # type: ignore

def __hash__(self):
return hash(self.rev)


class GitCommit(GitObject):
def __init__(
Expand Down
38 changes: 37 additions & 1 deletion tests/test_changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -1529,10 +1529,46 @@ def test_get_smart_tag_range_returns_an_extra_for_a_range(tags):
assert 4 == len(res)


def test_get_smart_tag_range_returns_all_tags_for_a_range(tags):
start = tags[0]

end = tags[-1]
res = changelog.get_smart_tag_range(tags, start.name, end.name)
assert len(tags) == len(res)

end = tags[-2]
res = changelog.get_smart_tag_range(tags, start.name, end.name)
assert len(tags) == len(res)

end = tags[-3]
res = changelog.get_smart_tag_range(tags, start.name, end.name)
assert len(tags) - 1 == len(res)


def test_get_smart_tag_range_returns_an_extra_for_a_single_tag(tags):
start = tags[0] # len here is 1, but we expect one more tag as designed
res = changelog.get_smart_tag_range(tags, start.name)
assert 2 == len(res)
assert res[0].name == tags[0].name
assert res[1].name == tags[1].name


def test_get_smart_tag_range_returns_an_empty_list_for_nonexistent_end_tag(tags):
start = tags[0]
res = changelog.get_smart_tag_range(tags, start.name, "nonexistent")
assert len(tags) == len(res)


def test_get_smart_tag_range_returns_an_empty_list_for_nonexistent_start_tag(tags):
end = tags[0]
res = changelog.get_smart_tag_range(tags, "nonexistent", end.name)
assert res[0].name == tags[1].name


def test_get_smart_tag_range_returns_an_empty_list_for_nonexistent_start_and_end_tags(
tags,
):
res = changelog.get_smart_tag_range(tags, "nonexistent", "nonexistent")
assert 0 == len(res)


@dataclass
Expand Down