Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
2 changes: 1 addition & 1 deletion .github/workflows/pythonpackage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ jobs:
python-check:
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
platform: [ubuntu-22.04, macos-latest, windows-latest]
runs-on: ${{ matrix.platform }}
steps:
Expand Down
11 changes: 4 additions & 7 deletions commitizen/changelog_formats/__init__.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
from __future__ import annotations

import sys
from typing import TYPE_CHECKING, Callable, ClassVar, Protocol

if sys.version_info >= (3, 10):
from importlib import metadata
else:
import importlib_metadata as metadata
from importlib import metadata
from typing import TYPE_CHECKING, ClassVar, Protocol

from commitizen.exceptions import ChangelogFormatUnknown

if TYPE_CHECKING:
from collections.abc import Callable

from commitizen.changelog import Metadata
from commitizen.config.base_config import BaseConfig

Expand Down
50 changes: 17 additions & 33 deletions commitizen/commands/check.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

from commitizen import factory, git, out
from commitizen.exceptions import (
CommitMessageLengthExceededError,
InvalidCommandArgumentError,
InvalidCommitMessageError,
NoCommitsFoundError,
Expand Down Expand Up @@ -83,26 +82,32 @@ def __call__(self) -> None:
"""Validate if commit messages follows the conventional pattern.

Raises:
InvalidCommitMessageError: if the commit provided not follows the conventional pattern
InvalidCommitMessageError: if the commit provided does not follow the conventional pattern
NoCommitsFoundError: if no commit is found with the given range
"""
commits = self._get_commits()
if not commits:
raise NoCommitsFoundError(f"No commit found with range: '{self.rev_range}'")

pattern = re.compile(self.cz.schema_pattern())
invalid_msgs_content = "\n".join(
f'commit "{commit.rev}": "{commit.message}"'
invalid_commits = [
(commit, check.errors)
for commit in commits
if not self._validate_commit_message(commit.message, pattern, commit.rev)
)
if invalid_msgs_content:
# TODO: capitalize the first letter of the error message for consistency in v5
if not (
check := self.cz.validate_commit_message(
commit_msg=commit.message,
pattern=pattern,
allow_abort=self.allow_abort,
allowed_prefixes=self.allowed_prefixes,
max_msg_length=self.max_msg_length,
commit_hash=commit.rev,
)
).is_valid
]

if invalid_commits:
raise InvalidCommitMessageError(
"commit validation: failed!\n"
"please enter a commit message in the commitizen format.\n"
f"{invalid_msgs_content}\n"
f"pattern: {pattern.pattern}"
self.cz.format_exception_message(invalid_commits)
)
out.success("Commit validation: successful!")

Expand Down Expand Up @@ -157,24 +162,3 @@ def _filter_comments(msg: str) -> str:
if not line.startswith("#"):
lines.append(line)
return "\n".join(lines)

def _validate_commit_message(
self, commit_msg: str, pattern: re.Pattern[str], commit_hash: str
) -> bool:
if not commit_msg:
return self.allow_abort

if any(map(commit_msg.startswith, self.allowed_prefixes)):
return True

if self.max_msg_length is not None:
msg_len = len(commit_msg.partition("\n")[0].strip())
if msg_len > self.max_msg_length:
raise CommitMessageLengthExceededError(
f"commit validation: failed!\n"
f"commit message length exceeds the limit.\n"
f'commit "{commit_hash}": "{commit_msg}"\n'
f"message length limit: {self.max_msg_length} (actual: {msg_len})"
)

return bool(pattern.match(commit_msg))
9 changes: 2 additions & 7 deletions commitizen/cz/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,9 @@

import importlib
import pkgutil
import sys
import warnings

if sys.version_info >= (3, 10):
from importlib import metadata
else:
import importlib_metadata as metadata

from collections.abc import Iterable
from importlib import metadata
from typing import TYPE_CHECKING

if TYPE_CHECKING:
Expand Down
67 changes: 64 additions & 3 deletions commitizen/cz/base.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
from __future__ import annotations

from abc import ABCMeta, abstractmethod
from typing import TYPE_CHECKING, Any, Callable, Protocol
from collections.abc import Iterable, Mapping
from typing import TYPE_CHECKING, Any, NamedTuple, Protocol

from jinja2 import BaseLoader, PackageLoader
from prompt_toolkit.styles import Style

from commitizen.exceptions import CommitMessageLengthExceededError

if TYPE_CHECKING:
from collections.abc import Iterable, Mapping
import re
from collections.abc import Callable, Iterable, Mapping

from commitizen import git
from commitizen.config.base_config import BaseConfig
Expand All @@ -26,6 +30,11 @@ def __call__(
) -> dict[str, Any]: ...


class ValidationResult(NamedTuple):
is_valid: bool
errors: list


class BaseCommitizen(metaclass=ABCMeta):
bump_pattern: str | None = None
bump_map: dict[str, str] | None = None
Expand All @@ -43,7 +52,7 @@ class BaseCommitizen(metaclass=ABCMeta):
("disabled", "fg:#858585 italic"),
]

# The whole subject will be parsed as message by default
# The whole subject will be parsed as a message by default
# This allows supporting changelog for any rule system.
# It can be modified per rule
commit_parser: str | None = r"(?P<message>.*)"
Expand Down Expand Up @@ -101,3 +110,55 @@ def schema_pattern(self) -> str:
@abstractmethod
def info(self) -> str:
"""Information about the standardized commit message."""

def validate_commit_message(
self,
*,
commit_msg: str,
pattern: re.Pattern[str],
allow_abort: bool,
allowed_prefixes: list[str],
max_msg_length: int | None,
commit_hash: str,
) -> ValidationResult:
"""Validate commit message against the pattern."""
if not commit_msg:
return ValidationResult(
allow_abort, [] if allow_abort else ["commit message is empty"]
)

if any(map(commit_msg.startswith, allowed_prefixes)):
return ValidationResult(True, [])

if max_msg_length is not None:
msg_len = len(commit_msg.partition("\n")[0].strip())
if msg_len > max_msg_length:
# TODO: capitalize the first letter of the error message for consistency in v5
raise CommitMessageLengthExceededError(
f"commit validation: failed!\n"
f"commit message length exceeds the limit.\n"
f'commit "{commit_hash}": "{commit_msg}"\n'
f"message length limit: {max_msg_length} (actual: {msg_len})"
)

return ValidationResult(
bool(pattern.match(commit_msg)),
[f"pattern: {pattern.pattern}"],
)

def format_exception_message(
self, invalid_commits: list[tuple[git.GitCommit, list]]
) -> str:
"""Format commit errors."""
displayed_msgs_content = "\n".join(
[
f'commit "{commit.rev}": "{commit.message}\n"' + "\n".join(errors)
for commit, errors in invalid_commits
]
)
# TODO: capitalize the first letter of the error message for consistency in v5
return (
"commit validation: failed!\n"
"please enter a commit message in the commitizen format.\n"
f"{displayed_msgs_content}"
)
8 changes: 2 additions & 6 deletions commitizen/providers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
from __future__ import annotations

import sys
from importlib import metadata
from typing import TYPE_CHECKING, cast

if sys.version_info >= (3, 10):
from importlib import metadata
else:
import importlib_metadata as metadata

from commitizen.config.base_config import BaseConfig
from commitizen.exceptions import VersionProviderUnknown
from commitizen.providers.cargo_provider import CargoProvider
from commitizen.providers.commitizen_provider import CommitizenProvider
Expand Down
5 changes: 3 additions & 2 deletions commitizen/question.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from typing import Callable, Literal, TypedDict, Union
from collections.abc import Callable
from typing import Literal, TypedDict


class Choice(TypedDict, total=False):
Expand Down Expand Up @@ -29,4 +30,4 @@ class ConfirmQuestion(TypedDict):
default: bool


CzQuestion = Union[ListQuestion, InputQuestion, ConfirmQuestion]
CzQuestion = ListQuestion | InputQuestion | ConfirmQuestion
2 changes: 1 addition & 1 deletion commitizen/tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ def normalize_tag(
version = self.scheme(version) if isinstance(version, str) else version
tag_format = tag_format or self.tag_format

major, minor, patch = version.release
major, minor, patch = (list(version.release) + [0, 0, 0])[:3]
prerelease = version.prerelease or ""

t = Template(tag_format)
Expand Down
14 changes: 3 additions & 11 deletions commitizen/version_schemes.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from __future__ import annotations

import re
import sys
import warnings
from importlib import metadata
from itertools import zip_longest
from typing import (
TYPE_CHECKING,
Expand All @@ -14,23 +14,15 @@
runtime_checkable,
)

if sys.version_info >= (3, 10):
from importlib import metadata
else:
import importlib_metadata as metadata

from packaging.version import InvalidVersion # noqa: F401 (expose the common exception)
from packaging.version import Version as _BaseVersion

from commitizen.defaults import MAJOR, MINOR, PATCH, Settings
from commitizen.exceptions import VersionSchemeUnknown

if TYPE_CHECKING:
# TypeAlias is Python 3.10+ but backported in typing-extensions
if sys.version_info >= (3, 10):
from typing import TypeAlias
else:
from typing_extensions import TypeAlias
import sys
from typing import TypeAlias

# Self is Python 3.11+ but backported in typing-extensions
if sys.version_info < (3, 11):
Expand Down
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ This standardization makes your commit history more readable and meaningful, whi

Before installing Commitizen, ensure you have:

- [Python](https://www.python.org/downloads/) `3.9+`
- [Python](https://www.python.org/downloads/) `3.10+`
- [Git][gitscm] `1.8.5.2+`

### Installation
Expand Down
2 changes: 1 addition & 1 deletion docs/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ If you're a first-time contributor, please check out issues labeled [good first
### Required Tools

1. **Python Environment**
- Python `>=3.9`
- Python `>=3.10`
- [Poetry](https://python-poetry.org/docs/#installing-with-the-official-installer) `>=2.2.0`
2. **Version Control & Security**
- Git
Expand Down
6 changes: 3 additions & 3 deletions docs/contributing_tldr.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ Feel free to send a PR to update this file if you find anything useful. 🙇

## Environment

- Python `>=3.9`
- Python `>=3.10`
- [Poetry](https://python-poetry.org/docs/#installing-with-the-official-installer) `>=2.2.0`

## Useful commands
Expand All @@ -21,8 +21,8 @@ poetry format
# Check if ruff and mypy are happy
poetry lint

# Check if mypy is happy in python 3.9
mypy --python-version 3.9
# Check if mypy is happy in python 3.10
mypy --python-version 3.10

# Run tests in parallel.
pytest -n auto # This may take a while.
Expand Down
Loading