Skip to content

Commit 80e05bf

Browse files
committed
feat(scope): add optional restriction to available scopes
1 parent 511c1fb commit 80e05bf

File tree

3 files changed

+85
-29
lines changed

3 files changed

+85
-29
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ After modifying `.pre-commit-config.yaml`, re-run the install command (`pre-comm
8080

8181
The linter accepts several configurable parameters to tailor commit message validation:
8282
- `--types`: Define the types of commits allowed (default: [`change`, `ci`, `docs`, `feat`, `fix`, `refactor`, `remove`, `revert`]).
83+
- `--scopes`: Specifies a list of allowed scopes. If not defined, all scopes are allowed (restriction is `disabled`).
8384
- `--subject-min-length`: Set the minimum length for the summary (default: `20`).
8485
- `--subject-max-length`: Set the maximum length for the summary (default: `72`).
8586
- `--body-max-line-length`: Set the maximum line length for the body (default: `100`).
@@ -96,6 +97,7 @@ The **custom configuration** can be specified in `.pre-commit-config.yaml` like
9697
stages: [commit-msg]
9798
args:
9899
- --types=change,ci,docs,feat,fix,refactor,remove,revert,fox
100+
- --scopes=bt,wifi,ethernet
99101
- --subject-min-length=10
100102
```
101103

@@ -137,7 +139,7 @@ We welcome contributions! To contribute to this repository, please follow these
137139

138140
... or with arguments:
139141
```sh
140-
python -m conventional_precommit_linter.hook --subject-min-length 20 --subject-max-length 72 --body-max-line-length 100 test_message.txt
142+
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"
141143
```
142144

143145

conventional_precommit_linter/hook.py

Lines changed: 58 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
'empty_message': False,
1717
'error_body_format': False,
1818
'error_body_length': False,
19+
'error_scope_allowed': False,
1920
'error_scope_capitalization': False,
2021
'error_scope_format': False,
2122
'error_summary_capitalization': False,
@@ -26,11 +27,17 @@
2627
}
2728

2829

29-
def allowed_types(args: argparse.Namespace) -> str:
30+
def get_allowed_types(args: argparse.Namespace) -> List[str]:
3031
default_types = ['change', 'ci', 'docs', 'feat', 'fix', 'refactor', 'remove', 'revert']
3132
# Provided types take precedence over default types
32-
types = args.types[0].split(',') if args.types else default_types
33-
return ', '.join(types)
33+
types: List[str] = args.types[0].split(',') if args.types else default_types
34+
return types
35+
36+
37+
def get_allowed_scopes(args: argparse.Namespace) -> List[str]:
38+
default_scopes: List[str] = []
39+
scopes: List[str] = args.scopes[0].split(',') if args.scopes else default_scopes
40+
return scopes
3441

3542

3643
def read_commit_message(file_path: str) -> str:
@@ -69,17 +76,22 @@ def check_colon_after_type(message_title: str) -> bool:
6976

7077
def check_allowed_types(commit_type: str, args: argparse.Namespace) -> None:
7178
"""Check for allowed types."""
72-
types = allowed_types(args)
79+
types: List[str] = get_allowed_types(args)
7380
if commit_type not in types:
7481
rules_output_status['error_type'] = True
7582

7683

77-
def check_scope(commit_scope: str) -> None:
84+
def check_scope(commit_scope: str, args: argparse.Namespace) -> None:
7885
"""Check for scope capitalization and allowed characters"""
7986
regex_scope = r'^[a-z0-9_/.,*-]*$'
8087
if commit_scope and not re.match(regex_scope, commit_scope):
8188
rules_output_status['error_scope_capitalization'] = True
8289

90+
# Check against the list of allowed scopes if provided
91+
allowed_scopes: List[str] = get_allowed_scopes(args)
92+
if allowed_scopes and commit_scope not in allowed_scopes:
93+
rules_output_status['error_scope_allowed'] = True
94+
8395

8496
def check_summary_length(commit_summary: str, args: argparse.Namespace) -> None:
8597
"""Check for summary length (between min and max allowed characters)"""
@@ -125,23 +137,45 @@ def print_report(commit_type: str, commit_scope: Optional[str], commit_summary:
125137
f'{_color_purple(commit_type)}({ _color_blue( commit_scope)}): { _color_orange( commit_summary)}'
126138
)
127139

128-
# Rule messages that are always included
129-
rule_messages = [
130-
f"{_get_icon_for_rule(rules_output_status['error_type'])} {_color_purple('<type>')} is mandatory, use one of the following: [{_color_purple(allowed_types(args))}]",
131-
f"{_get_icon_for_rule(rules_output_status['error_scope_format'])} {_color_blue('(<optional-scope>)')} if used, must be enclosed in parentheses",
132-
f"{_get_icon_for_rule(rules_output_status['error_scope_capitalization'])} {_color_blue('(<optional-scope>)')} if used, must be written in lower case without whitespace",
133-
f"{_get_icon_for_rule(rules_output_status['error_summary_period'])} {_color_orange('<summary>')} must not end with a period",
134-
f"{_get_icon_for_rule(rules_output_status['error_summary_length'])} {_color_orange('<summary>')} must be between {args.subject_min_length} and {args.subject_max_length} characters long",
135-
f"{_get_icon_for_rule(rules_output_status['error_body_length'])} {_color_grey('<body>')} lines must be no longer than {args.body_max_line_length} characters",
136-
f"{_get_icon_for_rule(rules_output_status['error_body_format'])} {_color_grey('<body>')} must be separated from the 'summary' by a blank line",
137-
]
138-
139-
# Dynamically add the additional rules set by arguments
140+
rule_messages: List[str] = []
141+
142+
# TYPES messages
143+
rule_messages.append(
144+
f"{_get_icon_for_rule(rules_output_status['error_type'])} {_color_purple('<type>')} is mandatory, use one of the following: [{_color_purple(', '.join(get_allowed_types(args)))}]"
145+
)
146+
147+
# SCOPE messages
148+
rule_messages.append(
149+
f"{_get_icon_for_rule(rules_output_status['error_scope_format'])} {_color_blue('(<optional-scope>)')} if used, must be enclosed in parentheses"
150+
)
151+
rule_messages.append(
152+
f"{_get_icon_for_rule(rules_output_status['error_scope_capitalization'])} {_color_blue('(<optional-scope>)')} if used, must be written in lower case without whitespace"
153+
)
154+
if args.scopes:
155+
rule_messages.append(
156+
f"{_get_icon_for_rule(rules_output_status['error_scope_allowed'])} {_color_blue('(<optional-scope>)')} if used, must be one of the following allowed scopes: [{_color_blue(', '.join(args.scopes))}]"
157+
)
158+
159+
# SUMMARY messages
160+
rule_messages.append(
161+
f"{_get_icon_for_rule(rules_output_status['error_summary_period'])} {_color_orange('<summary>')} must not end with a period"
162+
)
163+
rule_messages.append(
164+
f"{_get_icon_for_rule(rules_output_status['error_summary_length'])} {_color_orange('<summary>')} must be between {args.subject_min_length} and {args.subject_max_length} characters long"
165+
)
140166
if args.summary_uppercase:
141167
rule_messages.append(
142168
f"{_get_icon_for_rule(rules_output_status['error_summary_capitalization'])} {_color_orange('<summary>')} must start with an uppercase letter"
143169
)
144170

171+
# BODY messages
172+
rule_messages.append(
173+
f"{_get_icon_for_rule(rules_output_status['error_body_length'])} {_color_grey('<body>')} lines must be no longer than {args.body_max_line_length} characters"
174+
)
175+
rule_messages.append(
176+
f"{_get_icon_for_rule(rules_output_status['error_body_format'])} {_color_grey('<body>')} must be separated from the 'summary' by a blank line"
177+
)
178+
145179
# Combine the rule messages into the final report block
146180
message_rules_block = ' ' + '\n '.join(rule_messages)
147181

@@ -165,11 +199,14 @@ def parse_args(argv: List[str]) -> argparse.Namespace:
165199
parser = argparse.ArgumentParser(
166200
prog='conventional-pre-commit', description='Check a git commit message for Conventional Commits formatting.'
167201
)
168-
parser.add_argument('--types', type=str, nargs='*', help='Optional list of types to support')
202+
parser.add_argument('--types', type=str, nargs='*', help="Redefine the list of allowed 'Types'")
203+
parser.add_argument('--scopes', type=str, nargs='*', help="Setting the list of allowed 'Scopes'")
169204
parser.add_argument('--subject-min-length', type=int, default=20, help="Minimum length of the 'Summary'")
170205
parser.add_argument('--subject-max-length', type=int, default=72, help="Maximum length of the 'Summary'")
171-
parser.add_argument('--body-max-line-length', type=int, default=100, help='Maximum length of the body line')
172-
parser.add_argument('--summary-uppercase', action='store_true', help='Summary must start with an uppercase letter')
206+
parser.add_argument('--body-max-line-length', type=int, default=100, help="Maximum length of the 'Body' line")
207+
parser.add_argument(
208+
'--summary-uppercase', action='store_true', help="'Summary' must start with an uppercase letter"
209+
)
173210
parser.add_argument('input', type=str, help='A file containing a git commit message')
174211
return parser.parse_args(argv)
175212

@@ -200,7 +237,7 @@ def main(argv: Optional[List[str]] = None) -> int:
200237
# Commit message title (first line) checks
201238
check_allowed_types(commit_type, args)
202239
if commit_scope:
203-
check_scope(commit_scope)
240+
check_scope(commit_scope, args)
204241
check_summary_length(commit_summary, args)
205242
check_summary_period(commit_summary)
206243
if args.summary_uppercase:

tests/test_custom_args.py

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
from conventional_precommit_linter.hook import rules_output_status
77

88
# Default values for the commit message format
9-
TYPES = 'change, ci, docs, feat, fix, refactor, remove, revert, fox'
9+
TYPES = 'change,ci,docs,feat,fix,refactor,remove,revert,fox'
10+
SCOPES = 'bootloader,bt,esp32,esp-rom,examples,examples*storage,rom,wifi'
1011
SUBJECT_MIN_LENGTH = 21
1112
SUBJECT_MAX_LENGTH = 53
1213
BODY_MAX_LINE_LENGTH = 107
@@ -17,6 +18,7 @@
1718
'empty_message': False,
1819
'error_body_format': False,
1920
'error_body_length': False,
21+
'error_scope_allowed': False,
2022
'error_scope_capitalization': False,
2123
'error_scope_format': False,
2224
'error_summary_capitalization': False,
@@ -55,14 +57,19 @@ def commit_message_id(commit_message): # pylint: disable=redefined-outer-name
5557
{},
5658
),
5759
(
58-
# Expected PASS: Message with scope (with comma in scope), without body
60+
# Expected FAIL: Message with not allowed scope and body
61+
'feat(tomas): This is commit message with scope and body\n\nThis is a text of body',
62+
{'error_scope_allowed': True},
63+
),
64+
(
65+
# Expected FAIL: Message with scope (with comma in scope), without body
5966
'change(examples,storage): This is commit message with comma in scope',
60-
{},
67+
{'error_scope_allowed': True},
6168
),
6269
(
6370
# Expected PASS: Message with scope (with slash in scope), without body
6471
'change(examples/storage): This is commit message with slash in scope',
65-
{},
72+
{'error_scope_allowed': True},
6673
),
6774
(
6875
# Expected PASS: Message without scope, with body
@@ -117,12 +124,19 @@ def commit_message_id(commit_message): # pylint: disable=redefined-outer-name
117124
(
118125
# Expected FAIL: uppercase in 'scope', with body
119126
'change(Bt): Added new feature with change\n\nThis feature adds functionality',
120-
{'error_scope_capitalization': True},
127+
{
128+
'error_scope_capitalization': True,
129+
'error_scope_allowed': True,
130+
},
121131
),
122132
(
123133
# Expected FAIL: uppercase in 'scope', no body
124134
'fix(dangerGH): Update token permissions - allow Danger to add comments to PR',
125-
{'error_scope_capitalization': True, 'error_summary_length': True},
135+
{
136+
'error_scope_capitalization': True,
137+
'error_summary_length': True,
138+
'error_scope_allowed': True,
139+
},
126140
),
127141
(
128142
# Expected FAIL: not allowed 'type' with scope and body
@@ -155,7 +169,7 @@ def commit_message_id(commit_message): # pylint: disable=redefined-outer-name
155169
{'error_body_length': True},
156170
),
157171
(
158-
# Expected FAIL: 'scope' missing parenthese
172+
# Expected FAIL: 'scope' missing parentheses
159173
'fix(bt: Update database schemas\n\nUpdating the database schema to include new fields.',
160174
{'error_scope_format': True},
161175
),
@@ -164,6 +178,7 @@ def commit_message_id(commit_message): # pylint: disable=redefined-outer-name
164178
'fox(BT): update database schemas. Updating the database schema to include new fields and user profile preferences, cleaning up unnecessary calls.',
165179
{
166180
'error_summary_capitalization': True,
181+
'error_scope_allowed': True,
167182
'error_scope_capitalization': True,
168183
'error_summary_length': True,
169184
'error_summary_period': True,
@@ -195,6 +210,8 @@ def test_commit_message_with_args(commit_message): # pylint: disable=redefined-
195210
argv = [
196211
'--types',
197212
TYPES,
213+
'--scopes',
214+
SCOPES,
198215
'--subject-min-length',
199216
str(SUBJECT_MIN_LENGTH),
200217
'--subject-max-length',

0 commit comments

Comments
 (0)