def validate(self, commit: Any) -> Optional[List[RuleViolation]]: title_components = commit.message.title.split(": ") violations = [] # Return just this violation, since latter checks assume an area error = ("Title should start with at least one area, " "followed by a colon and space") if len(title_components) < 2: return [RuleViolation(self.id, error, line_nr=1)] exclusions = self.options['exclusions'].value exclusions_text = ", or ".join(exclusions) if exclusions_text: exclusions_text = " (or {})".format(exclusions_text) error = ("Areas at start of title should be lower case{}, " "followed by ': '".format(exclusions_text)) def deny_capital_text(text: str) -> bool: if text in exclusions: return False if not text.islower(): return True return False for area in title_components[:-1]: if (any(deny_capital_text(word) for word in area.split('/')) or ' ' in area): violations += [RuleViolation(self.id, error, line_nr=1)] error = "Summary of change, after area(s), should be capitalized" if not title_components[-1][0].isupper(): violations += [RuleViolation(self.id, error, line_nr=1)] return violations
def test_named_rules(self): """ Test that when named rules are present, both them and the original (non-named) rules executed """ lint_config = LintConfig() for rule_name in [u"my-ïd", u"another-rule-ïd"]: rule_id = TitleMustNotContainWord.id + ":" + rule_name lint_config.rules.add_rule(TitleMustNotContainWord, rule_id) lint_config.set_rule_option(rule_id, "words", [u"Föo"]) linter = GitLinter(lint_config) violations = [ RuleViolation("T5", u"Title contains the word 'WIP' (case-insensitive)", u"WIP: Föo bar", 1), RuleViolation(u"T5:another-rule-ïd", u"Title contains the word 'Föo' (case-insensitive)", u"WIP: Föo bar", 1), RuleViolation(u"T5:my-ïd", u"Title contains the word 'Föo' (case-insensitive)", u"WIP: Föo bar", 1) ] self.assertListEqual( violations, linter.lint( self.gitcommit(u"WIP: Föo bar\n\nFoo bår hur dur bla bla")))
def test_ignore_named_rules(self): """ Test that named rules can be ignored """ # Add named rule to lint config config_builder = LintConfigBuilder() rule_id = TitleMustNotContainWord.id + u":my-ïd" config_builder.set_option(rule_id, "words", [u"Föo"]) lint_config = config_builder.build() linter = GitLinter(lint_config) commit = self.gitcommit(u"WIP: Föo bar\n\nFoo bår hur dur bla bla") # By default, we expect both the violations of the regular rule as well as the named rule to show up violations = [ RuleViolation("T5", u"Title contains the word 'WIP' (case-insensitive)", u"WIP: Föo bar", 1), RuleViolation(u"T5:my-ïd", u"Title contains the word 'Föo' (case-insensitive)", u"WIP: Föo bar", 1) ] self.assertListEqual(violations, linter.lint(commit)) # ignore regular rule: only named rule violations show up lint_config.ignore = ["T5"] self.assertListEqual(violations[1:], linter.lint(commit)) # ignore named rule by id: only regular rule violations show up lint_config.ignore = [rule_id] self.assertListEqual(violations[:-1], linter.lint(commit)) # ignore named rule by name: only regular rule violations show up lint_config.ignore = [TitleMustNotContainWord.name + u":my-ïd"] self.assertListEqual(violations[:-1], linter.lint(commit))
def test_lint_sample3(self): linter = GitLinter(LintConfig()) gitcontext = self.gitcontext(self.get_sample("commit_message/sample3")) violations = linter.lint(gitcontext.commits[-1]) title = u" Commit title containing 'WIP', \tleading and tråiling whitespace and longer than 72 characters." expected = [ RuleViolation("T1", "Title exceeds max length (95>72)", title, 1), RuleViolation("T3", "Title has trailing punctuation (.)", title, 1), RuleViolation("T4", "Title contains hard tab characters (\\t)", title, 1), RuleViolation("T5", "Title contains the word 'WIP' (case-insensitive)", title, 1), RuleViolation("T6", "Title has leading whitespace", title, 1), RuleViolation("B4", "Second line is not empty", "This line should be empty", 2), RuleViolation( "B1", "Line exceeds max length (101>80)", u"This is the first line is meånt to test a line that exceeds the maximum line " + "length of 80 characters.", 3), RuleViolation("B2", "Line has trailing whitespace", "This line has a trailing space. ", 4), RuleViolation("B2", "Line has trailing whitespace", u"This line has a tråiling tab.\t", 5), RuleViolation("B3", "Line contains hard tab characters (\\t)", u"This line has a tråiling tab.\t", 5) ] self.assertListEqual(violations, expected)
def test_print_violations(self): violations = [ RuleViolation("RULE_ID_1", u"Error Messåge 1", "Violating Content 1", None), RuleViolation("RULE_ID_2", "Error Message 2", u"Violåting Content 2", 2) ] linter = GitLinter(LintConfig()) # test output with increasing verbosity with patch('gitlint.display.stderr', new=StringIO()) as stderr: linter.config.verbosity = 0 linter.print_violations(violations) self.assertEqual("", stderr.getvalue()) with patch('gitlint.display.stderr', new=StringIO()) as stderr: linter.config.verbosity = 1 linter.print_violations(violations) expected = u"-: RULE_ID_1\n2: RULE_ID_2\n" self.assertEqual(expected, stderr.getvalue()) with patch('gitlint.display.stderr', new=StringIO()) as stderr: linter.config.verbosity = 2 linter.print_violations(violations) expected = u"-: RULE_ID_1 Error Messåge 1\n2: RULE_ID_2 Error Message 2\n" self.assertEqual(expected, stderr.getvalue()) with patch('gitlint.display.stderr', new=StringIO()) as stderr: linter.config.verbosity = 3 linter.print_violations(violations) expected = u"-: RULE_ID_1 Error Messåge 1: \"Violating Content 1\"\n" + \ u"2: RULE_ID_2 Error Message 2: \"Violåting Content 2\"\n" self.assertEqual(expected, stderr.getvalue())
def validate(self, commit): violations = [ RuleViolation(self.id, u"GitCommit.branches: {0}".format(sstr(commit.branches)), line_nr=1), RuleViolation(self.id, u"GitCommit.custom_prop: {0}".format(commit.custom_prop), line_nr=1), ] return violations
def test_min_line_length(self): rule = TitleMinLength() # assert no error violation = rule.validate("å" * 72, None) self.assertIsNone(violation) # assert error on line length < 5 expected_violation = RuleViolation("T8", "Title is too short (4<5)", "å" * 4, 1) violations = rule.validate("å" * 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("å" * 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("å" * 3, None) self.assertIsNone(violations) # assert raise on 2 expected_violation = RuleViolation("T8", "Title is too short (2<3)", "å" * 2, 1) violations = rule.validate("å" * 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])
def validate(self, commit): violations = [ RuleViolation(self.id, u"GitContext.current_branch: {0}".format(commit.context.current_branch), line_nr=1), RuleViolation(self.id, u"GitContext.commentchar: {0}".format(commit.context.commentchar), line_nr=1) ] return violations
def test_leading_whitespace(self): rule = TitleLeadingWhitespace() # assert no error violations = rule.validate("a", None) self.assertIsNone(violations) # leading space expected_violation = RuleViolation("T6", "Title has leading whitespace", " a") violations = rule.validate(" a", None) self.assertListEqual(violations, [expected_violation]) # leading tab expected_violation = RuleViolation("T6", "Title has leading whitespace", "\ta") violations = rule.validate("\ta", None) self.assertListEqual(violations, [expected_violation]) # unicode test expected_violation = RuleViolation("T6", "Title has leading whitespace", u" ☺") violations = rule.validate(u" ☺", None) self.assertListEqual(violations, [expected_violation])
def test_lint_regex_rules(self): """ Additional test for title-match-regex, body-match-regex """ commit = self.gitcommit( self.get_sample("commit_message/no-violations")) lintconfig = LintConfig() linter = GitLinter(lintconfig) violations = linter.lint(commit) # No violations by default self.assertListEqual(violations, []) # Matching regexes shouldn't be a problem rule_regexes = [("title-match-regex", u"Tïtle$"), ("body-match-regex", u"Sïgned-Off-By: (.*)$")] for rule_regex in rule_regexes: lintconfig.set_rule_option(rule_regex[0], "regex", rule_regex[1]) violations = linter.lint(commit) self.assertListEqual(violations, []) # Non-matching regexes should return violations rule_regexes = [("title-match-regex", ), ("body-match-regex", )] lintconfig.set_rule_option("title-match-regex", "regex", u"^Tïtle") lintconfig.set_rule_option("body-match-regex", "regex", u"Sügned-Off-By: (.*)$") expected_violations = [ RuleViolation("T7", u"Title does not match regex (^Tïtle)", u"Normal Commit Tïtle", 1), RuleViolation("B8", u"Body does not match regex (Sügned-Off-By: (.*)$)", None, 6) ] violations = linter.lint(commit) self.assertListEqual(violations, expected_violations)
def validate(self, _): violations = [ RuleViolation(self.id, u"int-öption: {0}".format(self.options[u'int-öption'].value), line_nr=1), RuleViolation(self.id, u"str-öption: {0}".format(self.options[u'str-öption'].value), line_nr=1), RuleViolation(self.id, u"list-öption: {0}".format(sstr(self.options[u'list-öption'].value)), line_nr=1), ] return violations
def test_conventional_commits(self): rule = ConventionalCommit() # No violations when using a correct type and format for type in ["fix", "feat", "chore", "docs", "style", "refactor", "perf", "test", "revert", "ci", "build"]: violations = rule.validate(type + ": föo", None) self.assertListEqual([], violations) # assert violation on wrong type expected_violation = RuleViolation("CT1", "Title does not start with one of fix, feat, chore, docs," " style, refactor, perf, test, revert, ci, build", "bår: foo") violations = rule.validate("bår: foo", None) self.assertListEqual([expected_violation], violations) # assert violation when use strange chars after correct type expected_violation = RuleViolation("CT1", "Title does not start with one of fix, feat, chore, docs," " style, refactor, perf, test, revert, ci, build", "feat_wrong_chars: föo") violations = rule.validate("feat_wrong_chars: föo", None) self.assertListEqual([expected_violation], violations) # assert violation when use strange chars after correct type expected_violation = RuleViolation("CT1", "Title does not start with one of fix, feat, chore, docs," " style, refactor, perf, test, revert, ci, build", "feat_wrong_chars(scope): föo") violations = rule.validate("feat_wrong_chars(scope): föo", None) self.assertListEqual([expected_violation], violations) # assert violation on wrong format expected_violation = RuleViolation("CT1", "Title does not follow ConventionalCommits.org format " "'type(optional-scope): description'", "fix föo") violations = rule.validate("fix föo", None) self.assertListEqual([expected_violation], violations) # assert no violation when use ! for breaking changes without scope violations = rule.validate("feat!: föo", None) self.assertListEqual([], violations) # assert no violation when use ! for breaking changes with scope violations = rule.validate("fix(scope)!: föo", None) self.assertListEqual([], violations) # assert no violation when adding new type rule = ConventionalCommit({'types': ["föo", "bär"]}) for typ in ["föo", "bär"]: violations = rule.validate(typ + ": hür dur", None) self.assertListEqual([], violations) # assert violation when using incorrect type when types have been reconfigured violations = rule.validate("fix: hür dur", None) expected_violation = RuleViolation("CT1", "Title does not start with one of föo, bär", "fix: hür dur") self.assertListEqual([expected_violation], violations) # assert no violation when adding new type named with numbers rule = ConventionalCommit({'types': ["föo123", "123bär"]}) for typ in ["föo123", "123bär"]: violations = rule.validate(typ + ": hür dur", None) self.assertListEqual([], violations)
def test_lint_sample2(self): linter = GitLinter(LintConfig()) gitcontext = self.gitcontext(self.get_sample("commit_message/sample2")) violations = linter.lint(gitcontext.commits[-1]) expected = [RuleViolation("T5", "Title contains the word 'WIP' (case-insensitive)", u"Just a title contåining WIP", 1), RuleViolation("B6", "Body message is missing", None, 3)] self.assertListEqual(violations, expected)
def validate(self, commit): flags = re.UNICODE flags |= re.IGNORECASE for line in commit.message.body: if line.lower().startswith("signed-off-by"): if not re.search(r"(^)Signed-off-by: ([-'\w.]+) ([-'\w.]+) (.*)", line, flags=flags): return [RuleViolation(self.id, "Signed-off-by: must have a full name", line_nr=1)] else: return return [RuleViolation(self.id, "Body does not contain a 'Signed-off-by:' line", line_nr=1)]
def test_lint_ignore(self): lint_config = LintConfig() lint_config.ignore = ["T1", "T3", "T4", "T5", "T6", "B1", "B2"] linter = GitLinter(lint_config) violations = linter.lint(self.gitcommit(self.get_sample("commit_message/sample3"))) expected = [RuleViolation("B4", "Second line is not empty", "This line should be empty", 2), RuleViolation("B3", "Line contains hard tab characters (\\t)", u"This line has a tråiling tab.\t", 5)] self.assertListEqual(violations, expected)
def validate(self, commit): violations = [ RuleViolation(self.id, f"GitCommit.branches: {commit.branches}", line_nr=1), RuleViolation(self.id, f"GitCommit.custom_prop: {commit.custom_prop}", line_nr=1), ] return violations
def test_lint_meta(self): """ Lint sample2 but also add some metadata to the commit so we that get's linted as well """ linter = GitLinter(LintConfig()) gitcontext = self.gitcontext(self.get_sample("commit_message/sample2")) gitcontext.commits[0].author_email = u"foo bår" violations = linter.lint(gitcontext.commits[-1]) expected = [RuleViolation("M1", "Author email for commit is invalid", u"foo bår", None), RuleViolation("T5", "Title contains the word 'WIP' (case-insensitive)", u"Just a title contåining WIP", 1), RuleViolation("B6", "Body message is missing", None, 3)] self.assertListEqual(violations, expected)
def validate(self, commit): violations = [ RuleViolation( self.id, f"GitContext.current_branch: {commit.context.current_branch}", line_nr=1), RuleViolation( self.id, f"GitContext.commentchar: {commit.context.commentchar}", line_nr=1) ] return violations
def validate(self, _): violations = [ RuleViolation(self.id, f"int-öption: {self.options[u'int-öption'].value}", line_nr=1), RuleViolation(self.id, f"str-öption: {self.options[u'str-öption'].value}", line_nr=1), RuleViolation(self.id, f"list-öption: {self.options[u'list-öption'].value}", line_nr=1), ] return violations
def validate(self, line, _commit): violations = [] match = RULE_REGEX.match(line) 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: msg = "Title does not start with one of {0}".format(', '.join( self.options['types'].value)) violations.append(RuleViolation(self.id, msg, line)) return violations
def validate(self, line, _commit): violations = [] for commit_type in self.options["types"].value: if line.startswith(ustr(commit_type)): break else: msg = u"Title does not start with one of {0}".format(', '.join(self.options['types'].value)) violations.append(RuleViolation(self.id, msg, line)) if not RULE_REGEX.match(line): msg = u"Title does not follow ConventionalCommits.org format 'type(optional-scope): description'" violations.append(RuleViolation(self.id, msg, line)) return violations
def validate(self, commit): filtered = [x for x in commit.message.body if not x.lower().startswith("signed-off-by") and x != ''] line_count = len(filtered) min_line_count = self.options['min-line-count'].value if line_count < min_line_count: message = "Body has no content, should at least have {} line.".format(min_line_count) return [RuleViolation(self.id, message, line_nr=1)]
def test_author_valid_email_rule(self): rule = AuthorValidEmail() # valid email addresses valid_email_addresses = [ "fö[email protected]", "Jö[email protected]", "jö[email protected]", "jöhn/[email protected]", "jö[email protected]" ] for email in valid_email_addresses: commit = self.gitcommit("", author_email=email) violations = rule.validate(commit) self.assertIsNone(violations) # No email address (=allowed for now, as gitlint also lints messages passed via stdin that don't have an # email address) commit = self.gitcommit("") violations = rule.validate(commit) self.assertIsNone(violations) # Invalid email addresses: no TLD, no domain, no @, space anywhere (=valid but not allowed by gitlint) invalid_email_addresses = [ "föo@bar", "JöhnDoe", "Jöhn Doe", "Jöhn [email protected]", " Jö[email protected]", "JöhnDoe@ foo.com", "JöhnDoe@foo. com", "JöhnDoe@foo. com", "@bår.com", "fö[email protected]" ] for email in invalid_email_addresses: commit = self.gitcommit("", author_email=email) violations = rule.validate(commit) self.assertListEqual(violations, [ RuleViolation("M1", "Author email for commit is invalid", email) ])
def test_author_valid_email_rule_custom_regex(self): # regex=None -> the rule isn't applied rule = AuthorValidEmail() rule.options['regex'].set(None) emailadresses = ["föo", None, "hür dür"] for email in emailadresses: commit = self.gitcommit("", author_email=email) violations = rule.validate(commit) self.assertIsNone(violations) # Custom domain rule = AuthorValidEmail({'regex': "[^@]+@bår.com"}) valid_email_addresses = [ "föo@bår.com", "Jöhn.Doe@bår.com", "jöhn+doe@bår.com", "jöhn/doe@bår.com" ] for email in valid_email_addresses: commit = self.gitcommit("", author_email=email) violations = rule.validate(commit) self.assertIsNone(violations) # Invalid email addresses invalid_email_addresses = ["fö[email protected]"] for email in invalid_email_addresses: commit = self.gitcommit("", author_email=email) violations = rule.validate(commit) self.assertListEqual(violations, [ RuleViolation("M1", "Author email for commit is invalid", email) ])
def validate(self, line, _commit): if line == "": return first_word = line.split()[0] for suffix in self.options['suffixes'].value: if first_word.endswith(suffix): return [RuleViolation(self.id, self.violation_message, line)]
def validate(self, commit): for line in commit.message.body: if line.startswith("Signed-off-by"): return msg = "Body does not contain a 'Signed-Off-By' line" return [RuleViolation(self.id, msg, line_nr=1)]
def test_lint_configuration_rule(self): # Test that all rules are ignored because of matching regex lint_config = LintConfig() lint_config.set_rule_option("I1", "regex", "^Just a title(.*)") linter = GitLinter(lint_config) violations = linter.lint( self.gitcommit(self.get_sample("commit_message/sample2"))) self.assertListEqual(violations, []) # Test ignoring only certain rules lint_config = LintConfig() lint_config.set_rule_option("I1", "regex", "^Just a title(.*)") lint_config.set_rule_option("I1", "ignore", "B6") linter = GitLinter(lint_config) violations = linter.lint( self.gitcommit(self.get_sample("commit_message/sample2"))) # Normally we'd expect a B6 violation, but that one is skipped because of the specific ignore set above expected = [ RuleViolation("T5", "Title contains the word 'WIP' (case-insensitive)", u"Just a title contåining WIP", 1) ] self.assertListEqual(violations, expected)
def validate(self, commit): line_count = len(commit.message.body) max_line_count = self.options['max-line-count'].value if line_count > max_line_count: message = "Body contains too many lines ({0} > {1})".format( line_count, max_line_count) return [RuleViolation(self.id, message, line_nr=1)]
def validate(self, commit): """Validate user defined gitlint rules.""" for line in commit.message.body: if line.startswith("Signed-off-by"): return [] msg = "Body does not contain a 'Signed-off-by' line" return [RuleViolation(self.id, msg, line_nr=1)]
def validate(self, line, _commit): max_length = self.options['line-length'].value if len(line) > max_length and not line.startswith("Revert"): return [ RuleViolation( self.id, self.violation_message.format(len(line), max_length), line) ]