diff --git a/commitizen/bump.py b/commitizen/bump.py index bfda1c4541..112d27e89a 100644 --- a/commitizen/bump.py +++ b/commitizen/bump.py @@ -6,7 +6,6 @@ from packaging.version import Version -from commitizen.git import GitCommit from commitizen.defaults import ( MAJOR, MINOR, @@ -15,6 +14,7 @@ bump_message, bump_pattern, ) +from commitizen.git import GitCommit def find_increment( diff --git a/commitizen/cli.py b/commitizen/cli.py index f7e9f28687..aab677f870 100644 --- a/commitizen/cli.py +++ b/commitizen/cli.py @@ -151,13 +151,18 @@ "arguments": [ { "name": "--commit-msg-file", - "required": True, "help": ( "ask for the name of the temporal file that contains " "the commit message. " "Using it in a git hook script: MSG_FILE=$1" ), - } + "exclusive_group": "group1", + }, + { + "name": "--rev-range", + "help": ("a reange of git rev to check. e.g, master..HEAD"), + "exclusive_group": "group1", + }, ], }, { diff --git a/commitizen/commands/__init__.py b/commitizen/commands/__init__.py index e315a987c9..b87906d8b4 100644 --- a/commitizen/commands/__init__.py +++ b/commitizen/commands/__init__.py @@ -3,11 +3,10 @@ from .commit import Commit from .example import Example from .info import Info +from .init import Init from .list_cz import ListCz from .schema import Schema from .version import Version -from .init import Init - __all__ = ( "Bump", diff --git a/commitizen/commands/bump.py b/commitizen/commands/bump.py index 605fde9878..90ec715744 100644 --- a/commitizen/commands/bump.py +++ b/commitizen/commands/bump.py @@ -1,4 +1,4 @@ -from typing import Optional, List +from typing import List, Optional import questionary from packaging.version import Version @@ -6,10 +6,10 @@ from commitizen import bump, factory, git, out from commitizen.config import BaseConfig from commitizen.error_codes import ( + COMMIT_FAILED, NO_COMMITS_FOUND, - NO_VERSION_SPECIFIED, NO_PATTERN_MAP, - COMMIT_FAILED, + NO_VERSION_SPECIFIED, TAG_FAILED, ) diff --git a/commitizen/commands/check.py b/commitizen/commands/check.py index de5f5e39f9..8f3d9dad44 100644 --- a/commitizen/commands/check.py +++ b/commitizen/commands/check.py @@ -1,7 +1,8 @@ import os import re +from typing import Dict -from commitizen import factory, out +from commitizen import factory, git, out from commitizen.config import BaseConfig from commitizen.error_codes import INVALID_COMMIT_MSG @@ -9,7 +10,7 @@ class Check: """Check if the current commit msg matches the commitizen format.""" - def __init__(self, config: BaseConfig, arguments: dict, cwd=os.getcwd()): + def __init__(self, config: BaseConfig, arguments: Dict[str, str], cwd=os.getcwd()): """Init method. Parameters @@ -21,12 +22,21 @@ def __init__(self, config: BaseConfig, arguments: dict, cwd=os.getcwd()): the flags provided by the user """ + self.commit_msg_file: str = arguments.get("commit_msg_file") + self.rev_range: str = arguments.get("rev_range") + + self._valid_command_argument() + self.config: BaseConfig = config self.cz = factory.commiter_factory(self.config) - self.arguments: dict = arguments + + def _valid_command_argument(self): + if bool(self.commit_msg_file) is bool(self.rev_range): + out.error("One and only one argument is required for check command!") + raise SystemExit() def __call__(self): - """Validate if a commit message follows the conventional pattern. + """Validate if commit messages follows the conventional pattern. Raises ------ @@ -34,18 +44,31 @@ def __call__(self): if the commit provided not follows the conventional pattern """ - commit_msg_content = self._get_commit_msg() + commit_msgs = self._get_commit_messages() pattern = self.cz.schema_pattern() - if self._has_proper_format(pattern, commit_msg_content) is not None: - out.success("Commit validation: successful!") - else: - out.error("commit validation: failed!") - out.error("please enter a commit message in the commitizen format.") - raise SystemExit(INVALID_COMMIT_MSG) - - def _get_commit_msg(self): - temp_filename: str = self.arguments.get("commit_msg_file") - return open(temp_filename, "r").read() - - def _has_proper_format(self, pattern, commit_msg): + for commit_msg in commit_msgs: + if not Check.validate_commit_message(commit_msg, pattern): + out.error( + "commit validation: failed!\n" + "please enter a commit message in the commitizen format.\n" + f"commit: {commit_msg}\n" + f"pattern: {pattern}" + ) + raise SystemExit(INVALID_COMMIT_MSG) + out.success("Commit validation: successful!") + + def _get_commit_messages(self): + # Get commit message from file (--commit-msg-file) + if self.commit_msg_file: + with open(self.commit_msg_file, "r") as commit_file: + commit_msg = commit_file.read() + return [commit_msg] + + # Get commit messages from git log (--rev-range) + return [commit.message for commit in git.get_commits(end=self.rev_range)] + + @staticmethod + def validate_commit_message(commit_msg: str, pattern: str) -> bool: + if commit_msg.startswith("Merge") or commit_msg.startswith("Revert"): + return True return re.match(pattern, commit_msg) diff --git a/commitizen/commands/commit.py b/commitizen/commands/commit.py index 0dbbe8fe9d..57444011fd 100644 --- a/commitizen/commands/commit.py +++ b/commitizen/commands/commit.py @@ -5,14 +5,14 @@ import questionary from commitizen import factory, git, out -from commitizen.cz.exceptions import CzException from commitizen.config import BaseConfig +from commitizen.cz.exceptions import CzException from commitizen.error_codes import ( - NO_ANSWERS, COMMIT_ERROR, + CUSTOM_ERROR, + NO_ANSWERS, NO_COMMIT_BACKUP, NOTHING_TO_COMMIT, - CUSTOM_ERROR, ) diff --git a/commitizen/commands/init.py b/commitizen/commands/init.py index cd10edf7f7..b428d696cd 100644 --- a/commitizen/commands/init.py +++ b/commitizen/commands/init.py @@ -1,12 +1,11 @@ -from packaging.version import Version - import questionary +from packaging.version import Version from commitizen import factory, out +from commitizen.config import BaseConfig, IniConfig, TomlConfig from commitizen.cz import registry -from commitizen.config import BaseConfig, TomlConfig, IniConfig -from commitizen.git import get_latest_tag_name, get_tag_names from commitizen.defaults import long_term_support_config_files +from commitizen.git import get_latest_tag_name, get_tag_names class Init: diff --git a/commitizen/commands/list_cz.py b/commitizen/commands/list_cz.py index 3f418f440b..99701865af 100644 --- a/commitizen/commands/list_cz.py +++ b/commitizen/commands/list_cz.py @@ -1,6 +1,6 @@ from commitizen import out -from commitizen.cz import registry from commitizen.config import BaseConfig +from commitizen.cz import registry class ListCz: diff --git a/commitizen/commands/version.py b/commitizen/commands/version.py index d64d8d82af..344920c72a 100644 --- a/commitizen/commands/version.py +++ b/commitizen/commands/version.py @@ -1,6 +1,6 @@ from commitizen import out -from commitizen.config import BaseConfig from commitizen.__version__ import __version__ +from commitizen.config import BaseConfig class Version: diff --git a/commitizen/config/__init__.py b/commitizen/config/__init__.py index 88949f327d..9b317f1e36 100644 --- a/commitizen/config/__init__.py +++ b/commitizen/config/__init__.py @@ -4,9 +4,10 @@ from commitizen import defaults, git, out from commitizen.error_codes import NOT_A_GIT_PROJECT + from .base_config import BaseConfig -from .toml_config import TomlConfig from .ini_config import IniConfig +from .toml_config import TomlConfig def load_global_conf() -> Optional[IniConfig]: diff --git a/commitizen/cz/conventional_commits/conventional_commits.py b/commitizen/cz/conventional_commits/conventional_commits.py index c67b2a5a0d..a9bfcf2039 100644 --- a/commitizen/cz/conventional_commits/conventional_commits.py +++ b/commitizen/cz/conventional_commits/conventional_commits.py @@ -172,7 +172,7 @@ def schema(self) -> str: def schema_pattern(self) -> str: PATTERN = ( r"(build|ci|docs|feat|fix|perf|refactor|style|test|chore|revert|bump)" - r"(\([\w\-]+\))?:\s.*" + r"(\(\S+\))?:\s.*" ) return PATTERN diff --git a/commitizen/cz/customize/customize.py b/commitizen/cz/customize/customize.py index 46ea0602d9..1e10975fe8 100644 --- a/commitizen/cz/customize/customize.py +++ b/commitizen/cz/customize/customize.py @@ -4,8 +4,8 @@ from string import Template from commitizen import defaults -from commitizen.cz.base import BaseCommitizen from commitizen.config import BaseConfig +from commitizen.cz.base import BaseCommitizen __all__ = ["CustomizeCommitsCz"] diff --git a/commitizen/factory.py b/commitizen/factory.py index 62b7ba9cfa..8dab0c408a 100644 --- a/commitizen/factory.py +++ b/commitizen/factory.py @@ -1,6 +1,6 @@ from commitizen import BaseCommitizen, out -from commitizen.cz import registry from commitizen.config import BaseConfig +from commitizen.cz import registry from commitizen.error_codes import NO_COMMITIZEN_FOUND diff --git a/commitizen/git.py b/commitizen/git.py index 0853d76005..058ba89a00 100644 --- a/commitizen/git.py +++ b/commitizen/git.py @@ -1,7 +1,7 @@ import os from pathlib import Path from tempfile import NamedTemporaryFile -from typing import Optional, List +from typing import List, Optional from commitizen import cmd @@ -64,7 +64,7 @@ def get_commits( git_log_cmd = f"git log --pretty={log_format}{delimiter}" if start: - c = cmd.run(f"{git_log_cmd} {start}...{end}") + c = cmd.run(f"{git_log_cmd} {start}..{end}") else: c = cmd.run(f"{git_log_cmd} {end}") diff --git a/pyproject.toml b/pyproject.toml index a62517c2f6..d09c9d4d3f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,6 +70,13 @@ isort = "^4.3.21" cz = "commitizen.cli:main" git-cz = "commitizen.cli:main" +[tool.isort] +multi_line_output = 3 +include_trailing_comma = true +force_grid_wrap = 0 +combine_as_imports = true +line_length = 88 + [tool.coverage] [tool.coverage.report] show_missing = true diff --git a/scripts/lint b/scripts/lint index d737c8f476..c643dbc14b 100755 --- a/scripts/lint +++ b/scripts/lint @@ -1,11 +1,11 @@ #!/bin/sh -e -export PREFIX="" +export PREFIX="poetry run python -m " if [ -d 'venv' ] ; then export PREFIX="venv/bin/" fi set -x -${PREFIX}isort --multi-line=3 --trailing-comma --force-grid-wrap=0 --combine-as --line-width 88 --recursive --apply commitizen tests +${PREFIX}isort --recursive --apply commitizen tests ${PREFIX}black commitizen tests diff --git a/scripts/test b/scripts/test index 119cf67098..6a8082162e 100755 --- a/scripts/test +++ b/scripts/test @@ -7,4 +7,5 @@ fi ${PREFIX}pytest --cov-report term-missing --cov-report=xml:coverage.xml --cov=commitizen tests/ ${PREFIX}black commitizen tests --check +${PREFIX}isort --recursive --check-only commitizen tests ${PREFIX}flake8 --max-line-length=88 commitizen/ tests/ diff --git a/tests/commands/test_check_command.py b/tests/commands/test_check_command.py index a53e57b495..e5fe53b518 100644 --- a/tests/commands/test_check_command.py +++ b/tests/commands/test_check_command.py @@ -1,9 +1,48 @@ import sys +from typing import List import pytest -from commitizen import cli -from commitizen import commands +from commitizen import cli, commands, git + +COMMIT_LOG = [ + "refactor: A code change that neither fixes a bug nor adds a feature", + r"refactor(cz/connventional_commit): use \S to check scope", + "refactor(git): remove unnecessary dot between git range", + "bump: version 1.16.3 → 1.16.4", + ( + "Merge pull request #139 from Lee-W/fix-init-clean-config-file\n" + "Fix init clean config file" + ), + "ci(pyproject.toml): add configuration for coverage", + "fix(commands/init): fix clean up file when initialize commitizen config\n#138", + "refactor(defaults): split config files into long term support and deprecated ones", + "bump: version 1.16.2 → 1.16.3", + ( + "Merge pull request #136 from Lee-W/remove-redundant-readme\n" + "Remove redundant readme" + ), + "fix: replace README.rst with docs/README.md in config files", + ( + "refactor(docs): remove README.rst and use docs/README.md\n" + "By removing README.rst, we no longer need to maintain " + "two document with almost the same content\n" + "Github can read docs/README.md as README for the project." + ), + "docs(check): pin pre-commit to v1.16.2", + "docs(check): fix pre-commit setup", + "bump: version 1.16.1 → 1.16.2", + "Merge pull request #135 from Lee-W/fix-pre-commit-hook\nFix pre commit hook", + "docs(check): enforce cz check only whem committing", + ( + 'Revert "fix(pre-commit): set pre-commit check stage to commit-msg"\n' + "This reverts commit afc70133e4a81344928561fbf3bb20738dfc8a0b." + ), +] + + +def _build_fake_git_commits(commit_msgs: List[str]) -> List[git.GitCommit]: + return [git.GitCommit("test_rev", commit_msg) for commit_msg in commit_msgs] def test_check_jira_fails(mocker, capsys): @@ -115,4 +154,43 @@ def test_check_conventional_commit(commit_msg, config, mocker, tmpdir): def test_check_command_when_commit_file_not_found(config): with pytest.raises(FileNotFoundError): - commands.Check(config=config, arguments={"commit_msg_file": ""})() + commands.Check(config=config, arguments={"commit_msg_file": "no_such_file"})() + + +def test_check_a_range_of_git_commits(config, mocker): + success_mock = mocker.patch("commitizen.out.success") + mocker.patch( + "commitizen.git.get_commits", return_value=_build_fake_git_commits(COMMIT_LOG) + ) + + check_cmd = commands.Check( + config=config, arguments={"rev_range": "HEAD~10..master"} + ) + + check_cmd() + success_mock.assert_called_once() + + +def test_check_a_range_of_git_commits_and_failed(config, mocker): + error_mock = mocker.patch("commitizen.out.error") + mocker.patch( + "commitizen.git.get_commits", + return_value=_build_fake_git_commits(["This commit does not follow rule"]), + ) + check_cmd = commands.Check( + config=config, arguments={"rev_range": "HEAD~10..master"} + ) + + with pytest.raises(SystemExit): + check_cmd() + error_mock.assert_called_once() + + +@pytest.mark.parametrize( + "args", [{"rev_range": "HEAD~10..master", "commit_msg_file": "some_file"}, {}] +) +def test_check_command_with_invalid_argment(args, config, capsys): + with pytest.raises(SystemExit): + commands.Check(config=config, arguments=args) + _, err = capsys.readouterr() + assert "One and only one argument is required for check command!" in err diff --git a/tests/test_conf.py b/tests/test_conf.py index 5320051044..277234e5e5 100644 --- a/tests/test_conf.py +++ b/tests/test_conf.py @@ -5,7 +5,6 @@ from commitizen import config, defaults, git - PYPROJECT = """ [tool.commitizen] name = "cz_jira" diff --git a/tests/test_cz_base.py b/tests/test_cz_base.py index 82925388ef..b3b8fe4fb8 100644 --- a/tests/test_cz_base.py +++ b/tests/test_cz_base.py @@ -1,8 +1,8 @@ import pytest from commitizen import defaults -from commitizen.cz.base import BaseCommitizen from commitizen.config import BaseConfig +from commitizen.cz.base import BaseCommitizen @pytest.fixture() diff --git a/tests/test_cz_conventional_commits.py b/tests/test_cz_conventional_commits.py index dd6cfbff30..41ed9b0b7d 100644 --- a/tests/test_cz_conventional_commits.py +++ b/tests/test_cz_conventional_commits.py @@ -1,14 +1,13 @@ import pytest from commitizen import defaults +from commitizen.config import BaseConfig from commitizen.cz.conventional_commits.conventional_commits import ( ConventionalCommitsCz, parse_scope, parse_subject, ) from commitizen.cz.exceptions import AnswerRequiredError -from commitizen.config import BaseConfig - valid_scopes = ["", "simple", "dash-separated", "camelCase" "UPPERCASE"] diff --git a/tests/test_cz_jira.py b/tests/test_cz_jira.py index 0c05b46a3c..b725a46fa9 100644 --- a/tests/test_cz_jira.py +++ b/tests/test_cz_jira.py @@ -1,8 +1,8 @@ import pytest from commitizen import defaults -from commitizen.cz.jira import JiraSmartCz from commitizen.config import BaseConfig +from commitizen.cz.jira import JiraSmartCz @pytest.fixture() diff --git a/tests/test_cz_utils.py b/tests/test_cz_utils.py index a8a56d762c..94cc7f5b87 100644 --- a/tests/test_cz_utils.py +++ b/tests/test_cz_utils.py @@ -1,6 +1,6 @@ import pytest -from commitizen.cz import utils, exceptions +from commitizen.cz import exceptions, utils def test_required_validator():