blob: ea31c6737f39885eca02b819e10565dfac0152e6 [file] [log] [blame]
Anas Nashif3ae52622019-04-06 09:08:09 -04001# SPDX-License-Identifier: Apache-2.0
2
Anas Nashifa35378e2017-04-22 11:59:30 -04003"""
4The classes below are examples of user-defined CommitRules. Commit rules are gitlint rules that
5act on the entire commit at once. Once the rules are discovered, gitlint will automatically take care of applying them
6to the entire commit. This happens exactly once per commit.
7
8A CommitRule contrasts with a LineRule (see examples/my_line_rules.py) in that a commit rule is only applied once on
9an entire commit. This allows commit rules to implement more complex checks that span multiple lines and/or checks
10that should only be done once per gitlint run.
11
12While every LineRule can be implemented as a CommitRule, it's usually easier and more concise to go with a LineRule if
13that fits your needs.
14"""
15
Ulf Magnussond5b0bd12019-03-19 19:37:07 +010016from gitlint.rules import CommitRule, RuleViolation, CommitMessageTitle, LineRule, CommitMessageBody
17from gitlint.options import IntOption, StrOption
Ulf Magnusson7da00532019-03-25 20:23:42 +010018import re
19
Anas Nashif2dd5cef2018-01-10 19:12:00 -050020class BodyMinLineCount(CommitRule):
21 # A rule MUST have a human friendly name
22 name = "body-min-line-count"
23
24 # A rule MUST have an *unique* id, we recommend starting with UC (for User-defined Commit-rule).
25 id = "UC6"
26
Marc Herbert8e7c7c62023-09-20 19:45:22 +000027 # A rule MAY have an options_spec if its behavior should be configurable.
28 options_spec = [IntOption('min-line-count', 1, "Minimum body line count excluding Signed-off-by")]
Anas Nashif2dd5cef2018-01-10 19:12:00 -050029
30 def validate(self, commit):
31 filtered = [x for x in commit.message.body if not x.lower().startswith("signed-off-by") and x != '']
32 line_count = len(filtered)
33 min_line_count = self.options['min-line-count'].value
Anas Nashifabfed532018-01-11 09:57:03 -050034 if line_count < min_line_count:
Marti Bolivar2149d212023-01-27 15:06:48 -080035 message = "Commit message body is empty, should at least have {} line(s).".format(min_line_count)
Anas Nashif2dd5cef2018-01-10 19:12:00 -050036 return [RuleViolation(self.id, message, line_nr=1)]
Anas Nashifa35378e2017-04-22 11:59:30 -040037
38class BodyMaxLineCount(CommitRule):
39 # A rule MUST have a human friendly name
40 name = "body-max-line-count"
41
42 # A rule MUST have an *unique* id, we recommend starting with UC (for User-defined Commit-rule).
43 id = "UC1"
44
Marc Herbert8e7c7c62023-09-20 19:45:22 +000045 # A rule MAY have an options_spec if its behavior should be configurable.
46 options_spec = [IntOption('max-line-count', 200, "Maximum body line count")]
Anas Nashifa35378e2017-04-22 11:59:30 -040047
48 def validate(self, commit):
49 line_count = len(commit.message.body)
50 max_line_count = self.options['max-line-count'].value
51 if line_count > max_line_count:
Marti Bolivar2149d212023-01-27 15:06:48 -080052 message = "Commit message body contains too many lines ({0} > {1})".format(line_count, max_line_count)
Anas Nashifa35378e2017-04-22 11:59:30 -040053 return [RuleViolation(self.id, message, line_nr=1)]
54
Anas Nashifa35378e2017-04-22 11:59:30 -040055class SignedOffBy(CommitRule):
Paul Sokolovsky26e562c2019-02-18 19:52:59 +030056 """ This rule will enforce that each commit contains a "Signed-off-by" line.
57 We keep things simple here and just check whether the commit body contains a line that starts with "Signed-off-by".
Anas Nashifa35378e2017-04-22 11:59:30 -040058 """
59
60 # A rule MUST have a human friendly name
61 name = "body-requires-signed-off-by"
62
63 # A rule MUST have an *unique* id, we recommend starting with UC (for User-defined Commit-rule).
64 id = "UC2"
65
66 def validate(self, commit):
Anas Nashif1338f492017-05-05 16:39:34 -040067 flags = re.UNICODE
68 flags |= re.IGNORECASE
Anas Nashifa35378e2017-04-22 11:59:30 -040069 for line in commit.message.body:
70 if line.lower().startswith("signed-off-by"):
Ulf Magnussona449c982019-03-21 21:38:03 +010071 if not re.search(r"(^)Signed-off-by: ([-'\w.]+) ([-'\w.]+) (.*)", line, flags=flags):
Anas Nashif1338f492017-05-05 16:39:34 -040072 return [RuleViolation(self.id, "Signed-off-by: must have a full name", line_nr=1)]
73 else:
74 return
Marti Bolivar2149d212023-01-27 15:06:48 -080075 return [RuleViolation(self.id, "Commit message does not contain a 'Signed-off-by:' line", line_nr=1)]
Anas Nashif3c27c462017-05-05 19:37:52 -040076
Anas Nashif87766a22017-08-08 08:36:01 -040077class TitleMaxLengthRevert(LineRule):
78 name = "title-max-length-no-revert"
79 id = "UC5"
80 target = CommitMessageTitle
Marc Herbert8e7c7c62023-09-20 19:45:22 +000081 options_spec = [IntOption('line-length', 75, "Max line length")]
Tristan Honscheidae979712022-12-06 16:43:45 -070082 violation_message = "Commit title exceeds max length ({0}>{1})"
Anas Nashif87766a22017-08-08 08:36:01 -040083
84 def validate(self, line, _commit):
85 max_length = self.options['line-length'].value
86 if len(line) > max_length and not line.startswith("Revert"):
87 return [RuleViolation(self.id, self.violation_message.format(len(line), max_length), line)]
Anas Nashif3c27c462017-05-05 19:37:52 -040088
89class TitleStartsWithSubsystem(LineRule):
90 name = "title-starts-with-subsystem"
91 id = "UC3"
92 target = CommitMessageTitle
93 options_spec = [StrOption('regex', ".*", "Regex the title should match")]
94
95 def validate(self, title, _commit):
96 regex = self.options['regex'].value
97 pattern = re.compile(regex, re.UNICODE)
Tristan Honscheidae979712022-12-06 16:43:45 -070098 violation_message = "Commit title does not follow [subsystem]: [subject] (and should not start with literal subsys:)"
Anas Nashif3c27c462017-05-05 19:37:52 -040099 if not pattern.search(title):
100 return [RuleViolation(self.id, violation_message, title)]
Anas Nashifb5200752017-06-06 08:50:11 -0400101
102class MaxLineLengthExceptions(LineRule):
103 name = "max-line-length-with-exceptions"
104 id = "UC4"
105 target = CommitMessageBody
Marc Herbert8e7c7c62023-09-20 19:45:22 +0000106 options_spec = [IntOption('line-length', 75, "Max line length")]
Marti Bolivar2149d212023-01-27 15:06:48 -0800107 violation_message = "Commit message body line exceeds max length ({0}>{1})"
Anas Nashifb5200752017-06-06 08:50:11 -0400108
109 def validate(self, line, _commit):
110 max_length = self.options['line-length'].value
Ulf Magnussona449c982019-03-21 21:38:03 +0100111 urls = re.findall(r'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+', line)
Anas Nashif9e87bd72023-09-26 23:04:33 +0000112 if line.lower().startswith('signed-off-by') or line.lower().startswith('co-authored-by'):
Anas Nashif408a61d2017-08-08 08:07:00 -0400113 return
114
Ulf Magnusson1c57b222019-09-02 14:38:31 +0200115 if urls:
Anas Nashif408a61d2017-08-08 08:07:00 -0400116 return
117
118 if len(line) > max_length:
Anas Nashifb5200752017-06-06 08:50:11 -0400119 return [RuleViolation(self.id, self.violation_message.format(len(line), max_length), line)]
Fabio Baltierid42b2e62022-04-20 15:06:11 +0100120
121class BodyContainsBlockedTags(LineRule):
122 name = "body-contains-blocked-tags"
123 id = "UC7"
124 target = CommitMessageBody
125 tags = ["Change-Id"]
126
127 def validate(self, line, _commit):
128 flags = re.IGNORECASE
129 for tag in self.tags:
130 if re.search(rf"^\s*{tag}:", line, flags=flags):
Marti Bolivar2149d212023-01-27 15:06:48 -0800131 return [RuleViolation(self.id, f"Commit message contains a blocked tag: {tag}")]
Fabio Baltierid42b2e62022-04-20 15:06:11 +0100132 return