From d542b14cb5b2ee591d074230fe8df4f1b6c326d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20=C5=A0uppa?= Date: Sun, 18 Oct 2020 11:47:40 +0200 Subject: [PATCH] New Rule: title-min-length (#140) The title-min-length rule enforces a minimum length on commit titles. This closes #138 --- CHANGELOG.md | 1 + docs/configuration.md | 7 +++- docs/rules.md | 25 +++++++++++++- gitlint/config.py | 1 + gitlint/files/gitlint | 7 +++- gitlint/rules.py | 14 ++++++++ gitlint/tests/cli/test_cli.py | 2 +- .../tests/config/test_config_precedence.py | 8 ++--- .../tests/expected/cli/test_cli/test_debug_1 | 4 ++- .../cli/test_cli/test_input_stream_debug_2 | 2 ++ .../test_cli/test_lint_staged_msg_filename_2 | 2 ++ .../cli/test_cli/test_lint_staged_stdin_2 | 2 ++ .../expected/cli/test_cli/test_named_rules_2 | 2 ++ gitlint/tests/rules/test_title_rules.py | 34 ++++++++++++++++++- .../test_lint_staged_msg_filename_1 | 2 ++ .../test_commits/test_lint_staged_stdin_1 | 2 ++ .../test_config/test_config_from_env_1 | 2 ++ .../test_config/test_config_from_env_2 | 2 ++ .../test_config/test_config_from_file_debug_1 | 2 ++ 19 files changed, 111 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c72d12c..ed9e33fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Most general options can now be set through environment variables (e.g. set the `general.ignore` option via `GITLINT_IGNORE=T1,T2`). The list of available environment variables can be found in the [configuration documentation](TODO). - Users can now use `self.log.debug("my message")` for debugging purposes in their user-defined rules - Breaking: User-defined rule id's can no longer start with 'I', as those are reserved for built-in gitlint ignore rules. +- **New Rule**: [title-min-length](TODO) enforces a minimum length on titles (default: 5 chars) ([#130](https://github.com/jorisroovers/gitlint/issues/138)) - **New Rule**: [ignore-body-lines](TODO) allows users to [ignore parts of a commit](http://jorisroovers.github.io/gitlint/#ignoring-commits) by matching a regex against the lines in a commit message body. ([#126](https://github.com/jorisroovers/gitlint/issues/126)). diff --git a/docs/configuration.md b/docs/configuration.md index 7b8fa8a1..9f298b90 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -68,6 +68,11 @@ extra-path=examples/ [title-max-length] line-length=80 +# Conversely, you can also enforce minimal length of a title with the +# "title-min-length" rule: +# [title-min-length] +# min-length=5 + [title-must-not-contain-word] # Comma-separated list of words that should not occur in the title. Matching is case # insensitive. It's fine if the keyword occurs as part of a larger word (so "WIPING" @@ -449,4 +454,4 @@ GITLINT_STAGED=1 gitlint # using env variable #.gitlint [general] staged=true -``` \ No newline at end of file +``` diff --git a/docs/rules.md b/docs/rules.md index 427c8442..97bc012d 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -14,6 +14,7 @@ T4 | title-hard-tab | >= 0.1.0 | Title cannot contain h T5 | title-must-not-contain-word | >= 0.1.0 | Title cannot contain certain words (default: "WIP") T6 | title-leading-whitespace | >= 0.4.0 | Title cannot have leading whitespace (space or tab) T7 | title-match-regex | >= 0.5.0 | Title must match a given regex (default: None) +T8 | title-max-length | >= 0.14.0 | Title length must be > 5 chars. B1 | body-max-line-length | >= 0.1.0 | Lines in the body must be < 80 chars B2 | body-trailing-whitespace | >= 0.1.0 | Body cannot have trailing whitespace (space or tab) B3 | body-hard-tab | >= 0.1.0 | Body cannot contain hard tab characters (\t) @@ -126,6 +127,28 @@ regex | >= 0.5 | .* | [Python regex](https://docs.python. regex=^US[1-9][0-9]* ``` +## T8: title-min-length ## + +ID | Name | gitlint version | Description +------|-----------------------------|-----------------|------------------------------------------- +T1 | title-min-length | >= | Title length must be > 5 chars. + + +### Options + +Name | gitlint version | Default | Description +---------------|-----------------|---------|---------------------------------- +min-length | >= 0.14.0 | 5 | Minimum required title length + +### Examples + +#### .gitlint + +```ini +# Titles should be min 3 chars +[title-min-length] +min-length=3 +``` ## B1: body-max-line-length @@ -378,4 +401,4 @@ regex=(^Co-Authored-By)|(^Signed-Off-By) # Ignore lines that contain 'foobar' [ignore-body-lines] regex=(.*)foobar(.*) -``` \ No newline at end of file +``` diff --git a/gitlint/config.py b/gitlint/config.py index fff4972b..4dad7077 100644 --- a/gitlint/config.py +++ b/gitlint/config.py @@ -52,6 +52,7 @@ class LintConfig(object): rules.TitleHardTab, rules.TitleMustNotContainWord, rules.TitleRegexMatches, + rules.TitleMinLength, rules.BodyMaxLineLength, rules.BodyMinLength, rules.BodyMissing, diff --git a/gitlint/files/gitlint b/gitlint/files/gitlint index 0e3ca397..bec092f3 100644 --- a/gitlint/files/gitlint +++ b/gitlint/files/gitlint @@ -43,6 +43,11 @@ # [title-max-length] # line-length=50 +# Conversely, you can also enforce minimal length of a title with the +# "title-min-length" rule: +# [title-min-length] +# min-length=5 + # [title-must-not-contain-word] # Comma-separated list of words that should not occur in the title. Matching is case # insensitive. It's fine if the keyword occurs as part of a larger word (so "WIPING" @@ -111,4 +116,4 @@ # under [general] section above. # [contrib-title-conventional-commits] # Specify allowed commit types. For details see: https://www.conventionalcommits.org/ -# types = bugfix,user-story,epic \ No newline at end of file +# types = bugfix,user-story,epic diff --git a/gitlint/rules.py b/gitlint/rules.py index 5d045806..1cb50dab 100644 --- a/gitlint/rules.py +++ b/gitlint/rules.py @@ -243,6 +243,20 @@ def validate(self, title, _commit): return [RuleViolation(self.id, violation_msg, title)] +class TitleMinLength(LineRule): + name = "title-min-length" + id = "T8" + target = CommitMessageTitle + options_spec = [IntOption('min-length', 5, "Minimum required title length")] + + def validate(self, title, _commit): + min_length = self.options['min-length'].value + actual_length = len(title) + if actual_length < min_length: + violation_message = "Title is too short ({0}<{1})".format(actual_length, min_length) + return [RuleViolation(self.id, violation_message, title, 1)] + + class BodyMaxLineLength(MaxLineLength): name = "body-max-line-length" id = "B1" diff --git a/gitlint/tests/cli/test_cli.py b/gitlint/tests/cli/test_cli.py index 0c124d3a..88bcfb7a 100644 --- a/gitlint/tests/cli/test_cli.py +++ b/gitlint/tests/cli/test_cli.py @@ -365,7 +365,7 @@ def test_debug(self, sh, _): u"commit-2-branch-1\ncommit-2-branch-2\n", # git branch --contains u"commit-2/file-1\ncommit-2/file-2\n", # git diff-tree u"test åuthor3\x00test-email3@föo.com\x002016-12-05 15:28:15 +0100\x00abc\n" - u"föo\nbar", + u"föobar\nbar", u"commit-3-branch-1\ncommit-3-branch-2\n", # git branch --contains u"commit-3/file-1\ncommit-3/file-2\n", # git diff-tree ] diff --git a/gitlint/tests/config/test_config_precedence.py b/gitlint/tests/config/test_config_precedence.py index f131e977..a0eeccda 100644 --- a/gitlint/tests/config/test_config_precedence.py +++ b/gitlint/tests/config/test_config_precedence.py @@ -25,7 +25,7 @@ class LintConfigPrecedenceTests(BaseTestCase): def setUp(self): self.cli = CliRunner() - @patch('gitlint.cli.get_stdin_data', return_value=u"WIP\n\nThis is å test message\n") + @patch('gitlint.cli.get_stdin_data', return_value=u"WIP:fö\n\nThis is å test message\n") def test_config_precedence(self, _): # TODO(jroovers): this test really only test verbosity, we need to do some refactoring to gitlint.cli # to more easily test everything @@ -41,14 +41,14 @@ def test_config_precedence(self, _): with patch('gitlint.display.stderr', new=StringIO()) as stderr: result = self.cli.invoke(cli.cli, ["-vvv", "-c", "general.verbosity=2", "--config", config_path]) self.assertEqual(result.output, "") - self.assertEqual(stderr.getvalue(), "1: T5 Title contains the word 'WIP' (case-insensitive): \"WIP\"\n") + self.assertEqual(stderr.getvalue(), u"1: T5 Title contains the word 'WIP' (case-insensitive): \"WIP:fö\"\n") # 2. environment variables with patch('gitlint.display.stderr', new=StringIO()) as stderr: result = self.cli.invoke(cli.cli, ["-c", "general.verbosity=2", "--config", config_path], env={"GITLINT_VERBOSITY": "3"}) self.assertEqual(result.output, "") - self.assertEqual(stderr.getvalue(), "1: T5 Title contains the word 'WIP' (case-insensitive): \"WIP\"\n") + self.assertEqual(stderr.getvalue(), u"1: T5 Title contains the word 'WIP' (case-insensitive): \"WIP:fö\"\n") # 3. commandline -c flags with patch('gitlint.display.stderr', new=StringIO()) as stderr: @@ -66,7 +66,7 @@ def test_config_precedence(self, _): with patch('gitlint.display.stderr', new=StringIO()) as stderr: result = self.cli.invoke(cli.cli) self.assertEqual(result.output, "") - self.assertEqual(stderr.getvalue(), "1: T5 Title contains the word 'WIP' (case-insensitive): \"WIP\"\n") + self.assertEqual(stderr.getvalue(), u"1: T5 Title contains the word 'WIP' (case-insensitive): \"WIP:fö\"\n") @patch('gitlint.cli.get_stdin_data', return_value=u"WIP: This is å test") def test_ignore_precedence(self, get_stdin_data): diff --git a/gitlint/tests/expected/cli/test_cli/test_debug_1 b/gitlint/tests/expected/cli/test_cli/test_debug_1 index b6ef0e74..a95a58d9 100644 --- a/gitlint/tests/expected/cli/test_cli/test_debug_1 +++ b/gitlint/tests/expected/cli/test_cli/test_debug_1 @@ -39,6 +39,8 @@ target: {target} words=WIP,bögus T7: title-match-regex regex=None + T8: title-min-length + min-length=5 B1: body-max-line-length line-length=30 B5: body-min-length @@ -103,7 +105,7 @@ DEBUG: gitlint.git ('branch', '--contains', '4da2656b0dadc76c7ee3fd0243a96cb6400 DEBUG: gitlint.git ('diff-tree', '--no-commit-id', '--name-only', '-r', '--root', '4da2656b0dadc76c7ee3fd0243a96cb64007f125') DEBUG: gitlint.lint Commit Object --- Commit Message ---- -föo +föobar bar --- Meta info --------- Author: test åuthor3 diff --git a/gitlint/tests/expected/cli/test_cli/test_input_stream_debug_2 b/gitlint/tests/expected/cli/test_cli/test_input_stream_debug_2 index 86f2a1ba..c05d147d 100644 --- a/gitlint/tests/expected/cli/test_cli/test_input_stream_debug_2 +++ b/gitlint/tests/expected/cli/test_cli/test_input_stream_debug_2 @@ -39,6 +39,8 @@ target: {target} words=WIP T7: title-match-regex regex=None + T8: title-min-length + min-length=5 B1: body-max-line-length line-length=80 B5: body-min-length diff --git a/gitlint/tests/expected/cli/test_cli/test_lint_staged_msg_filename_2 b/gitlint/tests/expected/cli/test_cli/test_lint_staged_msg_filename_2 index 9e18acb6..e8e9f330 100644 --- a/gitlint/tests/expected/cli/test_cli/test_lint_staged_msg_filename_2 +++ b/gitlint/tests/expected/cli/test_cli/test_lint_staged_msg_filename_2 @@ -39,6 +39,8 @@ target: {target} words=WIP T7: title-match-regex regex=None + T8: title-min-length + min-length=5 B1: body-max-line-length line-length=80 B5: body-min-length diff --git a/gitlint/tests/expected/cli/test_cli/test_lint_staged_stdin_2 b/gitlint/tests/expected/cli/test_cli/test_lint_staged_stdin_2 index f8746967..b822edc6 100644 --- a/gitlint/tests/expected/cli/test_cli/test_lint_staged_stdin_2 +++ b/gitlint/tests/expected/cli/test_cli/test_lint_staged_stdin_2 @@ -39,6 +39,8 @@ target: {target} words=WIP T7: title-match-regex regex=None + T8: title-min-length + min-length=5 B1: body-max-line-length line-length=80 B5: body-min-length diff --git a/gitlint/tests/expected/cli/test_cli/test_named_rules_2 b/gitlint/tests/expected/cli/test_cli/test_named_rules_2 index ec1f0a71..828e2963 100644 --- a/gitlint/tests/expected/cli/test_cli/test_named_rules_2 +++ b/gitlint/tests/expected/cli/test_cli/test_named_rules_2 @@ -39,6 +39,8 @@ target: {target} words=WIP,bögus T7: title-match-regex regex=None + T8: title-min-length + min-length=5 B1: body-max-line-length line-length=80 B5: body-min-length diff --git a/gitlint/tests/rules/test_title_rules.py b/gitlint/tests/rules/test_title_rules.py index 07d23231..049735e9 100644 --- a/gitlint/tests/rules/test_title_rules.py +++ b/gitlint/tests/rules/test_title_rules.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from gitlint.tests.base import BaseTestCase from gitlint.rules import TitleMaxLength, TitleTrailingWhitespace, TitleHardTab, TitleMustNotContainWord, \ - TitleTrailingPunctuation, TitleLeadingWhitespace, TitleRegexMatches, RuleViolation + TitleTrailingPunctuation, TitleLeadingWhitespace, TitleRegexMatches, RuleViolation, TitleMinLength class TitleRuleTests(BaseTestCase): @@ -152,3 +152,35 @@ def test_regex_matches(self): violations = rule.validate(commit.message.title, commit) expected_violation = RuleViolation("T7", u"Title does not match regex (^UÅ[0-9]*)", u"US1234: åbc") self.assertListEqual(violations, [expected_violation]) + + def test_min_line_length(self): + rule = TitleMinLength() + + # assert no error + violation = rule.validate(u"å" * 72, None) + self.assertIsNone(violation) + + # assert error on line length < 5 + expected_violation = RuleViolation("T8", "Title is too short (4<5)", u"å" * 4, 1) + violations = rule.validate(u"å" * 4, None) + self.assertListEqual(violations, [expected_violation]) + + # set line length to 3, and check no violation on length 4 + rule = TitleMinLength({'min-length': 3}) + violations = rule.validate(u"å" * 4, None) + self.assertIsNone(violations) + + # assert no violations on length 3 (this asserts we've implemented a *strict* less than) + rule = TitleMinLength({'min-length': 3}) + violations = rule.validate(u"å" * 3, None) + self.assertIsNone(violations) + + # assert raise on 2 + expected_violation = RuleViolation("T8", "Title is too short (2<3)", u"å" * 2, 1) + violations = rule.validate(u"å" * 2, None) + self.assertListEqual(violations, [expected_violation]) + + # assert raise on empty title + expected_violation = RuleViolation("T8", "Title is too short (0<3)", "", 1) + violations = rule.validate("", None) + self.assertListEqual(violations, [expected_violation]) diff --git a/qa/expected/test_commits/test_lint_staged_msg_filename_1 b/qa/expected/test_commits/test_lint_staged_msg_filename_1 index 5928cc16..eb2682f7 100644 --- a/qa/expected/test_commits/test_lint_staged_msg_filename_1 +++ b/qa/expected/test_commits/test_lint_staged_msg_filename_1 @@ -40,6 +40,8 @@ target: {target} words=WIP T7: title-match-regex regex=None + T8: title-min-length + min-length=5 B1: body-max-line-length line-length=80 B5: body-min-length diff --git a/qa/expected/test_commits/test_lint_staged_stdin_1 b/qa/expected/test_commits/test_lint_staged_stdin_1 index 672a5a7a..76b50480 100644 --- a/qa/expected/test_commits/test_lint_staged_stdin_1 +++ b/qa/expected/test_commits/test_lint_staged_stdin_1 @@ -40,6 +40,8 @@ target: {target} words=WIP T7: title-match-regex regex=None + T8: title-min-length + min-length=5 B1: body-max-line-length line-length=80 B5: body-min-length diff --git a/qa/expected/test_config/test_config_from_env_1 b/qa/expected/test_config/test_config_from_env_1 index b9a57cb5..dd761da4 100644 --- a/qa/expected/test_config/test_config_from_env_1 +++ b/qa/expected/test_config/test_config_from_env_1 @@ -40,6 +40,8 @@ target: {target} words=WIP T7: title-match-regex regex=None + T8: title-min-length + min-length=5 B1: body-max-line-length line-length=80 B5: body-min-length diff --git a/qa/expected/test_config/test_config_from_env_2 b/qa/expected/test_config/test_config_from_env_2 index 39456d2a..8d36672a 100644 --- a/qa/expected/test_config/test_config_from_env_2 +++ b/qa/expected/test_config/test_config_from_env_2 @@ -40,6 +40,8 @@ target: {target} words=WIP T7: title-match-regex regex=None + T8: title-min-length + min-length=5 B1: body-max-line-length line-length=80 B5: body-min-length diff --git a/qa/expected/test_config/test_config_from_file_debug_1 b/qa/expected/test_config/test_config_from_file_debug_1 index 6659a28b..540c3a0b 100644 --- a/qa/expected/test_config/test_config_from_file_debug_1 +++ b/qa/expected/test_config/test_config_from_file_debug_1 @@ -40,6 +40,8 @@ target: {target} words=WIP,thåt T7: title-match-regex regex=None + T8: title-min-length + min-length=5 B1: body-max-line-length line-length=30 B5: body-min-length