From 656ca71a8c7f0dd19e1305c85e1a345c8054c3c0 Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Sun, 6 Apr 2025 20:47:24 +0300 Subject: [PATCH 01/22] docs: remove .readthedocs.yaml (#231) * docs: remove .readthedocs.yaml * chore: conf.py clean up --- .readthedocs.yaml | 17 ----------------- docs/conf.py | 5 ----- 2 files changed, 22 deletions(-) delete mode 100644 .readthedocs.yaml diff --git a/.readthedocs.yaml b/.readthedocs.yaml deleted file mode 100644 index 9bfb5c2..0000000 --- a/.readthedocs.yaml +++ /dev/null @@ -1,17 +0,0 @@ -# Read the Docs configuration file for Sphinx projects -# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details -version: 2 - -build: - os: ubuntu-22.04 - tools: - python: "3.12" - -sphinx: - builder: "dirhtml" - configuration: docs/conf.py - -# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html -python: - install: - - requirements: docs/requirements.txt diff --git a/docs/conf.py b/docs/conf.py index 53d5b56..2f3e1d9 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -25,11 +25,6 @@ "sphinx.ext.viewcode", ] -intersphinx_mapping = { - "python": ("https://docs.python.org/3", None), - "requests": ("https://requests.readthedocs.io/en/latest/", None), -} - autodoc_member_order = "bysource" templates_path = ["_templates"] From 742fc42d011930827a7fc2b9129a7ef7dc15ba6d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 7 Apr 2025 21:20:01 +0300 Subject: [PATCH 02/22] ci: pre-commit autoupdate (#232) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cc6ee16..cf938ad 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,7 +19,7 @@ repos: - id: requirements-txt-fixer - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.11.2 + rev: v0.11.4 hooks: # Run the linter. - id: ruff @@ -35,7 +35,7 @@ repos: hooks: - id: codespell - repo: https://github.com/commit-check/commit-check - rev: v0.9.4 + rev: v0.9.5 hooks: - id: check-message # - id: check-branch # uncomment if you need. From 0219eb33ad5099ab9459c52c876d9c24dc8e1bc1 Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Sun, 1 Jun 2025 19:30:04 +0300 Subject: [PATCH 03/22] chore: move deps from `requirements.txt` to `pyproject.toml` (#233) * chore: remove requirements.txt files * install nox * install nox * Apply suggestions from code review Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * update noxfile.py * update noxfile.py and pyproject.tom * fix pyproject.toml * move sphinx-autobuild into pyproject.toml --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .github/workflows/main.yml | 14 +++++++++++--- .gitpod.yml | 4 ++-- docs/requirements.txt | 3 --- noxfile.py | 14 +++----------- pyproject.toml | 5 +++++ requirements-dev.txt | 5 ----- 6 files changed, 21 insertions(+), 24 deletions(-) delete mode 100644 docs/requirements.txt delete mode 100644 requirements-dev.txt diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6333198..e470c14 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -21,7 +21,11 @@ jobs: - uses: actions/setup-python@v5 with: python-version: '3.x' - - run: pip install -r requirements-dev.txt + + - name: Install nox + run: | + python -m pip install --upgrade pip + python -m pip install nox - name: Run pre-commit run: | @@ -63,7 +67,7 @@ jobs: python-version: ${{ matrix.py }} - run: | pip install --upgrade pip - pip install -r requirements-dev.txt + pip install .[dev] - name: Download wheel artifact uses: actions/download-artifact@v4 @@ -85,7 +89,11 @@ jobs: - uses: actions/setup-python@v5 with: python-version: "3.10" - - run: pip install -r requirements-dev.txt + + - name: Install nox + run: | + python -m pip install --upgrade pip + python -m pip install nox - name: Build docs run: nox -s docs diff --git a/.gitpod.yml b/.gitpod.yml index 8123636..4f2f475 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -3,6 +3,6 @@ # and commit this file to your remote git repository to share the goodness with others. tasks: - - before: pip install --upgrade pip && pip install -r requirements-dev.txt + - before: pip install --upgrade pip init: pre-commit install - command: pip install -e . + command: pip install -e .[dev] diff --git a/docs/requirements.txt b/docs/requirements.txt deleted file mode 100644 index af718d9..0000000 --- a/docs/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ - -. -sphinx-immaterial diff --git a/noxfile.py b/noxfile.py index 079df8a..50599af 100644 --- a/noxfile.py +++ b/noxfile.py @@ -4,11 +4,6 @@ nox.options.reuse_existing_virtualenvs = True nox.options.sessions = ["lint"] -REQUIREMENTS = { - "dev": "requirements-dev.txt", - "docs": "docs/requirements.txt", -} - # ----------------------------------------------------------------------------- # Development Commands # ----------------------------------------------------------------------------- @@ -54,8 +49,7 @@ def commit_check(session): @nox.session() def coverage(session): - session.install(".") - session.install("-r", REQUIREMENTS["dev"]) + session.install('.[test]') session.run("coverage", "run", "--source", "commit_check", "-m", "pytest") session.run("coverage", "report") session.run("coverage", "xml") @@ -63,13 +57,11 @@ def coverage(session): @nox.session() def docs(session): - session.install(".") - session.install("-r", REQUIREMENTS["docs"]) + session.install('.[docs]') session.run("sphinx-build", "-E", "-W", "-b", "html", "docs", "_build/html") @nox.session(name="docs-live") def docs_live(session): - session.install(".") - session.install("-r", REQUIREMENTS["docs"], "sphinx-autobuild") + session.install('.[docs]') session.run("sphinx-autobuild", "-b", "html", "docs", "_build/html") diff --git a/pyproject.toml b/pyproject.toml index c84d181..3f0759e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,11 @@ tracker = "https://github.com/commit-check/commit-check/issues" # ... other project metadata fields as specified in: # https://packaging.python.org/en/latest/specifications/declaring-project-metadata/ +[project.optional-dependencies] +dev = ['nox'] +test = ['coverage', 'pytest', 'pytest-mock'] +docs = ['sphinx-immaterial', 'sphinx-autobuild'] + [tool.setuptools] zip-safe = false packages = ["commit_check"] diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index 120645f..0000000 --- a/requirements-dev.txt +++ /dev/null @@ -1,5 +0,0 @@ -coverage -git+https://github.com/wntrblm/nox.git@main -pre-commit -pytest -pytest-mock From 309bc74baf7b3a0b38049d8af0ef601ad44cdad4 Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Fri, 13 Jun 2025 11:02:48 +0300 Subject: [PATCH 04/22] Potential fix Workflow does not contain permissions (#234) Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .github/workflows/main.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e470c14..3163c6c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,5 +1,8 @@ name: main +permissions: + contents: write + on: push: branches: From 53ea82057400fea2b90b53b9614ef305cedead57 Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Fri, 13 Jun 2025 11:05:37 +0300 Subject: [PATCH 05/22] fix: potential fix workflow does not contain permissions (#235) Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .github/workflows/release-drafter.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index d25c13e..0b36c74 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -6,6 +6,10 @@ on: - "main" workflow_dispatch: +permissions: + contents: read + pull-requests: write + jobs: draft-release: uses: commit-check/.github/.github/workflows/release-drafter.yml@main From 634075918a736be7440fa612d018ad5360283459 Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Fri, 13 Jun 2025 11:21:17 +0300 Subject: [PATCH 06/22] fix: potential fix workflow does not contain permissions (#236) Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .github/workflows/publish-image.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/publish-image.yml b/.github/workflows/publish-image.yml index 39dd16e..9c148f6 100644 --- a/.github/workflows/publish-image.yml +++ b/.github/workflows/publish-image.yml @@ -1,5 +1,8 @@ name: publish image +permissions: + contents: read + on: push: paths: From 6cdeb1405270b21f3e7df33b730a5decf871bdbc Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Fri, 13 Jun 2025 11:31:31 +0300 Subject: [PATCH 07/22] fix: potential fix workflow does not contain permissions (#237) Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .github/workflows/labeler.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 52689d6..659f080 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -1,5 +1,9 @@ name: PR Autolabeler +permissions: + contents: read + pull-requests: write + on: # pull_request event is required for autolabeler pull_request: From 485e745ec7b7a675680b93cd62d2138d3f10e4f4 Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Fri, 13 Jun 2025 11:33:54 +0300 Subject: [PATCH 08/22] fix: Potential fix Workflow does not contain permissions #235 --- .github/workflows/release-drafter.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index 0b36c74..182ecec 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -7,7 +7,7 @@ on: workflow_dispatch: permissions: - contents: read + contents: write pull-requests: write jobs: From 031950fa5b33d7832a553c2cf9db23b365ba8617 Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Sun, 22 Jun 2025 16:53:25 +0200 Subject: [PATCH 09/22] chore: update CODEOWNERS (#238) * chore: update CODEOWNERS * chore: update CODEOWNERS * fix: Update labeler.yml to fix permission issue * chore: Update CODEOWNERS --- .github/CODEOWNERS | 2 +- .github/workflows/labeler.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 4f08a45..9861b85 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @shenxianpeng +* @commit-check/commit-check-maintain diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 659f080..13e989a 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -1,7 +1,7 @@ name: PR Autolabeler permissions: - contents: read + contents: write pull-requests: write on: From 9f8b78e98c1b9a6448f0f41ac608c085d9698fad Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Wed, 25 Jun 2025 10:01:14 +0300 Subject: [PATCH 10/22] chore: Update CODEOWNERS --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 9861b85..37cffb1 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @commit-check/commit-check-maintain +* @commit-check/developers From d87094260d660c645b3f0ddd91f55baae7e136eb Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Fri, 4 Jul 2025 03:37:00 +0300 Subject: [PATCH 11/22] docs: update README.rst (#239) --- README.rst | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 2f19257..b9ac03a 100644 --- a/README.rst +++ b/README.rst @@ -42,15 +42,14 @@ Configuration Use Default Configuration ~~~~~~~~~~~~~~~~~~~~~~~~~ -- If you don't set ``.commit-check.yml``, Commit Check will use the `default configuration `_. +- **Commit Check** uses a `default configuration `_ if you do not provide a ``.commit-check.yml`` file. -- The commit message will follow the rules of `Conventional Commits `_, - branch naming follow the rules of `Conventional Branch `_. +- The default configuration enforces commit message rules based on the `Conventional Commits `_ specification and branch naming rules based on the `Conventional Branch `_ convention. Use Custom Configuration ~~~~~~~~~~~~~~~~~~~~~~~~ -Create a config file ``.commit-check.yml`` under your repository's root directory, e.g., `.commit-check.yml `_ +To customize the behavior, create a config file ``.commit-check.yml`` under your repository's root directory, e.g., `.commit-check.yml `_ Usage ----- From dac2281855bf9a39202e766b7caaf58fb3fbe8e2 Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Mon, 7 Jul 2025 01:05:33 +0300 Subject: [PATCH 12/22] fix: display a clear message when there are no commits (#244) * fix: check if there are commit in the current branch * fix: check if there are commit in the current branch * add output when no commits found * docs: change output when no commits found * test: add more test for util --- .pre-commit-config.yaml | 4 +-- commit_check/util.py | 17 +++++++++++ tests/util_test.py | 66 ++++++++++++++++++++++++++++++++++++++++- 3 files changed, 84 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cf938ad..d289da2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -41,5 +41,5 @@ repos: # - id: check-branch # uncomment if you need. - id: check-author-name # uncomment if you need. - id: check-author-email # uncomment if you need. - # - id: commit-signoff # uncomment if you need. - # - id: check-merge-base # requires download all git history + # - id: check-commit-signoff # uncomment if you need. + # - id: check-merge-base # requires download all git history diff --git a/commit_check/util.py b/commit_check/util.py index a63d6c1..52b4eaa 100644 --- a/commit_check/util.py +++ b/commit_check/util.py @@ -29,6 +29,21 @@ def get_branch_name() -> str: return branch_name.strip() +def has_commits() -> bool: + """Check if there are any commits in the current branch. + :returns: `True` if there are commits, `False` otherwise. + """ + try: + subprocess.run( + ["git", "rev-parse", "--verify", "HEAD"], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + check=True + ) + return True + except subprocess.CalledProcessError: + return False + def get_commit_info(format_string: str, sha: str = "HEAD") -> str: """Get latest commits information :param format_string: could be @@ -41,6 +56,8 @@ def get_commit_info(format_string: str, sha: str = "HEAD") -> str: :returns: A `str`. """ + if has_commits() is False: + return 'Repo has no commits yet.' try: commands = [ 'git', 'log', '-n', '1', f"--pretty=format:%{format_string}", f"{sha}", diff --git a/tests/util_test.py b/tests/util_test.py index 42870ea..e42ba24 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -1,6 +1,7 @@ import pytest import subprocess from commit_check.util import get_branch_name +from commit_check.util import has_commits from commit_check.util import git_merge_base from commit_check.util import get_commit_info from commit_check.util import cmd_output @@ -46,6 +47,43 @@ def test_get_branch_name_with_exception(self, mocker): ] assert retval == "" + class TestHasCommits: + def test_has_commits_true(self, mocker): + # Must return True when git rev-parse HEAD succeeds + m_subprocess_run = mocker.patch( + "subprocess.run", + return_value=None + ) + retval = has_commits() + assert m_subprocess_run.call_count == 1 + assert m_subprocess_run.call_args[0][0] == [ + "git", "rev-parse", "--verify", "HEAD" + ] + assert m_subprocess_run.call_args[1] == { + 'stdout': subprocess.DEVNULL, + 'stderr': subprocess.DEVNULL, + 'check': True + } + assert retval is True + + def test_has_commits_false(self, mocker): + # Must return False when git rev-parse HEAD fails + m_subprocess_run = mocker.patch( + "subprocess.run", + side_effect=subprocess.CalledProcessError(128, "git rev-parse") + ) + retval = has_commits() + assert m_subprocess_run.call_count == 1 + assert m_subprocess_run.call_args[0][0] == [ + "git", "rev-parse", "--verify", "HEAD" + ] + assert m_subprocess_run.call_args[1] == { + 'stdout': subprocess.DEVNULL, + 'stderr': subprocess.DEVNULL, + 'check': True + } + assert retval is False + class TestGitMergeBase: @pytest.mark.parametrize("returncode,expected", [ (0, 0), # ancestor exists @@ -78,20 +116,45 @@ class TestGetCommitInfo: ] ) def test_get_commit_info(self, mocker, format_string): - # Must call get_commit_info with given argument. + # Must call get_commit_info with given argument when there are commits. + m_has_commits = mocker.patch( + "commit_check.util.has_commits", + return_value=True + ) m_cmd_output = mocker.patch( "commit_check.util.cmd_output", return_value=" fake commit message " ) retval = get_commit_info(format_string) + assert m_has_commits.call_count == 1 assert m_cmd_output.call_count == 1 assert m_cmd_output.call_args[0][0] == [ "git", "log", "-n", "1", f"--pretty=format:%{format_string}", "HEAD" ] assert retval == " fake commit message " + def test_get_commit_info_no_commits(self, mocker): + # Must return 'Repo has no commits yet.' when there are no commits. + m_has_commits = mocker.patch( + "commit_check.util.has_commits", + return_value=False + ) + m_cmd_output = mocker.patch( + "commit_check.util.cmd_output", + return_value=" fake commit message " + ) + format_string = "s" + retval = get_commit_info(format_string) + assert m_has_commits.call_count == 1 + assert m_cmd_output.call_count == 0 # Should not call cmd_output + assert retval == "Repo has no commits yet." + def test_get_commit_info_with_exception(self, mocker): # Must return empty string when exception raises in cmd_output. + m_has_commits = mocker.patch( + "commit_check.util.has_commits", + return_value=True + ) m_cmd_output = mocker.patch( "commit_check.util.cmd_output", return_value=" fake commit message " @@ -104,6 +167,7 @@ def test_get_commit_info_with_exception(self, mocker): ) format_string = "s" retval = get_commit_info(format_string) + assert m_has_commits.call_count == 1 assert m_cmd_output.call_count == 1 assert m_cmd_output.call_args[0][0] == [ "git", "log", "-n", "1", f"--pretty=format:%{format_string}", "HEAD" From 5a2b5e5c6c2eee858255600bc0e5a32e8d150a89 Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Mon, 7 Jul 2025 12:30:44 +0300 Subject: [PATCH 13/22] feat: support codspeed (#245) * feat: support codspeed * Potential fix for code scanning alert no. 46: Workflow does not contain permissions Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --------- Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .github/workflows/codspeed.yml | 44 ++++++++++++++++++++++++++++++++++ pyproject.toml | 2 +- tests/author_test.py | 12 ++++++++++ tests/branch_test.py | 11 ++++++++- tests/commit_test.py | 12 ++++++++++ tests/error_test.py | 5 ++++ tests/main_test.py | 5 ++++ tests/util_test.py | 17 +++++++++++++ 8 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/codspeed.yml diff --git a/.github/workflows/codspeed.yml b/.github/workflows/codspeed.yml new file mode 100644 index 0000000..f4bbb59 --- /dev/null +++ b/.github/workflows/codspeed.yml @@ -0,0 +1,44 @@ +name: CodSpeed + +permissions: + contents: read + +on: + push: + branches: + - "main" + paths: + - "commit_check/**" + - "tests/**" + - ".github/workflows/codspeed.yml" + - "pyproject.toml" + pull_request: + branches: + - "main" + paths: + - "commit_check/**" + - "tests/**" + - ".github/workflows/codspeed.yml" + - "pyproject.toml" + # `workflow_dispatch` allows CodSpeed to trigger backtest + # performance analysis in order to generate initial data. + workflow_dispatch: + +jobs: + benchmarks: + name: Run benchmarks + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Install dependencies + run: pip install -e .[test] + + - name: Run benchmarks + uses: CodSpeedHQ/action@v3 + with: + token: ${{ secrets.CODSPEED_TOKEN }} + run: pytest tests/ --codspeed diff --git a/pyproject.toml b/pyproject.toml index 3f0759e..8c0db4c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,7 @@ tracker = "https://github.com/commit-check/commit-check/issues" [project.optional-dependencies] dev = ['nox'] -test = ['coverage', 'pytest', 'pytest-mock'] +test = ['coverage', 'pytest', 'pytest-mock', 'pytest-codspeed'] docs = ['sphinx-immaterial', 'sphinx-autobuild'] [tool.setuptools] diff --git a/tests/author_test.py b/tests/author_test.py index 8936223..fcc4939 100644 --- a/tests/author_test.py +++ b/tests/author_test.py @@ -1,3 +1,4 @@ +import pytest from commit_check import PASS, FAIL from commit_check.author import check_author @@ -11,6 +12,7 @@ class TestAuthorName: fake_author_value_an = "fake_author_name" fake_accented_author_value_an = "fáké_áúthór_námé" + @pytest.mark.benchmark def test_check_author(self, mocker): # Must call get_commit_info, re.match. checks = [{ @@ -30,6 +32,7 @@ def test_check_author(self, mocker): assert m_get_commit_info.call_count == 1 assert m_re_match.call_count == 1 + @pytest.mark.benchmark def test_check_author_with_accented_letters(self, mocker): # Must call get_commit_info, re.match. checks = [{ @@ -49,6 +52,7 @@ def test_check_author_with_accented_letters(self, mocker): assert m_get_commit_info.call_count == 1 assert m_re_match.call_count == 1 + @pytest.mark.benchmark def test_check_author_with_empty_checks(self, mocker): # Must NOT call get_commit_info, re.match. with `checks` param with length 0. checks = [] @@ -65,6 +69,7 @@ def test_check_author_with_empty_checks(self, mocker): assert m_get_commit_info.call_count == 0 assert m_re_match.call_count == 0 + @pytest.mark.benchmark def test_check_author_with_different_check(self, mocker): # Must NOT call get_commit_info, re.match with not `author_name`. checks = [{ @@ -84,6 +89,7 @@ def test_check_author_with_different_check(self, mocker): assert m_get_commit_info.call_count == 0 assert m_re_match.call_count == 0 + @pytest.mark.benchmark def test_check_author_with_len0_regex(self, mocker, capfd): # Must NOT call get_commit_info, re.match with `regex` with length 0. checks = [ @@ -107,6 +113,7 @@ def test_check_author_with_len0_regex(self, mocker, capfd): out, _ = capfd.readouterr() assert "Not found regex for author_name." in out + @pytest.mark.benchmark def test_check_author_with_result_none(self, mocker): # Must call print_error_message, print_suggestion when re.match returns NONE. checks = [{ @@ -140,6 +147,7 @@ class TestAuthorEmail: # used by get_commit_info mock fake_author_value_ae = "fake_author_email" + @pytest.mark.benchmark def test_check_author(self, mocker): # Must call get_commit_info, re.match. checks = [{ @@ -159,6 +167,7 @@ def test_check_author(self, mocker): assert m_get_commit_info.call_count == 1 assert m_re_match.call_count == 1 + @pytest.mark.benchmark def test_check_author_with_empty_checks(self, mocker): # Must NOT call get_commit_info, re.match. with `checks` param with length 0. checks = [] @@ -175,6 +184,7 @@ def test_check_author_with_empty_checks(self, mocker): assert m_get_commit_info.call_count == 0 assert m_re_match.call_count == 0 + @pytest.mark.benchmark def test_check_author_with_different_check(self, mocker): # Must NOT call get_commit_info, re.match with not `author_email`. checks = [{ @@ -194,6 +204,7 @@ def test_check_author_with_different_check(self, mocker): assert m_get_commit_info.call_count == 0 assert m_re_match.call_count == 0 + @pytest.mark.benchmark def test_check_author_with_len0_regex(self, mocker, capfd): # Must NOT call get_commit_info, re.match with `regex` with length 0. checks = [ @@ -217,6 +228,7 @@ def test_check_author_with_len0_regex(self, mocker, capfd): out, _ = capfd.readouterr() assert "Not found regex for author_email." in out + @pytest.mark.benchmark def test_check_author_with_result_none(self, mocker): # Must call print_error_message, print_suggestion when re.match returns NONE. checks = [{ diff --git a/tests/branch_test.py b/tests/branch_test.py index 083c5bd..34e3a3b 100644 --- a/tests/branch_test.py +++ b/tests/branch_test.py @@ -1,3 +1,4 @@ +import pytest from commit_check import PASS, FAIL from commit_check.branch import check_branch, check_merge_base @@ -7,6 +8,7 @@ class TestCheckBranch: + @pytest.mark.benchmark def test_check_branch(self, mocker): # Must call get_branch_name, re.match at once. checks = [{ @@ -26,6 +28,7 @@ def test_check_branch(self, mocker): assert m_get_branch_name.call_count == 1 assert m_re_match.call_count == 1 + @pytest.mark.benchmark def test_check_branch_with_empty_checks(self, mocker): # Must NOT call get_branch_name, re.match with `checks` param with length 0. checks = [] @@ -42,6 +45,7 @@ def test_check_branch_with_empty_checks(self, mocker): assert m_get_branch_name.call_count == 0 assert m_re_match.call_count == 0 + @pytest.mark.benchmark def test_check_branch_with_different_check(self, mocker): # Must NOT call get_branch_name, re.match with not `branch`. checks = [{ @@ -61,6 +65,7 @@ def test_check_branch_with_different_check(self, mocker): assert m_get_branch_name.call_count == 0 assert m_re_match.call_count == 0 + @pytest.mark.benchmark def test_check_branch_with_len0_regex(self, mocker, capfd): # Must NOT call get_branch_name, re.match with `regex` with length 0. checks = [ @@ -84,6 +89,7 @@ def test_check_branch_with_len0_regex(self, mocker, capfd): out, _ = capfd.readouterr() assert "Not found regex for branch naming." in out + @pytest.mark.benchmark def test_check_branch_with_result_none(self, mocker): # Must call print_error_message, print_suggestion when re.match returns NONE. checks = [{ @@ -115,6 +121,7 @@ def test_check_branch_with_result_none(self, mocker): class TestCheckMergeBase: + @pytest.mark.benchmark def test_check_merge_base_with_empty_checks(self, mocker): checks = [] m_check_merge = mocker.patch(f"{LOCATION}.check_merge_base") @@ -122,7 +129,7 @@ def test_check_merge_base_with_empty_checks(self, mocker): assert retval == PASS assert m_check_merge.call_count == 0 - + @pytest.mark.benchmark def test_check_merge_base_with_empty_regex(self, mocker): checks = [{ "check": "merge_base", @@ -133,6 +140,7 @@ def test_check_merge_base_with_empty_regex(self, mocker): assert retval == PASS assert m_check_merge.call_count == 0 + @pytest.mark.benchmark def test_check_merge_base_with_different_check(self, mocker): checks = [{ "check": "branch", @@ -143,6 +151,7 @@ def test_check_merge_base_with_different_check(self, mocker): assert retval == PASS assert m_check_merge.call_count == 0 + @pytest.mark.benchmark def test_check_merge_base_fail_with_messages(self, mocker, capfd): checks = [{ "check": "merge_base", diff --git a/tests/commit_test.py b/tests/commit_test.py index a458636..c733968 100644 --- a/tests/commit_test.py +++ b/tests/commit_test.py @@ -1,3 +1,4 @@ +import pytest from commit_check import PASS, FAIL from commit_check.commit import check_commit_msg, get_default_commit_msg_file, read_commit_msg, check_commit_signoff @@ -9,11 +10,13 @@ MSG_FILE = '.git/COMMIT_EDITMSG' +@pytest.mark.benchmark def test_get_default_commit_msg_file(mocker): retval = get_default_commit_msg_file() assert retval == ".git/COMMIT_EDITMSG" +@pytest.mark.benchmark def test_read_commit_msg_from_existing_file(tmp_path): # Create a temporary file with a known content commit_msg_content = "Test commit message content." @@ -24,12 +27,14 @@ def test_read_commit_msg_from_existing_file(tmp_path): assert result == commit_msg_content +@pytest.mark.benchmark def test_read_commit_msg_file_not_found(mocker): m_commits_info = mocker.patch('commit_check.util.get_commit_info', return_value='mocked_commits_info') read_commit_msg("non_existent_file.txt") assert m_commits_info.call_count == 0 +@pytest.mark.benchmark def test_check_commit_msg_no_commit_msg_file(mocker): mock_get_default_commit_msg_file = mocker.patch( "commit_check.commit.get_default_commit_msg_file", @@ -49,6 +54,7 @@ def test_check_commit_msg_no_commit_msg_file(mocker): assert result == 0 +@pytest.mark.benchmark def test_check_commit_with_empty_checks(mocker): checks = [] m_re_match = mocker.patch( @@ -60,6 +66,7 @@ def test_check_commit_with_empty_checks(mocker): assert m_re_match.call_count == 0 +@pytest.mark.benchmark def test_check_commit_with_different_check(mocker): checks = [{ "check": "branch", @@ -74,6 +81,7 @@ def test_check_commit_with_different_check(mocker): assert m_re_match.call_count == 0 +@pytest.mark.benchmark def test_check_commit_with_len0_regex(mocker, capfd): checks = [ { @@ -92,6 +100,7 @@ def test_check_commit_with_len0_regex(mocker, capfd): assert "Not found regex for commit message." in out +@pytest.mark.benchmark def test_check_commit_with_result_none(mocker): checks = [{ "check": "message", @@ -116,6 +125,7 @@ def test_check_commit_with_result_none(mocker): assert m_print_suggestion.call_count == 1 +@pytest.mark.benchmark def test_check_commit_signoff(mocker): checks = [{ "check": "commit_signoff", @@ -140,6 +150,7 @@ def test_check_commit_signoff(mocker): assert m_print_suggestion.call_count == 1 +@pytest.mark.benchmark def test_check_commit_signoff_with_empty_regex(mocker): checks = [{ "check": "commit_signoff", @@ -156,6 +167,7 @@ def test_check_commit_signoff_with_empty_regex(mocker): assert m_re_match.call_count == 0 +@pytest.mark.benchmark def test_check_commit_signoff_with_empty_checks(mocker): checks = [] m_re_match = mocker.patch( diff --git a/tests/error_test.py b/tests/error_test.py index d0a2880..ab9e948 100644 --- a/tests/error_test.py +++ b/tests/error_test.py @@ -3,6 +3,7 @@ from commit_check.error import error_handler, log_and_exit +@pytest.mark.benchmark def test_error_handler_RuntimeError(): with pytest.raises(SystemExit) as exit_info: with error_handler(): @@ -10,6 +11,7 @@ def test_error_handler_RuntimeError(): assert exit_info.value.code == 1 +@pytest.mark.benchmark def test_error_handler_KeyboardInterrupt(): with pytest.raises(SystemExit) as exit_info: with error_handler(): @@ -17,6 +19,7 @@ def test_error_handler_KeyboardInterrupt(): assert exit_info.value.code == 130 +@pytest.mark.benchmark def test_error_handler_unexpected_error(): with pytest.raises(SystemExit) as exit_info: with error_handler(): @@ -24,6 +27,7 @@ def test_error_handler_unexpected_error(): assert exit_info.value.code == 3 +@pytest.mark.benchmark def test_error_handler_cannot_access(mocker): with pytest.raises(SystemExit): store_dir = "/fake/commit-check" @@ -50,6 +54,7 @@ def test_error_handler_cannot_access(mocker): mock_open().write.assert_any_call(f"Failed to write to log at {log_path}\n") +@pytest.mark.benchmark @pytest.mark.xfail def test_log_and_exit(monkeypatch): monkeypatch.setenv("COMMIT_CHECK_HOME", "") diff --git a/tests/main_test.py b/tests/main_test.py index ca77032..c232efd 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -7,6 +7,7 @@ class TestMain: + @pytest.mark.benchmark @pytest.mark.parametrize("argv, check_commit_call_count, check_branch_call_count, check_author_call_count, check_commit_signoff_call_count, check_merge_base_call_count", [ ([CMD, "--message"], 1, 0, 0, 0, 0), ([CMD, "--branch"], 0, 1, 0, 0, 0), @@ -53,6 +54,7 @@ def test_main( assert m_check_commit_signoff.call_count == check_commit_signoff_call_count assert m_check_merge_base.call_count == check_merge_base_call_count + @pytest.mark.benchmark def test_main_help(self, mocker, capfd): mocker.patch( "commit_check.main.validate_config", @@ -78,6 +80,7 @@ def test_main_help(self, mocker, capfd): stdout, _ = capfd.readouterr() assert "usage: " in stdout + @pytest.mark.benchmark def test_main_version(self, mocker): mocker.patch( "commit_check.main.validate_config", @@ -101,6 +104,7 @@ def test_main_version(self, mocker): assert m_check_commit_signoff.call_count == 0 assert m_check_merge_base.call_count == 0 + @pytest.mark.benchmark def test_main_validate_config_ret_none(self, mocker): mocker.patch( "commit_check.main.validate_config", @@ -116,6 +120,7 @@ def test_main_validate_config_ret_none(self, mocker): assert m_check_commit.call_count == 1 assert m_check_commit.call_args[0][0] == DEFAULT_CONFIG["checks"] + @pytest.mark.benchmark @pytest.mark.parametrize( "argv, message_result, branch_result, author_name_result, author_email_result, commit_signoff_result, merge_base_result, final_result", [ diff --git a/tests/util_test.py b/tests/util_test.py index e42ba24..c426831 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -15,6 +15,7 @@ class TestUtil: class TestGetBranchName: + @pytest.mark.benchmark def test_get_branch_name(self, mocker): # Must call cmd_output with given argument. m_cmd_output = mocker.patch( @@ -28,6 +29,7 @@ def test_get_branch_name(self, mocker): ] assert retval == "fake_branch_name" + @pytest.mark.benchmark def test_get_branch_name_with_exception(self, mocker): # Must return empty string when exception raises in cmd_output. m_cmd_output = mocker.patch( @@ -48,6 +50,7 @@ def test_get_branch_name_with_exception(self, mocker): assert retval == "" class TestHasCommits: + @pytest.mark.benchmark def test_has_commits_true(self, mocker): # Must return True when git rev-parse HEAD succeeds m_subprocess_run = mocker.patch( @@ -66,6 +69,7 @@ def test_has_commits_true(self, mocker): } assert retval is True + @pytest.mark.benchmark def test_has_commits_false(self, mocker): # Must return False when git rev-parse HEAD fails m_subprocess_run = mocker.patch( @@ -85,6 +89,7 @@ def test_has_commits_false(self, mocker): assert retval is False class TestGitMergeBase: + @pytest.mark.benchmark @pytest.mark.parametrize("returncode,expected", [ (0, 0), # ancestor exists (1, 1), # no ancestor @@ -109,6 +114,7 @@ def test_git_merge_base(self, mocker, returncode, expected): assert result == expected class TestGetCommitInfo: + @pytest.mark.benchmark @pytest.mark.parametrize("format_string", [ ("s"), ("an"), @@ -133,6 +139,7 @@ def test_get_commit_info(self, mocker, format_string): ] assert retval == " fake commit message " + @pytest.mark.benchmark def test_get_commit_info_no_commits(self, mocker): # Must return 'Repo has no commits yet.' when there are no commits. m_has_commits = mocker.patch( @@ -149,6 +156,7 @@ def test_get_commit_info_no_commits(self, mocker): assert m_cmd_output.call_count == 0 # Should not call cmd_output assert retval == "Repo has no commits yet." + @pytest.mark.benchmark def test_get_commit_info_with_exception(self, mocker): # Must return empty string when exception raises in cmd_output. m_has_commits = mocker.patch( @@ -182,6 +190,7 @@ def __init__(self, returncode, stdout, stderr): self.stdout = stdout self.stderr = stderr + @pytest.mark.benchmark def test_cmd_output(self, mocker): # Must subprocess.run with given argument. m_subprocess_run = mocker.patch( @@ -192,6 +201,7 @@ def test_cmd_output(self, mocker): assert m_subprocess_run.call_count == 1 assert retval == "ok" + @pytest.mark.benchmark @pytest.mark.parametrize("returncode, stdout, stderr", [ (1, "ok", "err"), (0, None, "err"), @@ -216,6 +226,7 @@ def test_cmd_output_err(self, mocker, returncode, stdout, stderr): "stdout": PIPE } + @pytest.mark.benchmark @pytest.mark.parametrize("returncode, stdout, stderr", [ (1, "ok", ""), (0, None, ""), @@ -241,6 +252,7 @@ def test_cmd_output_err_with_len0_stderr(self, mocker, returncode, stdout, stder } class TestValidateConfig: + @pytest.mark.benchmark def test_validate_config(self, mocker): # Must call yaml.safe_load. mocker.patch("builtins.open") @@ -253,6 +265,7 @@ def test_validate_config(self, mocker): assert m_yaml_safe_load.call_count == 1 assert retval == dummy_resp + @pytest.mark.benchmark def test_validate_config_file_not_found(self, mocker): # Must return empty dictionary when FileNotFoundError raises in built-in open. mocker.patch("builtins.open").side_effect = FileNotFoundError @@ -262,6 +275,7 @@ def test_validate_config_file_not_found(self, mocker): assert retval == {} class TestPrintErrorMessage: + @pytest.mark.benchmark def test_print_error_header(self, capfd): # Must print on stdout with given argument. print_error_header() @@ -269,6 +283,7 @@ def test_print_error_header(self, capfd): assert "Commit rejected by Commit-Check" in stdout assert "Commit rejected." in stdout + @pytest.mark.benchmark @pytest.mark.parametrize("check_type, type_failed_msg", [ ("message", "check failed =>"), ("branch", "check failed =>"), @@ -294,12 +309,14 @@ def test_print_error_message(self, capfd, check_type, type_failed_msg): assert dummy_error in stdout class TestPrintSuggestion: + @pytest.mark.benchmark def test_print_suggestion(self, capfd): # Must print on stdout with given argument. print_suggestion("dummy suggest") stdout, _ = capfd.readouterr() assert "Suggest:" in stdout + @pytest.mark.benchmark def test_print_suggestion_exit1(self, capfd): # Must exit with 1 when "" passed with pytest.raises(SystemExit) as e: From 9ac3f4ce141c8efd33b4a3353755ace05c16a04a Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Mon, 7 Jul 2025 13:59:32 +0300 Subject: [PATCH 14/22] chore: delete .github/ISSUE_TEMPLATE/config.yml (#246) --- .github/ISSUE_TEMPLATE/config.yml | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/config.yml diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml deleted file mode 100644 index 375a8f2..0000000 --- a/.github/ISSUE_TEMPLATE/config.yml +++ /dev/null @@ -1,8 +0,0 @@ -# Documentation for this file can be found at: -# https://help.github.com/en/github/building-a-strong-community/configuring-issue-templates-for-your-repository - -blank_issues_enabled: false -contact_links: - - name: "(maintainers only) Blank issue" - url: https://github.com/commit-check/commit-check/issues/new/ - about: For maintainers only. From 54738e64db30c0ea54bc077ef6e524637c841049 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 7 Jul 2025 21:28:24 +0300 Subject: [PATCH 15/22] ci: pre-commit autoupdate (#248) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d289da2..8f6f962 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,13 +19,13 @@ repos: - id: requirements-txt-fixer - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.11.4 + rev: v0.12.2 hooks: # Run the linter. - id: ruff args: [ --fix ] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.15.0 + rev: v1.16.1 hooks: - id: mypy additional_dependencies: [types-PyYAML] @@ -35,7 +35,7 @@ repos: hooks: - id: codespell - repo: https://github.com/commit-check/commit-check - rev: v0.9.5 + rev: v0.9.8 hooks: - id: check-message # - id: check-branch # uncomment if you need. From cf9d2454133417b01c155ffb4ba30648911d3ea0 Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Wed, 9 Jul 2025 23:55:30 +0300 Subject: [PATCH 16/22] fix: use `git branch --show-current` to get branch name (#252) * fix: use git branch --show-current to get branch name * fix: set branch name to HEAD if empty --- commit_check/util.py | 5 +++-- tests/util_test.py | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/commit_check/util.py b/commit_check/util.py index 52b4eaa..fe8211f 100644 --- a/commit_check/util.py +++ b/commit_check/util.py @@ -22,8 +22,9 @@ def get_branch_name() -> str: :returns: A `str` describing the current branch name. """ try: - commands = ['git', 'rev-parse', '--abbrev-ref', 'HEAD'] - branch_name = cmd_output(commands) + # Git 2.22 and above supports `git branch --show-current` + commands = ['git', 'branch', '--show-current'] + branch_name = cmd_output(commands) or "HEAD" except CalledProcessError: branch_name = '' return branch_name.strip() diff --git a/tests/util_test.py b/tests/util_test.py index c426831..a9d9f51 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -25,7 +25,7 @@ def test_get_branch_name(self, mocker): retval = get_branch_name() assert m_cmd_output.call_count == 1 assert m_cmd_output.call_args[0][0] == [ - "git", "rev-parse", "--abbrev-ref", "HEAD" + "git", "branch", "--show-current" ] assert retval == "fake_branch_name" @@ -45,7 +45,7 @@ def test_get_branch_name_with_exception(self, mocker): retval = get_branch_name() assert m_cmd_output.call_count == 1 assert m_cmd_output.call_args[0][0] == [ - "git", "rev-parse", "--abbrev-ref", "HEAD" + "git", "branch", "--show-current" ] assert retval == "" From 55b000f5992da397487eacc5bbf39bf53d94fd5f Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Thu, 10 Jul 2025 01:08:48 +0300 Subject: [PATCH 17/22] fix: make sure git repo has commit before checking (#253) * fix: make sure git repo has commit before checking * chore: remove print * chore: add # pragma: no cover * chore: add # pragma: no cover --- commit_check/author.py | 5 ++++- commit_check/branch.py | 5 ++++- commit_check/commit.py | 9 ++++++++- commit_check/util.py | 2 -- tests/util_test.py | 15 ++++++--------- 5 files changed, 22 insertions(+), 14 deletions(-) diff --git a/commit_check/author.py b/commit_check/author.py index a68397e..e3cfd20 100644 --- a/commit_check/author.py +++ b/commit_check/author.py @@ -1,10 +1,13 @@ """Check git author name and email""" import re from commit_check import YELLOW, RESET_COLOR, PASS, FAIL -from commit_check.util import get_commit_info, print_error_header, print_error_message, print_suggestion +from commit_check.util import get_commit_info, has_commits, print_error_header, print_error_message, print_suggestion def check_author(checks: list, check_type: str) -> int: + if has_commits() is False: + return PASS # pragma: no cover + for check in checks: if check['check'] == check_type: if check['regex'] == "": diff --git a/commit_check/branch.py b/commit_check/branch.py index 1cc8a35..b7446cc 100644 --- a/commit_check/branch.py +++ b/commit_check/branch.py @@ -1,7 +1,7 @@ """Check git branch naming convention.""" import re from commit_check import YELLOW, RESET_COLOR, PASS, FAIL -from commit_check.util import get_branch_name, git_merge_base, print_error_header, print_error_message, print_suggestion +from commit_check.util import get_branch_name, git_merge_base, print_error_header, print_error_message, print_suggestion, has_commits def check_branch(checks: list) -> int: @@ -33,6 +33,9 @@ def check_merge_base(checks: list) -> int: :returns PASS(0) if merge base check succeeds, FAIL(1) otherwise """ + if has_commits() is False: + return PASS # pragma: no cover + for check in checks: if check['check'] == 'merge_base': if check['regex'] == "": diff --git a/commit_check/commit.py b/commit_check/commit.py index 60b8d2f..ac1c63a 100644 --- a/commit_check/commit.py +++ b/commit_check/commit.py @@ -2,7 +2,7 @@ import re from pathlib import PurePath from commit_check import YELLOW, RESET_COLOR, PASS, FAIL -from commit_check.util import cmd_output, get_commit_info, print_error_header, print_error_message, print_suggestion +from commit_check.util import cmd_output, get_commit_info, print_error_header, print_error_message, print_suggestion, has_commits def get_default_commit_msg_file() -> str: @@ -22,6 +22,10 @@ def read_commit_msg(commit_msg_file) -> str: def check_commit_msg(checks: list, commit_msg_file: str = "") -> int: + """Check commit message against the provided checks.""" + if has_commits() is False: + return PASS # pragma: no cover + if commit_msg_file is None or commit_msg_file == "": commit_msg_file = get_default_commit_msg_file() @@ -51,6 +55,9 @@ def check_commit_msg(checks: list, commit_msg_file: str = "") -> int: def check_commit_signoff(checks: list, commit_msg_file: str = "") -> int: + if has_commits() is False: + return PASS # pragma: no cover + if commit_msg_file is None or commit_msg_file == "": commit_msg_file = get_default_commit_msg_file() diff --git a/commit_check/util.py b/commit_check/util.py index fe8211f..6d3c33b 100644 --- a/commit_check/util.py +++ b/commit_check/util.py @@ -57,8 +57,6 @@ def get_commit_info(format_string: str, sha: str = "HEAD") -> str: :returns: A `str`. """ - if has_commits() is False: - return 'Repo has no commits yet.' try: commands = [ 'git', 'log', '-n', '1', f"--pretty=format:%{format_string}", f"{sha}", diff --git a/tests/util_test.py b/tests/util_test.py index a9d9f51..8937005 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -123,7 +123,7 @@ class TestGetCommitInfo: ) def test_get_commit_info(self, mocker, format_string): # Must call get_commit_info with given argument when there are commits. - m_has_commits = mocker.patch( + mocker.patch( "commit_check.util.has_commits", return_value=True ) @@ -132,7 +132,6 @@ def test_get_commit_info(self, mocker, format_string): return_value=" fake commit message " ) retval = get_commit_info(format_string) - assert m_has_commits.call_count == 1 assert m_cmd_output.call_count == 1 assert m_cmd_output.call_args[0][0] == [ "git", "log", "-n", "1", f"--pretty=format:%{format_string}", "HEAD" @@ -142,24 +141,23 @@ def test_get_commit_info(self, mocker, format_string): @pytest.mark.benchmark def test_get_commit_info_no_commits(self, mocker): # Must return 'Repo has no commits yet.' when there are no commits. - m_has_commits = mocker.patch( + mocker.patch( "commit_check.util.has_commits", return_value=False ) - m_cmd_output = mocker.patch( + mocker.patch( "commit_check.util.cmd_output", return_value=" fake commit message " ) format_string = "s" retval = get_commit_info(format_string) - assert m_has_commits.call_count == 1 - assert m_cmd_output.call_count == 0 # Should not call cmd_output - assert retval == "Repo has no commits yet." + assert retval == " fake commit message " + @pytest.mark.benchmark def test_get_commit_info_with_exception(self, mocker): # Must return empty string when exception raises in cmd_output. - m_has_commits = mocker.patch( + mocker.patch( "commit_check.util.has_commits", return_value=True ) @@ -175,7 +173,6 @@ def test_get_commit_info_with_exception(self, mocker): ) format_string = "s" retval = get_commit_info(format_string) - assert m_has_commits.call_count == 1 assert m_cmd_output.call_count == 1 assert m_cmd_output.call_args[0][0] == [ "git", "log", "-n", "1", f"--pretty=format:%{format_string}", "HEAD" From 9d219e981ec71a9b5865dfa7539609d1384ee48d Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Thu, 10 Jul 2025 01:49:31 +0300 Subject: [PATCH 18/22] fix: add package write permission (#255) --- .github/workflows/publish-image.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/publish-image.yml b/.github/workflows/publish-image.yml index 9c148f6..6e4fe40 100644 --- a/.github/workflows/publish-image.yml +++ b/.github/workflows/publish-image.yml @@ -2,6 +2,7 @@ name: publish image permissions: contents: read + packages: write on: push: From 41b53a9400dd3bfd6417eb790cf97fbaf48adbeb Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Fri, 11 Jul 2025 14:51:05 +0300 Subject: [PATCH 19/22] chore: remove Docker support (#257) --- .github/dependabot.yml | 4 ---- .github/workflows/publish-image.yml | 36 ----------------------------- Dockerfile | 21 ----------------- 3 files changed, 61 deletions(-) delete mode 100644 .github/workflows/publish-image.yml delete mode 100644 Dockerfile diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 95bb02f..755e981 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,10 +5,6 @@ version: 2 updates: - - package-ecosystem: docker - directory: / - schedule: - interval: "weekly" - package-ecosystem: github-actions directory: / schedule: diff --git a/.github/workflows/publish-image.yml b/.github/workflows/publish-image.yml deleted file mode 100644 index 6e4fe40..0000000 --- a/.github/workflows/publish-image.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: publish image - -permissions: - contents: read - packages: write - -on: - push: - paths: - - 'Dockerfile' - workflow_dispatch: - inputs: - tag: - description: 'Which tag want to build' - default: '' - required: false - -jobs: - publish: - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@v4 - - name: Build to check Dockerfile - if: github.event.inputs.tag == '' - run: | - docker build -f Dockerfile -t commit-check . - - name: Build and publish docker image - if: github.event.inputs.tag != '' - run: | - echo "tag = ${{ github.event.inputs.tag }}" - docker build -f Dockerfile --build-arg VERSION=${{ github.event.inputs.tag }} -t commit-check:${{ github.event.inputs.tag }} . - echo $CR_PAT | docker login ghcr.io -u shenxianpeng --password-stdin - docker tag commit-check:${{ github.event.inputs.tag }} ghcr.io/commit-check/commit-check:${{ github.event.inputs.tag }} - docker push ghcr.io/commit-check/commit-check:${{ github.event.inputs.tag }} - env: - CR_PAT: ${{ secrets.CR_PAT }} diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 7776999..0000000 --- a/Dockerfile +++ /dev/null @@ -1,21 +0,0 @@ -FROM python:3.13-slim - -ARG VERSION - -LABEL com.github.actions.name="Commit Check" -LABEL com.github.actions.description="Check commit message formatting, branch naming, commit author, email, and more." -LABEL com.github.actions.icon="code" -LABEL com.github.actions.color="gray-dark" - -LABEL repository="https://github.com/commit-check/commit-check" -LABEL maintainer="shenxianpeng <20297606+shenxianpeng@users.noreply.github.com>" - -RUN if [ -z "$VERSION" ]; then \ - pip3 install commit-check; \ - else \ - pip3 install commit-check==$VERSION; \ - fi - -USER nobody - -ENTRYPOINT [ "commit-check" ] From 2b9711cd369bdceb34b6418ee3d74b3ea3658d57 Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Sun, 13 Jul 2025 00:27:14 +0300 Subject: [PATCH 20/22] feat: add impreative mode (#258) * feat: add impreative mode * feat: introduce imperatives.py file * chore: rename to --imperative * chore: rename to --imperative * chore: fix docs formatting * feat: add check imperative hook --- .commit-check.yml | 5 + .pre-commit-config.yaml | 3 +- .pre-commit-hooks.yaml | 7 ++ README.rst | 3 +- commit_check/__init__.py | 6 + commit_check/commit.py | 95 +++++++++++++++ commit_check/imperatives.py | 237 ++++++++++++++++++++++++++++++++++++ commit_check/main.py | 10 ++ tests/commit_test.py | 223 ++++++++++++++++++++++++++++++++- tests/main_test.py | 34 +++--- 10 files changed, 606 insertions(+), 17 deletions(-) create mode 100644 commit_check/imperatives.py diff --git a/.commit-check.yml b/.commit-check.yml index ad339d0..d215d90 100644 --- a/.commit-check.yml +++ b/.commit-check.yml @@ -32,3 +32,8 @@ checks: regex: main # it can be master, develop, devel etc based on your project. error: Current branch is not rebased onto target branch suggest: Please ensure your branch is rebased with the target branch + + - check: imperative + regex: '' # Not used for imperative mood check + error: 'Commit message should use imperative mood (e.g., "Add feature" not "Added feature")' + suggest: 'Use imperative mood in commit message like "Add", "Fix", "Update", "Remove"' diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8f6f962..58c47aa 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -42,4 +42,5 @@ repos: - id: check-author-name # uncomment if you need. - id: check-author-email # uncomment if you need. # - id: check-commit-signoff # uncomment if you need. - # - id: check-merge-base # requires download all git history + # - id: check-merge-base # requires download all git history + # - id: check-imperative # uncomment if you need. diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index 1fca51a..6a73596 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -41,3 +41,10 @@ args: [--merge-base] pass_filenames: false language: python +- id: check-imperative + name: check imperative mood + description: ensures commit message uses imperative mood + entry: commit-check + args: [--imperative] + pass_filenames: true + language: python diff --git a/README.rst b/README.rst index b9ac03a..35a68e1 100644 --- a/README.rst +++ b/README.rst @@ -77,6 +77,7 @@ Running as pre-commit hook - id: check-author-email - id: check-commit-signoff - id: check-merge-base # requires download all git history + - id: check-imperative Running as CLI ~~~~~~~~~~~~~~ @@ -109,7 +110,7 @@ To configure the hook, create a script file in the ``.git/hooks/`` directory. .. code-block:: bash #!/bin/sh - commit-check --message --branch --author-name --author-email --commit-signoff --merge-base + commit-check --message --branch --author-name --author-email --commit-signoff --merge-base --imperative Save the script file as ``pre-push`` and make it executable: diff --git a/commit_check/__init__.py b/commit_check/__init__.py index 521f68f..8b78762 100644 --- a/commit_check/__init__.py +++ b/commit_check/__init__.py @@ -54,6 +54,12 @@ 'error': 'Current branch is not rebased onto target branch', 'suggest': 'Please ensure your branch is rebased with the target branch', }, + { + 'check': 'imperative', + 'regex': r'', # Not used for imperative mood check + 'error': 'Commit message should use imperative mood (e.g., "Add feature" not "Added feature")', + 'suggest': 'Use imperative mood in commit message like "Add", "Fix", "Update", "Remove"', + }, ], } diff --git a/commit_check/commit.py b/commit_check/commit.py index ac1c63a..05a08e3 100644 --- a/commit_check/commit.py +++ b/commit_check/commit.py @@ -3,6 +3,12 @@ from pathlib import PurePath from commit_check import YELLOW, RESET_COLOR, PASS, FAIL from commit_check.util import cmd_output, get_commit_info, print_error_header, print_error_message, print_suggestion, has_commits +from commit_check.imperatives import IMPERATIVES + + +def _load_imperatives() -> set: + """Load imperative verbs from imperatives module.""" + return IMPERATIVES def get_default_commit_msg_file() -> str: @@ -84,3 +90,92 @@ def check_commit_signoff(checks: list, commit_msg_file: str = "") -> int: return FAIL return PASS + + +def check_imperative(checks: list, commit_msg_file: str = "") -> int: + """Check if commit message uses imperative mood.""" + if has_commits() is False: + return PASS # pragma: no cover + + if commit_msg_file is None or commit_msg_file == "": + commit_msg_file = get_default_commit_msg_file() + + for check in checks: + if check['check'] == 'imperative': + commit_msg = read_commit_msg(commit_msg_file) + + # Extract the subject line (first line of commit message) + subject = commit_msg.split('\n')[0].strip() + + # Skip if empty or merge commit + if not subject or subject.startswith('Merge'): + return PASS + + # For conventional commits, extract description after the colon + if ':' in subject: + description = subject.split(':', 1)[1].strip() + else: + description = subject + + # Check if the description uses imperative mood + if not _is_imperative(description): + if not print_error_header.has_been_called: + print_error_header() # pragma: no cover + print_error_message( + check['check'], 'imperative mood pattern', + check['error'], subject, + ) + if check['suggest']: + print_suggestion(check['suggest']) + return FAIL + + return PASS + + +def _is_imperative(description: str) -> bool: + """Check if a description uses imperative mood.""" + if not description: + return True + + # Get the first word of the description + first_word = description.split()[0].lower() + + # Load imperative verbs from file + imperatives = _load_imperatives() + + # Check for common past tense pattern (-ed ending) but be more specific + if (first_word.endswith('ed') and len(first_word) > 3 and + first_word not in {'red', 'bed', 'fed', 'led', 'wed', 'shed', 'fled'}): + return False + + # Check for present continuous pattern (-ing ending) but be more specific + if (first_word.endswith('ing') and len(first_word) > 4 and + first_word not in {'ring', 'sing', 'king', 'wing', 'thing', 'string', 'bring'}): + return False + + # Check for third person singular (-s ending) but be more specific + # Only flag if it's clearly a verb in third person singular form + if first_word.endswith('s') and len(first_word) > 3: + # Common nouns ending in 's' that should be allowed + common_nouns_ending_s = {'process', 'access', 'address', 'progress', 'express', 'stress', 'success', 'class', 'pass', 'mass', 'loss', 'cross', 'gross', 'boss', 'toss', 'less', 'mess', 'dress', 'press', 'bless', 'guess', 'chess', 'glass', 'grass', 'brass'} + + # Words ending in 'ss' or 'us' are usually not third person singular verbs + if first_word.endswith('ss') or first_word.endswith('us'): + return True # Allow these + + # If it's a common noun, allow it + if first_word in common_nouns_ending_s: + return True + + # Otherwise, it's likely a third person singular verb + return False + + # If we have imperatives loaded, check if the first word is imperative + if imperatives: + # Check if the first word is in our imperative list + if first_word in imperatives: + return True + + # If word is not in imperatives list, apply some heuristics + # If it passes all the negative checks above, it's likely imperative + return True diff --git a/commit_check/imperatives.py b/commit_check/imperatives.py new file mode 100644 index 0000000..0c3d091 --- /dev/null +++ b/commit_check/imperatives.py @@ -0,0 +1,237 @@ +# https://github.com/crate-ci/imperative/blob/master/assets/imperatives.txt +# Imperative forms of verbs +# +# This file contains the imperative form of frequently encountered +# docstring verbs. Some of these may be more commonly encountered as +# nouns, but blacklisting them for this may cause false positives. + +IMPERATIVES = { + 'accept', + 'access', + 'add', + 'adjust', + 'aggregate', + 'allow', + 'append', + 'apply', + 'archive', + 'assert', + 'assign', + 'attempt', + 'authenticate', + 'authorize', + 'break', + 'build', + 'cache', + 'calculate', + 'call', + 'cancel', + 'capture', + 'change', + 'check', + 'clean', + 'clear', + 'close', + 'collect', + 'combine', + 'commit', + 'compare', + 'compute', + 'configure', + 'confirm', + 'connect', + 'construct', + 'control', + 'convert', + 'copy', + 'count', + 'create', + 'customize', + 'declare', + 'decode', + 'decorate', + 'define', + 'delegate', + 'delete', + 'deprecate', + 'derive', + 'describe', + 'detect', + 'determine', + 'display', + 'download', + 'drop', + 'dump', + 'emit', + 'empty', + 'enable', + 'encapsulate', + 'encode', + 'end', + 'ensure', + 'enumerate', + 'establish', + 'evaluate', + 'examine', + 'execute', + 'exit', + 'expand', + 'expect', + 'export', + 'extend', + 'extract', + 'feed', + 'fetch', + 'fill', + 'filter', + 'finalize', + 'find', + 'fire', + 'fix', + 'flag', + 'force', + 'format', + 'forward', + 'generate', + 'get', + 'give', + 'go', + 'group', + 'handle', + 'help', + 'hold', + 'identify', + 'implement', + 'import', + 'indicate', + 'init', + 'initialise', + 'initialize', + 'initiate', + 'input', + 'insert', + 'instantiate', + 'intercept', + 'invoke', + 'iterate', + 'join', + 'keep', + 'launch', + 'list', + 'listen', + 'load', + 'log', + 'look', + 'make', + 'manage', + 'manipulate', + 'map', + 'mark', + 'match', + 'merge', + 'mock', + 'modify', + 'monitor', + 'move', + 'normalize', + 'note', + 'obtain', + 'open', + 'output', + 'override', + 'overwrite', + 'package', + 'pad', + 'parse', + 'partial', + 'pass', + 'perform', + 'persist', + 'pick', + 'plot', + 'poll', + 'populate', + 'post', + 'prepare', + 'print', + 'process', + 'produce', + 'provide', + 'publish', + 'pull', + 'put', + 'query', + 'raise', + 'read', + 'record', + 'refer', + 'refresh', + 'register', + 'reload', + 'remove', + 'rename', + 'render', + 'replace', + 'reply', + 'report', + 'represent', + 'request', + 'require', + 'reset', + 'resolve', + 'retrieve', + 'return', + 'roll', + 'rollback', + 'round', + 'run', + 'sample', + 'save', + 'scan', + 'search', + 'select', + 'send', + 'serialise', + 'serialize', + 'serve', + 'set', + 'show', + 'simulate', + 'source', + 'specify', + 'split', + 'start', + 'step', + 'stop', + 'store', + 'strip', + 'submit', + 'subscribe', + 'sum', + 'swap', + 'sync', + 'synchronise', + 'synchronize', + 'take', + 'tear', + 'test', + 'time', + 'transform', + 'translate', + 'transmit', + 'truncate', + 'try', + 'turn', + 'tweak', + 'update', + 'upload', + 'use', + 'validate', + 'verify', + 'view', + 'wait', + 'walk', + 'wrap', + 'write', + 'yield', +} diff --git a/commit_check/main.py b/commit_check/main.py index 6faa070..f81af48 100644 --- a/commit_check/main.py +++ b/commit_check/main.py @@ -92,6 +92,14 @@ def get_parser() -> argparse.ArgumentParser: required=False, ) + parser.add_argument( + '-i', + '--imperative', + help='check commit message uses imperative mood', + action="store_true", + required=False, + ) + return parser @@ -122,6 +130,8 @@ def main() -> int: check_results.append(commit.check_commit_signoff(checks)) if args.merge_base: check_results.append(branch.check_merge_base(checks)) + if args.imperative: + check_results.append(commit.check_imperative(checks, args.commit_msg_file)) return PASS if all(val == PASS for val in check_results) else FAIL diff --git a/tests/commit_test.py b/tests/commit_test.py index c733968..9a1235d 100644 --- a/tests/commit_test.py +++ b/tests/commit_test.py @@ -1,6 +1,6 @@ import pytest from commit_check import PASS, FAIL -from commit_check.commit import check_commit_msg, get_default_commit_msg_file, read_commit_msg, check_commit_signoff +from commit_check.commit import check_commit_msg, get_default_commit_msg_file, read_commit_msg, check_commit_signoff, check_imperative # used by get_commit_info mock FAKE_BRANCH_NAME = "fake_commits_info" @@ -177,3 +177,224 @@ def test_check_commit_signoff_with_empty_checks(mocker): retval = check_commit_signoff(checks) assert retval == PASS assert m_re_match.call_count == 0 + + +@pytest.mark.benchmark +def test_check_imperative_pass(mocker): + """Test imperative mood check passes for valid imperative mood.""" + checks = [{ + "check": "imperative", + "regex": "", + "error": "Commit message should use imperative mood", + "suggest": "Use imperative mood" + }] + + mocker.patch( + "commit_check.commit.read_commit_msg", + return_value="feat: Add new feature\n\nThis adds a new feature to the application." + ) + + retval = check_imperative(checks, MSG_FILE) + assert retval == PASS + + +@pytest.mark.benchmark +def test_check_imperative_fail_past_tense(mocker): + """Test imperative mood check fails for past tense.""" + checks = [{ + "check": "imperative", + "regex": "", + "error": "Commit message should use imperative mood", + "suggest": "Use imperative mood" + }] + + mocker.patch( + "commit_check.commit.read_commit_msg", + return_value="feat: Added new feature" + ) + + m_print_error_message = mocker.patch( + f"{LOCATION}.print_error_message" + ) + m_print_suggestion = mocker.patch( + f"{LOCATION}.print_suggestion" + ) + + retval = check_imperative(checks, MSG_FILE) + assert retval == FAIL + assert m_print_error_message.call_count == 1 + assert m_print_suggestion.call_count == 1 + + +@pytest.mark.benchmark +def test_check_imperative_fail_present_continuous(mocker): + """Test imperative mood check fails for present continuous.""" + checks = [{ + "check": "imperative", + "regex": "", + "error": "Commit message should use imperative mood", + "suggest": "Use imperative mood" + }] + + mocker.patch( + "commit_check.commit.read_commit_msg", + return_value="feat: Adding new feature" + ) + + m_print_error_message = mocker.patch( + f"{LOCATION}.print_error_message" + ) + m_print_suggestion = mocker.patch( + f"{LOCATION}.print_suggestion" + ) + + retval = check_imperative(checks, MSG_FILE) + assert retval == FAIL + assert m_print_error_message.call_count == 1 + assert m_print_suggestion.call_count == 1 + + +@pytest.mark.benchmark +def test_check_imperative_skip_merge_commit(mocker): + """Test imperative mood check skips merge commits.""" + checks = [{ + "check": "imperative", + "regex": "", + "error": "Commit message should use imperative mood", + "suggest": "Use imperative mood" + }] + + mocker.patch( + "commit_check.commit.read_commit_msg", + return_value="Merge branch 'feature/test' into main" + ) + + retval = check_imperative(checks, MSG_FILE) + assert retval == PASS + + +@pytest.mark.benchmark +def test_check_imperative_different_check_type(mocker): + """Test imperative mood check skips different check types.""" + checks = [{ + "check": "message", + "regex": "dummy_regex" + }] + + m_read_commit_msg = mocker.patch( + "commit_check.commit.read_commit_msg", + return_value="feat: Added new feature" + ) + + retval = check_imperative(checks, MSG_FILE) + assert retval == PASS + assert m_read_commit_msg.call_count == 0 + + +@pytest.mark.benchmark +def test_check_imperative_no_commits(mocker): + """Test imperative mood check passes when there are no commits.""" + checks = [{ + "check": "imperative", + "regex": "", + "error": "Commit message should use imperative mood", + "suggest": "Use imperative mood" + }] + + mocker.patch("commit_check.commit.has_commits", return_value=False) + + retval = check_imperative(checks, MSG_FILE) + assert retval == PASS + + +@pytest.mark.benchmark +def test_check_imperative_empty_checks(mocker): + """Test imperative mood check with empty checks list.""" + checks = [] + + m_read_commit_msg = mocker.patch( + "commit_check.commit.read_commit_msg", + return_value="feat: Added new feature" + ) + + retval = check_imperative(checks, MSG_FILE) + assert retval == PASS + assert m_read_commit_msg.call_count == 0 + + +@pytest.mark.benchmark +def test_is_imperative_valid_cases(): + """Test _is_imperative function with valid imperative mood cases.""" + from commit_check.commit import _is_imperative + + valid_cases = [ + "Add new feature", + "Fix bug in authentication", + "Update documentation", + "Remove deprecated code", + "Refactor user service", + "Optimize database queries", + "Create new component", + "Delete unused files", + "Improve error handling", + "Enhance user experience", + "Implement new API", + "Configure CI/CD pipeline", + "Setup testing framework", + "Handle edge cases", + "Process user input", + "Validate form data", + "Transform data format", + "Initialize application", + "Load configuration", + "Save user preferences", + "", # Empty description should pass + ] + + for case in valid_cases: + assert _is_imperative(case), f"'{case}' should be imperative mood" + + +@pytest.mark.benchmark +def test_is_imperative_invalid_cases(): + """Test _is_imperative function with invalid imperative mood cases.""" + from commit_check.commit import _is_imperative + + invalid_cases = [ + "Added new feature", + "Fixed bug in authentication", + "Updated documentation", + "Removed deprecated code", + "Refactored user service", + "Optimized database queries", + "Created new component", + "Deleted unused files", + "Improved error handling", + "Enhanced user experience", + "Implemented new API", + "Adding new feature", + "Fixing bug in authentication", + "Updating documentation", + "Removing deprecated code", + "Refactoring user service", + "Optimizing database queries", + "Creating new component", + "Deleting unused files", + "Improving error handling", + "Enhancing user experience", + "Implementing new API", + "Adds new feature", + "Fixes bug in authentication", + "Updates documentation", + "Removes deprecated code", + "Refactors user service", + "Optimizes database queries", + "Creates new component", + "Deletes unused files", + "Improves error handling", + "Enhances user experience", + "Implements new API", + ] + + for case in invalid_cases: + assert not _is_imperative(case), f"'{case}' should not be imperative mood" diff --git a/tests/main_test.py b/tests/main_test.py index c232efd..4e277a6 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -8,20 +8,22 @@ class TestMain: @pytest.mark.benchmark - @pytest.mark.parametrize("argv, check_commit_call_count, check_branch_call_count, check_author_call_count, check_commit_signoff_call_count, check_merge_base_call_count", [ - ([CMD, "--message"], 1, 0, 0, 0, 0), - ([CMD, "--branch"], 0, 1, 0, 0, 0), - ([CMD, "--author-name"], 0, 0, 1, 0, 0), - ([CMD, "--author-email"], 0, 0, 1, 0, 0), - ([CMD, "--commit-signoff"], 0, 0, 0, 1, 0), - ([CMD, "--merge-base"], 0, 0, 0, 0, 1), - ([CMD, "--message", "--author-email"], 1, 0, 1, 0, 0), - ([CMD, "--branch", "--message"], 1, 1, 0, 0, 0), - ([CMD, "--author-name", "--author-email"], 0, 0, 2, 0, 0), - ([CMD, "--message", "--branch", "--author-email"], 1, 1, 1, 0, 0), - ([CMD, "--branch", "--message", "--author-name", "--author-email"], 1, 1, 2, 0, 0), - ([CMD, "--message", "--branch", "--author-name", "--author-email", "--commit-signoff", "--merge-base"], 1, 1, 2, 1, 1), - ([CMD, "--dry-run"], 0, 0, 0, 0, 0), + @pytest.mark.parametrize("argv, check_commit_call_count, check_branch_call_count, check_author_call_count, check_commit_signoff_call_count, check_merge_base_call_count, check_imperative_call_count", [ + ([CMD, "--message"], 1, 0, 0, 0, 0, 0), + ([CMD, "--branch"], 0, 1, 0, 0, 0, 0), + ([CMD, "--author-name"], 0, 0, 1, 0, 0, 0), + ([CMD, "--author-email"], 0, 0, 1, 0, 0, 0), + ([CMD, "--commit-signoff"], 0, 0, 0, 1, 0, 0), + ([CMD, "--merge-base"], 0, 0, 0, 0, 1, 0), + ([CMD, "--imperative"], 0, 0, 0, 0, 0, 1), + ([CMD, "--message", "--author-email"], 1, 0, 1, 0, 0, 0), + ([CMD, "--branch", "--message"], 1, 1, 0, 0, 0, 0), + ([CMD, "--author-name", "--author-email"], 0, 0, 2, 0, 0, 0), + ([CMD, "--message", "--branch", "--author-email"], 1, 1, 1, 0, 0, 0), + ([CMD, "--branch", "--message", "--author-name", "--author-email"], 1, 1, 2, 0, 0, 0), + ([CMD, "--message", "--branch", "--author-name", "--author-email", "--commit-signoff", "--merge-base"], 1, 1, 2, 1, 1, 0), + ([CMD, "--message", "--imperative"], 1, 0, 0, 0, 0, 1), + ([CMD, "--dry-run"], 0, 0, 0, 0, 0, 0), ]) def test_main( self, @@ -32,6 +34,7 @@ def test_main( check_author_call_count, check_commit_signoff_call_count, check_merge_base_call_count, + check_imperative_call_count, ): mocker.patch( "commit_check.main.validate_config", @@ -46,6 +49,7 @@ def test_main( m_check_author = mocker.patch("commit_check.author.check_author") m_check_commit_signoff = mocker.patch("commit_check.commit.check_commit_signoff") m_check_merge_base = mocker.patch("commit_check.branch.check_merge_base") + m_check_imperative = mocker.patch("commit_check.commit.check_imperative") sys.argv = argv main() assert m_check_commit.call_count == check_commit_call_count @@ -53,6 +57,7 @@ def test_main( assert m_check_author.call_count == check_author_call_count assert m_check_commit_signoff.call_count == check_commit_signoff_call_count assert m_check_merge_base.call_count == check_merge_base_call_count + assert m_check_imperative.call_count == check_imperative_call_count @pytest.mark.benchmark def test_main_help(self, mocker, capfd): @@ -168,6 +173,7 @@ def test_main_multiple_checks( mocker.patch( "commit_check.branch.check_merge_base", return_value=merge_base_result ) + mocker.patch("commit_check.commit.check_imperative", return_value=PASS) # this is messy. why isn't this a private implementation detail with a # public check_author_name and check_author email? From 952b91fe468859833ce652218390408c210b3875 Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Sun, 13 Jul 2025 00:35:06 +0300 Subject: [PATCH 21/22] docs: add example for failed imperative mood checks (#259) docs: update README.md --- README.rst | 46 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/README.rst b/README.rst index 35a68e1..bfda1a0 100644 --- a/README.rst +++ b/README.rst @@ -157,14 +157,14 @@ Check Branch Naming Failed Commit rejected by Commit-Check. - (c).-.(c) (c).-.(c) (c).-.(c) (c).-.(c) (c).-.(c) - / ._. \ / ._. \ / ._. \ / ._. \ / ._. \ - __\( C )/__ __\( H )/__ __\( E )/__ __\( C )/__ __\( K )/__ + (c).-.(c) (c).-.(c) (c).-.(c) (c).-.(c) (c).-.(c) + / ._. \ / ._. \ / ._. \ / ._. \ / ._. \ + __\( C )/__ __\( H )/__ __\( E )/__ __\( C )/__ __\( K )/__ (_.-/'-'\-._)(_.-/'-'\-._)(_.-/'-'\-._)(_.-/'-'\-._)(_.-/'-'\-._) - || E || || R || || R || || O || || R || - _.' '-' '._ _.' '-' '._ _.' '-' '._ _.' '-' '._ _.' '-' '._ + || E || || R || || R || || O || || R || + _.' '-' '._ _.' '-' '._ _.' '-' '._ _.' '-' '._ _.' '-' '._ (.-./`-´\.-.)(.-./`-´\.-.)(.-./`-´\.-.)(.-./`-´\.-.)(.-./`-´\.-.) - `-´ `-´ `-´ `-´ `-´ `-´ `-´ `-´ `-´ `-´ + `-´ `-´ `-´ `-´ `-´ `-´ `-´ `-´ `-´ `-´ Commit rejected. @@ -180,14 +180,14 @@ Check Commit Signature Failed Commit rejected by Commit-Check. - (c).-.(c) (c).-.(c) (c).-.(c) (c).-.(c) (c).-.(c) - / ._. \ / ._. \ / ._. \ / ._. \ / ._. \ - __\( C )/__ __\( H )/__ __\( E )/__ __\( C )/__ __\( K )/__ + (c).-.(c) (c).-.(c) (c).-.(c) (c).-.(c) (c).-.(c) + / ._. \ / ._. \ / ._. \ / ._. \ / ._. \ + __\( C )/__ __\( H )/__ __\( E )/__ __\( C )/__ __\( K )/__ (_.-/'-'\-._)(_.-/'-'\-._)(_.-/'-'\-._)(_.-/'-'\-._)(_.-/'-'\-._) - || E || || R || || R || || O || || R || - _.' '-' '._ _.' '-' '._ _.' '-' '._ _.' '-' '._ _.' '-' '._ + || E || || R || || R || || O || || R || + _.' '-' '._ _.' '-' '._ _.' '-' '._ _.' '-' '._ _.' '-' '._ (.-./`-´\.-.)(.-./`-´\.-.)(.-./`-´\.-.)(.-./`-´\.-.)(.-./`-´\.-.) - `-´ `-´ `-´ `-´ `-´ `-´ `-´ `-´ `-´ `-´ + `-´ `-´ `-´ `-´ `-´ `-´ `-´ `-´ `-´ `-´ Commit rejected. @@ -197,6 +197,28 @@ Check Commit Signature Failed Suggest: run command `git commit -m "conventional commit message" --signoff` +Check Imperative Mood Failed + +.. code-block:: text + + Commit rejected by Commit-Check. + + (c).-.(c) (c).-.(c) (c).-.(c) (c).-.(c) (c).-.(c) + / ._. \ / ._. \ / ._. \ / ._. \ / ._. \ + __\( C )/__ __\( H )/__ __\( E )/__ __\( C )/__ __\( K )/__ + (_.-/'-'\-._)(_.-/'-'\-._)(_.-/'-'\-._)(_.-/'-'\-._)(_.-/'-'\-._) + || E || || R || || R || || O || || R || + _.' '-' '._ _.' '-' '._ _.' '-' '._ _.' '-' '._ _.' '-' '._ + (.-./`-´\.-.)(.-./`-´\.-.)(.-./`-´\.-.)(.-./`-´\.-.)(.-./`-´\.-.) + `-´ `-´ `-´ `-´ `-´ `-´ `-´ `-´ `-´ `-´ + + Commit rejected. + + Type imperative check failed => Added file + It doesn't match regex: imperative mood pattern + Commit message should use imperative mood (e.g., "Add feature" not "Added feature") + Suggest: Use imperative mood in commit message like "Add", "Fix", "Update", "Remove" + Badging your repository ----------------------- From 24912964ba2bab31f28bdd4c76e469da74e51acb Mon Sep 17 00:00:00 2001 From: Randolph Sapp Date: Mon, 14 Jul 2025 15:33:45 -0500 Subject: [PATCH 22/22] fix(message): protect the message regex check (#260) --- commit_check/commit.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/commit_check/commit.py b/commit_check/commit.py index 05a08e3..fccc5da 100644 --- a/commit_check/commit.py +++ b/commit_check/commit.py @@ -38,13 +38,13 @@ def check_commit_msg(checks: list, commit_msg_file: str = "") -> int: commit_msg = read_commit_msg(commit_msg_file) for check in checks: - if check['regex'] == "": - print( - f"{YELLOW}Not found regex for commit message. skip checking.{RESET_COLOR}", - ) - return PASS - if check['check'] == 'message': + if check['regex'] == "": + print( + f"{YELLOW}Not found regex for commit message. skip checking.{RESET_COLOR}", + ) + return PASS + result = re.match(check['regex'], commit_msg) if result is None: if not print_error_header.has_been_called: pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy