├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── dangerjs.yml │ ├── issue_comment.yml │ ├── new_issues.yml │ ├── new_prs.yml │ └── pytest.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .pre-commit-hooks.yaml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── conventional_precommit_linter ├── __init__.py ├── helpers.py └── hook.py ├── docs ├── conventional-precommit-linter.jpg ├── example-output-custom-args.png └── example-output-default-args.png ├── pyproject.toml └── tests ├── __init__.py ├── conftest.py ├── test_custom_args.py └── test_default_args.py /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # The global '*' pattern specifies code owners for all files in the repository. 2 | * @tomassebestik @kumekay 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | 4 | updates: 5 | - package-ecosystem: pip 6 | versioning-strategy: increase-if-necessary # Only increase the version if necessary (instead always latest) 7 | directory: '/' # The directory where Dependabot should look for pip dependencies. 8 | # schedule: { interval: weekly, day: sunday, time: '09:00' } # UTC 9 | schedule: { interval: daily, time: '09:00' } # UTC 10 | open-pull-requests-limit: 0 # Disables PRs for version-updates, only security updates 11 | groups: 12 | # pip-version-updates: # (disabled) 13 | # applies-to: version-updates 14 | # patterns: ['*'] 15 | # update-types: [major, minor, patch] 16 | pip-security-updates: 17 | applies-to: security-updates 18 | patterns: ['*'] 19 | update-types: [major, minor, patch] # All types in same group (same PR) 20 | commit-message: { prefix: 'ci(deps-pip): [skip ci]' } # Commit message prefix; [skip ci] disables GitHub workflows 21 | reviewers: ['tomassebestik'] # the CI team Espressif GitHub organization 22 | labels: ['dependencies', 'Status: Reviewing'] # Labels automatically added to the pull request 23 | pull-request-branch-name: { separator: '-' } # Separator for PR branch names 24 | 25 | - package-ecosystem: github-actions 26 | open-pull-requests-limit: 0 # Only security updates 27 | directory: '/' 28 | # schedule: { interval: weekly, day: sunday, time: '09:00' } # UTC 29 | schedule: { interval: daily, time: '09:00' } # UTC 30 | groups: 31 | # github-actions-version-updates: # (disabled) 32 | # applies-to: version-updates 33 | # patterns: ['*'] 34 | # update-types: [major, minor, patch] 35 | github-actions-security-updates: 36 | applies-to: security-updates 37 | patterns: ['*'] 38 | update-types: [major, minor, patch] 39 | commit-message: { prefix: 'ci(deps-gh-actions): [skip ci]' } 40 | reviewers: ['tomassebestik'] 41 | labels: ['dependencies', 'Status: Reviewing'] 42 | pull-request-branch-name: { separator: '-' } 43 | -------------------------------------------------------------------------------- /.github/workflows/dangerjs.yml: -------------------------------------------------------------------------------- 1 | name: DangerJS Pull Request linter 2 | on: 3 | pull_request_target: 4 | types: [opened, edited, reopened, synchronize] 5 | 6 | permissions: 7 | pull-requests: write 8 | contents: read 9 | 10 | jobs: 11 | pull-request-style-linter: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Check out PR head 15 | uses: actions/checkout@v4 16 | with: 17 | ref: ${{ github.event.pull_request.head.sha }} 18 | 19 | - name: DangerJS pull request linter 20 | uses: espressif/shared-github-dangerjs@v1 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | with: 24 | instructions-contributions-file: 'CONTRIBUTING.md' 25 | -------------------------------------------------------------------------------- /.github/workflows/issue_comment.yml: -------------------------------------------------------------------------------- 1 | name: Sync issue comments to JIRA 2 | 3 | # This workflow will be triggered when new issue comment is created (including PR comments) 4 | on: issue_comment 5 | 6 | jobs: 7 | sync_issue_comments_to_jira: 8 | name: Sync Issue Comments to Jira 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@master 12 | - name: Sync issue comments to JIRA 13 | uses: espressif/github-actions/sync_issues_to_jira@master 14 | env: 15 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 16 | JIRA_PASS: ${{ secrets.JIRA_PASS }} 17 | JIRA_PROJECT: RDT 18 | JIRA_COMPONENT: GitHub 19 | JIRA_URL: ${{ secrets.JIRA_URL }} 20 | JIRA_USER: ${{ secrets.JIRA_USER }} 21 | -------------------------------------------------------------------------------- /.github/workflows/new_issues.yml: -------------------------------------------------------------------------------- 1 | name: Sync issues to Jira 2 | 3 | # This workflow will be triggered when a new issue is opened 4 | on: issues 5 | 6 | jobs: 7 | sync_issues_to_jira: 8 | name: Sync issues to Jira 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@master 12 | - name: Sync GitHub issues to Jira project 13 | uses: espressif/github-actions/sync_issues_to_jira@master 14 | env: 15 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 16 | JIRA_PASS: ${{ secrets.JIRA_PASS }} 17 | JIRA_PROJECT: RDT 18 | JIRA_COMPONENT: GitHub 19 | JIRA_URL: ${{ secrets.JIRA_URL }} 20 | JIRA_USER: ${{ secrets.JIRA_USER }} 21 | -------------------------------------------------------------------------------- /.github/workflows/new_prs.yml: -------------------------------------------------------------------------------- 1 | name: Sync remain PRs to Jira 2 | 3 | # This workflow will be triggered every hour, to sync remaining PRs (i.e. PRs with zero comment) to Jira project 4 | # Note that, PRs can also get synced when new PR comment is created 5 | on: 6 | schedule: 7 | - cron: "0 * * * *" 8 | 9 | jobs: 10 | sync_prs_to_jira: 11 | name: Sync PRs to Jira 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@master 15 | - name: Sync PRs to Jira project 16 | uses: espressif/github-actions/sync_issues_to_jira@master 17 | with: 18 | cron_job: true 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | JIRA_PASS: ${{ secrets.JIRA_PASS }} 22 | JIRA_PROJECT: RDT 23 | JIRA_COMPONENT: GitHub 24 | JIRA_URL: ${{ secrets.JIRA_URL }} 25 | JIRA_USER: ${{ secrets.JIRA_USER }} 26 | -------------------------------------------------------------------------------- /.github/workflows/pytest.yml: -------------------------------------------------------------------------------- 1 | name: Tests Pytest (multi-os) 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | 8 | jobs: 9 | test: 10 | runs-on: ${{ matrix.os }} 11 | strategy: 12 | matrix: 13 | python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] 14 | os: [ubuntu-latest, macos-latest, windows-latest] 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: Set up Python ${{ matrix.python-version }} 20 | uses: actions/setup-python@v5 21 | with: 22 | python-version: ${{ matrix.python-version }} 23 | 24 | - name: Install dependencies 25 | run: | 26 | python -m pip install --upgrade pip 27 | pip install -e '.[test]' 28 | 29 | - name: Test with pytest 30 | run: | 31 | pytest 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .pytest_cache 2 | __pycache__/ 3 | .mypy_cache 4 | *.egg* 5 | test_message.txt 6 | .coverage 7 | .vscode 8 | venv 9 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | minimum_pre_commit_version: 3.3.0 # Specifies the minimum version of pre-commit required for this configuration 3 | default_install_hook_types: [pre-commit,commit-msg] # Default hook types to install if not specified in individual hooks 4 | 5 | repos: 6 | - repo: https://github.com/pre-commit/pre-commit-hooks 7 | rev: v4.5.0 8 | hooks: 9 | - id: trailing-whitespace # Removes trailing whitespaces from lines 10 | - id: end-of-file-fixer # Ensures files end with a newline 11 | - id: check-executables-have-shebangs # Checks executables have a proper shebang 12 | - id: mixed-line-ending # Detects mixed line endings (CRLF/LF) 13 | args: ['-f=lf'] # Forces files to use LF line endings 14 | - id: double-quote-string-fixer # Converts single quotes to double quotes in strings 15 | 16 | - repo: https://github.com/pylint-dev/pylint 17 | rev: v3.1.0 18 | hooks: 19 | - id: pylint # Runs pylint on Python code 20 | 21 | - repo: https://github.com/astral-sh/ruff-pre-commit 22 | rev: v0.3.4 23 | hooks: 24 | - id: ruff # Linter 25 | args: [--fix, --exit-non-zero-on-fix] 26 | - id: ruff-format # Formatter (replaces Black) 27 | 28 | - repo: https://github.com/asottile/reorder_python_imports 29 | rev: v3.12.0 30 | hooks: 31 | - id: reorder-python-imports # Reorders Python imports to a standard format (replaces isort) 32 | 33 | - repo: https://github.com/pre-commit/mirrors-mypy 34 | rev: v1.9.0 35 | hooks: 36 | - id: mypy # Runs mypy for Python type checking 37 | 38 | - repo: https://github.com/espressif/conventional-precommit-linter 39 | rev: v1.6.0 40 | hooks: 41 | - id: conventional-precommit-linter 42 | stages: [commit-msg] 43 | -------------------------------------------------------------------------------- /.pre-commit-hooks.yaml: -------------------------------------------------------------------------------- 1 | - id: conventional-precommit-linter 2 | name: Conventional Commit 3 | entry: conventional-precommit-linter 4 | language: python 5 | description: Checks commit message for Conventional Commits formatting 6 | always_run: true 7 | stages: [commit-msg] 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v1.10.0 (2024-07-24) 2 | 3 | 4 | - ci: update versions of GH actions in GH workflows 5 | - ci: add CODEOWNERS file, removed unused config files 6 | - feat: handle fixup and squash messages 7 | - ci(danger-github-action): limit more permission for workflow file 8 | 9 | ## v1.9.0 (2024-06-13) 10 | 11 | 12 | - feat: Add '--scope-case-insensitive' optional argument 13 | - Closes https://github.com/espressif/conventional-precommit-linter/issues/25 14 | 15 | ## v1.8.0 (2024-04-22) 16 | 17 | 18 | - fix(output-message): fixes presence bang/breaking change in output message 19 | - Closes https://github.com/espressif/conventional-precommit-linter/issues/24 20 | - fix: unicode issues when utf-8 encoding is not set in system 21 | - Closes https://github.com/espressif/conventional-precommit-linter/issues/22 22 | 23 | ## v1.7.0 (2024-04-05) 24 | 25 | 26 | - change: update output message for help command (works with "git worktree") 27 | - docs: updated README file - usage, tip for git alias 28 | - ci: replace Black formatter by Ruff formatter 29 | - removed linting tools from dev dependencies 30 | 31 | ## v1.6.0 (2024-01-02) 32 | 33 | 34 | - change(default-types): add "test:" to default commit types 35 | 36 | ## v1.5.0 (2023-12-15) 37 | 38 | 39 | - feat: add support for optional `!` in scope 40 | - Closes https://github.com/espressif/conventional-precommit-linter/issues/12 41 | - docs: add CONTRIBUTING Guide 42 | - ci: add PR linter Danger 43 | 44 | ## v1.4.1 (2023-12-09) 45 | 46 | 47 | - fix: fix partial type matching 48 | - Closes https://github.com/espressif/conventional-precommit-linter/issues/11 49 | 50 | ## v1.4.0 (2023-12-04) 51 | 52 | 53 | - feat(scope): add optional restriction to available scopes 54 | - ci: update commitizen auto release message 55 | - update actions version pytest.yml workflow 56 | - docs: update thumbnails example messages 57 | - change(output): coloring only keywords in output 58 | 59 | ## v1.3.0 (2023-11-09) 60 | 61 | 62 | - fix: commitizen versions settings in pyproject.toml 63 | - change(user-output): update user output marking all issues with message - Dynamic messages in output report - Color input commit message same as message elements - Tests updated 64 | - ci: update project settings configuration (pyproject.toml) 65 | - add CHANGELOG.md, commitizen, test packages definitions 66 | - GitHub action - testing on multiple OSes 67 | 68 | ## v1.2.1 (2023-07-31) 69 | 70 | 71 | - fix(scope-capitalization): Update scope regex to be consistent with commitlint in DangerJS (#6) 72 | - docs(README) Update default max summary length 73 | 74 | ## v1.2.0 (2023-06-29) 75 | 76 | 77 | - Ignore comment lines from linted commit message (#5) 78 | - fix: Ignore # lines from linted commit message 79 | - feat: Add hint for preserving commit message to output report 80 | - fix: Allow in scope special characters " _ / . , * -" 81 | - docs: Update hook install process guide (#4) 82 | 83 | ## v1.1.0 (2023-06-27) 84 | 85 | 86 | - Update default rules (#3) 87 | - change(rules): Set maximum summary length to 72 characters 88 | - change(rules): Summary uppercase letter as optional rules 89 | - docs: Update argument usage example in README.md 90 | 91 | ## v1.0.0 (2023-06-21) 92 | 93 | 94 | - Merge pull request #1 from espressif/add_linter_hook 95 | - feat: Add linter pre-commit hook logic (RDT-471) 96 | - feat: Add linter pre-commit hook logic 97 | - Init 98 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing and Development 2 | 3 | We welcome contributions! To contribute to this repository, please follow these steps: 4 | 5 | ## Code and Testing 6 | 7 | - **Code Style and Structure:** 8 | 9 | - **Pre-Commit Hooks:** Install pre-commit hooks in this repository using the `pre-commit install` command. 10 | 11 | - **Readable Code Structure:** Structure your code in a readable manner. The main logic should be in the default rule function, with implementation details in helper functions. Avoid nested `if` statements and unnecessary `else` statements to maintain code clarity and simplicity. 12 | 13 | - **Remove Debug Statements:** Remove any debug statements from your rules. 14 | 15 | - **Automated Tests:** We aim for full test coverage, so **partial tests will not be accepted**. The tests should cover all typical usage scenarios as well as edge cases to ensure robustness. 16 | 17 | - **Testing Tool:** It is recommended to run `pytest` frequently during development to ensure that all aspects of your code are functioning as expected. 18 | 19 | 20 | ## Documentation and Maintenance 21 | 22 | - **Changelog:** `CHANGELOG.md` is generated automatically by `comittizen` from commit messages. Not need to update `CHANGELOG.md` manually. Focus on informative and clear commit messages which end in the release notes. 23 | 24 | - **Documentation:** Regularly check and update the documentation to keep it current. 25 | 26 | - **PR Descriptions and Documentation:** When contributing, describe all changes or new features in the PR (Pull Request) description as well as in the documentation. When changing the style to the output style, attach a thumbnail after the change. 27 | 28 | ## Development and local testing 29 | 30 | 31 | 1. **Clone the Project**: Clone the repository to your local machine using: 32 | ```sh 33 | git clone https://github.com/espressif/conventional-precommit-linter.git 34 | ``` 35 | 36 | 2. **Set Up Development Environment:** 37 | 38 | - Create and activate a virtual environment: 39 | ```sh 40 | virtualenv venv -p python3.8 && source ./venv/bin/activate 41 | ``` 42 | or 43 | ```sh 44 | python -m venv venv && source ./venv/bin/activate 45 | ``` 46 | 47 | - Install the project and development dependencies: 48 | ```sh 49 | pip install -e '.[dev]' 50 | ``` 51 | 52 | 3. **Testing Your Changes:** 53 | 54 | - Create a file named `test_message.txt` in the root of the repository (this file is git-ignored) and place an example commit message in it. 55 | 56 | - Run the tool to lint the message: 57 | ```sh 58 | python -m conventional_precommit_linter.hook test_message.txt 59 | ``` 60 | 61 | ... or with arguments: 62 | ```sh 63 | python -m conventional_precommit_linter.hook test_message.txt --subject-min-length="12" --subject-max-length="55" --body-max-line-length="88" --types="feat,change,style" --scopes="bt,wifi,ethernet" 64 | ``` 65 | 66 | 67 | Before submitting a pull request, ensure your changes pass all the tests. You can run the test suite with the following command: 68 | ```sh 69 | pytest 70 | ``` 71 | 72 | --- 73 | 74 | 👏**Thank you for your contributions.** 75 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2023 Espressif Systems (Shanghai) CO LTD 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

Conventional Precommit Linter

3 | 4 |
5 |
6 | 7 | release 8 | tests 9 |
10 | The Conventional Precommit Linter is a tool designed to ensure commit messages follow the Conventional Commits standard, enhancing the readability and traceability of your project's history. 11 |
12 | 13 | - [Usage](#usage) 14 | - [Commit Message Structure](#commit-message-structure) 15 | - [Setup](#setup) 16 | - [Install Commit-msg Hooks](#install-commit-msg-hooks) 17 | - [Configuration](#configuration) 18 | - [Project issues](#project-issues) 19 | - [Contributing](#contributing) 20 | - [Credits](#credits) 21 | 22 | --- 23 | 24 | ## Usage 25 | 26 | The _conventional-precommit-linter hook_ runs every time you execute the `git commit` command (when you want to commit your changes). Since this hook operates in the `commit-msg` stage, simply running a pre-commit check without actually committing (using `pre-commit run`), will have no effect, and this hook will be ignored. 27 | 28 | The same applies to running pre-commit hooks in CI (Continuous Integration) job environments - **this hook is simply skipped when you run pre-commit checks in your CI system**. 29 | 30 | ### Commit Message Structure 31 | 32 | Commit messages are validated against the following format: 33 | 34 | ``` 35 | (): 36 | < ... empty line ... > 37 | 38 | 39 | 40 | ``` 41 | 42 | Each component is checked for compliance with the provided or default configuration. 43 | 44 | If your commit message does not meet the required format, the hook will fail, producing a **report that shows which part of your commit message needs correction**: 45 | 46 | 47 | 48 | For a custom configuration, the report might look like this: 49 | 50 | 51 | The hint message suggests that you can preserve your original message and simply edit it in your default editor, without the need to type the whole message again. 52 | 53 | To edit failed message, run the command (as the hint suggests): 54 | 55 | ```sh 56 | git commit --edit --file=$(git rev-parse --git-dir)/COMMIT_EDITMSG 57 | ``` 58 | 59 | Since this command is quite complex and you may use this functionality often, **creating a Git alias might be a good idea**: 60 | 61 | ```sh 62 | git config --global alias.again '!git commit --edit --file=$(git rev-parse --git-dir)/COMMIT_EDITMSG' 63 | ``` 64 | 65 | This command adds a `git again` alias to your machine's Git configuration. You can run then simply `git again` whenever your commit message check fails. 66 | 67 | --- 68 | 69 | ## Setup 70 | 71 | To integrate the **Conventional Precommit Linter** into your project, add to your `.pre-commit-config.yaml`: 72 | 73 | ```yaml 74 | # FILE: .pre-commit-config.yaml 75 | repos: 76 | - repo: https://github.com/espressif/conventional-precommit-linter 77 | rev: v1.7.0 # The version tag you wish to use 78 | hooks: 79 | - id: conventional-precommit-linter 80 | stages: [commit-msg] 81 | ``` 82 | 83 | ### Install Commit-msg Hooks 84 | 85 | **IMPORTANT:** `commit-msg` hooks require a specific installation command: 86 | 87 | ```sh 88 | pre-commit install -t pre-commit -t commit-msg 89 | ``` 90 | 91 | **Note:** The `pre-commit install` command by default sets up only the `pre-commit` stage hooks. The additional flag `-t commit-msg` is necessary to set up `commit-msg` stage hooks. 92 | 93 | For a simplified setup (just with `pre-commit install` without flags), ensure your `.pre-commit-config.yaml` contains the following: 94 | 95 | ```yaml 96 | # FILE: .pre-commit-config.yaml 97 | --- 98 | minimum_pre_commit_version: 3.3.0 99 | default_install_hook_types: [pre-commit, commit-msg] 100 | ``` 101 | 102 | After modifying `.pre-commit-config.yaml`, re-run the installation command (`pre-commit install`) for changes to take effect. 103 | 104 | - 105 | 106 | ### Configuration 107 | 108 | The linter accepts several configurable parameters to tailor commit message validation: 109 | 110 | - `--types`: Define the types of commits allowed (default: [`change`, `ci`, `docs`, `feat`, `fix`, `refactor`, `remove`, `revert`, `test`]). 111 | - `--scopes`: Specifies a list of allowed scopes. If not defined, all scopes are allowed (restriction is `disabled`). 112 | - `--scope-case-insensitive`: Allows uppercase letters in scope. 113 | - `--subject-min-length`: Set the minimum length for the summary (default: `20`). 114 | - `--subject-max-length`: Set the maximum length for the summary (default: `72`). 115 | - `--body-max-line-length`: Set the maximum line length for the body (default: `100`). 116 | - `--summary-uppercase`: Enforce the summary to start with an uppercase letter (default: `disabled`). 117 | - `--allow-breaking`: Allow exclamation mark in the commit type (default: `false`). 118 | 119 | The **custom configuration** can be specified in `.pre-commit-config.yaml` like this: 120 | 121 | ```yaml 122 | # FILE: .pre-commit-config.yaml 123 | --- 124 | - repo: https://github.com/espressif/conventional-precommit-linter 125 | rev: v1.7.0 # The version tag you wish to use 126 | hooks: 127 | - id: conventional-precommit-linter 128 | stages: [commit-msg] 129 | args: 130 | - --types=build,ci,docs,feat,fix,perf,refactor,style,test # default Angular / @commitlint-conventional types 131 | - --scopes=bt,wifi,ethernet 132 | - --subject-min-length=10 133 | ``` 134 | 135 | --- 136 | 137 | ## Project issues 138 | 139 | If you encounter any issues, feel free to report them in the [project's issues](https://github.com/espressif/conventional-precommit-linter/issues) or create Pull Request with your suggestion. 140 | 141 | --- 142 | 143 | ## Contributing 144 | 145 | 📘 If you are interested in contributing to this project, see the [project Contributing Guide](CONTRIBUTING.md). 146 | 147 | --- 148 | 149 | ## Credits 150 | 151 | Inspired by project: https://github.com/compilerla/conventional-pre-commit 152 | -------------------------------------------------------------------------------- /conventional_precommit_linter/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espressif/conventional-precommit-linter/ce5f1291713a88eda63b0519dd825fda3759ca4e/conventional_precommit_linter/__init__.py -------------------------------------------------------------------------------- /conventional_precommit_linter/helpers.py: -------------------------------------------------------------------------------- 1 | from colorama import Fore 2 | from colorama import init 3 | from colorama import Style 4 | 5 | init(autoreset=True) # Automatically reset the style after each print 6 | 7 | 8 | def _color_bold_green(text: str) -> str: 9 | return f'{Style.BRIGHT}{Fore.GREEN}{text}{Style.RESET_ALL}' 10 | 11 | 12 | def _color_purple(text: str) -> str: 13 | return f'{Fore.MAGENTA}{text}{Style.RESET_ALL}' 14 | 15 | 16 | def _color_orange(text: str) -> str: 17 | return f'{Fore.YELLOW}{text}{Style.RESET_ALL}' 18 | 19 | 20 | def _color_blue(text: str) -> str: 21 | return f'{Fore.LIGHTBLUE_EX}{text}{Style.RESET_ALL}' 22 | 23 | 24 | def _color_grey(text: str) -> str: 25 | return f'{Fore.LIGHTBLACK_EX}{text}{Style.RESET_ALL}' 26 | 27 | 28 | def _color_red(text: str) -> str: 29 | return f'{Fore.RED}{text}{Style.RESET_ALL}' 30 | 31 | 32 | def _color_green(text: str) -> str: 33 | return f'{Fore.GREEN}{text}{Style.RESET_ALL}' 34 | -------------------------------------------------------------------------------- /conventional_precommit_linter/hook.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import re 3 | import sys 4 | from typing import List 5 | from typing import Optional 6 | from typing import Tuple 7 | 8 | from .helpers import _color_blue 9 | from .helpers import _color_bold_green 10 | from .helpers import _color_green 11 | from .helpers import _color_grey 12 | from .helpers import _color_orange 13 | from .helpers import _color_purple 14 | from .helpers import _color_red 15 | 16 | DEFAULT_TYPES = ['change', 'ci', 'docs', 'feat', 'fix', 'refactor', 'remove', 'revert', 'test'] 17 | 18 | rules_output_status = { 19 | 'empty_message': False, 20 | 'error_body_format': False, 21 | 'error_body_length': False, 22 | 'error_scope_allowed': False, 23 | 'error_scope_capitalization': False, 24 | 'error_scope_format': False, 25 | 'error_breaking': False, 26 | 'error_summary_capitalization': False, 27 | 'error_summary_length': False, 28 | 'error_summary_period': False, 29 | 'error_type': False, 30 | 'missing_colon': False, 31 | } 32 | 33 | 34 | def get_allowed_types(args: argparse.Namespace) -> List[str]: 35 | # Provided types take precedence over default types 36 | types: List[str] = args.types[0].split(',') if args.types else DEFAULT_TYPES 37 | return [commit_type.strip() for commit_type in types] 38 | 39 | 40 | def get_allowed_scopes(args: argparse.Namespace) -> List[str]: 41 | default_scopes: List[str] = [] 42 | scopes: List[str] = args.scopes[0].split(',') if args.scopes else default_scopes 43 | return [scope.strip() for scope in scopes] 44 | 45 | 46 | def read_commit_message(file_path: str) -> str: 47 | with open(file_path, encoding='utf-8') as file: 48 | lines = file.readlines() 49 | lines = [line for line in lines if not line.startswith('#')] # Remove comment lines (starting with '#') 50 | content = ''.join(lines) 51 | if not content.strip(): 52 | rules_output_status['empty_message'] = True 53 | return content 54 | 55 | 56 | def split_message_title(message_title: str, args: argparse.Namespace) -> Tuple[str, Optional[str], str, bool]: 57 | """Split 'message title' into 'type/scope' and 'summary'.""" 58 | type_and_scope, _, commit_summary = message_title.partition(': ') 59 | commit_summary = commit_summary.strip() 60 | 61 | # Regex for type and scope of commitizen: 62 | regex_type_and_scope = r'^(?P\w+)(\((?P[^\)]+)\))?(?P!)?$' 63 | match = re.match(regex_type_and_scope, type_and_scope) 64 | 65 | if not match: 66 | if '(' in type_and_scope and ')' not in type_and_scope: 67 | rules_output_status['error_scope_format'] = True 68 | else: 69 | rules_output_status['error_type'] = True 70 | 71 | commit_type = type_and_scope.split('(')[0] 72 | 73 | # Return None for the scope due to the error 74 | return commit_type, None, commit_summary, False 75 | 76 | commit_type = match.group('type') 77 | commit_scope = match.group('scope') 78 | breaking_change = bool(match.group('breaking')) 79 | 80 | # Check if 'breaking' is not allowed but was used 81 | if breaking_change and not args.allow_breaking: 82 | rules_output_status['error_breaking'] = True 83 | 84 | return commit_type, commit_scope, commit_summary, breaking_change 85 | 86 | 87 | def check_colon_after_type(message_title: str) -> bool: 88 | """Check for missing column between type / type(scope) and summary.""" 89 | message_parts = message_title.split(': ', 1) # split only on first occurrence 90 | if len(message_parts) != 2: 91 | rules_output_status['missing_colon'] = True 92 | return False 93 | return True 94 | 95 | 96 | def check_allowed_types(commit_type: str, args: argparse.Namespace) -> None: 97 | """Check for allowed types.""" 98 | types: List[str] = get_allowed_types(args) 99 | if commit_type not in types: 100 | rules_output_status['error_type'] = True 101 | 102 | 103 | def check_scope(commit_scope: str, args: argparse.Namespace) -> None: 104 | """Check for scope capitalization and allowed characters""" 105 | regex_scope = r'^[a-z0-9_/.,*-]*$' 106 | if args.scope_case_insensitive: 107 | regex_scope = r'^[a-zA-Z0-9_/.,*-]*$' # adds A-Z to the allowed character set 108 | 109 | if commit_scope and not re.match(regex_scope, commit_scope): 110 | rules_output_status['error_scope_capitalization'] = True 111 | 112 | # Check against the list of allowed scopes if provided 113 | allowed_scopes: List[str] = get_allowed_scopes(args) 114 | if allowed_scopes and commit_scope not in allowed_scopes: 115 | rules_output_status['error_scope_allowed'] = True 116 | 117 | 118 | def check_summary_length(commit_summary: str, args: argparse.Namespace) -> None: 119 | """Check for summary length (between min and max allowed characters)""" 120 | summary_length = len(commit_summary) 121 | if summary_length < args.subject_min_length or summary_length > args.subject_max_length: 122 | rules_output_status['error_summary_length'] = True 123 | 124 | 125 | def check_summary_lowercase(commit_summary: str) -> None: 126 | """Check for summary starting with an uppercase letter (rule disabled in default config)""" 127 | if commit_summary[0].islower(): 128 | rules_output_status['error_summary_capitalization'] = True 129 | 130 | 131 | def check_summary_period(commit_summary: str) -> None: 132 | """Check for summary ending with a period""" 133 | if commit_summary[-1] == '.': 134 | rules_output_status['error_summary_period'] = True 135 | 136 | 137 | def check_body_empty_lines(message_body: List[str]) -> None: 138 | """Check for empty line between summary and body""" 139 | if not message_body[0].strip() == '': 140 | rules_output_status['error_body_format'] = True 141 | 142 | 143 | def check_body_lines_length(message_body: List[str], args: argparse.Namespace) -> None: 144 | """Check for body lines length (shorter than max allowed characters)""" 145 | if not all(len(line) <= args.body_max_line_length for line in message_body): 146 | rules_output_status['error_body_length'] = True 147 | 148 | 149 | def _get_icon_for_rule(status: bool) -> str: 150 | """Return a icon depending on the status of the rule (True = error found, False = success))""" 151 | return f'{ _color_red("FAIL:")}' if status else f'{_color_green("OK: ")}' 152 | 153 | 154 | def print_report(commit_type: str, commit_scope: Optional[str], commit_summary: str, breaking_change: bool, args) -> None: 155 | # Color the input commit message with matching element colors 156 | append_bang = '' if not breaking_change else '!' 157 | commit_message = f'{_color_purple(commit_type)}{_color_purple(append_bang)}: { _color_orange( commit_summary)}' 158 | if commit_scope: 159 | commit_message = f'{_color_purple(commit_type)}({ _color_blue( commit_scope)}){_color_purple(append_bang)}: { _color_orange( commit_summary)}' 160 | 161 | rule_messages: List[str] = [] 162 | 163 | # TYPES messages 164 | rule_messages.append( 165 | f"{_get_icon_for_rule(rules_output_status['error_type'])} {_color_purple('')} is mandatory, use one of the following: [{_color_purple(', '.join(get_allowed_types(args)))}]" 166 | ) 167 | 168 | if not args.allow_breaking: 169 | rule_messages.append( 170 | f"{_get_icon_for_rule(rules_output_status['error_breaking'])} {_color_purple('')} must not include {_color_purple('!')} to indicate a breaking change" 171 | ) 172 | 173 | # SCOPE messages 174 | rule_messages.append( 175 | f"{_get_icon_for_rule(rules_output_status['error_scope_format'])} {_color_blue('()')} if used, must be enclosed in parentheses" 176 | ) 177 | 178 | if args.scope_case_insensitive: 179 | rule_messages.append( 180 | f"{_get_icon_for_rule(rules_output_status['error_scope_capitalization'])} {_color_blue('()')} if used, must not contain whitespace" 181 | ) 182 | else: 183 | rule_messages.append( 184 | f"{_get_icon_for_rule(rules_output_status['error_scope_capitalization'])} {_color_blue('()')} if used, must be written in lower case without whitespace" 185 | ) 186 | if args.scopes: 187 | rule_messages.append( 188 | f"{_get_icon_for_rule(rules_output_status['error_scope_allowed'])} {_color_blue('()')} if used, must be one of the following allowed scopes: [{_color_blue(', '.join(args.scopes))}]" 189 | ) 190 | 191 | # SUMMARY messages 192 | rule_messages.append(f"{_get_icon_for_rule(rules_output_status['error_summary_period'])} {_color_orange('')} must not end with a period '.'") 193 | rule_messages.append( 194 | f"{_get_icon_for_rule(rules_output_status['error_summary_length'])} {_color_orange('')} must be between {args.subject_min_length} and {args.subject_max_length} characters long" 195 | ) 196 | if args.summary_uppercase: 197 | rule_messages.append( 198 | f"{_get_icon_for_rule(rules_output_status['error_summary_capitalization'])} {_color_orange('')} must start with an uppercase letter" 199 | ) 200 | 201 | # BODY messages 202 | rule_messages.append( 203 | f"{_get_icon_for_rule(rules_output_status['error_body_length'])} {_color_grey('')} lines must be no longer than {args.body_max_line_length} characters" 204 | ) 205 | rule_messages.append( 206 | f"{_get_icon_for_rule(rules_output_status['error_body_format'])} {_color_grey('')} must be separated from the 'summary' by a blank line" 207 | ) 208 | 209 | # Combine the rule messages into the final report block 210 | message_rules_block = ' ' + '\n '.join(rule_messages) 211 | 212 | full_guide_message = f"""\n {_color_red("INVALID COMMIT MESSAGE ---> ")}{commit_message} 213 | _______________________________________________________________ 214 | Commit message structure: {_color_purple('')}{_color_blue("()")}: {_color_orange('')} 215 | <... empty line ...> 216 | {_color_grey('')} 217 | {_color_grey('')} 218 | _______________________________________________________________ 219 | Commit message rules: 220 | {message_rules_block} 221 | """ 222 | print(full_guide_message) 223 | print(f'To preserve and correct a commit message, run: {_color_bold_green("git commit --edit --file=$(git rev-parse --git-dir)/COMMIT_EDITMSG")}\n') 224 | 225 | 226 | def parse_args(argv: List[str]) -> argparse.Namespace: 227 | parser = argparse.ArgumentParser(prog='conventional-pre-commit', description='Check a git commit message for Conventional Commits formatting.') 228 | parser.add_argument('--types', type=str, nargs='*', help="Redefine the list of allowed 'Types'") 229 | parser.add_argument('--scopes', type=str, nargs='*', help="Setting the list of allowed 'Scopes'") 230 | parser.add_argument('--subject-min-length', type=int, default=20, help="Minimum length of the 'Summary'") 231 | parser.add_argument('--subject-max-length', type=int, default=72, help="Maximum length of the 'Summary'") 232 | parser.add_argument('--body-max-line-length', type=int, default=100, help="Maximum length of the 'Body' line") 233 | parser.add_argument('--summary-uppercase', action='store_true', help="'Summary' must start with an uppercase letter") 234 | parser.add_argument('--scope-case-insensitive', action='store_true', help='Allow uppercase letters in the optional scope.') 235 | parser.add_argument('--allow-breaking', action='store_true', help='Allow exclamation mark in the commit type') 236 | parser.add_argument('input', type=str, help='A file containing a git commit message') 237 | return parser.parse_args(argv) 238 | 239 | 240 | def main(argv: Optional[List[str]] = None) -> int: 241 | argv = argv or sys.argv[1:] 242 | args = parse_args(argv) 243 | 244 | # Parse the commit message in to parts 245 | input_commit_message = read_commit_message(args.input) 246 | 247 | if not input_commit_message.strip(): 248 | print('FAIL: Commit message seems to be empty.') 249 | return 1 250 | 251 | message_lines = input_commit_message.strip().split('\n') # Split the commit message into lines 252 | message_title = message_lines[0] # The summary is the first line 253 | message_body = message_lines[1:] # The body is everything after the summary, if it exists 254 | 255 | # Skip message lining if the commit message is 'fixup!' or 'squash!' (will not stay in git history anyway) 256 | if re.match(r'^(fixup|squash)', message_title): 257 | return 0 258 | 259 | if not check_colon_after_type(message_title): 260 | print(f'FAIL: Missing colon after {_color_purple("")} or {_color_blue("()")}.') 261 | print(f'\nEnsure the commit message has the format "{_color_purple("")}{_color_blue("()")}: {_color_orange("")}"') 262 | return 1 263 | 264 | commit_type, commit_scope, commit_summary, breaking_change = split_message_title(message_title, args) 265 | 266 | # Commit message title (first line) checks 267 | check_allowed_types(commit_type, args) 268 | if commit_scope: 269 | check_scope(commit_scope, args) 270 | check_summary_length(commit_summary, args) 271 | check_summary_period(commit_summary) 272 | if args.summary_uppercase: 273 | check_summary_lowercase(commit_summary) 274 | 275 | # Commit message body checks 276 | if message_body: 277 | check_body_empty_lines(message_body) 278 | check_body_lines_length(message_body, args) 279 | 280 | # Create report if issues found 281 | if any(value for value in rules_output_status.values()): 282 | print_report(commit_type, commit_scope, commit_summary, breaking_change, args) 283 | return 1 284 | 285 | # No output and exit RC 0 if no issues found 286 | return 0 287 | 288 | 289 | if __name__ == '__main__': 290 | raise SystemExit(main()) 291 | -------------------------------------------------------------------------------- /docs/conventional-precommit-linter.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espressif/conventional-precommit-linter/ce5f1291713a88eda63b0519dd825fda3759ca4e/docs/conventional-precommit-linter.jpg -------------------------------------------------------------------------------- /docs/example-output-custom-args.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espressif/conventional-precommit-linter/ce5f1291713a88eda63b0519dd825fda3759ca4e/docs/example-output-custom-args.png -------------------------------------------------------------------------------- /docs/example-output-default-args.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espressif/conventional-precommit-linter/ce5f1291713a88eda63b0519dd825fda3759ca4e/docs/example-output-default-args.png -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | authors = [ 3 | { name = "Tomas Sebestik (Espressif Systems)", email = "tomas.sebestik@espressif.com" }, 4 | ] 5 | classifiers = ["Programming Language :: Python :: 3 :: Only"] 6 | dependencies = ["colorama==0.4.6"] 7 | description = "A pre-commit hook that checks commit messages for Conventional Commits formatting." 8 | dynamic = ["version"] 9 | keywords = ["conventional-commits", "git", "pre-commit"] 10 | license = { file = "LICENSE" } 11 | name = "conventional_precommit_linter" 12 | readme = "README.md" 13 | requires-python = ">=3.8" 14 | 15 | [project.urls] 16 | code = "https://github.com/espressif/conventional-precommit-linter" 17 | tracker = "https://github.com/espressif/conventional-precommit-linter/issues" 18 | 19 | [project.optional-dependencies] 20 | dev = [ 21 | "commitizen==3.10.1", 22 | "pre-commit==3.3.3", 23 | "pytest-cov~=4.1.0", 24 | "pytest~=7.4.0", 25 | ] 26 | test = ["pytest-cov~=4.1.0", "pytest~=7.4.0"] 27 | 28 | [project.scripts] 29 | conventional-precommit-linter = "conventional_precommit_linter.hook:main" 30 | 31 | [build-system] 32 | build-backend = "setuptools.build_meta" 33 | requires = ["setuptools>=64", "wheel"] 34 | 35 | [tool.pylint] 36 | [tool.pylint.MASTER] 37 | ignore-paths = ["tests/.*"] # Paths to ignore during linting 38 | [tool.pylint.'BASIC'] 39 | variable-rgx = "[a-z_][a-z0-9_]{1,30}$" # Variable names must start with a lowercase letter or underscore, followed by any combination of lowercase letters, numbers, or underscores, with a total length of 2 to 30 characters. 40 | [tool.pylint.'MESSAGES CONTROL'] 41 | disable = [ 42 | "duplicate-code", # R0801: Similar lines in %s files 43 | "fixme", # W0511: Used when TODO/FIXME is encountered 44 | "import-error", # E0401: Used when pylint has been unable to import a module 45 | "import-outside-toplevel", # E0402: Imports should usually be on top of the module 46 | "line-too-long", # C0301: Line too long 47 | "logging-fstring-interpolation", # W1202: Use % formatting in logging functions and pass the % parameters as arguments 48 | "missing-class-docstring", # C0115: Missing class docstring 49 | "missing-function-docstring", # C0116: Missing function or method docstring 50 | "missing-module-docstring", # C0114: Missing module docstring 51 | "no-name-in-module", # W0611: Used when a name cannot be found in a module 52 | "too-few-public-methods", # R0903: Too few public methods of class 53 | "too-many-branches", # R0912: Too many branches 54 | "too-many-locals", # R0914: Too many local variables 55 | "too-many-return-statements", # R0911: Too many return statements 56 | "too-many-statements", # R0915: Too many statements 57 | "ungrouped-imports", # C0412: Imports should be grouped by packages 58 | ] 59 | [tool.pylint.'FORMAT'] 60 | max-line-length = 160 # Specifies the maximum line length for pylint checks 61 | 62 | [tool.ruff] 63 | line-length = 160 # Specifies the maximum line length for ruff checks 64 | lint.ignore = [ 65 | "E501", # line-too-long 66 | ] 67 | lint.select = ['E', 'F', 'W'] # Types of issues ruff should check for 68 | target-version = "py38" # Specifies the target Python version for ruff checks 69 | 70 | [tool.ruff.format] # See formatter config options at https://docs.astral.sh/ruff/formatter 71 | quote-style = "single" 72 | 73 | [tool.mypy] 74 | disallow_incomplete_defs = false # Disallows defining functions with incomplete type annotations 75 | disallow_untyped_defs = true # Disallows defining functions without type annotations or with incomplete type annotations 76 | ignore_missing_imports = true # Suppress error messages about imports that cannot be resolved 77 | python_version = "3.9" # Specifies the Python version used to parse and check the target program 78 | warn_no_return = true # Shows errors for missing return statements on some execution paths 79 | warn_return_any = true # Shows a warning when returning a value with type Any from a function declared with a non- Any return type 80 | 81 | 82 | [tool.pytest.ini_options] 83 | addopts = "-ra -v -p no:print --cov=conventional_precommit_linter --cov-report=term" 84 | python_classes = ["Test*"] 85 | python_files = ["test_*.py"] 86 | python_functions = ["test_*"] 87 | testpaths = ["tests"] 88 | 89 | [tool.coverage.run] 90 | omit = ["__*__.py", "tests/*"] 91 | 92 | [tool.commitizen] 93 | annotated_tag = true 94 | bump_message = "change: bump release version to v$new_version" 95 | name = "cz_customize" 96 | tag_format = "v$version" 97 | update_changelog_on_bump = true 98 | version_provider = "scm" 99 | 100 | [tool.commitizen.customize] 101 | bump_map = { "change" = "MINOR", "feat" = "MINOR", "fix" = "PATCH", "refactor" = "PATCH", "remove" = "PATCH", "revert" = "PATCH" } 102 | bump_pattern = "^(change|feat|fix|refactor|remove|revert)" 103 | change_type_order = [ 104 | "change", 105 | "ci", 106 | "docs", 107 | "feat", 108 | "fix", 109 | "refactor", 110 | "remove", 111 | "revert", 112 | "test", 113 | ] 114 | example = "change: this is a custom change type" 115 | message_template = "{% if scope %}{{change_type}}({{scope}}): {{message}}{% else %}{{change_type}}: {{message}}{% endif %}{% if body %}\n\n{{body}}{% endif %}{% if is_breaking_change %}\n\nBREAKING CHANGE{% endif %}{% if footer %}\n\n{{footer}}{% endif %}" 116 | schema = "(): " 117 | schema_pattern = "^([a-z]+)(\\([\\w\\-\\.]+\\))?:\\s.*" 118 | 119 | [[tool.commitizen.customize.questions]] 120 | choices = [ 121 | { value = "change", name = "change: A change made to the codebase." }, 122 | { value = "ci", name = "ci: Changes to our CI configuration files and scripts." }, 123 | { value = "docs", name = "docs: Documentation only changes." }, 124 | { value = "feat", name = "feat: A new feature." }, 125 | { value = "fix", name = "fix: A bug fix." }, 126 | { value = "refactor", name = "refactor: A code change that neither fixes a bug nor adds a feature." }, 127 | { value = "remove", name = "remove: Removing code or files." }, 128 | { value = "revert", name = "revert: Revert to a commit." }, 129 | { value = "test", name = "test: Test script changes." }, 130 | ] 131 | message = "Select the TYPE of change you are committing" 132 | name = "change_type" 133 | type = "list" 134 | 135 | [[tool.commitizen.customize.questions]] 136 | message = "What is the SCOPE of this change (press enter to skip)?" 137 | name = "scope" 138 | type = "input" 139 | 140 | [[tool.commitizen.customize.questions]] 141 | message = "Describe the changes made (SUMMARY of commit message):" 142 | name = "message" 143 | type = "input" 144 | 145 | [[tool.commitizen.customize.questions]] 146 | message = "Provide additional contextual information - commit message BODY: (press [enter] to skip)" 147 | name = "body" 148 | type = "input" 149 | 150 | [[tool.commitizen.customize.questions]] 151 | default = false 152 | message = "Is this a BREAKING CHANGE? Correlates with MAJOR in SemVer" 153 | name = "is_breaking_change" 154 | type = "confirm" 155 | 156 | [[tool.commitizen.customize.questions]] 157 | message = "Footer. Information about Breaking Changes and reference issues that this commit closes: (press [enter] to skip)" 158 | name = "footer" 159 | type = "input" 160 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espressif/conventional-precommit-linter/ce5f1291713a88eda63b0519dd825fda3759ca4e/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from conventional_precommit_linter.hook import rules_output_status 4 | 5 | 6 | @pytest.fixture() 7 | def default_rules_output_status(): 8 | return rules_output_status.copy() 9 | -------------------------------------------------------------------------------- /tests/test_custom_args.py: -------------------------------------------------------------------------------- 1 | import tempfile 2 | from typing import List 3 | from typing import Tuple 4 | 5 | import pytest 6 | 7 | from conventional_precommit_linter.hook import main 8 | from conventional_precommit_linter.hook import rules_output_status 9 | 10 | # Default values for the commit message format 11 | TYPES = 'change,ci,docs,feat,fix,refactor,remove,revert,fox' 12 | SCOPES = 'bootloader,bt,Bt,esp32,esp-rom,examples,examples*storage,rom,wifi' 13 | SUBJECT_MIN_LENGTH = 21 14 | SUBJECT_MAX_LENGTH = 53 15 | BODY_MAX_LINE_LENGTH = 107 16 | DEFAULT_ARGV_TUPLE: Tuple[str, ...] = ( 17 | '--types', 18 | TYPES, 19 | '--scopes', 20 | SCOPES, 21 | '--subject-min-length', 22 | str(SUBJECT_MIN_LENGTH), 23 | '--subject-max-length', 24 | str(SUBJECT_MAX_LENGTH), 25 | '--body-max-line-length', 26 | str(BODY_MAX_LINE_LENGTH), 27 | '--summary-uppercase', 28 | '--allow-breaking', 29 | ) 30 | 31 | 32 | # Construct the argument list for main with the new constants 33 | def get_argv_list(scope_case_insensitive_arg=False) -> List: 34 | argv_list: List = list(DEFAULT_ARGV_TUPLE) 35 | if scope_case_insensitive_arg: 36 | argv_list.append('--scope-case-insensitive') 37 | 38 | print(argv_list) 39 | return argv_list 40 | 41 | 42 | # Dynamic test naming based on the commit message 43 | def commit_message_id(commit_message): # pylint: disable=redefined-outer-name 44 | return commit_message[0] # Use the first line of the commit message as the test ID 45 | 46 | 47 | @pytest.fixture( 48 | params=[ 49 | ( 50 | # Expected PASS: Message with scope and body 51 | 'feat(bootloader): This is commit message with scope and body\n\nThis is a text of body', 52 | {}, 53 | get_argv_list(), 54 | ), 55 | ( 56 | # Expected PASS: Message with scope, without body 57 | 'change(wifi): This is commit message with scope without body', 58 | {}, 59 | get_argv_list(), 60 | ), 61 | ( 62 | # Expected PASS: Message with scope (with hyphen in scope), without body 63 | 'change(esp-rom): This is commit message with hyphen in scope', 64 | {}, 65 | get_argv_list(), 66 | ), 67 | ( 68 | # Expected PASS: Message with scope (with asterisk in scope), without body 69 | 'change(examples*storage): This is commit message with asterisk in scope', 70 | {}, 71 | get_argv_list(), 72 | ), 73 | ( 74 | # Expected FAIL: Message with not allowed scope and body 75 | 'feat(tomas): This is commit message with scope and body\n\nThis is a text of body', 76 | {'error_scope_allowed': True}, 77 | get_argv_list(), 78 | ), 79 | ( 80 | # Expected FAIL: Message with scope (with comma in scope), without body 81 | 'change(examples,storage): This is commit message with comma in scope', 82 | {'error_scope_allowed': True}, 83 | get_argv_list(), 84 | ), 85 | ( 86 | # Expected PASS: Message with scope (with slash in scope), without body 87 | 'change(examples/storage): This is commit message with slash in scope', 88 | {'error_scope_allowed': True}, 89 | get_argv_list(), 90 | ), 91 | ( 92 | # Expected PASS: Message without scope, with body 93 | "change: This is commit message without scope with body\n\nThis is a text of body\n# Please enter the commit message for your changes. Lines starting\n# with '#' will be ignored, and an empty message aborts the commit.\n#", 94 | {}, 95 | get_argv_list(), 96 | ), 97 | ( 98 | # Expected PASS: Message without scope, without body 99 | 'change: This is commit message without scope and body', 100 | {}, 101 | get_argv_list(), 102 | ), 103 | ( 104 | # Expected PASS: Test of additional types 105 | 'fox(esp32): Testing additional types\n\nThis is a text of body', 106 | {}, 107 | get_argv_list(), 108 | ), 109 | ( 110 | # Expected PASS: 'body' line longer (custom arg 107 chars) 111 | 'fix(bt): Update database schemas\n\nUpdating the database schema to include fields and user profile preferences, cleaning up unnecessary calls', 112 | {}, 113 | get_argv_list(), 114 | ), 115 | ( 116 | # Expected PASS: Message without scope with exclamation mark 117 | 'change!: This is commit with exclamation mark', 118 | {}, 119 | get_argv_list(), 120 | ), 121 | ( 122 | # Expected PASS: Message with scope with exclamation mark 123 | 'change(rom)!: This is commit with exclamation mark', 124 | {}, 125 | get_argv_list(), 126 | ), 127 | ( 128 | # Expected PASS: FIXUP Message with scope and body 129 | 'fixup! feat(bootloader): This is fixup commit message with scope and body\n\nThis is a text of body', 130 | {}, 131 | get_argv_list(), 132 | ), 133 | ( 134 | # Expected PASS: SQUASH Message with scope and body 135 | 'squash! feat(bootloader): This is fixup commit message with scope and body\n\nThis is a text of body', 136 | {}, 137 | get_argv_list(), 138 | ), 139 | ( 140 | # Expected FAIL: Message with scope with 2 exclamation marks 141 | 'change(rom)!!: This is commit message with 2 exclamations', 142 | {'error_type': True}, 143 | get_argv_list(), 144 | ), 145 | ( 146 | # Expected FAIL: missing colon between 'type' (and 'scope') and 'summary' 147 | 'change this is commit message without body', 148 | {'missing_colon': True}, 149 | get_argv_list(), 150 | ), 151 | ( 152 | # Expected FAIL: empty commit message 153 | ' \n\n \n', 154 | {'empty_message': True}, 155 | get_argv_list(), 156 | ), 157 | ( 158 | # Expected FAIL: 'summary' too short 159 | 'fix: Fix bug', 160 | {'error_summary_length': True}, 161 | get_argv_list(), 162 | ), 163 | ( 164 | # Expected FAIL: 'summary' too long 165 | 'change(rom): Refactor authentication flow for enhanced security measures', 166 | {'error_summary_length': True}, 167 | get_argv_list(), 168 | ), 169 | ( 170 | # Expected FAIL: 'summary' ends with period 171 | 'change(rom): Fixed the another bug.', 172 | {'error_summary_period': True}, 173 | get_argv_list(), 174 | ), 175 | ( 176 | # Expected FAIL: 'summary' starts with lowercase 177 | 'change(rom): this message starts with lowercase', 178 | {'error_summary_capitalization': True}, 179 | get_argv_list(), 180 | ), 181 | ( 182 | # Expected FAIL: uppercase in 'scope', no body 183 | 'fix(dangerGH): Update token permissions - allow Danger to add comments to PR', 184 | { 185 | 'error_scope_capitalization': True, 186 | 'error_summary_length': True, 187 | 'error_scope_allowed': True, 188 | }, 189 | get_argv_list(), 190 | ), 191 | ( 192 | # Expected FAIL: not allowed 'type' with scope and body 193 | 'delete(bt): Added new feature with change\n\nThis feature adds functionality', 194 | {'error_type': True}, 195 | get_argv_list(), 196 | ), 197 | ( 198 | # Expected FAIL: not allowed 'type' without scope and without body 199 | 'delete: Added new feature with change', 200 | {'error_type': True}, 201 | get_argv_list(), 202 | ), 203 | ( 204 | # Expected FAIL: not allowed 'type' without scope and without body 205 | 'wip: Added new feature with change', 206 | {'error_type': True}, 207 | get_argv_list(), 208 | ), 209 | ( 210 | # Expected FAIL: not allowed 'type' (type starts with uppercase) 211 | 'Fix(bt): Added new feature with change\n\nThis feature adds functionality', 212 | {'error_type': True}, 213 | get_argv_list(), 214 | ), 215 | ( 216 | # Expected FAIL: missing blank line between 'summary' and 'body' 217 | 'change: Added new feature with change\nThis feature adds functionality', 218 | {'error_body_format': True}, 219 | get_argv_list(), 220 | ), 221 | ( 222 | # Expected FAIL: 'body' line too long 223 | 'fix(bt): Update database schemas\n\nUpdating the database schema to include new fields and user profile preferences, cleaning up unnecessary calls', 224 | {'error_body_length': True}, 225 | get_argv_list(), 226 | ), 227 | ( 228 | # Expected FAIL: 'scope' missing parentheses 229 | 'fix(bt: Update database schemas\n\nUpdating the database schema to include new fields.', 230 | {'error_scope_format': True}, 231 | get_argv_list(), 232 | ), 233 | ( 234 | # Expected FAIL: allowed special 'type', uppercase in 'scope' required, 'summary' too long, 'summary' ends with period 235 | 'fox(BT): update database schemas. Updating the database schema to include new fields and user profile preferences, cleaning up unnecessary calls.', 236 | { 237 | 'error_summary_capitalization': True, 238 | 'error_scope_allowed': True, 239 | 'error_scope_capitalization': True, 240 | 'error_summary_length': True, 241 | 'error_summary_period': True, 242 | }, 243 | get_argv_list(), 244 | ), 245 | ( 246 | # Expected FAIL: uppercase in 'scope' 247 | 'change(Bt): Added new feature with change\n\nThis feature adds functionality', 248 | { 249 | 'error_scope_capitalization': True, 250 | }, 251 | get_argv_list(), 252 | ), 253 | ( 254 | # Expected Pass: Allowed uppercase in 'scope'. 255 | 'change(Bt): Added new feature with change\n\nThis feature adds functionality', 256 | {}, 257 | get_argv_list(scope_case_insensitive_arg=True), 258 | ), 259 | ( 260 | # Expected Fail: Summary ending with a period '.' 261 | 'change(Bt): Added new feature with change.', 262 | {'error_summary_period': True}, 263 | get_argv_list(scope_case_insensitive_arg=True), 264 | ), 265 | ], 266 | # Use the commit message to generate IDs for each test case 267 | ids=commit_message_id, 268 | ) 269 | def commit_message(request, default_rules_output_status): 270 | # Combine the default dictionary with the test-specific dictionary 271 | combined_output = {**default_rules_output_status, **request.param[1]} 272 | return request.param[0], combined_output, request.param[2] 273 | 274 | 275 | def test_commit_message_with_args(commit_message, default_rules_output_status): # pylint: disable=redefined-outer-name 276 | message_text, expected_output, cli_arguments = commit_message 277 | 278 | # Reset rules_output_status to its default state before each test case 279 | rules_output_status.clear() 280 | rules_output_status.update(default_rules_output_status) 281 | 282 | # Create a temporary file to mock a commit message file input 283 | with tempfile.NamedTemporaryFile(mode='w', delete=False) as temp: 284 | temp.write(message_text) 285 | temp_file_name = temp.name 286 | 287 | cli_arguments.append(temp_file_name) 288 | 289 | # Run the main function of your script with the temporary file and arguments 290 | main(cli_arguments) 291 | 292 | # Retrieve the actual rules_output_status after running the main function 293 | actual_output = rules_output_status 294 | 295 | # Assert that the actual rules_output_status matches the expected output 296 | assert actual_output == expected_output, f'Failed on commit message: {message_text}' 297 | -------------------------------------------------------------------------------- /tests/test_default_args.py: -------------------------------------------------------------------------------- 1 | import tempfile 2 | 3 | import pytest 4 | 5 | from conventional_precommit_linter.hook import main 6 | from conventional_precommit_linter.hook import rules_output_status 7 | 8 | # Default values for the commit message format 9 | TYPES = 'change, ci, docs, feat, fix, refactor, remove, revert, test' 10 | SUBJECT_MIN_LENGTH = 20 11 | SUBJECT_MAX_LENGTH = 72 12 | BODY_MAX_LINE_LENGTH = 100 13 | 14 | 15 | # Dynamic test naming based on the commit message 16 | def commit_message_id(commit_message): # pylint: disable=redefined-outer-name 17 | return commit_message[0] # Use the first line of the commit message as the test ID 18 | 19 | 20 | @pytest.fixture( 21 | params=[ 22 | ( 23 | # Expected PASS: Message with scope and body 24 | 'feat(bootloader): This is commit message with scope and body\n\nThis is a text of body', 25 | {}, 26 | ), 27 | ( 28 | # Expected PASS: Message with scope and body, type "test" 29 | 'test(sync-script): This is commit message with scope and type test\n\nThis is a text of body', 30 | {}, 31 | ), 32 | ( 33 | # Expected PASS: Message with scope, without body 34 | 'change(wifi): This is commit message with scope without body', 35 | {}, 36 | ), 37 | ( 38 | # Expected PASS: Message with scope (with hyphen in scope), without body 39 | 'change(esp-rom): This is commit message with hyphen in scope', 40 | {}, 41 | ), 42 | ( 43 | # Expected PASS: Message with scope (with asterisk in scope), without body 44 | 'change(examples*storage): This is commit message with asterisk in scope', 45 | {}, 46 | ), 47 | ( 48 | # Expected PASS: Message with scope (with comma in scope), without body 49 | 'change(examples,storage): This is commit message with comma in scope', 50 | {}, 51 | ), 52 | ( 53 | # Expected PASS: Message with scope (with slash in scope), without body 54 | 'change(examples/storage): This is commit message with slash in scope', 55 | {}, 56 | ), 57 | ( 58 | # Expected PASS: Message without scope, with body 59 | "change: This is commit message without scope with body\n\nThis is a text of body\n# Please enter the commit message for your changes. Lines starting\n# with '#' will be ignored, and an empty message aborts the commit.\n#", 60 | {}, 61 | ), 62 | ( 63 | # Expected PASS: Message without scope, without body 64 | 'change: This is commit message without scope and body', 65 | {}, 66 | ), 67 | ( 68 | # Expected PASS: 'summary' starts with lowercase 69 | 'change(rom): this message starts with lowercase', 70 | {}, 71 | ), 72 | ( 73 | # Expected PASS: FIXUP Message with scope and body 74 | 'fixup! feat(bootloader): This is fixup commit message with scope and body\n\nThis is a text of body', 75 | {}, 76 | ), 77 | ( 78 | # Expected PASS: SQUASH Message with scope and body 79 | 'squash! feat(bootloader): This is fixup commit message with scope and body\n\nThis is a text of body', 80 | {}, 81 | ), 82 | ( 83 | # Expected FAIL: Message without scope with exclamation mark 84 | 'change!: This is commit message without scope but with exclamation mark', 85 | {'error_breaking': True}, 86 | ), 87 | ( 88 | # Expected FAIL: Message with scope with exclamation mark 89 | 'change(rom)!: This is commit message with scope and with exclamation mark', 90 | {'error_breaking': True}, 91 | ), 92 | ( 93 | # Expected FAIL: Message with scope with 2 exclamation marks 94 | 'change(rom)!!: This is commit message with !!', 95 | {'error_type': True}, 96 | ), 97 | ( 98 | # Expected FAIL: missing colon between 'type' (and 'scope') and 'summary' 99 | 'change this is commit message without body', 100 | {'missing_colon': True}, 101 | ), 102 | ( 103 | # Expected FAIL: empty commit message 104 | ' \n\n \n', 105 | {'empty_message': True}, 106 | ), 107 | ( 108 | # Expected FAIL: 'summary' too short 109 | 'fix: Fix bug', 110 | {'error_summary_length': True}, 111 | ), 112 | ( 113 | # Expected FAIL: 'summary' too long 114 | 'change(rom): Refactor authentication flow for enhanced security measures and improved user experience', 115 | {'error_summary_length': True}, 116 | ), 117 | ( 118 | # Expected FAIL: 'summary' ends with period 119 | 'change(rom): Fixed the another bug.', 120 | {'error_summary_period': True}, 121 | ), 122 | ( 123 | # Expected FAIL: uppercase in 'scope', with body 124 | 'change(Bt): Added new feature with change\n\nThis feature adds functionality', 125 | {'error_scope_capitalization': True}, 126 | ), 127 | ( 128 | # Expected FAIL: uppercase in 'scope', no body 129 | 'fix(dangerGH): Update token permissions - allow Danger to add comments to PR', 130 | {'error_scope_capitalization': True}, 131 | ), 132 | ( 133 | # Expected FAIL: not allowed 'type' with scope and body 134 | 'delete(bt): Added new feature with change\n\nThis feature adds functionality', 135 | {'error_type': True}, 136 | ), 137 | ( 138 | # Expected FAIL: not allowed 'type' without scope and without body 139 | 'fox: Added new feature with change', 140 | {'error_type': True}, 141 | ), 142 | ( 143 | # Expected FAIL: not allowed 'type' (type starts with uppercase) 144 | 'Fix(bt): Added new feature with change\n\nThis feature adds functionality', 145 | {'error_type': True}, 146 | ), 147 | ( 148 | # Expected Fail: partial type 149 | 'chan: This is commit message with partial type', 150 | {'error_type': True}, 151 | ), 152 | ( 153 | # Expected FAIL: missing blank line between 'summary' and 'body' 154 | 'change: Added new feature with change\nThis feature adds functionality', 155 | {'error_body_format': True}, 156 | ), 157 | ( 158 | # Expected FAIL: 'body' line too long 159 | 'fix(bt): Update database schemas\n\nUpdating the database schema to include new fields and user profile preferences, cleaning up unnecessary calls', 160 | {'error_body_length': True}, 161 | ), 162 | ( 163 | # Expected FAIL: 'scope' missing parenthese 164 | 'fix(bt: Update database schemas\n\nUpdating the database schema to include new fields.', 165 | {'error_scope_format': True}, 166 | ), 167 | ( 168 | # Expected FAIL: wrong 'type', uppercase in 'scope', 'summary' too long, 'summary' ends with period 169 | 'fox(BT): Update database schemas. Updating the database schema to include new fields and user profile preferences, cleaning up unnecessary calls.', 170 | { 171 | 'error_scope_capitalization': True, 172 | 'error_summary_length': True, 173 | 'error_summary_period': True, 174 | 'error_type': True, 175 | }, 176 | ), 177 | ], 178 | # Use the commit message to generate IDs for each test case 179 | ids=commit_message_id, 180 | ) 181 | def commit_message(request, default_rules_output_status): 182 | # Combine the default dictionary with the test-specific dictionary 183 | combined_output = {**default_rules_output_status, **request.param[1]} 184 | return request.param[0], combined_output 185 | 186 | 187 | def test_commit_message(commit_message, default_rules_output_status): # pylint: disable=redefined-outer-name 188 | message_text, expected_output = commit_message 189 | 190 | # Reset rules_output_status to its default state before each test case 191 | rules_output_status.clear() 192 | rules_output_status.update(default_rules_output_status) 193 | 194 | # Create a temporary file to mock a commit message file input 195 | with tempfile.NamedTemporaryFile(mode='w', delete=False) as temp: 196 | temp.write(message_text) 197 | temp_file_name = temp.name 198 | 199 | main([temp_file_name]) # Pass the file name as a positional argument 200 | 201 | # Retrieve the actual rules_output_status after running the main function 202 | actual_output = rules_output_status 203 | 204 | # Assert that the actual rules_output_status matches the expected output 205 | assert actual_output == expected_output, f'Failed on commit message: {message_text}' 206 | --------------------------------------------------------------------------------