Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
73a51ce
refactor(ParseArgs): simplify __call__ function body
bearomorphism Jun 12, 2025
abb2ab2
refactor(ExpectedExit): make the constructor more compact
bearomorphism Jun 12, 2025
1c55aa6
refactor(ScmProvider): replace sorted with max
bearomorphism Jun 12, 2025
d76af75
refactor(git): remove redundant if branch
bearomorphism Jun 12, 2025
4f4c407
fix(Bump): rewrite --get-next NotAllowed error message for consistency
bearomorphism Jun 13, 2025
221e0cd
fix(Changelog): fix _export_template variable type
bearomorphism Jun 13, 2025
6624d89
refactor(TagRules): extract tag_formats property and simplify list co…
bearomorphism Jun 13, 2025
568f5cf
refactor(Init): use ternary operator
bearomorphism Jun 10, 2025
5b88bc1
refactor(bump): use a loop to shorten a series of similar NotAllowed …
bearomorphism Jun 9, 2025
a9460c8
fix: raise NoVersionSpecifiedError if version is None, and adjust cal…
bearomorphism Jun 13, 2025
8f2bbd6
docs(taplo): add toml formatter
bearomorphism May 25, 2025
a587a1b
docs(pre-commit): add taplo to pre-commit hook
bearomorphism May 25, 2025
6d222c1
refactor(Changelog): remove unnecessary intermediate variables for be…
bearomorphism Jun 13, 2025
c5b4a31
refactor(changelog): shorten condition expression and early return
bearomorphism Jun 12, 2025
6d4c7b0
refactor(Init): extract _get_config_data for readability
bearomorphism Jun 11, 2025
016094d
refactor(process_commit_message): better type and early return
bearomorphism Jun 12, 2025
4817c2d
refactor(init): remote extra words
timsu92 Aug 6, 2025
3203b7a
refactor(Init): fix unbounded variable in _ask_tag_format
bearomorphism Jun 10, 2025
eae8841
test(Init): improve coverage for _ask_tag_format
bearomorphism Jun 10, 2025
26926bb
fix(ExitCode): add from_str in ExitCode and replace parse_no_raise wi…
bearomorphism Jun 12, 2025
e4c450c
fix(Init): fix a typo in _ask_version_provider options and remove unn…
bearomorphism Jun 10, 2025
e03f093
fix(init): make welcome message easier to read
bearomorphism Jun 9, 2025
1c8c0be
refactor(Init): remove unnecessary methods from ProjectInfo and refac…
bearomorphism Jun 10, 2025
55f66e2
test(Init): cover _ask_tag test
bearomorphism Jun 10, 2025
1d7c53b
fix(init): use pre-push as pre-commit stage
timsu92 Aug 6, 2025
c7b77cc
test(init): check "pre-" is showing in outputs
timsu92 Aug 6, 2025
d7b2ed1
refactor(Init): remove the variable values_to_add and the update_conf…
bearomorphism Jun 11, 2025
864d67d
test(Init): improve test coverage on config initialization
bearomorphism Jun 11, 2025
15a030d
refactor(changelog): shorten generate_tree_from_commits
bearomorphism Jun 12, 2025
62ebd5d
feat(check): add check against default branch
bearomorphism Jun 3, 2025
2018c2b
test(changelog): ensure error on missing changelog template filename
ongdisheng Jul 6, 2025
187eb07
fix(init): use pre-push as pre-commit stage
timsu92 Aug 6, 2025
3ae6404
refactor(changelog): simplify logic for get_oldest_and_newest_rev
bearomorphism Jun 12, 2025
9b9829a
refactor(changelog): add get_next_tag_name_after_version and test, ma…
bearomorphism Jun 12, 2025
79b9e5b
fix(changelog): mark get_smart_tag_range as deprecated
bearomorphism Aug 24, 2025
8b969db
style(pyproject.toml): fix toml format
Lee-W Aug 30, 2025
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
11 changes: 8 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ repos:
- id: debug-statements
- id: no-commit-to-branch
- id: check-merge-conflict
- id: check-toml
- id: check-toml # TOML linter (syntax checker)
- id: check-yaml
args: [ '--unsafe' ] # for mkdocs.yml
- id: detect-private-key
Expand Down Expand Up @@ -55,17 +55,22 @@ repos:
stages:
- post-commit

- repo: https://github.com/ComPWA/taplo-pre-commit
rev: v0.9.3
hooks:
- id: taplo-format

- repo: local
hooks:
- id: format
name: Format
name: Format Python code via Poetry
language: system
pass_filenames: false
entry: poetry format
types: [ python ]

- id: linter and test
name: Linters
name: Linters via Poetry
language: system
pass_filenames: false
entry: poetry lint
Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-hooks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@
pass_filenames: false
language: python
language_version: python3
minimum_pre_commit_version: "1.4.3"
minimum_pre_commit_version: "3.2.0"
4 changes: 4 additions & 0 deletions .taplo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
include = ["pyproject.toml", ".taplo.toml"]

[formatting]
indent_string = " "
165 changes: 81 additions & 84 deletions commitizen/changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,13 @@

import re
from collections import OrderedDict, defaultdict
from collections.abc import Generator, Iterable, Mapping, Sequence
from collections.abc import Generator, Iterable, Mapping, MutableMapping, Sequence
from dataclasses import dataclass
from datetime import date
from itertools import chain
from typing import TYPE_CHECKING, Any

from deprecated import deprecated
from jinja2 import (
BaseLoader,
ChoiceLoader,
Expand Down Expand Up @@ -88,33 +90,32 @@ 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()

# 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

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

used_tags: set[GitTag] = set()
if current_tag:
used_tags.add(current_tag)
if current_tag.name:
current_tag_name = current_tag.name
current_tag_date = current_tag.date

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

if (
commit_tag
(commit_tag := get_commit_tag(commit, tags))
and commit_tag not in used_tags
and rules.include_in_changelog(commit_tag)
):
used_tags.append(commit_tag)
used_tags.add(commit_tag)
release = {
"version": current_tag_name,
"date": current_tag_date,
Expand All @@ -127,24 +128,15 @@ def generate_tree_from_commits(
current_tag_date = commit_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
if message := map_pat.match(commit.message):
process_commit_message(
changelog_message_builder_hook,
message,
commit,
changes,
change_type_map,
)

# Process body from commit message
body_parts = commit.body.split("\n\n")
for body_part in body_parts:
if message := body_map_pat.match(body_part):
# Process subject and body from commit message
for message in chain(
[map_pat.match(commit.message)],
(body_map_pat.match(block) for block in commit.body.split("\n\n")),
):
if message:
process_commit_message(
changelog_message_builder_hook,
message,
Expand All @@ -167,8 +159,8 @@ def process_commit_message(
hook: MessageBuilderHook | None,
parsed: re.Match[str],
commit: GitCommit,
changes: dict[str | None, list],
change_type_map: dict[str, str] | None = None,
ref_changes: MutableMapping[str | None, list],
change_type_map: Mapping[str, str] | None = None,
) -> None:
message: dict[str, Any] = {
"sha1": commit.rev,
Expand All @@ -178,13 +170,16 @@ def process_commit_message(
**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)
processed_msg = hook(message, commit) if hook else message
if not processed_msg:
return

messages = [processed_msg] if isinstance(processed_msg, dict) else processed_msg
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)
ref_changes[change_type].append(msg)


def generate_ordered_changelog_tree(
Expand Down Expand Up @@ -251,6 +246,7 @@ def incremental_build(
unreleased_start = metadata.unreleased_start
unreleased_end = metadata.unreleased_end
latest_version_position = metadata.latest_version_position

skip = False
output_lines: list[str] = []
for index, line in enumerate(lines):
Expand All @@ -260,9 +256,7 @@ def incremental_build(
skip = False
if (
latest_version_position is None
or isinstance(latest_version_position, int)
and isinstance(unreleased_end, int)
and latest_version_position > unreleased_end
or latest_version_position > unreleased_end
):
continue

Expand All @@ -271,16 +265,32 @@ 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 latest_version_position is not None:
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


def get_next_tag_name_after_version(tags: Iterable[GitTag], version: str) -> str | None:
it = iter(tag.name for tag in tags)
for name in it:
if name == version:
return next(it, None)

raise NoCommitsFoundError(f"Could not find a valid revision range. {version=}")


@deprecated(
reason="This function is unused and will be removed in v5",
version="5.0.0",
category=DeprecationWarning,
)
def get_smart_tag_range(
tags: Sequence[GitTag], newest: str, oldest: str | None = None
) -> list[GitTag]:
Expand Down Expand Up @@ -308,7 +318,7 @@ def get_smart_tag_range(


def get_oldest_and_newest_rev(
tags: Sequence[GitTag],
tags: Iterable[GitTag],
version: str,
rules: TagRules,
) -> tuple[str | None, str]:
Expand All @@ -318,39 +328,26 @@ def get_oldest_and_newest_rev(
- `0.1.0..0.4.0`: as a range
- `0.3.0`: as a single version
"""
oldest: str | None = None
newest: str | None = None
try:
oldest, newest = version.split("..")
except ValueError:
newest = version
if not (newest_tag := rules.find_tag_for(tags, newest)):
oldest_version, sep, newest_version = version.partition("..")
if not sep:
newest_version = version
oldest_version = ""

def get_tag_name(v: str) -> str:
if tag := rules.find_tag_for(tags, v):
return tag.name
raise NoCommitsFoundError("Could not find a valid revision range.")

oldest_tag = None
oldest_tag_name = None
if oldest:
if not (oldest_tag := rules.find_tag_for(tags, oldest)):
raise NoCommitsFoundError("Could not find a valid revision range.")
oldest_tag_name = oldest_tag.name
newest_tag_name = get_tag_name(newest_version)
oldest_tag_name = get_tag_name(oldest_version) if oldest_version else None

tags_range = get_smart_tag_range(
tags, newest=newest_tag.name, oldest=oldest_tag_name
oldest_rev = get_next_tag_name_after_version(
tags, oldest_tag_name or newest_tag_name
)
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 oldest_rev, 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
# 2. The oldest and the newest tag are the same
if oldest_rev == newest_tag_name:
return None, newest_tag_name
return oldest_rev, newest_tag_name
41 changes: 22 additions & 19 deletions commitizen/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,16 +52,13 @@ def __call__(
) -> None:
if not isinstance(values, str):
return
if "=" not in values:

key, sep, value = values.partition("=")
if not key or not sep:
raise InvalidCommandArgumentError(
f"Option {option_string} expect a key=value format"
)
kwargs = getattr(namespace, self.dest, None) or {}
key, value = values.split("=", 1)
if not key:
raise InvalidCommandArgumentError(
f"Option {option_string} expect a key=value format"
)
kwargs[key] = value.strip("'\"")
setattr(namespace, self.dest, kwargs)

Expand Down Expand Up @@ -474,6 +471,13 @@ def __call__(
"help": "a range of git rev to check. e.g, master..HEAD",
"exclusive_group": "group1",
},
{
"name": ["-d", "--use-default-range"],
"action": "store_true",
"default": False,
"help": "check from the default branch to HEAD. e.g, refs/remotes/origin/master..HEAD",
"exclusive_group": "group1",
},
{
"name": ["-m", "--message"],
"help": "commit message that needs to be checked",
Expand Down Expand Up @@ -583,20 +587,19 @@ def parse_no_raise(comma_separated_no_raise: str) -> list[int]:
Receives digits and strings and outputs the parsed integer which
represents the exit code found in exceptions.
"""
no_raise_items: list[str] = comma_separated_no_raise.split(",")
no_raise_codes: list[int] = []
for item in no_raise_items:
if item.isdecimal():
no_raise_codes.append(int(item))
continue

def exit_code_from_str_or_skip(s: str) -> ExitCode | None:
try:
exit_code = ExitCode[item.strip()]
except KeyError:
out.warn(f"WARN: no_raise key `{item}` does not exist. Skipping.")
continue
else:
no_raise_codes.append(exit_code.value)
return no_raise_codes
return ExitCode.from_str(s)
except (KeyError, ValueError):
out.warn(f"WARN: no_raise value `{s}` is not a valid exit code. Skipping.")
return None

return [
code.value
for s in comma_separated_no_raise.split(",")
if (code := exit_code_from_str_or_skip(s)) is not None
]


if TYPE_CHECKING:
Expand Down
Loading
Loading