├── .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 |

8 |

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 |
--------------------------------------------------------------------------------