diff options
author | Joris Roovers <jroovers@cisco.com> | 2017-05-13 20:35:17 -0400 |
---|---|---|
committer | Joris Roovers <jroovers@cisco.com> | 2017-05-13 20:35:17 -0400 |
commit | 0c66f6dca43eac634d37752dd41d37b7fdbec608 (patch) | |
tree | a9987964cb33cbb888afe2fdb46eeb42b91103e9 | |
parent | 3c1b3c3284534d44f1a720e96497030bf5c9abd6 (diff) |
New rule: author-valid-email
This commits adds a new rule (author-valid-email) that validates that the commit
author email address is a valid email address. The rule has a regex option that
allows users to configure specify which email addresses they want to be
considered valid.
Also changed the 'unittest' import back to 'unittest2' to fix a regression with
the integration tests in python 2.6.
-rw-r--r-- | CHANGELOG.md | 6 | ||||
-rw-r--r-- | docs/configuration.md | 6 | ||||
-rw-r--r-- | docs/rules.md | 26 | ||||
-rw-r--r-- | gitlint/config.py | 3 | ||||
-rw-r--r-- | gitlint/files/gitlint | 6 | ||||
-rw-r--r-- | gitlint/lint.py | 15 | ||||
-rw-r--r-- | gitlint/rules.py | 20 | ||||
-rw-r--r-- | gitlint/tests/expected/debug_configuration_output1 | 2 | ||||
-rw-r--r-- | gitlint/tests/test_lint.py | 21 | ||||
-rw-r--r-- | gitlint/tests/test_meta_rules.py | 51 | ||||
-rw-r--r-- | qa/base.py | 2 | ||||
-rw-r--r-- | qa/expected/debug_output1 | 2 |
12 files changed, 148 insertions, 12 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 50632af..fba098f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog # +## v0.8.3 (In Progress) ## + +- New Rule: ```author-valid-email``` enforces a valid author email address. Details can be found in the + [Rules section of the documentation](http://jorisroovers.github.io/gitlint/rules/). +- Debug output improvements + ## v0.8.2 (2017-04-25) ## The 0.8.2 release brings minor improvements, bugfixes and some under-the-hood changes. Special thanks to diff --git a/docs/configuration.md b/docs/configuration.md index 814840e..866808e 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -62,6 +62,12 @@ ignore-merge-commits=false # By specifying this rule, developers can only change the file when they explicitly reference # it in the commit message. files=gitlint/rules.py,README.md + +# [author-valid-email] +# python like regex (https://docs.python.org/2/library/re.html) that the +# commit author email address should be matched to +# For example, use the following regex if you only want to allow email addresses from foo.com +# regex = "[^@]+@foo.com" ``` # Commandline config # diff --git a/docs/rules.md b/docs/rules.md index 343984c..a75c324 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -21,6 +21,8 @@ B4 | body-first-line-empty | >= 0.1 | First line of the body ( B5 | body-min-length | >= 0.4 | Body length must be at least 20 characters B6 | body-is-missing | >= 0.4 | Body message must be specified B7 | body-changed-file-mention | >= 0.4 | Body must contain references to certain files if those files are changed in the last commit +M1 | author-valid-email | >= 0.8.3 | Author email address must be a valid email address + ## T1: title-max-length ## @@ -155,3 +157,27 @@ B7 | body-changed-file-mention | >= 0.4 | Body must contain refere Name | gitlint version | Default | Description ----------------------|-----------------|--------------|---------------------------------- files | >= 0.4 | (empty) | Comma-separated list of files that need to an explicit mention in the commit message in case they are changed. + + + +## M1: author-valid-email ## + +ID | Name | gitlint version | Description +------|-----------------------------|-----------------|------------------------------------------- +M1 | author-valid-email | >= 0.8.3 | Author email address must be a valid email address + +!!! note + Email addresses are [notoriously hard to validate and the official email valid spec is often too loose for any real world application](http://stackoverflow.com/a/201378/381010). + Gitlint by default takes a pragmatic approach and requires users to enter email addresses that contain a name, domain and tld and has no spaces. + + + +### Options ### + +Name | gitlint version | Default | Description +----------------------|-------------------|------------------------------|---------------------------------- +regex | >= 0.8.3 | ```[^@ ]+@[^@ ]+\.[^@ ]+``` | Regex the commit author email address is matched against + + +!!! note + An often recurring use-case is to only allow email addresses from a certain domain. The following regular expression achieves this: ```[^@]+@foo.com```
\ No newline at end of file diff --git a/gitlint/config.py b/gitlint/config.py index 860eba1..a665b47 100644 --- a/gitlint/config.py +++ b/gitlint/config.py @@ -59,7 +59,8 @@ class LintConfig(object): rules.BodyTrailingWhitespace, rules.BodyHardTab, rules.BodyFirstLineEmpty, - rules.BodyChangedFileMention) + rules.BodyChangedFileMention, + rules.AuthorValidEmail) def __init__(self): # Use an ordered dict so that the order in which rules are applied is always the same diff --git a/gitlint/files/gitlint b/gitlint/files/gitlint index 597d6e3..644e5fc 100644 --- a/gitlint/files/gitlint +++ b/gitlint/files/gitlint @@ -46,3 +46,9 @@ # By specifying this rule, developers can only change the file when they explicitly reference # it in the commit message. # files=gitlint/rules.py,README.md + +# [author-valid-email] +# python like regex (https://docs.python.org/2/library/re.html) that the +# commit author email address should be matched to +# For example, use the following regex if you only want to allow email addresses from foo.com +# regex = "[^@]+@foo.com"
\ No newline at end of file diff --git a/gitlint/lint.py b/gitlint/lint.py index 30f0643..a31d165 100644 --- a/gitlint/lint.py +++ b/gitlint/lint.py @@ -74,17 +74,20 @@ class GitLinter(object): violations.extend(self._apply_line_rules(commit.message.body, commit, self.body_line_rules, 2)) violations.extend(self._apply_commit_rules(self.commit_rules, commit)) - # sort violations by line number - violations.sort(key=lambda v: (v.line_nr, v.rule_id)) # sort violations by line number and rule_id + # Sort violations by line number and rule_id. If there's no line nr specified (=common certain commit rules), + # we replace None with -1 so that it always get's placed first. Note that we need this to do this to support + # python 3, as None is not allowed in a list that is being sorted. + violations.sort(key=lambda v: (-1 if v.line_nr is None else v.line_nr, v.rule_id)) return violations def print_violations(self, violations): """ Print a given set of violations to the standard error output """ for v in violations: - self.display.e(u"{0}: {1}".format(v.line_nr, v.rule_id), exact=True) - self.display.ee(u"{0}: {1} {2}".format(v.line_nr, v.rule_id, v.message), exact=True) + line_nr = v.line_nr if v.line_nr else "-" + self.display.e(u"{0}: {1}".format(line_nr, v.rule_id), exact=True) + self.display.ee(u"{0}: {1} {2}".format(line_nr, v.rule_id, v.message), exact=True) if v.content: - self.display.eee(u"{0}: {1} {2}: \"{3}\"".format(v.line_nr, v.rule_id, v.message, v.content), + self.display.eee(u"{0}: {1} {2}: \"{3}\"".format(line_nr, v.rule_id, v.message, v.content), exact=True) else: - self.display.eee(u"{0}: {1} {2}".format(v.line_nr, v.rule_id, v.message), exact=True) + self.display.eee(u"{0}: {1} {2}".format(line_nr, v.rule_id, v.message), exact=True) diff --git a/gitlint/rules.py b/gitlint/rules.py index 69a4517..5c38f4c 100644 --- a/gitlint/rules.py +++ b/gitlint/rules.py @@ -286,3 +286,23 @@ class BodyChangedFileMention(CommitRule): violation_message = u"Body does not mention changed file '{0}'".format(needs_mentioned_file) violations.append(RuleViolation(self.id, violation_message, None, len(commit.message.body) + 1)) return violations if violations else None + + +class AuthorValidEmail(CommitRule): + name = "author-valid-email" + id = "M1" + options_spec = [StrOption('regex', "[^@ ]+@[^@ ]+\.[^@ ]+", "Regex that author email address should match")] + email_regex = None + + # Note that unicode is allowed in email addresses + # See http://stackoverflow.com/questions/3844431/are-email-addresses-allowed-to-contain-non-alphanumeric-characters + + def validate(self, commit): + + # Only compile the regex the first time so that we don't need to re-compile it if we are linting + # multiple commits at once + if not self.email_regex: + self.email_regex = re.compile(self.options['regex'].value, re.UNICODE) + + if commit.author_email and not self.email_regex.match(commit.author_email): + return [RuleViolation(self.id, "Author email for commit is invalid", commit.author_email)] diff --git a/gitlint/tests/expected/debug_configuration_output1 b/gitlint/tests/expected/debug_configuration_output1 index 18d9e53..ca9a6cf 100644 --- a/gitlint/tests/expected/debug_configuration_output1 +++ b/gitlint/tests/expected/debug_configuration_output1 @@ -29,3 +29,5 @@ target: {target} B4: body-first-line-empty B7: body-changed-file-mention files= + M1: author-valid-email + regex=[^@ ]+@[^@ ]+\.[^@ ]+ diff --git a/gitlint/tests/test_lint.py b/gitlint/tests/test_lint.py index dba3426..e7f8f03 100644 --- a/gitlint/tests/test_lint.py +++ b/gitlint/tests/test_lint.py @@ -96,6 +96,19 @@ class RuleOptionTests(BaseTestCase): "This line has a trailing tab.\t", 5)] self.assertListEqual(violations, expected) + 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 test_lint_ignore(self): lint_config = LintConfig() lint_config.ignore = ["T1", "T3", "T4", "T5", "T6", "B1", "B2"] @@ -124,7 +137,7 @@ class RuleOptionTests(BaseTestCase): self.assertTrue(len(violations) > 0) def test_print_violations(self): - violations = [RuleViolation("RULE_ID_1", u"Error Messåge 1", "Violating Content 1", 1), + 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()) @@ -137,18 +150,18 @@ class RuleOptionTests(BaseTestCase): with patch('gitlint.display.stderr', new=StringIO()) as stderr: linter.config.verbosity = 1 linter.print_violations(violations) - expected = u"1: RULE_ID_1\n2: RULE_ID_2\n" + 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"1: RULE_ID_1 Error Messåge 1\n2: RULE_ID_2 Error Message 2\n" + 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"1: RULE_ID_1 Error Messåge 1: \"Violating Content 1\"\n" + \ + 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()) diff --git a/gitlint/tests/test_meta_rules.py b/gitlint/tests/test_meta_rules.py new file mode 100644 index 0000000..88a0be9 --- /dev/null +++ b/gitlint/tests/test_meta_rules.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +from gitlint.tests.base import BaseTestCase +from gitlint.rules import AuthorValidEmail, RuleViolation + +from gitlint.git import GitCommit + + +class MetaRuleTests(BaseTestCase): + def test_author_valid_email_rule(self): + rule = AuthorValidEmail() + + # valid email addresses + valid_email_addresses = [u"föo@bar.com", u"Jöhn.Doe@bar.com", u"jöhn+doe@bar.com", u"jöhn/doe@bar.com", + u"jöhn.doe@subdomain.bar.com"] + for email in valid_email_addresses: + commit = GitCommit(None, None, 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 = GitCommit(None, None) + 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 = [u"föo@bar", u"JöhnDoe", u"Jöhn Doe", u"Jöhn Doe@foo.com", u" JöhnDoe@foo.com", + u"JöhnDoe@ foo.com", u"JöhnDoe@foo. com", u"JöhnDoe@foo. com", u"@bår.com", + u"föo@.com"] + for email in invalid_email_addresses: + commit = GitCommit(None, None, 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): + # Custom domain + rule = AuthorValidEmail({'regex': u"[^@]+@bår.com"}) + valid_email_addresses = [u"föo@bår.com", u"Jöhn.Doe@bår.com", u"jöhn+doe@bår.com", u"jöhn/doe@bår.com"] + for email in valid_email_addresses: + commit = GitCommit(None, None, author_email=email) + violations = rule.validate(commit) + self.assertIsNone(violations) + + # Invalid email addresses + invalid_email_addresses = [u"föo@hur.com"] + for email in invalid_email_addresses: + commit = GitCommit(None, None, author_email=email) + violations = rule.validate(commit) + self.assertListEqual(violations, + [RuleViolation("M1", "Author email for commit is invalid", email)]) @@ -5,7 +5,7 @@ import sys from datetime import datetime from uuid import uuid4 -from unittest import TestCase +from unittest2 import TestCase from sh import git, rm, touch, DEFAULT_ENCODING # pylint: disable=no-name-in-module diff --git a/qa/expected/debug_output1 b/qa/expected/debug_output1 index ef47440..57d9343 100644 --- a/qa/expected/debug_output1 +++ b/qa/expected/debug_output1 @@ -33,6 +33,8 @@ target: {target} B4: body-first-line-empty B7: body-changed-file-mention files= + M1: author-valid-email + regex=[^@ ]+@[^@ ]+\.[^@ ]+ DEBUG: gitlint.lint Linting commit {commit_sha} DEBUG: gitlint.lint Commit Object |