Skip to content

Commit 85f1a25

Browse files
committed
tests: rework gitlint_rules.py
Make doctests and code match. Now running `--doctest-modules` passes the doctests. Simplified the code a bit to make it easier to understand. Signed-off-by: Pablo Barbáchano <[email protected]>
1 parent c0a5c51 commit 85f1a25

File tree

1 file changed

+58
-72
lines changed

1 file changed

+58
-72
lines changed

tests/framework/gitlint_rules.py

Lines changed: 58 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
# SPDX-License-Identifier: Apache-2.0
33
"""The user defined rules for gitlint."""
44

5+
import re
6+
57
from gitlint.rules import CommitRule, RuleViolation
68

79

@@ -22,129 +24,113 @@ class EndsSigned(CommitRule):
2224
def validate(self, commit):
2325
r"""Validates Signed-off-by and Co-authored-by tags as Linux's scripts/checkpatch.pl
2426
25-
>>> from gitlint.tests.base import BaseTestCase
27+
>>> from gitlint.git import GitContext
2628
>>> from gitlint.rules import RuleViolation
2729
...
2830
>>> ends_signed = EndsSigned()
31+
>>> miss_sob_follows_coab = "Missing 'Signed-off-by' following 'Co-authored-by'"
32+
>>> miss_sob = "'Signed-off-by' not found in commit message body"
33+
>>> non_sign = "Non 'Co-authored-by' or 'Signed-off-by' string found following 1st 'Signed-off-by'"
34+
>>> email_no_match = "'Co-authored-by' and 'Signed-off-by' name/email do not match"
2935
...
3036
>>> msg1 = (
3137
... f"Title\n\nMessage.\n\n"
3238
... f"Signed-off-by: name <email@domain>"
3339
... )
34-
>>> commit1 = BaseTestCase.gitcommit(msg1)
40+
>>> commit1 = GitContext.from_commit_msg(msg1).commits[0]
3541
>>> ends_signed.validate(commit1)
3642
[]
3743
>>> msg2 = (
3844
... f"Title\n\nMessage.\n\n"
3945
... f"Co-authored-by: name <email>\n\n"
4046
... f"Signed-off-by: name <email>"
4147
... )
42-
>>> commit2 = BaseTestCase.gitcommit(msg2)
48+
>>> commit2 = GitContext.from_commit_msg(msg2).commits[0]
4349
>>> ends_signed.validate(commit2)
4450
[]
45-
>>> msg3 = (
46-
... f"Title\n\nMessage.\n\n"
47-
... )
48-
>>> commit3 = BaseTestCase.gitcommit(msg3)
51+
>>> msg3 = f"Title\n\nMessage.\n\n"
52+
>>> commit3 = GitContext.from_commit_msg(msg3).commits[0]
4953
>>> vio3 = ends_signed.validate(commit3)
50-
>>> vio_msg3 = (
51-
... f"'Signed-off-by:' not found in commit message body"
52-
... )
53-
>>> vio3 == [RuleViolation("UC2", vio_msg3)]
54+
>>> vio3 == [RuleViolation("UC2", miss_sob)]
5455
True
5556
>>> msg4 = (
5657
... f"Title\n\nMessage.\n\n"
5758
... f"Signed-off-by: name <email@domain>\n\na sentence"
5859
... )
59-
>>> commit4 = BaseTestCase.gitcommit(msg4)
60+
>>> commit4 = GitContext.from_commit_msg(msg4).commits[0]
6061
>>> vio4 = ends_signed.validate(commit4)
61-
>>> vio_msg4 = (
62-
... f"Non 'Co-authored-by:' or 'Signed-off-by:' string found following 1st 'Signed-off-by:'"
63-
... )
64-
>>> vio4 == [RuleViolation("UC2", vio_msg4, None, 5)]
62+
>>> vio4 == [RuleViolation("UC2", non_sign, None, 6)]
6563
True
6664
>>> msg5 = (
6765
... f"Title\n\nMessage.\n\n"
6866
... f"Co-authored-by: name <email@domain>"
6967
... )
70-
>>> commit5 = BaseTestCase.gitcommit(msg5)
68+
>>> commit5 = GitContext.from_commit_msg(msg5).commits[0]
7169
>>> vio5 = ends_signed.validate(commit5)
72-
>>> vio_msg5 = (
73-
... f"Missing 'Signed-off-by:' following 'Co-authored-by:'"
74-
... )
75-
>>> vio5 == [RuleViolation("UC2", vio_msg5, None, 2)]
70+
>>> vio5 == [
71+
... RuleViolation("UC2", miss_sob, None, None),
72+
... RuleViolation("UC2", miss_sob_follows_coab, None, 5)
73+
... ]
7674
True
7775
>>> msg6 = (
7876
... f"Title\n\nMessage.\n\n"
7977
... f"Co-authored-by: name <email@domain>\n\n"
8078
... f"Signed-off-by: different name <email@domain>"
8179
... )
82-
>>> commit6 = BaseTestCase.gitcommit(msg6)
80+
>>> commit6 = GitContext.from_commit_msg(msg6).commits[0]
8381
>>> vio6 = ends_signed.validate(commit6)
84-
>>> vio_msg6 = (
85-
... f"'Co-authored-by:' and 'Signed-off-by:' name/email do not match"
86-
... )
87-
>>> vio6 == [RuleViolation("UC2", vio_msg6, None, 6)]
82+
>>> vio6 == [RuleViolation("UC2", email_no_match, None, 6)]
8883
True
8984
"""
9085

9186
violations = []
9287

9388
# Utilities
9489
def vln(stmt, i):
95-
return RuleViolation(self.id, stmt, None, i)
96-
97-
co_auth = "Co-authored-by:"
98-
sig = "Signed-off-by:"
90+
violations.append(RuleViolation(self.id, stmt, None, i))
9991

100-
message_iter = enumerate(commit.message.original.split("\n"))
92+
coab = "Co-authored-by"
93+
sob = "Signed-off-by"
10194

102-
# Skip ahead to the first signoff or co-author tag
103-
104-
# Checks commit message contains a `Signed-off-by` string
105-
for i, line in message_iter:
106-
if line.startswith(sig) or line.startswith(co_auth):
107-
break
108-
else:
109-
# No signature was found in the message (before `message_iter` ended)
110-
# This check here can have false-negatives (e.g. if the body ends with only
111-
# a 'Co-authored-by' tag), but then below will realize that the co-authored-by
112-
# tag isnt followed by a Signed-off-by tag and fail (and also the DCO check will
113-
# complain).
114-
violations.append(vln(f"'{sig}' not found in commit message body", None))
115-
116-
# Check that from here on out we only have signatures and co-authors, and that
117-
# every co-author is immediately followed by a signature with the same name/email.
118-
for i, line in message_iter:
119-
if line.startswith(co_auth):
120-
try:
121-
_, next_line = next(message_iter)
122-
except StopIteration:
123-
violations.append(
124-
vln(f"Missing '{sig}' tag following '{co_auth}'", i)
125-
)
126-
else:
127-
if not next_line.startswith(sig):
128-
violations.append(
129-
vln(f"Missing '{sig}' tag following '{co_auth}'", i + 1)
130-
)
131-
continue
132-
133-
if next_line.split(":")[1].strip() != line.split(":")[1].strip():
134-
violations.append(
135-
vln(f"{co_auth} and {sig} name/email do not match", i + 1)
136-
)
137-
continue
138-
139-
if line.startswith(sig) or not line.strip():
95+
# find trailers
96+
trailers = []
97+
for i, line in enumerate(commit.message.original.splitlines()):
98+
# ignore empty lines
99+
if not line:
140100
continue
101+
match = re.match(r"([\w-]+):\s+(.*)", line)
102+
if match:
103+
key, val = match.groups()
104+
trailers.append((i, key, val))
105+
else:
106+
trailers.append((i, "line", line))
107+
# artificial line so we can check any "previous line" rules
108+
trailers.append((trailers[-1][0] + 1, None, None))
141109

142-
violations.append(
110+
# Checks commit message contains a `Signed-off-by` string
111+
if not [x for x in trailers if x[1] == sob]:
112+
vln(f"'{sob}' not found in commit message body", None)
113+
114+
prev_trailer, prev_value = None, None
115+
sig_trailers = False
116+
for i, trailer, value in trailers:
117+
if trailer in {sob, coab}:
118+
sig_trailers = True
119+
elif trailer not in {sob, coab, None} and sig_trailers:
143120
vln(
144-
f"Non '{co_auth}' or '{sig}' string found following 1st '{sig}'",
121+
f"Non '{coab}' or '{sob}' string found following 1st '{sob}'",
145122
i,
146123
)
147-
)
124+
# Every co-author is immediately followed by a signature
125+
if prev_trailer == coab:
126+
if trailer != sob:
127+
vln(f"Missing '{sob}' following '{coab}'", i)
128+
else:
129+
# with the same name/email.
130+
if value != prev_value:
131+
vln(f"'{coab}' and '{sob}' name/email do not match", i)
132+
133+
prev_trailer, prev_value = trailer, value
148134

149135
# Return errors
150136
return violations

0 commit comments

Comments
 (0)