summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJoris Roovers <jroovers@cisco.com>2018-03-30 17:47:12 +0200
committerJoris Roovers <jroovers@cisco.com>2018-03-31 00:58:17 +0200
commit7d447959e4220428d929c4aa1310675bb9e3bee1 (patch)
treec70ac581fcc1cc97696f92caa341ca6da9038ed0
parent18a892e2f706bb0c9100bd4448b410cd2dd46f4b (diff)
New IgnoreByTitle rule
Added a new IgnoreByTitle rule that is based on a new more generic rule construct called ConfigurationRule. ConfigurationRules can modify gitlint configuration (and therefore behavior) on a per commit basis. Added some tests and docs, more docs and integration tests to follow in additional commits. This is part of #54 and #57.
-rw-r--r--docs/rules.md18
-rw-r--r--gitlint/cli.py10
-rw-r--r--gitlint/config.py3
-rw-r--r--gitlint/lint.py20
-rw-r--r--gitlint/rules.py27
-rw-r--r--gitlint/tests/expected/debug_configuration_output13
-rw-r--r--gitlint/tests/test_cli.py35
-rw-r--r--gitlint/tests/test_configuration_rules.py38
-rw-r--r--gitlint/tests/test_lint.py23
-rw-r--r--qa/expected/debug_output13
10 files changed, 169 insertions, 11 deletions
diff --git a/docs/rules.md b/docs/rules.md
index e3895aa..47e9483 100644
--- a/docs/rules.md
+++ b/docs/rules.md
@@ -180,4 +180,20 @@ regex | >= 0.9.0 | ```[^@ ]+@[^@ ]+\.[^@ ]+``` | Rege
!!! 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
+ An often recurring use-case is to only allow email addresses from a certain domain. The following regular expression achieves this: ```[^@]+@foo.com```
+
+
+
+## I1: ignore-by-title ##
+
+ID | Name | gitlint version | Description
+------|-----------------------------|-----------------|-------------------------------------------
+I1 | ignore-by-title | >= 0.10.0 | Ignore a commit based on matching its title.
+
+
+### Options ###
+
+Name | gitlint version | Default | Description
+----------------------|-------------------|------------------------------|----------------------------------
+regex | >= 0.10.0 | None | Regex to match against commit title. On match, the commit will be ignored.
+ignore | >= 0.10.0 | all | Comma-seperated list of rule names or ids to ignore when this rule is matched.
diff --git a/gitlint/cli.py b/gitlint/cli.py
index 86ea5cd..8f57090 100644
--- a/gitlint/cli.py
+++ b/gitlint/cli.py
@@ -1,5 +1,6 @@
# pylint: disable=bad-option-value,wrong-import-position
# We need to disable the import position checks because of the windows check that we need to do below
+import copy
import logging
import os
import platform
@@ -185,13 +186,16 @@ def lint(ctx):
first_violation = True
exit_code = 0
for commit in gitcontext.commits:
- # Build a config_builder and linter taking into account the commit specific config (if any)
+ # Build a config_builder taking into account the commit specific config (if any)
config_builder = general_config_builder.clone()
config_builder.set_config_from_commit(commit)
- lint_config = config_builder.build(lint_config)
- linter = GitLinter(lint_config)
+
+ # Create a deepcopy from the original config, so we have a unique config object per commit
+ # This is important for configuration rules to be able to modifying the config on a per commit basis
+ commit_config = config_builder.build(copy.deepcopy(lint_config))
# Actually do the linting
+ linter = GitLinter(commit_config)
violations = linter.lint(commit)
# exit code equals the total number of violations in all commits
exit_code += len(violations)
diff --git a/gitlint/config.py b/gitlint/config.py
index 56a46f6..5637d06 100644
--- a/gitlint/config.py
+++ b/gitlint/config.py
@@ -46,7 +46,8 @@ class LintConfig(object):
"""
# Default tuple of rule classes (tuple because immutable).
- default_rule_classes = (rules.TitleMaxLength,
+ default_rule_classes = (rules.IgnoreByTitle,
+ rules.TitleMaxLength,
rules.TitleTrailingWhitespace,
rules.TitleLeadingWhitespace,
rules.TitleTrailingPunctuation,
diff --git a/gitlint/lint.py b/gitlint/lint.py
index 109960a..362d8ae 100644
--- a/gitlint/lint.py
+++ b/gitlint/lint.py
@@ -15,25 +15,31 @@ class GitLinter(object):
self.display = display.Display(config)
- def ignore_rule(self, rule):
+ def should_ignore_rule(self, rule):
+ """ Determines whether a rule should be ignored based on the general list of commits to ignore """
return rule.id in self.config.ignore or rule.name in self.config.ignore
@property
+ def configuration_rules(self):
+ return [rule for rule in self.config.rules if
+ isinstance(rule, gitlint_rules.ConfigurationRule) and not self.should_ignore_rule(rule)]
+
+ @property
def title_line_rules(self):
return [rule for rule in self.config.rules if
isinstance(rule, gitlint_rules.LineRule) and
- rule.target == gitlint_rules.CommitMessageTitle and not self.ignore_rule(rule)]
+ rule.target == gitlint_rules.CommitMessageTitle and not self.should_ignore_rule(rule)]
@property
def body_line_rules(self):
return [rule for rule in self.config.rules if
isinstance(rule, gitlint_rules.LineRule) and
- rule.target == gitlint_rules.CommitMessageBody and not self.ignore_rule(rule)]
+ rule.target == gitlint_rules.CommitMessageBody and not self.should_ignore_rule(rule)]
@property
def commit_rules(self):
return [rule for rule in self.config.rules if isinstance(rule, gitlint_rules.CommitRule) and
- not self.ignore_rule(rule)]
+ not self.should_ignore_rule(rule)]
@staticmethod
def _apply_line_rules(lines, commit, rules, line_nr_start):
@@ -61,10 +67,14 @@ class GitLinter(object):
return all_violations
def lint(self, commit):
- """ Lint the last commit in a given git context by applying all title, body and general rules. """
+ """ Lint the last commit in a given git context by applying all ignore, title, body and commit rules. """
LOG.debug("Linting commit %s", commit.sha or "[SHA UNKNOWN]")
LOG.debug("Commit Object\n" + ustr(commit))
+ # Apply config rules
+ for rule in self.configuration_rules:
+ rule.apply(self.config, commit)
+
# Skip linting if this is a special commit type that is configured to be ignored
ignore_commit_types = ["merge", "squash", "fixup"]
for commit_type in ignore_commit_types:
diff --git a/gitlint/rules.py b/gitlint/rules.py
index 9ce2351..aa341f5 100644
--- a/gitlint/rules.py
+++ b/gitlint/rules.py
@@ -1,9 +1,13 @@
import copy
+import logging
import re
from gitlint.options import IntOption, BoolOption, StrOption, ListOption
from gitlint.utils import sstr
+LOG = logging.getLogger(__name__)
+logging.basicConfig()
+
class Rule(object):
""" Class representing gitlint rules. """
@@ -36,6 +40,11 @@ class Rule(object):
return self.__str__() # pragma: no cover
+class ConfigurationRule(Rule):
+ """ Class representing rules that can dynamically change the configuration of gitlint during runtime. """
+ pass
+
+
class CommitRule(Rule):
""" Class representing rules that act on an entire commit at once """
pass
@@ -301,3 +310,21 @@ class AuthorValidEmail(CommitRule):
if commit.author_email and not email_regex.match(commit.author_email):
return [RuleViolation(self.id, "Author email for commit is invalid", commit.author_email)]
+
+
+class IgnoreByTitle(ConfigurationRule):
+ name = "ignore-by-title"
+ id = "I1"
+ options_spec = [StrOption('regex', None, "Regex that matches the titles of commits this rule should apply to"),
+ StrOption('ignore', "all", "Comman-seperate list of rules to ignore")]
+
+ def apply(self, config, commit):
+ title_regex = re.compile(self.options['regex'].value, re.UNICODE)
+
+ if title_regex.match(commit.message.title):
+ config.ignore = self.options['ignore'].value
+
+ message = u"Commit title '{0}' matches the regex '{1}', ignoring rules: {2}"
+ message = message.format(commit.message.title, self.options['regex'].value, self.options['ignore'].value)
+
+ LOG.debug("Ignoring commit because of rule '%s': %s", self.id, message)
diff --git a/gitlint/tests/expected/debug_configuration_output1 b/gitlint/tests/expected/debug_configuration_output1
index 3ff5423..382181d 100644
--- a/gitlint/tests/expected/debug_configuration_output1
+++ b/gitlint/tests/expected/debug_configuration_output1
@@ -10,6 +10,9 @@ verbosity: 1
debug: True
target: {target}
[RULES]
+ I1: ignore-by-title
+ regex=None
+ ignore=all
T1: title-max-length
line-length=20
T2: title-trailing-whitespace
diff --git a/gitlint/tests/test_cli.py b/gitlint/tests/test_cli.py
index 217fa89..f2d548d 100644
--- a/gitlint/tests/test_cli.py
+++ b/gitlint/tests/test_cli.py
@@ -124,7 +124,7 @@ class CLITests(BaseTestCase):
u"commït-title2.\n\ncommït-body2\ngitlint-ignore: T3\n",
u"file4.txt\npåth/to/file5.txt\n",
u"test åuthor3\x00test-email3@föo.com\x002016-12-05 15:28:15 01:00\x00åbc\n"
- u"commït-title3\n\ncommït-body3",
+ u"commït-title3.\n\ncommït-body3",
u"file6.txt\npåth/to/file7.txt\n"]
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
@@ -133,6 +133,39 @@ class CLITests(BaseTestCase):
expected = (u"Commit 6f29bf81a8:\n"
u'3: B5 Body message is too short (12<20): "commït-body1"\n\n'
u"Commit 4da2656b0d:\n"
+ u'1: T3 Title has trailing punctuation (.): "commït-title3."\n'
+ u'3: B5 Body message is too short (12<20): "commït-body3"\n')
+ self.assertEqual(stderr.getvalue(), expected)
+ self.assertEqual(result.exit_code, 3)
+
+ @patch('gitlint.cli.stdin_has_data', return_value=False)
+ @patch('gitlint.git.sh')
+ def test_lint_multiple_commits_configuration_rules(self, sh, _):
+ """ Test for --commits option where some of the commits have gitlint config in the commit message """
+
+ # Note that the second commit title has a trailing period that is being ignored by gitlint-ignore: T3
+ sh.git.side_effect = ["6f29bf81a8322a04071bb794666e48c443a90360\n" + # git rev-list <SHA>
+ "25053ccec5e28e1bb8f7551fdbb5ab213ada2401\n" +
+ "4da2656b0dadc76c7ee3fd0243a96cb64007f125\n",
+ # git log --pretty <FORMAT> <SHA>
+ u"test åuthor1\x00test-email1@föo.com\x002016-12-03 15:28:15 01:00\x00åbc\n"
+ u"commït-title1\n\ncommït-body1",
+ u"file1.txt\npåth/to/file2.txt\n", # git diff-tree <SHA>
+ u"test åuthor2\x00test-email3@föo.com\x002016-12-04 15:28:15 01:00\x00åbc\n"
+ u"commït-title2.\n\ncommït-body2\n",
+ u"file4.txt\npåth/to/file5.txt\n",
+ u"test åuthor3\x00test-email3@föo.com\x002016-12-05 15:28:15 01:00\x00åbc\n"
+ u"commït-title3\n\ncommït-body3",
+ u"file6.txt\npåth/to/file7.txt\n"]
+
+ with patch('gitlint.display.stderr', new=StringIO()) as stderr:
+ result = self.cli.invoke(cli.cli, ["--commits", "foo...bar", "-c", "I1.regex=^commït-title2(.*)"])
+ # We expect that the second commit has no failures because of it matching against I1.regex
+ # Because we do test for the 3th commit to return violations, this test also ensures that a unique
+ # config object is passed to each commit lint call
+ expected = (u"Commit 6f29bf81a8:\n"
+ u'3: B5 Body message is too short (12<20): "commït-body1"\n\n'
+ u"Commit 4da2656b0d:\n"
u'3: B5 Body message is too short (12<20): "commït-body3"\n')
self.assertEqual(stderr.getvalue(), expected)
self.assertEqual(result.exit_code, 2)
diff --git a/gitlint/tests/test_configuration_rules.py b/gitlint/tests/test_configuration_rules.py
new file mode 100644
index 0000000..e1b6882
--- /dev/null
+++ b/gitlint/tests/test_configuration_rules.py
@@ -0,0 +1,38 @@
+# -*- coding: utf-8 -*-
+from gitlint.tests.base import BaseTestCase
+from gitlint import rules
+from gitlint.config import LintConfig
+
+
+class ConfigurationRuleTests(BaseTestCase):
+ def test_ignore_by_title(self):
+ commit = self.gitcommit(u"Releäse\n\nThis is the secōnd body line")
+
+ # No regex specified -> Config shouldn't be changed
+ rule = rules.IgnoreByTitle()
+ config = LintConfig()
+ rule.apply(config, commit)
+ self.assertEqual(config, LintConfig())
+ self.assert_logged([])
+
+ # Matching regex -> expect config to ignore all rules
+ rule = rules.IgnoreByTitle({"regex": "^Releäse(.*)"})
+ expected_config = LintConfig()
+ expected_config.ignore = "all"
+ rule.apply(config, commit)
+ self.assertEqual(config, expected_config)
+
+ expected_log_message = u"DEBUG: gitlint.rules Ignoring commit because of rule 'I1': " + \
+ u"Commit title 'Releäse' matches the regex '^Releäse(.*)', ignoring rules: all"
+ self.assert_log_contains(expected_log_message)
+
+ # Matching regex with specific ignore
+ rule = rules.IgnoreByTitle({"regex": "^Releäse(.*)",
+ "ignore": "T1,B2"})
+ expected_config = LintConfig()
+ expected_config.ignore = "T1,B2"
+ rule.apply(config, commit)
+ self.assertEqual(config, expected_config)
+
+ expected_log_message = u"DEBUG: gitlint.rules Ignoring commit because of rule 'I1': " + \
+ u"Commit title 'Releäse' matches the regex '^Releäse(.*)', ignoring rules: T1,B2"
diff --git a/gitlint/tests/test_lint.py b/gitlint/tests/test_lint.py
index e52ea84..4d7e5f5 100644
--- a/gitlint/tests/test_lint.py
+++ b/gitlint/tests/test_lint.py
@@ -127,6 +127,29 @@ class LintTests(BaseTestCase):
self.assertListEqual(violations, expected)
+ 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 test_lint_special_commit(self):
for commit_type in ["merge", "squash", "fixup"]:
commit = self.gitcommit(self.get_sample("commit_message/{0}".format(commit_type)))
diff --git a/qa/expected/debug_output1 b/qa/expected/debug_output1
index 9815be4..80bee74 100644
--- a/qa/expected/debug_output1
+++ b/qa/expected/debug_output1
@@ -14,6 +14,9 @@ verbosity: 2
debug: True
target: {target}
[RULES]
+ I1: ignore-by-title
+ regex=None
+ ignore=all
T1: title-max-length
line-length=20
T2: title-trailing-whitespace