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
172 changes: 112 additions & 60 deletions commitizen/cz/conventional_commits/conventional_commits.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
from __future__ import annotations

from collections import OrderedDict
from pathlib import Path
from typing import TYPE_CHECKING, TypedDict

from commitizen import defaults
from commitizen.cz.base import BaseCommitizen
from commitizen.cz.utils import multiple_line_breaker, required_validator
from commitizen.question import Choice

if TYPE_CHECKING:
from commitizen.config import BaseConfig
from commitizen.question import CzQuestion
Comment on lines 7 to 14
Comment on lines 7 to 14
Comment on lines 7 to 14

__all__ = ["ConventionalCommitsCz"]
Expand Down Expand Up @@ -41,74 +44,123 @@ class ConventionalCommitsCz(BaseCommitizen):
"refactor": "Refactor",
"perf": "Perf",
}
change_type_choices = [
Choice(
Comment on lines 46 to +48
Comment on lines 46 to +48
value="fix",
name=("fix: A bug fix. Correlates with PATCH in SemVer"),
key="x",
),
Comment on lines +48 to +52
Comment on lines +48 to +52
Comment on lines +48 to +52
Choice(
value="feat",
name="feat: A new feature. Correlates with MINOR in SemVer",
key="f",
),
Comment on lines +53 to +57
Comment on lines +53 to +57
Comment on lines +53 to +57
Choice(
value="docs",
name="docs: Documentation only changes",
key="d",
),
Comment on lines +58 to +62
Comment on lines +58 to +62
Comment on lines +58 to +62
Choice(
value="style",
name=(
"style: Changes that do not affect the "
"meaning of the code (white-space, formatting,"
" missing semi-colons, etc)"
),
key="s",
),
Comment on lines +63 to +71
Comment on lines +63 to +71
Comment on lines +63 to +71
Choice(
value="refactor",
name=(
"refactor: A code change that neither fixes a bug nor adds a feature"
),
key="r",
),
Comment on lines +72 to +78
Comment on lines +72 to +78
Comment on lines +72 to +78
Choice(
value="perf",
name="perf: A code change that improves performance",
key="p",
),
Comment on lines +79 to +83
Comment on lines +79 to +83
Comment on lines +79 to +83
Choice(
value="test",
name="test: Adding missing or correcting existing tests",
key="t",
),
Comment on lines +84 to +88
Comment on lines +84 to +88
Comment on lines +84 to +88
Choice(
value="build",
name=(
"build: Changes that affect the build system or "
"external dependencies (example scopes: pip, docker, npm)"
),
key="b",
),
Comment on lines +89 to +96
Comment on lines +89 to +96
Comment on lines +89 to +96
Choice(
value="ci",
name=(
"ci: Changes to CI configuration files and "
"scripts (example scopes: GitLabCI)"
),
key="c",
),
Comment on lines +97 to +104
Comment on lines +97 to +104
Comment on lines +97 to +104
]

changelog_pattern = defaults.BUMP_PATTERN

def __init__(self, config: BaseConfig) -> None:
super().__init__(config)
self._isolate_mutable_defaults()

if override_settings := self.config.settings.get("override"):
self._apply_override_settings(override_settings)
elif extend_settings := self.config.settings.get("extend"):
self._apply_extend_settings(extend_settings)

def _isolate_mutable_defaults(self) -> None:
# Keep mutable class defaults isolated per instance.
self.bump_map = OrderedDict(self.bump_map)
self.bump_map_major_version_zero = OrderedDict(self.bump_map_major_version_zero)
self.change_type_map = dict(self.change_type_map)
self.change_type_choices = [*self.change_type_choices]

def _apply_override_settings(self, settings: defaults.CzOverrideSettings) -> None:
if bump_pattern := settings.get("bump_pattern"):
self.bump_pattern = bump_pattern
if bump_map := settings.get("bump_map"):
self.bump_map = OrderedDict(bump_map)
if bump_map_major_version_zero := settings.get("bump_map_major_version_zero"):
self.bump_map_major_version_zero = OrderedDict(bump_map_major_version_zero)
if commit_parser := settings.get("commit_parser"):
self.commit_parser = commit_parser
if changelog_pattern := settings.get("changelog_pattern"):
self.changelog_pattern = changelog_pattern
if change_type_map := settings.get("change_type_map"):
self.change_type_map = dict(change_type_map)
if change_type_choices := settings.get("change_type_choices"):
self.change_type_choices = [*change_type_choices]

Comment on lines +125 to +140
Comment on lines +125 to +140
Comment on lines +125 to +140
def _apply_extend_settings(self, settings: defaults.CzExtendSettings) -> None:
if bump_pattern := settings.get("bump_pattern"):
self.bump_pattern = bump_pattern
if bump_map := settings.get("bump_map"):
self.bump_map.update(bump_map)
if bump_map_major_version_zero := settings.get("bump_map_major_version_zero"):
self.bump_map_major_version_zero.update(bump_map_major_version_zero)
if commit_parser := settings.get("commit_parser"):
self.commit_parser = commit_parser
if changelog_pattern := settings.get("changelog_pattern"):
self.changelog_pattern = changelog_pattern
if change_type_map := settings.get("change_type_map"):
self.change_type_map.update(change_type_map)
if change_type_choices := settings.get("change_type_choices"):
self.change_type_choices.extend(change_type_choices)
Comment on lines +141 to +155
Comment on lines +141 to +155
Comment on lines +141 to +155

def questions(self) -> list[CzQuestion]:
return [
{
"type": "list",
"name": "prefix",
"message": "Select the type of change you are committing",
"choices": [
{
"value": "fix",
"name": "fix: A bug fix. Correlates with PATCH in SemVer",
"key": "x",
},
{
"value": "feat",
"name": "feat: A new feature. Correlates with MINOR in SemVer",
"key": "f",
},
{
"value": "docs",
"name": "docs: Documentation only changes",
"key": "d",
},
{
"value": "style",
"name": (
"style: Changes that do not affect the "
"meaning of the code (white-space, formatting,"
" missing semi-colons, etc)"
),
"key": "s",
},
{
"value": "refactor",
"name": (
"refactor: A code change that neither fixes "
"a bug nor adds a feature"
),
"key": "r",
},
{
"value": "perf",
"name": "perf: A code change that improves performance",
"key": "p",
},
{
"value": "test",
"name": ("test: Adding missing or correcting existing tests"),
"key": "t",
},
{
"value": "build",
"name": (
"build: Changes that affect the build system or "
"external dependencies (example scopes: pip, docker, npm)"
),
"key": "b",
},
{
"value": "ci",
"name": (
"ci: Changes to CI configuration files and "
"scripts (example scopes: GitLabCI)"
),
"key": "c",
},
],
"choices": self.change_type_choices,
},
{
"type": "input",
Expand Down
12 changes: 11 additions & 1 deletion commitizen/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
if TYPE_CHECKING:
import pathlib

from commitizen.question import CzQuestion
from commitizen.question import Choice, CzQuestion


class CzSettings(TypedDict, total=False):
Expand All @@ -29,6 +29,14 @@ class CzSettings(TypedDict, total=False):
change_type_map: dict[str, str] | None


class CzOverrideSettings(CzSettings, total=False):
change_type_choices: list[Choice]


class CzExtendSettings(CzSettings, total=False):
change_type_choices: list[Choice]


class Settings(TypedDict, total=False):
allow_abort: bool
allowed_prefixes: list[str]
Expand All @@ -43,13 +51,15 @@ class Settings(TypedDict, total=False):
changelog_start_rev: str | None
customize: CzSettings
encoding: str
extend: CzExtendSettings
extras: dict[str, Any]
gpg_sign: bool
ignored_tag_formats: Sequence[str]
legacy_tag_formats: Sequence[str]
major_version_zero: bool
message_length_limit: int
name: str
override: CzOverrideSettings
post_bump_hooks: list[str] | None
pre_bump_hooks: list[str] | None
prerelease_offset: int
Expand Down
92 changes: 92 additions & 0 deletions tests/test_cz_conventional_commits.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,3 +173,95 @@ def test_info(config):
conventional_commits = ConventionalCommitsCz(config)
info = conventional_commits.info()
assert isinstance(info, str)


def test_override_takes_precedence_over_extend(config):
config.settings["override"] = {
"bump_map": {
"^bar": "PATCH",
}
}
config.settings["extend"] = {
"bump_map": {
"^foo": "MINOR",
}
}

conventional_commits = ConventionalCommitsCz(config)
assert conventional_commits.bump_map == {"^bar": "PATCH"}


@pytest.mark.parametrize("mode", ["override", "extend"])
def test_apply_supported_settings(mode, config):
config.settings[mode] = {
"bump_pattern": r"^foo:",
"bump_map": {r"^foo": "MINOR"},
"bump_map_major_version_zero": {r"^foo": "PATCH"},
"commit_parser": r"^(?P<change_type>foo):\s(?P<message>.*)$",
"changelog_pattern": r"^(foo)",
"change_type_map": {"foo": "Foo"},
"change_type_choices": [
{
"value": "foo",
"name": "foo: custom type",
"key": "o",
}
],
}

conventional_commits = ConventionalCommitsCz(config)

assert conventional_commits.bump_pattern == r"^foo:"
assert (
conventional_commits.commit_parser
== r"^(?P<change_type>foo):\s(?P<message>.*)$"
)
assert conventional_commits.changelog_pattern == r"^(foo)"

if mode == "override":
assert conventional_commits.bump_map == {r"^foo": "MINOR"}
assert conventional_commits.bump_map_major_version_zero == {r"^foo": "PATCH"}
assert conventional_commits.change_type_map == {"foo": "Foo"}
assert conventional_commits.change_type_choices == [
{
"value": "foo",
"name": "foo: custom type",
"key": "o",
}
]
else:
assert conventional_commits.bump_map[r"^foo"] == "MINOR"
assert conventional_commits.bump_map_major_version_zero[r"^foo"] == "PATCH"
assert conventional_commits.change_type_map["foo"] == "Foo"
assert r"^feat" in conventional_commits.bump_map
assert any(
choice["value"] == "foo"
for choice in conventional_commits.change_type_choices
)
assert any(
choice["value"] == "fix"
for choice in conventional_commits.change_type_choices
)


def test_extend_settings_do_not_leak_to_other_instances(config):
config.settings["extend"] = {
"bump_map": {r"^foo": "MINOR"},
"change_type_choices": [
{
"value": "foo",
"name": "foo: custom type",
"key": "o",
}
],
}
first = ConventionalCommitsCz(config)

clean_config = config.__class__()
clean_config.settings.update({"name": "cz_conventional_commits"})
second = ConventionalCommitsCz(clean_config)

assert r"^foo" in first.bump_map
assert any(choice["value"] == "foo" for choice in first.change_type_choices)
assert r"^foo" not in second.bump_map
assert all(choice["value"] != "foo" for choice in second.change_type_choices)
Loading