diff --git a/docs/rules/contrib_rules.md b/docs/rules/contrib_rules.md index cf622b37..56425a9a 100644 --- a/docs/rules/contrib_rules.md +++ b/docs/rules/contrib_rules.md @@ -52,9 +52,10 @@ Enforces [Conventional Commits](https://www.conventionalcommits.org/) commit mes #### Options -| Name | Type | Default | gitlint version | Description | -| ------------- | -------------- | ------------- | ---------------------------------- | ----------------------------- | -| `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. | +| Name | Type | Default | gitlint version | Description | +|------------| -------------- |----------------------------------------------------------------|--------------------------------------|--------------------------------------------------| +| `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. | +| `scopes` | `#!python str` | | [:octicons-tag-24: v0.12.0][v0.12.0] | Optional comma separated list of allowed scopes. | === ":octicons-file-code-16: .gitlint" diff --git a/gitlint-core/gitlint/contrib/rules/conventional_commit.py b/gitlint-core/gitlint/contrib/rules/conventional_commit.py index 705b0839..fa066b09 100644 --- a/gitlint-core/gitlint/contrib/rules/conventional_commit.py +++ b/gitlint-core/gitlint/contrib/rules/conventional_commit.py @@ -3,7 +3,7 @@ from gitlint.options import ListOption from gitlint.rules import CommitMessageTitle, LineRule, RuleViolation -RULE_REGEX = re.compile(r"([^(]+?)(\([^)]+?\))?!?: .+") +RULE_REGEX = re.compile(r"([^(]+?)(?:\(([^)]+?)\))?!?: .+") class ConventionalCommit(LineRule): @@ -18,7 +18,12 @@ class ConventionalCommit(LineRule): "types", ["fix", "feat", "chore", "docs", "style", "refactor", "perf", "test", "revert", "ci", "build"], "Comma separated list of allowed commit types.", - ) + ), + ListOption( + "scopes", + [], + "Comma separated list of allowed scopes.", + ), ] def validate(self, line, _commit): @@ -28,10 +33,20 @@ def validate(self, line, _commit): if not match: msg = "Title does not follow ConventionalCommits.org format 'type(optional-scope): description'" violations.append(RuleViolation(self.id, msg, line)) - else: - line_commit_type = match.group(1) - if line_commit_type not in self.options["types"].value: - opt_str = ", ".join(self.options["types"].value) - violations.append(RuleViolation(self.id, f"Title does not start with one of {opt_str}", line)) + return violations + + line_commit_type = match.group(1) + if line_commit_type not in self.options["types"].value: + opt_str = ", ".join(self.options["types"].value) + violations.append(RuleViolation(self.id, f"Title does not start with one of {opt_str}", line)) + + valid_scopes = self.options["scopes"].value + if not valid_scopes: + return violations + + line_commit_scope = match.group(2) + if line_commit_scope and line_commit_scope not in valid_scopes: + opt_str = ", ".join(valid_scopes) + violations.append(RuleViolation(self.id, f"Title does not use one of these scopes: {opt_str}", line)) return violations diff --git a/gitlint-core/gitlint/tests/config/test_config.py b/gitlint-core/gitlint/tests/config/test_config.py index 422bb33c..48f99520 100644 --- a/gitlint-core/gitlint/tests/config/test_config.py +++ b/gitlint-core/gitlint/tests/config/test_config.py @@ -135,14 +135,23 @@ def test_contrib(self): self.assertEqual(actual_rule.name, "contrib-title-conventional-commits") self.assertEqual(actual_rule.target, rules.CommitMessageTitle) - expected_rule_option = options.ListOption( - "types", - ["fix", "feat", "chore", "docs", "style", "refactor", "perf", "test", "revert", "ci", "build"], - "Comma separated list of allowed commit types.", - ) + expected_rule_options = [ + options.ListOption( + "types", + ["fix", "feat", "chore", "docs", "style", "refactor", "perf", "test", "revert", "ci", "build"], + "Comma separated list of allowed commit types.", + ), + options.ListOption( + "scopes", + [], + "Comma separated list of allowed scopes.", + ), + ] - self.assertListEqual(actual_rule.options_spec, [expected_rule_option]) - self.assertDictEqual(actual_rule.options, {"types": expected_rule_option}) + self.assertListEqual(actual_rule.options_spec, expected_rule_options) + self.assertDictEqual( + actual_rule.options, {"types": expected_rule_options[0], "scopes": expected_rule_options[1]} + ) # Check contrib-body-requires-signed-off-by contrib rule actual_rule = config.rules.find_rule("contrib-body-requires-signed-off-by") diff --git a/gitlint-core/gitlint/tests/contrib/rules/test_conventional_commit.py b/gitlint-core/gitlint/tests/contrib/rules/test_conventional_commit.py index cbab6842..2652dcce 100644 --- a/gitlint-core/gitlint/tests/contrib/rules/test_conventional_commit.py +++ b/gitlint-core/gitlint/tests/contrib/rules/test_conventional_commit.py @@ -80,3 +80,19 @@ def test_conventional_commits(self): for typ in ["föo123", "123bär"]: violations = rule.validate(typ + ": hür dur", None) self.assertListEqual([], violations) + + # assert no violation in case of valid scope + rule = ConventionalCommit({"scopes": ["föo", "bär"]}) + for scope in ["föo", "bär"]: + violations = rule.validate("feat(" + scope + "): yep", None) + self.assertListEqual([], violations) + + # assert violation in case of invalid scope + expected_violation = RuleViolation( + "CT1", + "Title does not use one of these scopes: föo, bär", + "feat(invalid): nope", + ) + rule = ConventionalCommit({"scopes": ["föo", "bär"]}) + violations = rule.validate("feat(invalid): nope", None) + self.assertListEqual([expected_violation], violations)