diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2022-11-19 14:52:50 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2022-11-19 14:53:01 +0000 |
commit | f3b6c222fb11c96e2f8bbaa0622f46c8ec486874 (patch) | |
tree | 0f38497775e27d3e16b20573b36dd22aa5b24f3e /gitlint-core/gitlint/rules.py | |
parent | Releasing debian version 0.17.0-1. (diff) | |
download | gitlint-f3b6c222fb11c96e2f8bbaa0622f46c8ec486874.tar.xz gitlint-f3b6c222fb11c96e2f8bbaa0622f46c8ec486874.zip |
Merging upstream version 0.18.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | gitlint-core/gitlint/rules.py | 181 |
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 |