From 23d0ac82f3d68663ddc74a0e1f9b963beb8d62b9 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sat, 4 Dec 2021 04:31:49 +0100 Subject: Merging upstream version 0.17.0. Signed-off-by: Daniel Baumann --- gitlint/rules.py | 440 ------------------------------------------------------- 1 file changed, 440 deletions(-) delete mode 100644 gitlint/rules.py (limited to 'gitlint/rules.py') diff --git a/gitlint/rules.py b/gitlint/rules.py deleted file mode 100644 index 1c5a618..0000000 --- a/gitlint/rules.py +++ /dev/null @@ -1,440 +0,0 @@ -# pylint: disable=inconsistent-return-statements -import copy -import logging -import re - -from gitlint.options import IntOption, BoolOption, StrOption, ListOption, RegexOption -from gitlint.exception import GitlintError - - -class Rule: - """ Class representing gitlint rules. """ - options_spec = [] - id = None - name = None - target = None - _log = None - - def __init__(self, opts=None): - if not opts: - opts = {} - self.options = {} - for op_spec in self.options_spec: - self.options[op_spec.name] = copy.deepcopy(op_spec) - actual_option = opts.get(op_spec.name) - if actual_option is not None: - self.options[op_spec.name].set(actual_option) - - @property - def log(self): - if not self._log: - self._log = logging.getLogger(__name__) - logging.basicConfig() - 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 - - 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. """ - pass - - -class CommitRule(Rule): - """ 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 """ - pass - - -class LineRuleTarget: - """ 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. """ - pass - - -class CommitMessageTitle(LineRuleTarget): - """ 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 """ - 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. """ - - def __init__(self, rule_id, message, content=None, line_nr=None): - self.rule_id = rule_id - self.line_nr = line_nr - self.message = message - self.content = content - - def __eq__(self, other): - equal = self.rule_id == other.rule_id and self.message == other.message - equal = equal and self.content == other.content and self.line_nr == other.line_nr - return equal - - def __str__(self): - 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 """ - pass - - -class MaxLineLength(LineRule): - name = "max-line-length" - id = "R1" - 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 - if len(line) > max_length: - return [RuleViolation(self.id, self.violation_message.format(len(line), max_length), line)] - - -class TrailingWhiteSpace(LineRule): - name = "trailing-whitespace" - id = "R2" - violation_message = "Line has trailing whitespace" - pattern = re.compile(r"\s$", re.UNICODE) - - def validate(self, line, _commit): - if self.pattern.search(line): - return [RuleViolation(self.id, self.violation_message, line)] - - -class HardTab(LineRule): - name = "hard-tab" - id = "R3" - violation_message = "Line contains hard tab characters (\\t)" - - def validate(self, line, _commit): - if "\t" in line: - return [RuleViolation(self.id, self.violation_message, line)] - - -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.) """ - name = "line-must-not-contain" - id = "R5" - 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 - violations = [] - for string in strings: - regex = re.compile(rf"\b{string.lower()}\b", re.IGNORECASE | re.UNICODE) - match = regex.search(line.lower()) - if match: - violations.append(RuleViolation(self.id, self.violation_message.format(string), line)) - return violations if violations else None - - -class LeadingWhiteSpace(LineRule): - name = "leading-whitespace" - id = "R6" - violation_message = "Line has leading whitespace" - - def validate(self, line, _commit): - pattern = re.compile(r"^\s", re.UNICODE) - if pattern.search(line): - return [RuleViolation(self.id, self.violation_message, line)] - - -class TitleMaxLength(MaxLineLength): - name = "title-max-length" - id = "T1" - target = CommitMessageTitle - options_spec = [IntOption('line-length', 72, "Max line length")] - violation_message = "Title exceeds max length ({0}>{1})" - - -class TitleTrailingWhitespace(TrailingWhiteSpace): - name = "title-trailing-whitespace" - id = "T2" - target = CommitMessageTitle - violation_message = "Title has trailing whitespace" - - -class TitleTrailingPunctuation(LineRule): - name = "title-trailing-punctuation" - id = "T3" - target = CommitMessageTitle - - def validate(self, title, _commit): - 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)] - - -class TitleHardTab(HardTab): - name = "title-hard-tab" - id = "T4" - target = CommitMessageTitle - violation_message = "Title contains hard tab characters (\\t)" - - -class TitleMustNotContainWord(LineMustNotContainWord): - name = "title-must-not-contain-word" - id = "T5" - target = CommitMessageTitle - options_spec = [ListOption('words', ["WIP"], "Must not contain word")] - violation_message = "Title contains the word '{0}' (case-insensitive)" - - -class TitleLeadingWhitespace(LeadingWhiteSpace): - name = "title-leading-whitespace" - id = "T6" - target = CommitMessageTitle - violation_message = "Title has leading whitespace" - - -class TitleRegexMatches(LineRule): - name = "title-match-regex" - id = "T7" - target = CommitMessageTitle - 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: - return - - 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)] - - -class TitleMinLength(LineRule): - name = "title-min-length" - id = "T8" - target = CommitMessageTitle - options_spec = [IntOption('min-length', 5, "Minimum required title length")] - - def validate(self, title, _commit): - 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})" - return [RuleViolation(self.id, violation_message, title, 1)] - - -class BodyMaxLineLength(MaxLineLength): - name = "body-max-line-length" - id = "B1" - target = CommitMessageBody - - -class BodyTrailingWhitespace(TrailingWhiteSpace): - name = "body-trailing-whitespace" - id = "B2" - target = CommitMessageBody - - -class BodyHardTab(HardTab): - name = "body-hard-tab" - id = "B3" - target = CommitMessageBody - - -class BodyFirstLineEmpty(CommitRule): - name = "body-first-line-empty" - id = "B4" - - def validate(self, commit): - if len(commit.message.body) >= 1: - first_line = commit.message.body[0] - if first_line != "": - return [RuleViolation(self.id, "Second line is not empty", first_line, 2)] - - -class BodyMinLength(CommitRule): - name = "body-min-length" - id = "B5" - options_spec = [IntOption('min-length', 20, "Minimum body length")] - - def validate(self, commit): - 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: - violation_message = f"Body message is too short ({actual_length}<{min_length})" - return [RuleViolation(self.id, violation_message, body_message_no_newline, 3)] - - -class BodyMissing(CommitRule): - name = "body-is-missing" - id = "B6" - 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: - return - 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")] - - def validate(self, commit): - violations = [] - 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: - if needs_mentioned_file not in " ".join(commit.message.body): - violation_message = f"Body does not mention changed file '{needs_mentioned_file}'" - violations.append(RuleViolation(self.id, violation_message, None, len(commit.message.body) + 1)) - return violations if violations else None - - -class BodyRegexMatches(CommitRule): - name = "body-match-regex" - id = "B8" - 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: - return - - # We intentionally ignore the first line in the body as that's the empty line after the title, - # which most users are not going to expect to be part of the body when matching a regex. - # If this causes contention, we can always introduce an option to change the behavior in a backward- - # compatible way. - body_lines = commit.message.body[1:] if len(commit.message.body) > 1 else [] - - # Similarly, the last line is often empty, this has to do with how git returns commit messages - # User's won't expect this, so prune it off by default - if body_lines and body_lines[-1] == "": - body_lines.pop() - - full_body = "\n".join(body_lines) - - 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)] - - -class AuthorValidEmail(CommitRule): - name = "author-valid-email" - id = "M1" - options_spec = [RegexOption('regex', r"[^@ ]+@[^@ ]+\.[^@ ]+", "Regex that author email address should match")] - - def validate(self, commit): - # If no regex is specified, immediately return - if not self.options['regex'].value: - return - - if commit.author_email and not self.options['regex'].value.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 = [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: - return - - if self.options['regex'].value.match(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}" - - self.log.debug("Ignoring commit because of rule '%s': %s", self.id, message) - - -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")] - - def apply(self, config, commit): - # If no regex is specified, immediately return - if not self.options['regex'].value: - return - - for line in commit.message.body: - if self.options['regex'].value.match(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}" - - self.log.debug("Ignoring commit because of rule '%s': %s", self.id, message) - # No need to check other lines if we found a match - return - - -class IgnoreBodyLines(ConfigurationRule): - name = "ignore-body-lines" - id = "I3" - 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: - return - - new_body = [] - for line in commit.message.body: - if self.options['regex'].value.match(line): - debug_msg = "Ignoring line '%s' because it matches '%s'" - self.log.debug(debug_msg, line, self.options['regex'].value.pattern) - else: - new_body.append(line) - - commit.message.body = new_body - commit.message.full = "\n".join([commit.message.title] + new_body) - - -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")] - - def apply(self, config, commit): - # If no regex is specified, immediately return - if not self.options['regex'].value: - return - - if self.options['regex'].value.match(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}") - - self.log.debug("Ignoring commit because of rule '%s': %s", self.id, message) - # No need to check other lines if we found a match - return -- cgit v1.2.3