Skip to content

Commit

Permalink
New ignore-by-body rule
Browse files Browse the repository at this point in the history
New ignore-by-body (I2) that allows users to ignore commit messages based on
matching the commit body against a regex.

Also fixed an issue with unit test failures in Python 3.4 and 3.5 which had
a different default sort order for dictionaries.

This closes jorisroovers#54.
  • Loading branch information
jroovers-cisco committed Apr 2, 2018
1 parent 52dc67e commit 7a964c9
Show file tree
Hide file tree
Showing 12 changed files with 115 additions and 14 deletions.
6 changes: 4 additions & 2 deletions docs/contributing.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Contributing

We'd love for you to contribute to gitlint. Thanks for your interest!
Sometimes it takes a while for [me](https://github.com/jorisroovers) to
get back to you (this is a hobby project), but rest assured that we read your message and appreciate your interest!
Sometimes it takes a while for [me](https://github.com/jorisroovers) to get back to you (sometimes up to a few months,
this is a hobby project), but rest assured that we read your message and appreciate your interest!
We maintain a [wishlist on our wiki](https://github.com/jorisroovers/gitlint/wiki/Wishlist),
but we're obviously open to any suggestions!

Expand Down
5 changes: 5 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,11 @@ regex=^Release(.*)
ignore=title-max-length,body-min-length
# ignore all rules by setting ignore to 'all'
# ignore=all
[ignore-by-title]
# Match commits message bodies that have a line that contains 'release'
regex=(.*)release(.*)
ignore=all
```

!!! note
Expand Down
16 changes: 15 additions & 1 deletion docs/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,6 @@ regex | >= 0.9.0 | ```[^@ ]+@[^@ ]+\.[^@ ]+``` | Rege
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
Expand All @@ -197,3 +196,18 @@ Name | gitlint version | Default | Descr
----------------------|-------------------|------------------------------|----------------------------------
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.


## I2: ignore-by-body ##

ID | Name | gitlint version | Description
------|-----------------------------|-----------------|-------------------------------------------
I2 | ignore-by-body | >= 0.10.0 | Ignore a commit based on matching its body.


### Options ###

Name | gitlint version | Default | Description
----------------------|-------------------|------------------------------|----------------------------------
regex | >= 0.10.0 | None | Regex to match against each line of the body. 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.
3 changes: 2 additions & 1 deletion gitlint/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ class LintConfig(object):

# Default tuple of rule classes (tuple because immutable).
default_rule_classes = (rules.IgnoreByTitle,
rules.IgnoreByBody,
rules.TitleMaxLength,
rules.TitleTrailingWhitespace,
rules.TitleLeadingWhitespace,
Expand Down Expand Up @@ -255,7 +256,7 @@ def __str__(self):
return_str += u"[RULES]\n"
for rule in self.rules:
return_str += u" {0}: {1}\n".format(rule.id, rule.name)
for option_name, option_value in rule.options.items():
for option_name, option_value in sorted(rule.options.items()):
if isinstance(option_value.value, list):
option_val_repr = ",".join(option_value.value)
else:
Expand Down
9 changes: 9 additions & 0 deletions gitlint/files/gitlint
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,13 @@
#
# Ignore certain rules, you can reference them by their id or by their full name
# Use 'all' to ignore all rules
# ignore=T1,body-min-length

# [ignore-by-body]
# Ignore certain rules for commits of which the body has a line that matches a regex
# E.g. Match bodies that have a line that that contain "release"
# regex=(.*)release(.*)
#
# Ignore certain rules, you can reference them by their id or by their full name
# Use 'all' to ignore all rules
# ignore=T1,body-min-length
23 changes: 22 additions & 1 deletion gitlint/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ def validate(self, commit):
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"),
options_spec = [StrOption('regex', None, "Regex matching the titles of commits this rule should apply to"),
StrOption('ignore', "all", "Comman-seperate list of rules to ignore")]

def apply(self, config, commit):
Expand All @@ -328,3 +328,24 @@ def apply(self, config, commit):
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)


class IgnoreByBody(ConfigurationRule):
name = "ignore-by-body"
id = "I2"
options_spec = [StrOption('regex', None, "Regex matching lines of the body of commits this rule should apply to"),
StrOption('ignore', "all", "Comman-seperate list of rules to ignore")]

def apply(self, config, commit):
body_line_regex = re.compile(self.options['regex'].value, re.UNICODE)

for line in commit.message.body:
if body_line_regex.match(line):
config.ignore = self.options['ignore'].value

message = u"Commit message line '{0}' matches the regex '{1}', ignoring rules: {2}"
message = message.format(line, self.options['regex'].value, self.options['ignore'].value)

LOG.debug("Ignoring commit because of rule '%s': %s", self.id, message)
# No need to check other lines if we found a match
return
3 changes: 3 additions & 0 deletions gitlint/tests/expected/debug_configuration_output1
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@ debug: True
target: {target}
[RULES]
I1: ignore-by-title
ignore=all
regex=None
I2: ignore-by-body
ignore=all
regex=None
T1: title-max-length
line-length=20
T2: title-trailing-whitespace
Expand Down
15 changes: 10 additions & 5 deletions gitlint/tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,9 +141,10 @@ def test_lint_multiple_commits_config(self, sh, _):
@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 """
""" Test for --commits option where where we have configured gitlint to ignore certain rules for certain commits
"""

# Note that the second commit title has a trailing period that is being ignored by gitlint-ignore: T3
# Note that the second commit
sh.git.side_effect = ["6f29bf81a8322a04071bb794666e48c443a90360\n" + # git rev-list <SHA>
"25053ccec5e28e1bb8f7551fdbb5ab213ada2401\n" +
"4da2656b0dadc76c7ee3fd0243a96cb64007f125\n",
Expand All @@ -152,21 +153,25 @@ def test_lint_multiple_commits_configuration_rules(self, sh, _):
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"
# Normally T3 violation (trailing punctuation), but this commit is ignored because of
# config below
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",
# Normally T1 and B5 violations, now only T1 because we're ignoring B5 in config below
u"commït-title3.\n\ncommït-body3 foo",
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(.*)"])
result = self.cli.invoke(cli.cli, ["--commits", "foo...bar", "-c", "I1.regex=^commït-title2(.*)",
"-c", "I2.regex=^commït-body3(.*)", "-c", "I2.ignore=B5"])
# 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')
u'1: T3 Title has trailing punctuation (.): "commït-title3."\n')
self.assertEqual(stderr.getvalue(), expected)
self.assertEqual(result.exit_code, 2)

Expand Down
35 changes: 34 additions & 1 deletion gitlint/tests/test_configuration_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def test_ignore_by_title(self):
config = LintConfig()
rule.apply(config, commit)
self.assertEqual(config, LintConfig())
self.assert_logged([])
self.assert_logged([]) # nothing logged -> nothing ignored

# Matching regex -> expect config to ignore all rules
rule = rules.IgnoreByTitle({"regex": "^Releäse(.*)"})
Expand All @@ -36,3 +36,36 @@ def test_ignore_by_title(self):

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"

def test_ignore_by_body(self):
commit = self.gitcommit(u"Tïtle\n\nThis is\n a relëase body\n line")

# No regex specified -> Config shouldn't be changed
rule = rules.IgnoreByBody()
config = LintConfig()
rule.apply(config, commit)
self.assertEqual(config, LintConfig())
self.assert_logged([]) # nothing logged -> nothing ignored

# Matching regex -> expect config to ignore all rules
rule = rules.IgnoreByBody({"regex": "(.*)relëase(.*)"})
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 'I2': " + \
u"Commit message line ' a relëase body' matches the regex '(.*)relëase(.*)'," + \
u" ignoring rules: all"
self.assert_log_contains(expected_log_message)

# Matching regex with specific ignore
rule = rules.IgnoreByBody({"regex": "(.*)relëase(.*)",
"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 message line ' a relëase body' matches the regex '(.*)relëase(.*)', ignoring rules: T1,B2"
3 changes: 3 additions & 0 deletions qa/expected/debug_output1
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@ debug: True
target: {target}
[RULES]
I1: ignore-by-title
ignore=all
regex=None
I2: ignore-by-body
ignore=all
regex=None
T1: title-max-length
line-length=20
T2: title-trailing-whitespace
Expand Down
6 changes: 5 additions & 1 deletion qa/samples/config/ignore-release-commits
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
[ignore-by-title]
regex=^Release(.*)
ignore=T5,T3
ignore=T5,T3

[ignore-by-body]
regex=(.*)relëase(.*)
ignore=T3,B3
5 changes: 3 additions & 2 deletions qa/test_commits.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ def test_ignore_commits(self):
# But in this case only B5 because T3 and T5 are being ignored because of config
self._create_simple_commit(u"Release: WIP tïtle.\n\nShort", git_repo=tmp_git_repo)
# In the following 2 commits, the T3 violations are as normal
self._create_simple_commit(u"Sïmple title3.\n\nSimple bödy describing the commit3", git_repo=tmp_git_repo)
self._create_simple_commit(
u"Sïmple WIP title3.\n\nThis is \ta relëase commit\nMore info", git_repo=tmp_git_repo)
self._create_simple_commit(u"Sïmple title4.\n\nSimple bödy describing the commit4", git_repo=tmp_git_repo)
revlist = git("rev-list", "HEAD", _err_to_out=True, _cwd=tmp_git_repo).split()

Expand All @@ -95,7 +96,7 @@ def test_ignore_commits(self):
u"Commit {0}:\n".format(revlist[0][:10]) +
u"1: T3 Title has trailing punctuation (.): \"Sïmple title4.\"\n\n" +
u"Commit {0}:\n".format(revlist[1][:10]) +
u"1: T3 Title has trailing punctuation (.): \"Sïmple title3.\"\n\n" +
u"1: T5 Title contains the word 'WIP' (case-insensitive): \"Sïmple WIP title3.\"\n\n" +
u"Commit {0}:\n".format(revlist[2][:10]) +
u"3: B5 Body message is too short (5<20): \"Short\"\n\n" +
u"Commit {0}:\n".format(revlist[3][:10]) +
Expand Down

0 comments on commit 7a964c9

Please # to comment.