summaryrefslogtreecommitdiffstats
path: root/gitlint-core/gitlint/rules.py
diff options
context:
space:
mode:
Diffstat (limited to 'gitlint-core/gitlint/rules.py')
-rw-r--r--gitlint-core/gitlint/rules.py181
1 files changed, 115 insertions, 66 deletions
diff --git a/gitlint-core/gitlint/rules.py b/gitlint-core/gitlint/rules.py
index 1c5a618..6d486a5 100644
--- a/gitlint-core/gitlint/rules.py
+++ b/gitlint-core/gitlint/rules.py
@@ -5,15 +5,18 @@ import re
from gitlint.options import IntOption, BoolOption, StrOption, ListOption, RegexOption
from gitlint.exception import GitlintError
+from gitlint.deprecation import Deprecation
class Rule:
- """ Class representing gitlint rules. """
+ """Class representing gitlint rules."""
+
options_spec = []
id = None
name = None
target = None
_log = None
+ _log_deprecated_regex_style_search = None
def __init__(self, opts=None):
if not opts:
@@ -33,48 +36,58 @@ class Rule:
return self._log
def __eq__(self, other):
- return self.id == other.id and self.name == other.name and \
- self.options == other.options and self.target == other.target # noqa
+ return (
+ self.id == other.id
+ and self.name == other.name
+ and self.options == other.options
+ and self.target == other.target
+ )
def __str__(self):
return f"{self.id} {self.name}" # pragma: no cover
class ConfigurationRule(Rule):
- """ Class representing rules that can dynamically change the configuration of gitlint during runtime. """
+ """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 """
+ """Class representing rules that act on an entire commit at once"""
+
pass
class LineRule(Rule):
- """ Class representing rules that act on a line by line basis """
+ """Class representing rules that act on a line by line basis"""
+
pass
class LineRuleTarget:
- """ Base class for LineRule targets. A LineRuleTarget specifies where a given rule will be applied
+ """Base class for LineRule targets. A LineRuleTarget specifies where a given rule will be applied
(e.g. commit message title, commit message body).
- Each LineRule MUST have a target specified. """
+ Each LineRule MUST have a target specified."""
+
pass
class CommitMessageTitle(LineRuleTarget):
- """ Target class used for rules that apply to a commit message title """
+ """Target class used for rules that apply to a commit message title"""
+
pass
class CommitMessageBody(LineRuleTarget):
- """ Target class used for rules that apply to a commit message body """
+ """Target class used for rules that apply to a commit message body"""
+
pass
class RuleViolation:
- """ Class representing a violation of a rule. I.e.: When a rule is broken, the rule will instantiate this class
- to indicate how and where the rule was broken. """
+ """Class representing a violation of a rule. I.e.: When a rule is broken, the rule will instantiate this class
+ to indicate how and where the rule was broken."""
def __init__(self, rule_id, message, content=None, line_nr=None):
self.rule_id = rule_id
@@ -88,22 +101,23 @@ class RuleViolation:
return equal
def __str__(self):
- return f"{self.line_nr}: {self.rule_id} {self.message}: \"{self.content}\""
+ return f'{self.line_nr}: {self.rule_id} {self.message}: "{self.content}"'
class UserRuleError(GitlintError):
- """ Error used to indicate that an error occurred while trying to load a user rule """
+ """Error used to indicate that an error occurred while trying to load a user rule"""
+
pass
class MaxLineLength(LineRule):
name = "max-line-length"
id = "R1"
- options_spec = [IntOption('line-length', 80, "Max line length")]
+ options_spec = [IntOption("line-length", 80, "Max line length")]
violation_message = "Line exceeds max length ({0}>{1})"
def validate(self, line, _commit):
- max_length = self.options['line-length'].value
+ max_length = self.options["line-length"].value
if len(line) > max_length:
return [RuleViolation(self.id, self.violation_message.format(len(line), max_length), line)]
@@ -130,15 +144,16 @@ class HardTab(LineRule):
class LineMustNotContainWord(LineRule):
- """ Violation if a line contains one of a list of words (NOTE: using a word in the list inside another word is not
- a violation, e.g: WIPING is not a violation if 'WIP' is a word that is not allowed.) """
+ """Violation if a line contains one of a list of words (NOTE: using a word in the list inside another word is not
+ a violation, e.g: WIPING is not a violation if 'WIP' is a word that is not allowed.)"""
+
name = "line-must-not-contain"
id = "R5"
- options_spec = [ListOption('words', [], "Comma separated list of words that should not be found")]
+ options_spec = [ListOption("words", [], "Comma separated list of words that should not be found")]
violation_message = "Line contains {0}"
def validate(self, line, _commit):
- strings = self.options['words'].value
+ strings = self.options["words"].value
violations = []
for string in strings:
regex = re.compile(rf"\b{string.lower()}\b", re.IGNORECASE | re.UNICODE)
@@ -163,7 +178,7 @@ class TitleMaxLength(MaxLineLength):
name = "title-max-length"
id = "T1"
target = CommitMessageTitle
- options_spec = [IntOption('line-length', 72, "Max line length")]
+ options_spec = [IntOption("line-length", 72, "Max line length")]
violation_message = "Title exceeds max length ({0}>{1})"
@@ -180,7 +195,7 @@ class TitleTrailingPunctuation(LineRule):
target = CommitMessageTitle
def validate(self, title, _commit):
- punctuation_marks = '?:!.,;'
+ punctuation_marks = "?:!.,;"
for punctuation_mark in punctuation_marks:
if title.endswith(punctuation_mark):
return [RuleViolation(self.id, f"Title has trailing punctuation ({punctuation_mark})", title)]
@@ -197,7 +212,7 @@ class TitleMustNotContainWord(LineMustNotContainWord):
name = "title-must-not-contain-word"
id = "T5"
target = CommitMessageTitle
- options_spec = [ListOption('words', ["WIP"], "Must not contain word")]
+ options_spec = [ListOption("words", ["WIP"], "Must not contain word")]
violation_message = "Title contains the word '{0}' (case-insensitive)"
@@ -212,14 +227,14 @@ class TitleRegexMatches(LineRule):
name = "title-match-regex"
id = "T7"
target = CommitMessageTitle
- options_spec = [RegexOption('regex', None, "Regex the title should match")]
+ options_spec = [RegexOption("regex", None, "Regex the title should match")]
def validate(self, title, _commit):
# If no regex is specified, immediately return
- if not self.options['regex'].value:
+ if not self.options["regex"].value:
return
- if not self.options['regex'].value.search(title):
+ if not self.options["regex"].value.search(title):
violation_msg = f"Title does not match regex ({self.options['regex'].value.pattern})"
return [RuleViolation(self.id, violation_msg, title)]
@@ -228,10 +243,10 @@ class TitleMinLength(LineRule):
name = "title-min-length"
id = "T8"
target = CommitMessageTitle
- options_spec = [IntOption('min-length', 5, "Minimum required title length")]
+ options_spec = [IntOption("min-length", 5, "Minimum required title length")]
def validate(self, title, _commit):
- min_length = self.options['min-length'].value
+ min_length = self.options["min-length"].value
actual_length = len(title)
if actual_length < min_length:
violation_message = f"Title is too short ({actual_length}<{min_length})"
@@ -270,10 +285,10 @@ class BodyFirstLineEmpty(CommitRule):
class BodyMinLength(CommitRule):
name = "body-min-length"
id = "B5"
- options_spec = [IntOption('min-length', 20, "Minimum body length")]
+ options_spec = [IntOption("min-length", 20, "Minimum body length")]
def validate(self, commit):
- min_length = self.options['min-length'].value
+ min_length = self.options["min-length"].value
body_message_no_newline = "".join([line for line in commit.message.body if line is not None])
actual_length = len(body_message_no_newline)
if 0 < actual_length < min_length:
@@ -284,24 +299,24 @@ class BodyMinLength(CommitRule):
class BodyMissing(CommitRule):
name = "body-is-missing"
id = "B6"
- options_spec = [BoolOption('ignore-merge-commits', True, "Ignore merge commits")]
+ options_spec = [BoolOption("ignore-merge-commits", True, "Ignore merge commits")]
def validate(self, commit):
# ignore merges when option tells us to, which may have no body
- if self.options['ignore-merge-commits'].value and commit.is_merge_commit:
+ if self.options["ignore-merge-commits"].value and commit.is_merge_commit:
return
- if len(commit.message.body) < 2 or not ''.join(commit.message.body).strip():
+ if len(commit.message.body) < 2 or not "".join(commit.message.body).strip():
return [RuleViolation(self.id, "Body message is missing", None, 3)]
class BodyChangedFileMention(CommitRule):
name = "body-changed-file-mention"
id = "B7"
- options_spec = [ListOption('files', [], "Files that need to be mentioned")]
+ options_spec = [ListOption("files", [], "Files that need to be mentioned")]
def validate(self, commit):
violations = []
- for needs_mentioned_file in self.options['files'].value:
+ for needs_mentioned_file in self.options["files"].value:
# if a file that we need to look out for is actually changed, then check whether it occurs
# in the commit msg body
if needs_mentioned_file in commit.changed_files:
@@ -314,11 +329,11 @@ class BodyChangedFileMention(CommitRule):
class BodyRegexMatches(CommitRule):
name = "body-match-regex"
id = "B8"
- options_spec = [RegexOption('regex', None, "Regex the body should match")]
+ options_spec = [RegexOption("regex", None, "Regex the body should match")]
def validate(self, commit):
# If no regex is specified, immediately return
- if not self.options['regex'].value:
+ if not self.options["regex"].value:
return
# We intentionally ignore the first line in the body as that's the empty line after the title,
@@ -334,7 +349,7 @@ class BodyRegexMatches(CommitRule):
full_body = "\n".join(body_lines)
- if not self.options['regex'].value.search(full_body):
+ if not self.options["regex"].value.search(full_body):
violation_msg = f"Body does not match regex ({self.options['regex'].value.pattern})"
return [RuleViolation(self.id, violation_msg, None, len(commit.message.body) + 1)]
@@ -342,33 +357,51 @@ class BodyRegexMatches(CommitRule):
class AuthorValidEmail(CommitRule):
name = "author-valid-email"
id = "M1"
- options_spec = [RegexOption('regex', r"[^@ ]+@[^@ ]+\.[^@ ]+", "Regex that author email address should match")]
+ DEFAULT_AUTHOR_VALID_EMAIL_REGEX = r"^[^@ ]+@[^@ ]+\.[^@ ]+"
+ options_spec = [
+ RegexOption("regex", DEFAULT_AUTHOR_VALID_EMAIL_REGEX, "Regex that author email address should match")
+ ]
def validate(self, commit):
# If no regex is specified, immediately return
- if not self.options['regex'].value:
+ if not self.options["regex"].value:
return
- if commit.author_email and not self.options['regex'].value.match(commit.author_email):
+ # We're replacing regex match with search semantics, see https://github.com/jorisroovers/gitlint/issues/254
+ # In case the user is using the default regex, we can silently change to using search
+ # If not, it depends on config (handled by Deprecation class)
+ if self.DEFAULT_AUTHOR_VALID_EMAIL_REGEX == self.options["regex"].value.pattern:
+ regex_method = self.options["regex"].value.search
+ else:
+ regex_method = Deprecation.get_regex_method(self, self.options["regex"])
+
+ if commit.author_email and not regex_method(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 = [RegexOption('regex', None, "Regex matching the titles of commits this rule should apply to"),
- StrOption('ignore', "all", "Comma-separated list of rules to ignore")]
+ options_spec = [
+ RegexOption("regex", None, "Regex matching the titles of commits this rule should apply to"),
+ StrOption("ignore", "all", "Comma-separated list of rules to ignore"),
+ ]
def apply(self, config, commit):
# If no regex is specified, immediately return
- if not self.options['regex'].value:
+ if not self.options["regex"].value:
return
- if self.options['regex'].value.match(commit.message.title):
- config.ignore = self.options['ignore'].value
+ # We're replacing regex match with search semantics, see https://github.com/jorisroovers/gitlint/issues/254
+ regex_method = Deprecation.get_regex_method(self, self.options["regex"])
+
+ if regex_method(commit.message.title):
+ config.ignore = self.options["ignore"].value
- message = f"Commit title '{commit.message.title}' matches the regex " + \
- f"'{self.options['regex'].value.pattern}', ignoring rules: {self.options['ignore'].value}"
+ message = (
+ f"Commit title '{commit.message.title}' matches the regex "
+ f"'{self.options['regex'].value.pattern}', ignoring rules: {self.options['ignore'].value}"
+ )
self.log.debug("Ignoring commit because of rule '%s': %s", self.id, message)
@@ -376,20 +409,27 @@ class IgnoreByTitle(ConfigurationRule):
class IgnoreByBody(ConfigurationRule):
name = "ignore-by-body"
id = "I2"
- options_spec = [RegexOption('regex', None, "Regex matching lines of the body of commits this rule should apply to"),
- StrOption('ignore', "all", "Comma-separated list of rules to ignore")]
+ options_spec = [
+ RegexOption("regex", None, "Regex matching lines of the body of commits this rule should apply to"),
+ StrOption("ignore", "all", "Comma-separated list of rules to ignore"),
+ ]
def apply(self, config, commit):
# If no regex is specified, immediately return
- if not self.options['regex'].value:
+ if not self.options["regex"].value:
return
+ # We're replacing regex match with search semantics, see https://github.com/jorisroovers/gitlint/issues/254
+ regex_method = Deprecation.get_regex_method(self, self.options["regex"])
+
for line in commit.message.body:
- if self.options['regex'].value.match(line):
- config.ignore = self.options['ignore'].value
+ if regex_method(line):
+ config.ignore = self.options["ignore"].value
- message = f"Commit message line '{line}' matches the regex '{self.options['regex'].value.pattern}'," + \
- f" ignoring rules: {self.options['ignore'].value}"
+ message = (
+ f"Commit message line '{line}' matches the regex '{self.options['regex'].value.pattern}',"
+ f" ignoring rules: {self.options['ignore'].value}"
+ )
self.log.debug("Ignoring commit because of rule '%s': %s", self.id, message)
# No need to check other lines if we found a match
@@ -399,18 +439,21 @@ class IgnoreByBody(ConfigurationRule):
class IgnoreBodyLines(ConfigurationRule):
name = "ignore-body-lines"
id = "I3"
- options_spec = [RegexOption('regex', None, "Regex matching lines of the body that should be ignored")]
+ options_spec = [RegexOption("regex", None, "Regex matching lines of the body that should be ignored")]
def apply(self, _, commit):
# If no regex is specified, immediately return
- if not self.options['regex'].value:
+ if not self.options["regex"].value:
return
+ # We're replacing regex match with search semantics, see https://github.com/jorisroovers/gitlint/issues/254
+ regex_method = Deprecation.get_regex_method(self, self.options["regex"])
+
new_body = []
for line in commit.message.body:
- if self.options['regex'].value.match(line):
+ if regex_method(line):
debug_msg = "Ignoring line '%s' because it matches '%s'"
- self.log.debug(debug_msg, line, self.options['regex'].value.pattern)
+ self.log.debug(debug_msg, line, self.options["regex"].value.pattern)
else:
new_body.append(line)
@@ -421,19 +464,25 @@ class IgnoreBodyLines(ConfigurationRule):
class IgnoreByAuthorName(ConfigurationRule):
name = "ignore-by-author-name"
id = "I4"
- options_spec = [RegexOption('regex', None, "Regex matching the author name of commits this rule should apply to"),
- StrOption('ignore', "all", "Comma-separated list of rules to ignore")]
+ options_spec = [
+ RegexOption("regex", None, "Regex matching the author name of commits this rule should apply to"),
+ StrOption("ignore", "all", "Comma-separated list of rules to ignore"),
+ ]
def apply(self, config, commit):
# If no regex is specified, immediately return
- if not self.options['regex'].value:
+ if not self.options["regex"].value:
return
- if self.options['regex'].value.match(commit.author_name):
- config.ignore = self.options['ignore'].value
+ regex_method = Deprecation.get_regex_method(self, self.options["regex"])
+
+ if regex_method(commit.author_name):
+ config.ignore = self.options["ignore"].value
- message = (f"Commit Author Name '{commit.author_name}' matches the regex "
- f"'{self.options['regex'].value.pattern}', ignoring rules: {self.options['ignore'].value}")
+ message = (
+ f"Commit Author Name '{commit.author_name}' matches the regex "
+ f"'{self.options['regex'].value.pattern}', ignoring rules: {self.options['ignore'].value}"
+ )
self.log.debug("Ignoring commit because of rule '%s': %s", self.id, message)
# No need to check other lines if we found a match