Skip to content

Commit 4189955

Browse files
committed
Add more options for CT1
This adds two options to the conventional commit contrib linter: - An optional whitelist of scopes (nouns) - An option to require a scope to be specified
1 parent 4d91197 commit 4189955

File tree

4 files changed

+76
-12
lines changed

4 files changed

+76
-12
lines changed

docs/rules/contrib_rules.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ Enforces [Conventional Commits](https://www.conventionalcommits.org/) commit mes
5555
| Name | Type | Default | gitlint version | Description |
5656
| ------------- | -------------- | ------------- | ---------------------------------- | ----------------------------- |
5757
| `types` | `#!python str` | `fix,feat,chore,docs,style,refactor,perf,test,revert,ci,build` | [:octicons-tag-24: v0.12.0][v0.12.0] | Comma separated list of allowed commit types. |
58-
58+
| `scopes` | `#!python str` | | | Comma separated list of allowed scopes. An empty list will allow anything. |
59+
| `require-scope` | `#!python bool` | `False` | | Whether to require a scope. |
5960

6061
=== ":octicons-file-code-16: .gitlint"
6162

gitlint-core/gitlint/contrib/rules/conventional_commit.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import re
22

3-
from gitlint.options import ListOption
3+
from gitlint.options import ListOption, BoolOption
44
from gitlint.rules import CommitMessageTitle, LineRule, RuleViolation
55

6-
RULE_REGEX = re.compile(r"([^(]+?)(\([^)]+?\))?!?: .+")
6+
RULE_REGEX = re.compile(r"([^(]+?)(\(([^)]+?\)))?!?: .+")
77

88

99
class ConventionalCommit(LineRule):
@@ -18,7 +18,13 @@ class ConventionalCommit(LineRule):
1818
"types",
1919
["fix", "feat", "chore", "docs", "style", "refactor", "perf", "test", "revert", "ci", "build"],
2020
"Comma separated list of allowed commit types.",
21-
)
21+
),
22+
ListOption(
23+
"scopes",
24+
[],
25+
"Comma separated list of allowed scopes. An empty list will allow anything.",
26+
),
27+
BoolOption("require-scope", False, "Whether to require a scope."),
2228
]
2329

2430
def validate(self, line, _commit):
@@ -34,4 +40,12 @@ def validate(self, line, _commit):
3440
opt_str = ", ".join(self.options["types"].value)
3541
violations.append(RuleViolation(self.id, f"Title does not start with one of {opt_str}", line))
3642

43+
line_scope = match.group(3)
44+
allowed_scopes = self.options["scopes"].value
45+
if line_scope and allowed_scopes and line_scope not in allowed_scopes:
46+
opt_str = ", ".join(self.options["scopes"].value)
47+
violations.append(RuleViolation(self.id, f"Scope is not one of {opt_str}", line))
48+
elif not line_scope and self.options["require-scope"].value:
49+
violations.append(RuleViolation(self.id, "Scope is required", line))
50+
3751
return violations

gitlint-core/gitlint/tests/config/test_config.py

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -135,14 +135,29 @@ def test_contrib(self):
135135
self.assertEqual(actual_rule.name, "contrib-title-conventional-commits")
136136
self.assertEqual(actual_rule.target, rules.CommitMessageTitle)
137137

138-
expected_rule_option = options.ListOption(
139-
"types",
140-
["fix", "feat", "chore", "docs", "style", "refactor", "perf", "test", "revert", "ci", "build"],
141-
"Comma separated list of allowed commit types.",
142-
)
138+
expected_rule_options = [
139+
options.ListOption(
140+
"types",
141+
["fix", "feat", "chore", "docs", "style", "refactor", "perf", "test", "revert", "ci", "build"],
142+
"Comma separated list of allowed commit types.",
143+
),
144+
options.ListOption(
145+
"scopes",
146+
[],
147+
"Comma separated list of allowed scopes. An empty list will allow anything.",
148+
),
149+
options.BoolOption("require-scope", False, "Whether to require a scope."),
150+
]
143151

144-
self.assertListEqual(actual_rule.options_spec, [expected_rule_option])
145-
self.assertDictEqual(actual_rule.options, {"types": expected_rule_option})
152+
self.assertListEqual(actual_rule.options_spec, expected_rule_options)
153+
self.assertDictEqual(
154+
actual_rule.options,
155+
{
156+
"types": expected_rule_options[0],
157+
"scopes": expected_rule_options[1],
158+
"require-scope": expected_rule_options[2],
159+
},
160+
)
146161

147162
# Check contrib-body-requires-signed-off-by contrib rule
148163
actual_rule = config.rules.find_rule("contrib-body-requires-signed-off-by")

gitlint-core/gitlint/tests/contrib/rules/test_conventional_commit.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,19 @@ def test_conventional_commits(self):
1616
rule = ConventionalCommit()
1717

1818
# No violations when using a correct type and format
19-
for type in ["fix", "feat", "chore", "docs", "style", "refactor", "perf", "test", "revert", "ci", "build"]:
19+
for type in [
20+
"fix",
21+
"feat",
22+
"chore",
23+
"docs",
24+
"style",
25+
"refactor",
26+
"perf",
27+
"test",
28+
"revert",
29+
"ci",
30+
"build",
31+
]:
2032
violations = rule.validate(type + ": föo", None)
2133
self.assertListEqual([], violations)
2234

@@ -80,3 +92,25 @@ def test_conventional_commits(self):
8092
for typ in ["föo123", "123bär"]:
8193
violations = rule.validate(typ + ": hür dur", None)
8294
self.assertListEqual([], violations)
95+
96+
# assert violation if scope is not in scopes
97+
rule = ConventionalCommit({"scopes": ["foo", "bar"]})
98+
violations = rule.validate("fix(baz): hellö", None)
99+
expected_violation = RuleViolation("CT1", "Scope is not one of foo, bar", "fix(baz): hellö")
100+
self.assertListEqual([expected_violation], violations)
101+
102+
# assert no violation if scopes is empty
103+
rule = ConventionalCommit({"scopes": []})
104+
violations = rule.validate("fix(scope): hellö", None)
105+
self.assertListEqual([], violations)
106+
107+
# assert violation if scope is required but not specified
108+
rule = ConventionalCommit({"require-scope": True})
109+
violations = rule.validate("fix: hellö", None)
110+
expected_violation = RuleViolation("CT1", "Scope is required", "fix: hellö")
111+
self.assertListEqual([expected_violation], violations)
112+
113+
# assert no violation if scope is not required and not specified
114+
rule = ConventionalCommit({"require-scope": False})
115+
violations = rule.validate("fix: hellö", None)
116+
self.assertListEqual([], violations)

0 commit comments

Comments
 (0)