From 82612aa8995e3bde0564fcd57bbe48380c1b9d7b Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Thu, 14 Nov 2024 20:57:14 +0200 Subject: [PATCH 01/45] chore: update .pre-commit-config.yaml (#198) --- .pre-commit-config.yaml | 4 ++-- README.rst | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d65d917..101d2ee 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -32,11 +32,11 @@ repos: hooks: - id: codespell - repo: https://github.com/commit-check/commit-check - rev: v0.9.1 + rev: v0.9.2 hooks: - id: check-message # - 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 # uncomment if you need. + # - id: check-merge-base # requires download all git history diff --git a/README.rst b/README.rst index 09d71eb..465ab34 100644 --- a/README.rst +++ b/README.rst @@ -70,12 +70,12 @@ Running as pre-commit hook - repo: https://github.com/commit-check/commit-check rev: the tag or revision hooks: # support hooks - - id: check-message # it requires hook prepare-commit-msg + - id: check-message # requires hook prepare-commit-msg - id: check-branch - id: check-author-name - id: check-author-email - id: check-commit-signoff - - id: check-merge-base + - id: check-merge-base # requires download all git history Running as CLI ~~~~~~~~~~~~~~ From 1d194eb0561d11693f50564c1b8bcde3c07a9afa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Nov 2024 09:19:37 +0200 Subject: [PATCH 02/45] chore(deps): bump codecov/codecov-action from 4.6.0 to 5.0.2 (#199) --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3737c30..c7d0c3a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -49,7 +49,7 @@ jobs: run: | coverage run --source commit_check -m pytest coverage report && coverage xml - - uses: codecov/codecov-action@v4.6.0 + - uses: codecov/codecov-action@v5.0.2 with: token: ${{ secrets.CODECOV_TOKEN }} files: ./coverage.xml From 7de6ac2f4884f77ff6c9a0308414b184dd6046c2 Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Tue, 26 Nov 2024 16:59:50 +0200 Subject: [PATCH 03/45] feat: support nox and update readme (#202) * fix: update noxfile.py to fix Sessions defined in /workspace/commit-check/noxfile.py: * lint - test-hook - build - install-wheel - commit-check - coverage - docs sessions marked with * are selected, sessions marked with - are skipped. * feat: add docs-live * docs: update to add example * docs: update to add example * feat: use nox commands in pipeline * feat: use nox commands in pipeline * fix: remove cd to docs step * fix: get branch name to fix ci failure * fix: update noxfile.py * fix: add build session back * fix: install then run test * fix: install nox for test --- .github/workflows/main.yml | 32 +++++++++------------ README.rst | 59 +++++++++++++++++++++++++++----------- noxfile.py | 25 +++++++++------- requirements-dev.txt | 2 +- 4 files changed, 70 insertions(+), 48 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c7d0c3a..0a5169e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -21,34 +21,26 @@ jobs: - uses: actions/setup-python@v5 with: python-version: '3.x' - - - name: Install dependencies - run: | - pip install -r requirements-dev.txt - pip install -e . + - run: pip install -r requirements-dev.txt - name: Run pre-commit run: | - pre-commit run --all-files - pre-commit try-repo . + nox -s lint + nox -s test-hook - name: Build wheel - run: python3 -m pip wheel --no-deps -w dist . + run: nox -s build - name: Upload wheel as artifact uses: actions/upload-artifact@v4 with: name: commit-check_wheel path: ${{ github.workspace }}/dist/*.whl - name: Run commit-check - run: | - python3 -m pip install dist/*.whl - commit-check -h - commit-check --message --branch --author-email + run: nox -s commit-check - name: Collect Coverage - run: | - coverage run --source commit_check -m pytest - coverage report && coverage xml + run: nox -s coverage + - uses: codecov/codecov-action@v5.0.2 with: token: ${{ secrets.CODECOV_TOKEN }} @@ -69,6 +61,7 @@ jobs: - uses: actions/setup-python@v5 with: python-version: ${{ matrix.py }} + - run: pip install -r requirements-dev.txt - name: Download wheel artifact uses: actions/download-artifact@v4 @@ -79,20 +72,21 @@ jobs: - name: Install test # using a wildcard as filename on Windows requires a bash shell shell: bash - run: python3 -m pip install dist/*.whl + run: nox -s install-wheel docs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} # get current branch name - uses: actions/setup-python@v5 with: python-version: "3.10" - - run: python -m pip install . -r docs/requirements.txt + - run: pip install -r requirements-dev.txt - name: Build docs - working-directory: docs - run: sphinx-build -E -W -b html . _build/html + run: nox -s docs - name: Save built docs as artifact uses: actions/upload-artifact@v4 diff --git a/README.rst b/README.rst index 465ab34..2f19257 100644 --- a/README.rst +++ b/README.rst @@ -30,9 +30,11 @@ Commit Check Overview -------- -**Commit Check** is a free, powerful tool that enforces commit metadata standards, including commit message, branch naming, committer name/email, and commit signoff. Fully customizable with error messages and suggested commands, it ensures compliance across teams. +**Commit Check** is a free, powerful tool that enforces commit metadata standards, including commit message, branch naming, committer name/email, commit signoff and more. -As an alternative to GitHub Enterprise `Metadata restrictions `_ and Bitbucket's paid puglin `Yet Another Commit Checker `_, Commit Check stands out by integrating DevOps principles and Infrastructure as Code (IaC). +Fully customizable with error messages and suggested commands, it ensures compliance across teams. + +As an alternative to GitHub Enterprise `Metadata restrictions `_ and Bitbucket's paid plugin `Yet Another Commit Checker `_, Commit Check stands out by integrating DevOps principles and Infrastructure as Code (IaC). Configuration ------------- @@ -70,7 +72,7 @@ Running as pre-commit hook - repo: https://github.com/commit-check/commit-check rev: the tag or revision hooks: # support hooks - - id: check-message # requires hook prepare-commit-msg + - id: check-message # requires prepare-commit-msg hook - id: check-branch - id: check-author-name - id: check-author-email @@ -80,25 +82,25 @@ Running as pre-commit hook Running as CLI ~~~~~~~~~~~~~~ -Global Installation +Install globally .. code-block:: bash sudo pip3 install -U commit-check -User Installation +Install locally .. code-block:: bash pip install -U commit-check -Install from Git Repo +Install from source code .. code-block:: bash pip install git+https://github.com/commit-check/commit-check.git@main -Then, run ``commit-check`` from the command line. For more information, see the `docs `_. +Then, run ``commit-check --help`` from the command line. For more information, see the `docs `_. Running as Git Hooks ~~~~~~~~~~~~~~~~~~~~ @@ -118,8 +120,8 @@ Save the script file as ``pre-push`` and make it executable: Now, ``git push`` will trigger this hook automatically. -Example -------- +Examples +-------- Check Commit Message Failed @@ -155,24 +157,47 @@ 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. - Type branch check failed => my-test-branch + Type branch check failed => patch-1 It doesn't match regex: ^(bugfix|feature|release|hotfix|task|chore)\/.+|(master)|(main)|(HEAD)|(PR-.+) - Branches must begin with these types: bugfix/ feature/ release/ hotfix/ task/ chore/ Suggest: run command `git checkout -b type/branch_name` +Check Commit Signature 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 commit_signoff check failed => c92ce259ff041c91859c7fb61afdbb391e769d0f + It doesn't match regex: Signed-off-by:.*[A-Za-z0-9]\s+<.+@.+> + Signed-off-by not found in latest commit + Suggest: run command `git commit -m "conventional commit message" --signoff` + + + Badging your repository ----------------------- diff --git a/noxfile.py b/noxfile.py index 3600177..109e2df 100644 --- a/noxfile.py +++ b/noxfile.py @@ -2,7 +2,6 @@ import glob nox.options.reuse_existing_virtualenvs = True -nox.options.reuse_venv = True nox.options.sessions = ["lint"] REQUIREMENTS = { @@ -42,23 +41,20 @@ def build(session): @nox.session(name="install-wheel", requires=["build"]) def install_wheel(session): + session.run("python3", "-m", "pip", "wheel", "--no-deps", "-w", "dist", ".") whl_file = glob.glob("dist/*.whl") session.install(str(whl_file[0])) -# @nox.session(name="commit-check", requires=["install-wheel"]) -@nox.session(name="commit-check", requires=["install-wheel"]) +@nox.session(name="commit-check") def commit_check(session): - session.run( - "commit-check", - "--message", - "--branch", - "--author-email", - ) + session.install(".") + session.run("commit-check", "--message", "--branch", "--author-email") -@nox.session(requires=["install-wheel"]) +@nox.session() def coverage(session): + session.install(".") session.run("coverage", "run", "--source", "commit_check", "-m", "pytest") session.run("coverage", "report") session.run("coverage", "xml") @@ -66,6 +62,13 @@ def coverage(session): @nox.session() def docs(session): - session.install("-e", ".") + session.install(".") session.install("-r", REQUIREMENTS["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.run("sphinx-autobuild", "-b", "html", "docs", "_build/html") diff --git a/requirements-dev.txt b/requirements-dev.txt index cb4cde7..120645f 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,5 +1,5 @@ coverage -nox +git+https://github.com/wntrblm/nox.git@main pre-commit pytest pytest-mock From 369931a62fc3f0fe500ad7b19101878a641d0c17 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Dec 2024 12:37:08 +0200 Subject: [PATCH 04/45] chore(deps): bump actions/attest-build-provenance from 1 to 2 (#204) --- .github/workflows/publish-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish-package.yml b/.github/workflows/publish-package.yml index 77c5179..f9ce961 100644 --- a/.github/workflows/publish-package.yml +++ b/.github/workflows/publish-package.yml @@ -33,7 +33,7 @@ jobs: twine check dist/commit_check* - name: Create attestations - uses: actions/attest-build-provenance@v1 + uses: actions/attest-build-provenance@v2 with: subject-path: "dist/commit_check*" From b1cd769eada1ab64c01608a22d9082094013be63 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 29 Dec 2024 02:50:32 -0500 Subject: [PATCH 05/45] ci: pre-commit autoupdate (#206) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 101d2ee..9d278c3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,7 +22,7 @@ repos: args: [--max-line-length=100, --ignore=E501] exclude: ^commit_check/__init__.py - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.13.0 + rev: v1.14.0 hooks: - id: mypy additional_dependencies: [types-PyYAML] From c8602e5a88663264d90d22770ff38707ec724d84 Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Thu, 2 Jan 2025 07:17:58 -0500 Subject: [PATCH 06/45] fix: update publish_dir to fix online docs (#207) --- .github/workflows/main.yml | 8 +++++--- .gitpod.yml | 2 +- pyproject.toml | 1 + 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0a5169e..b190f6a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -54,14 +54,16 @@ jobs: fail-fast: false matrix: py: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] - os: ['windows-latest', 'ubuntu-latest', 'macos-latest'] + os: ['windows-latest', 'ubuntu-24.04', 'macos-latest'] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: ${{ matrix.py }} - - run: pip install -r requirements-dev.txt + - run: | + pip install --upgrade pip + pip install -r requirements-dev.txt - name: Download wheel artifact uses: actions/download-artifact@v4 @@ -100,4 +102,4 @@ jobs: uses: peaceiris/actions-gh-pages@v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./docs/_build/html + publish_dir: ./_build/html diff --git a/.gitpod.yml b/.gitpod.yml index e6de223..8123636 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 -r requirements-dev.txt + - before: pip install --upgrade pip && pip install -r requirements-dev.txt init: pre-commit install command: pip install -e . diff --git a/pyproject.toml b/pyproject.toml index 1e028b4..2710ff6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,6 +11,7 @@ license = {text = "MIT License"} authors = [ { name = "Xianpeng Shen", email = "xianpeng.shen@gmail.com" }, ] +requires-python = ">=3.8" dependencies = ["pyyaml"] classifiers = [ # https://pypi.org/pypi?%3Aaction=list_classifiers From 8593c6bfdee08090e3b358c40f3448af1dc11618 Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Thu, 2 Jan 2025 07:28:47 -0500 Subject: [PATCH 07/45] fixup: update path to fix online docs (#208) * fix: update publish_dir to fix online docs * fixup! fix: update publish_dir to fix online docs * chore: update ubuntu-latest to ubuntu-24.04 --- .github/workflows/main.yml | 6 +++--- .github/workflows/publish-image.yml | 2 +- .github/workflows/publish-package.yml | 2 +- docs/README.rst | 8 +++----- 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b190f6a..9106468 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,7 +15,7 @@ on: jobs: build: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 @@ -77,7 +77,7 @@ jobs: run: nox -s install-wheel docs: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 with: @@ -94,7 +94,7 @@ jobs: uses: actions/upload-artifact@v4 with: name: "commit-check_docs" - path: ${{ github.workspace }}/docs/_build/html + path: ${{ github.workspace }}/_build/html - name: Upload docs to github pages # only publish doc changes from main branch diff --git a/.github/workflows/publish-image.yml b/.github/workflows/publish-image.yml index 53649bc..39dd16e 100644 --- a/.github/workflows/publish-image.yml +++ b/.github/workflows/publish-image.yml @@ -13,7 +13,7 @@ on: jobs: publish: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - name: Build to check Dockerfile diff --git a/.github/workflows/publish-package.yml b/.github/workflows/publish-package.yml index f9ce961..9dcac79 100644 --- a/.github/workflows/publish-package.yml +++ b/.github/workflows/publish-package.yml @@ -12,7 +12,7 @@ permissions: jobs: publish: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 # use fetch --all for setuptools_scm to work diff --git a/docs/README.rst b/docs/README.rst index cdb46e5..d296410 100644 --- a/docs/README.rst +++ b/docs/README.rst @@ -9,15 +9,13 @@ From the root directory of the repository, do the following to steps .. code-block:: text - pip install -r docs/requirements.txt - - On Linux, you may need to use ``pip3`` instead. + pip install nox 2. Build the docs .. code-block:: text - sphinx-build docs docs/_build/html + nox -s docs - Browse the files in docs/_build/html with your internet browser to see the rendered + Browse the files in /_build/html with your internet browser to see the rendered output. From 91fb4bb67fbaa263b013a55b36403380fce83ab8 Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Tue, 7 Jan 2025 03:55:01 -0500 Subject: [PATCH 08/45] feat: Create labeler.yml (#210) --- .github/workflows/labeler.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .github/workflows/labeler.yml diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml new file mode 100644 index 0000000..52689d6 --- /dev/null +++ b/.github/workflows/labeler.yml @@ -0,0 +1,10 @@ +name: PR Autolabeler + +on: + # pull_request event is required for autolabeler + pull_request: + types: [opened, reopened, synchronize] + +jobs: + draft-release: + uses: commit-check/.github/.github/workflows/release-drafter.yml@main From 0032ba0408c74d47436f06ab456f80812b5f7b1b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 27 Jan 2025 20:16:06 +0200 Subject: [PATCH 09/45] ci: pre-commit autoupdate (#209) --- .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 9d278c3..e3fed27 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,13 +22,13 @@ repos: args: [--max-line-length=100, --ignore=E501] exclude: ^commit_check/__init__.py - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.14.0 + rev: v1.14.1 hooks: - id: mypy additional_dependencies: [types-PyYAML] exclude: ^testing/resources/ - repo: https://github.com/codespell-project/codespell - rev: v2.3.0 + rev: v2.4.0 hooks: - id: codespell - repo: https://github.com/commit-check/commit-check From 91ee12b3f5045523cbdacd07390c0ce98f3b8231 Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Tue, 28 Jan 2025 12:42:05 +0200 Subject: [PATCH 10/45] add more tests and update pre-commit hook to ruff (#211) * add test and update hook to ruff * feat: add more test * fix: update commit_test.py --- .pre-commit-config.yaml | 11 ++++++----- commit_check/branch.py | 4 ++-- commit_check/commit.py | 4 ++-- noxfile.py | 1 + tests/branch_test.py | 11 +++++++++++ tests/commit_test.py | 19 +++++++++++++++++++ tests/error_test.py | 26 ++++++++++++++++++++++++++ 7 files changed, 67 insertions(+), 9 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e3fed27..e1d96d8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,12 +15,13 @@ repos: - id: trailing-whitespace - id: name-tests-test - id: requirements-txt-fixer -- repo: https://github.com/PyCQA/flake8 - rev: 7.1.1 +- repo: https://github.com/astral-sh/ruff-pre-commit + # Ruff version. + rev: v0.9.3 hooks: - - id: flake8 - args: [--max-line-length=100, --ignore=E501] - exclude: ^commit_check/__init__.py + # Run the linter. + - id: ruff + args: [ --fix ] - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.14.1 hooks: diff --git a/commit_check/branch.py b/commit_check/branch.py index 39bc7ec..1cc8a35 100644 --- a/commit_check/branch.py +++ b/commit_check/branch.py @@ -16,7 +16,7 @@ def check_branch(checks: list) -> int: result = re.match(check['regex'], branch_name) if result is None: if not print_error_header.has_been_called: - print_error_header() + print_error_header() # pragma: no cover print_error_message( check['check'], check['regex'], check['error'], branch_name, @@ -45,7 +45,7 @@ def check_merge_base(checks: list) -> int: result = git_merge_base(target_branch, current_branch) if result != 0: if not print_error_header.has_been_called: - print_error_header() + print_error_header() # pragma: no cover print_error_message( check['check'], check['regex'], check['error'], current_branch, diff --git a/commit_check/commit.py b/commit_check/commit.py index 5505e77..60b8d2f 100644 --- a/commit_check/commit.py +++ b/commit_check/commit.py @@ -38,7 +38,7 @@ def check_commit_msg(checks: list, commit_msg_file: str = "") -> int: result = re.match(check['regex'], commit_msg) if result is None: if not print_error_header.has_been_called: - print_error_header() + print_error_header() # pragma: no cover print_error_message( check['check'], check['regex'], check['error'], commit_msg, @@ -67,7 +67,7 @@ def check_commit_signoff(checks: list, commit_msg_file: str = "") -> int: result = re.search(check['regex'], commit_msg) if result is None: if not print_error_header.has_been_called: - print_error_header() + print_error_header() # pragma: no cover print_error_message( check['check'], check['regex'], check['error'], commit_hash, diff --git a/noxfile.py b/noxfile.py index 109e2df..079df8a 100644 --- a/noxfile.py +++ b/noxfile.py @@ -55,6 +55,7 @@ def commit_check(session): @nox.session() def coverage(session): session.install(".") + session.install("-r", REQUIREMENTS["dev"]) session.run("coverage", "run", "--source", "commit_check", "-m", "pytest") session.run("coverage", "report") session.run("coverage", "xml") diff --git a/tests/branch_test.py b/tests/branch_test.py index 64ec3a4..083c5bd 100644 --- a/tests/branch_test.py +++ b/tests/branch_test.py @@ -122,6 +122,17 @@ def test_check_merge_base_with_empty_checks(self, mocker): assert retval == PASS assert m_check_merge.call_count == 0 + + def test_check_merge_base_with_empty_regex(self, mocker): + checks = [{ + "check": "merge_base", + "regex": "" + }] + m_check_merge = mocker.patch(f"{LOCATION}.check_merge_base") + retval = check_merge_base(checks) + assert retval == PASS + assert m_check_merge.call_count == 0 + def test_check_merge_base_with_different_check(self, mocker): checks = [{ "check": "branch", diff --git a/tests/commit_test.py b/tests/commit_test.py index 5642a52..a458636 100644 --- a/tests/commit_test.py +++ b/tests/commit_test.py @@ -30,6 +30,25 @@ def test_read_commit_msg_file_not_found(mocker): assert m_commits_info.call_count == 0 +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", + return_value=".git/COMMIT_EDITMSG" + ) + mock_read_commit_msg = mocker.patch( + "commit_check.commit.read_commit_msg", + return_value="Sample commit message" + ) + + checks = [{"regex": ".*", "check": "message", "error": "Invalid", "suggest": None}] + + result = check_commit_msg(checks, commit_msg_file="") + + mock_get_default_commit_msg_file.assert_called_once() + mock_read_commit_msg.assert_called_once_with(".git/COMMIT_EDITMSG") + assert result == 0 + + def test_check_commit_with_empty_checks(mocker): checks = [] m_re_match = mocker.patch( diff --git a/tests/error_test.py b/tests/error_test.py index 188a2b1..d0a2880 100644 --- a/tests/error_test.py +++ b/tests/error_test.py @@ -24,6 +24,32 @@ def test_error_handler_unexpected_error(): assert exit_info.value.code == 3 +def test_error_handler_cannot_access(mocker): + with pytest.raises(SystemExit): + store_dir = "/fake/commit-check" + log_path = os.path.join(store_dir, "commit-check.log") + mocker.patch.dict(os.environ, {"COMMIT_CHECK_HOME": store_dir}) + mock_os_access = mocker.patch("os.access", return_value=False) + mocker.patch("os.path.exists", return_value=True) + mocker.patch("os.makedirs") + mock_open = mocker.patch("builtins.open", mocker.mock_open()) + mocker.patch("commit_check.util.cmd_output", return_value="mock_version") + mocker.patch("sys.version", "Mock Python Version") + mocker.patch("sys.executable", "/mock/path/to/python") + + from commit_check.error import log_and_exit + log_and_exit( + msg="Test error message", + ret_code=1, + exc=ValueError("Test exception"), + formatted="Mocked formatted stack trace" + ) + + mock_os_access.assert_called_once_with(store_dir, os.W_OK) + mock_open.assert_called_with(log_path, "a") + mock_open().write.assert_any_call(f"Failed to write to log at {log_path}\n") + + @pytest.mark.xfail def test_log_and_exit(monkeypatch): monkeypatch.setenv("COMMIT_CHECK_HOME", "") From d9a8729bf047bf2a3d5e1ec90c2cc204aea44bac Mon Sep 17 00:00:00 2001 From: Bea McDonnell Date: Mon, 3 Feb 2025 00:54:54 -0800 Subject: [PATCH 11/45] fix: ensure all checks pass without overwriting previous failures (#213) * test: test multiple checks with different results * fix: ensure all checks pass without overwriting previous failures Passing checks could overwrite a previous failing check and result in returning with an exit code of 0 when a check failed * style: match formatting to other tests * style: revert automatic formatting * fix: add missing imports * chore: ignore missing return All code paths are covered here, checked by the assert * refactor: remove duplicate mock --- commit_check/main.py | 21 ++++++++------- tests/main_test.py | 64 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 74 insertions(+), 11 deletions(-) diff --git a/commit_check/main.py b/commit_check/main.py index 50b2350..c6b8b32 100644 --- a/commit_check/main.py +++ b/commit_check/main.py @@ -10,7 +10,7 @@ from commit_check import author from commit_check.util import validate_config from commit_check.error import error_handler -from . import CONFIG_FILE, DEFAULT_CONFIG, PASS, __version__ +from . import CONFIG_FILE, DEFAULT_CONFIG, PASS, FAIL, __version__ def get_parser() -> argparse.ArgumentParser: @@ -99,7 +99,7 @@ def main() -> int: """The main entrypoint of commit-check program.""" parser = get_parser() args = parser.parse_args() - retval = PASS + check_results: list[int] = [] with error_handler(): config = validate_config(args.config) if validate_config( @@ -107,21 +107,22 @@ def main() -> int: ) else DEFAULT_CONFIG checks = config['checks'] if args.message: - retval = commit.check_commit_msg(checks, args.commit_msg_file) + check_results.append(commit.check_commit_msg(checks, args.commit_msg_file)) if args.author_name: - retval = author.check_author(checks, "author_name") + check_results.append(author.check_author(checks, "author_name")) if args.author_email: - retval = author.check_author(checks, "author_email") + check_results.append(author.check_author(checks, "author_email")) if args.branch: - retval = branch.check_branch(checks) + check_results.append(branch.check_branch(checks)) if args.commit_signoff: - retval = commit.check_commit_signoff(checks) + check_results.append(commit.check_commit_signoff(checks)) if args.merge_base: - retval = branch.check_merge_base(checks) + check_results.append(branch.check_merge_base(checks)) if args.dry_run: - retval = PASS - return retval + return PASS + + return PASS if all(val == PASS for val in check_results) else FAIL if __name__ == '__main__': diff --git a/tests/main_test.py b/tests/main_test.py index 9d26ebe..ca77032 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -1,7 +1,7 @@ import sys import pytest from commit_check.main import main -from commit_check import DEFAULT_CONFIG +from commit_check import DEFAULT_CONFIG, PASS, FAIL CMD = "commit-check" @@ -115,3 +115,65 @@ def test_main_validate_config_ret_none(self, mocker): main() assert m_check_commit.call_count == 1 assert m_check_commit.call_args[0][0] == DEFAULT_CONFIG["checks"] + + @pytest.mark.parametrize( + "argv, message_result, branch_result, author_name_result, author_email_result, commit_signoff_result, merge_base_result, final_result", + [ + ([CMD, "--message"], PASS, PASS, PASS, PASS, PASS, PASS, PASS), + ([CMD, "--message"], FAIL, PASS, PASS, PASS, PASS, PASS, FAIL), + ([CMD, "--message", "--commit-signoff"], FAIL, PASS, PASS, PASS, PASS, PASS, FAIL,), + ([CMD, "--message", "--commit-signoff"], PASS, PASS, PASS, PASS, FAIL, PASS, FAIL,), + ([CMD, "--message", "--author-name", "--author-email"], PASS, PASS, PASS, PASS, PASS, PASS, PASS,), + ([CMD, "--message", "--author-name", "--author-email"], FAIL, PASS, PASS, PASS, PASS, PASS, FAIL,), + ([CMD, "--message", "--author-name", "--author-email"], PASS, PASS, FAIL, PASS, PASS, PASS, FAIL,), + ([CMD, "--message", "--author-name", "--author-email"], PASS, PASS, PASS, FAIL, PASS, PASS, FAIL,), + ([CMD, "--message", "--author-name", "--author-email"], PASS, PASS, FAIL, FAIL, PASS, PASS, FAIL,), + ([CMD, "--message", "--branch", "--author-name", "--author-email", "--commit-signoff", "--merge-base", ], PASS, PASS, PASS, PASS, PASS, PASS, PASS,), + ([CMD, "--message", "--branch", "--author-name", "--author-email", "--commit-signoff", "--merge-base", ], FAIL, FAIL, FAIL, FAIL, FAIL, FAIL, FAIL,), + ([CMD, "--message", "--branch", "--author-name", "--author-email", "--commit-signoff", "--merge-base", ], FAIL, PASS, PASS, PASS, PASS, PASS, FAIL,), + ([CMD, "--dry-run"], FAIL, FAIL, FAIL, FAIL, FAIL, FAIL, PASS), + ], + ) + def test_main_multiple_checks( + self, + mocker, + argv, + message_result, + branch_result, + author_name_result, + author_email_result, + commit_signoff_result, + merge_base_result, + final_result, + ): + mocker.patch( + "commit_check.main.validate_config", + return_value={}, + ) + + mocker.patch( + "commit_check.commit.check_commit_msg", return_value=message_result + ) + mocker.patch( + "commit_check.commit.check_commit_signoff", + return_value=commit_signoff_result, + ) + + mocker.patch("commit_check.branch.check_branch", return_value=branch_result) + mocker.patch( + "commit_check.branch.check_merge_base", return_value=merge_base_result + ) + + # this is messy. why isn't this a private implementation detail with a + # public check_author_name and check_author email? + def author_side_effect(_, check_type: str) -> int: # type: ignore[return] + assert check_type in ("author_name", "author_email") + if check_type == "author_name": + return author_name_result + elif check_type == "author_email": + return author_email_result + + mocker.patch("commit_check.author.check_author", side_effect=author_side_effect) + + sys.argv = argv + assert main() == final_result From 99bcd12c1efe36b3fa7659fce797d8910ce963c2 Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Mon, 3 Feb 2025 11:06:08 +0200 Subject: [PATCH 12/45] refactor: consider early return optimization for dry-run (#214) --- commit_check/main.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/commit_check/main.py b/commit_check/main.py index c6b8b32..6faa070 100644 --- a/commit_check/main.py +++ b/commit_check/main.py @@ -99,6 +99,10 @@ def main() -> int: """The main entrypoint of commit-check program.""" parser = get_parser() args = parser.parse_args() + + if args.dry_run: + return PASS + check_results: list[int] = [] with error_handler(): @@ -119,9 +123,6 @@ def main() -> int: if args.merge_base: check_results.append(branch.check_merge_base(checks)) - if args.dry_run: - return PASS - return PASS if all(val == PASS for val in check_results) else FAIL From 346cde88c30f49afadbd0464fa727188dd862fcb Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 4 Feb 2025 03:20:55 +0200 Subject: [PATCH 13/45] ci: pre-commit autoupdate (#215) --- .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 e1d96d8..3faaa2b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: - id: requirements-txt-fixer - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.9.3 + rev: v0.9.4 hooks: # Run the linter. - id: ruff @@ -29,11 +29,11 @@ repos: additional_dependencies: [types-PyYAML] exclude: ^testing/resources/ - repo: https://github.com/codespell-project/codespell - rev: v2.4.0 + rev: v2.4.1 hooks: - id: codespell - repo: https://github.com/commit-check/commit-check - rev: v0.9.2 + rev: v0.9.3 hooks: - id: check-message # - id: check-branch # uncomment if you need. From 8e6590d0ba11d8e057f177445380b9ca6ac27c36 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 11 Feb 2025 01:13:35 +0200 Subject: [PATCH 14/45] ci: pre-commit autoupdate (#216) --- .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 3faaa2b..2953fc4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,13 +17,13 @@ repos: - id: requirements-txt-fixer - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.9.4 + rev: v0.9.6 hooks: # Run the linter. - id: ruff args: [ --fix ] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.14.1 + rev: v1.15.0 hooks: - id: mypy additional_dependencies: [types-PyYAML] From 224970d124fac4997565de732ef0d8e21e208dcc Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Sun, 16 Feb 2025 21:00:19 +0800 Subject: [PATCH 15/45] feat: add .readthedocs.yaml (#218) * feat: add .readthedocs.yaml * ci: auto fixes from pre-commit.com hooks --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .readthedocs.yaml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .readthedocs.yaml diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..9bfb5c2 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,17 @@ +# 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 From 41e0244a12031740d786af555b780e39c248d5b0 Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Sun, 16 Feb 2025 21:18:16 +0800 Subject: [PATCH 16/45] fix: update docs requirements.txt (#219) * fix: Update requirements.txt * ci: auto fixes from pre-commit.com hooks --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- docs/requirements.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/requirements.txt b/docs/requirements.txt index d078f24..af718d9 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1 +1,3 @@ + +. sphinx-immaterial From a237cda9cd94d40cb8caee59b7aa878e4589b5c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Koh=C3=A1nyi=20R=C3=B3bert?= Date: Sun, 23 Feb 2025 18:00:11 +0100 Subject: [PATCH 17/45] fix: allow accented letters in author name (#221) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: allow accented letters in author name Allow accented letters (e.g. áéó, etc.) in an author's name. * fix: remove duplicate unicode regex range * fix: add broader regex range to .yml as well * fix: remove duplicate regex key --- .commit-check.yml | 2 +- commit_check/__init__.py | 2 +- tests/author_test.py | 20 ++++++++++++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/.commit-check.yml b/.commit-check.yml index 10f8d39..ad339d0 100644 --- a/.commit-check.yml +++ b/.commit-check.yml @@ -14,7 +14,7 @@ checks: suggest: run command `git checkout -b type/branch_name` - check: author_name - regex: ^[A-Za-z ,.\'-]+$|.*(\[bot]) + regex: ^[A-Za-zÀ-ÖØ-öø-ÿ\u0100-\u017F\u0180-\u024F ,.\'-]+$|.*(\[bot]) error: The committer name seems invalid suggest: run command `git config user.name "Your Name"` diff --git a/commit_check/__init__.py b/commit_check/__init__.py index 61de524..521f68f 100644 --- a/commit_check/__init__.py +++ b/commit_check/__init__.py @@ -32,7 +32,7 @@ }, { 'check': 'author_name', - 'regex': r'^[A-Za-z ,.\'-]+$|.*(\[bot])', + 'regex': r'^[A-Za-zÀ-ÖØ-öø-ÿ\u0100-\u017F\u0180-\u024F ,.\'-]+$|.*(\[bot])', 'error': 'The committer name seems invalid', 'suggest': 'run command `git config user.name "Your Name"`', }, diff --git a/tests/author_test.py b/tests/author_test.py index 21f637d..8936223 100644 --- a/tests/author_test.py +++ b/tests/author_test.py @@ -9,6 +9,7 @@ class TestAuthor: class TestAuthorName: # used by get_commit_info mock fake_author_value_an = "fake_author_name" + fake_accented_author_value_an = "fáké_áúthór_námé" def test_check_author(self, mocker): # Must call get_commit_info, re.match. @@ -29,6 +30,25 @@ def test_check_author(self, mocker): assert m_get_commit_info.call_count == 1 assert m_re_match.call_count == 1 + def test_check_author_with_accented_letters(self, mocker): + # Must call get_commit_info, re.match. + checks = [{ + "check": "author_name", + "regex": "dummy_regex" + }] + m_get_commit_info = mocker.patch( + f"{LOCATION}.get_commit_info", + return_value=self.fake_accented_author_value_an + ) + m_re_match = mocker.patch( + "re.match", + return_value="fake_rematch_resp" + ) + retval = check_author(checks, "author_name") + assert retval == PASS + assert m_get_commit_info.call_count == 1 + assert m_re_match.call_count == 1 + def test_check_author_with_empty_checks(self, mocker): # Must NOT call get_commit_info, re.match. with `checks` param with length 0. checks = [] From 9f02b131369fe59a354b4eb6e2c898f8d13d3e90 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 25 Feb 2025 01:50:27 +0800 Subject: [PATCH 18/45] ci: pre-commit autoupdate (#224) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.9.6 → v0.9.7](https://github.com/astral-sh/ruff-pre-commit/compare/v0.9.6...v0.9.7) - [github.com/commit-check/commit-check: v0.9.3 → v0.9.4](https://github.com/commit-check/commit-check/compare/v0.9.3...v0.9.4) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .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 2953fc4..b2a8641 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: - id: requirements-txt-fixer - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.9.6 + rev: v0.9.7 hooks: # Run the linter. - id: ruff @@ -33,7 +33,7 @@ repos: hooks: - id: codespell - repo: https://github.com/commit-check/commit-check - rev: v0.9.3 + rev: v0.9.4 hooks: - id: check-message # - id: check-branch # uncomment if you need. From 436c0c1048d0e4a4fb097e98cf20bb20b6bd194a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 4 Mar 2025 01:42:33 +0800 Subject: [PATCH 19/45] ci: pre-commit autoupdate (#225) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.9.7 → v0.9.9](https://github.com/astral-sh/ruff-pre-commit/compare/v0.9.7...v0.9.9) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b2a8641..67a0323 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: - id: requirements-txt-fixer - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.9.7 + rev: v0.9.9 hooks: # Run the linter. - id: ruff From bd71280ead000d9cf6b584f23136471a9cd59d4e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 11 Mar 2025 01:35:10 +0800 Subject: [PATCH 20/45] ci: pre-commit autoupdate (#226) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.9.9 → v0.9.10](https://github.com/astral-sh/ruff-pre-commit/compare/v0.9.9...v0.9.10) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 67a0323..1bc27cb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: - id: requirements-txt-fixer - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.9.9 + rev: v0.9.10 hooks: # Run the linter. - id: ruff From 6d7b4052d50f7facf9fde26ec3936968cf208907 Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Mon, 31 Mar 2025 01:05:06 +0800 Subject: [PATCH 21/45] chore: drop unsupported python version 3.8 (#229) --- .github/workflows/main.yml | 2 +- pyproject.toml | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9106468..6333198 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -53,7 +53,7 @@ jobs: strategy: fail-fast: false matrix: - py: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] + py: ['3.9', '3.10', '3.11', '3.12', '3.13'] os: ['windows-latest', 'ubuntu-24.04', 'macos-latest'] runs-on: ${{ matrix.os }} steps: diff --git a/pyproject.toml b/pyproject.toml index 2710ff6..3563062 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ license = {text = "MIT License"} authors = [ { name = "Xianpeng Shen", email = "xianpeng.shen@gmail.com" }, ] -requires-python = ">=3.8" +requires-python = ">=3.9" dependencies = ["pyyaml"] classifiers = [ # https://pypi.org/pypi?%3Aaction=list_classifiers @@ -20,8 +20,6 @@ classifiers = [ "Intended Audience :: Developers", "Natural Language :: English", "Operating System :: OS Independent", - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", From 8ad15262527eb83d10fc2f59b257b69df0158fc7 Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Mon, 31 Mar 2025 01:14:06 +0800 Subject: [PATCH 22/45] feat: adopt PEP639 (#228) --- .pre-commit-config.yaml | 2 +- pyproject.toml | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1bc27cb..a8263d3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: - id: requirements-txt-fixer - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.9.10 + rev: v0.11.2 hooks: # Run the linter. - id: ruff diff --git a/pyproject.toml b/pyproject.toml index 3563062..c84d181 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools>=61", "setuptools-scm"] +requires = ["setuptools>=77", "setuptools-scm"] build-backend = "setuptools.build_meta" [project] @@ -7,7 +7,7 @@ name = "commit-check" description = "Check commit message formatting, branch naming, commit author, email, and more." readme = "README.rst" keywords = ["commit conventions", "conventional commits", "conventional branch", "branch naming", "commit-check", "message", "lint message", "devops"] -license = {text = "MIT License"} +license = "MIT" authors = [ { name = "Xianpeng Shen", email = "xianpeng.shen@gmail.com" }, ] @@ -16,7 +16,6 @@ dependencies = ["pyyaml"] classifiers = [ # https://pypi.org/pypi?%3Aaction=list_classifiers "Development Status :: 5 - Production/Stable", - "License :: OSI Approved :: MIT License", "Intended Audience :: Developers", "Natural Language :: English", "Operating System :: OS Independent", From b3c05ead4eeaa153d6d1b37a49e0dd3bf2b48379 Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Mon, 31 Mar 2025 01:19:45 +0800 Subject: [PATCH 23/45] chore: update pre-commit.ci autoupdate to quarterly (#230) --- .pre-commit-config.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a8263d3..cc6ee16 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,8 +1,10 @@ -# https://pre-commit.com/ +# https://pre-commit.ci/ ci: autofix_commit_msg: 'ci: auto fixes from pre-commit.com hooks' autoupdate_commit_msg: 'ci: pre-commit autoupdate' + autoupdate_schedule: quarterly +# https://pre-commit.com/ # prepare-commit-msg is used by hook id: check-message default_install_hook_types: [pre-commit, prepare-commit-msg] repos: From 656ca71a8c7f0dd19e1305c85e1a345c8054c3c0 Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Sun, 6 Apr 2025 20:47:24 +0300 Subject: [PATCH 24/45] 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 25/45] 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 26/45] 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 27/45] 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 28/45] 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 29/45] 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 30/45] 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 31/45] 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 32/45] 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 33/45] 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 34/45] 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 35/45] 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 36/45] 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 37/45] 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 38/45] 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 39/45] 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 40/45] 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 41/45] 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 42/45] 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 43/45] 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 44/45] 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 45/45] 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