Skip to content

Commit e2fceff

Browse files
roypatzulinx86
authored andcommitted
fix: Make custom gitlint rule match checkpatch.pl behavior
Our custom gitlint rule used to enforce that only a single Signed-off-by line exists, and that all co-authored-by lines are after this signoff line. This does not match the behavior of scripts/checkpatch.pl [1], making our check very confusing for people who also work with linux patches. Thus change our gitlint rules to instead match the convention for the Linux kernel [2] (which is where DCO originated anyway). The only real change is that now all Co-authored-by lines require matching sign-offs. [1]: https://github.com/torvalds/linux/blob/master/scripts/checkpatch.pl [2]: https://docs.kernel.org/process/submitting-patches.html#when-to-use-acked-by-cc-and-co-developed-by Signed-off-by: Patrick Roy <[email protected]>
1 parent 82b81f3 commit e2fceff

File tree

1 file changed

+54
-49
lines changed

1 file changed

+54
-49
lines changed

tests/framework/gitlint_rules.py

Lines changed: 54 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ class EndsSigned(CommitRule):
2020
id = "UC2"
2121

2222
def validate(self, commit):
23-
r"""Validate user defined gitlint rules.
23+
r"""Validates Signed-off-by and Co-authored-by tags as Linux's scripts/checkpatch.pl
2424
2525
>>> from gitlint.tests.base import BaseTestCase
2626
>>> from gitlint.rules import RuleViolation
@@ -36,8 +36,8 @@ def validate(self, commit):
3636
[]
3737
>>> msg2 = (
3838
... f"Title\n\nMessage.\n\n"
39-
... f"Signed-off-by: name <email>\n\n"
40-
... f"Co-authored-by: name <email>"
39+
... f"Co-authored-by: name <email>\n\n"
40+
... f"Signed-off-by: name <email>"
4141
... )
4242
>>> commit2 = BaseTestCase.gitcommit(msg2)
4343
>>> ends_signed.validate(commit2)
@@ -48,8 +48,7 @@ def validate(self, commit):
4848
>>> commit3 = BaseTestCase.gitcommit(msg3)
4949
>>> vio3 = ends_signed.validate(commit3)
5050
>>> vio_msg3 = (
51-
... f"'Signed-off-by:' not found "
52-
... f"in commit message body"
51+
... f"'Signed-off-by:' not found in commit message body"
5352
... )
5453
>>> vio3 == [RuleViolation("UC2", vio_msg3)]
5554
True
@@ -60,86 +59,92 @@ def validate(self, commit):
6059
>>> commit4 = BaseTestCase.gitcommit(msg4)
6160
>>> vio4 = ends_signed.validate(commit4)
6261
>>> vio_msg4 = (
63-
... f"Non 'Co-authored-by:' or 'Signed-off-by:'"
64-
... f" string found following 1st 'Signed-off-by:'"
62+
... f"Non 'Co-authored-by:' or 'Signed-off-by:' string found following 1st 'Signed-off-by:'"
6563
... )
6664
>>> vio4 == [RuleViolation("UC2", vio_msg4, None, 5)]
6765
True
6866
>>> msg5 = (
6967
... f"Title\n\nMessage.\n\n"
70-
... f"Co-authored-by: name <email@domain>\n\n"
71-
... f"a sentence."
68+
... f"Co-authored-by: name <email@domain>"
7269
... )
7370
>>> commit5 = BaseTestCase.gitcommit(msg5)
7471
>>> vio5 = ends_signed.validate(commit5)
7572
>>> vio_msg5 = (
76-
... f"'Co-authored-by:' found before 'Signed-off-by:'"
73+
... f"Missing 'Signed-off-by:' following 'Co-authored-by:'"
7774
... )
78-
>>> vio5 == [RuleViolation("UC2", vio_msg5, None, 3)]
75+
>>> vio5 == [RuleViolation("UC2", vio_msg5, None, 2)]
7976
True
8077
>>> msg6 = (
8178
... f"Title\n\nMessage.\n\n"
82-
... f"Signed-off-by: name <email@domain>\n\n"
8379
... f"Co-authored-by: name <email@domain>\n\n"
84-
... f"a sentence"
80+
... f"Signed-off-by: different name <email@domain>"
8581
... )
8682
>>> commit6 = BaseTestCase.gitcommit(msg6)
8783
>>> vio6 = ends_signed.validate(commit6)
8884
>>> vio_msg6 = (
89-
... f"Non 'Co-authored-by:' string found "
90-
... f"after 1st 'Co-authored-by:'"
85+
... f"'Co-authored-by:' and 'Signed-off-by:' name/email do not match"
9186
... )
9287
>>> vio6 == [RuleViolation("UC2", vio_msg6, None, 6)]
9388
True
9489
"""
9590

91+
violations = []
92+
9693
# Utilities
97-
def rtn(stmt, i):
98-
return [RuleViolation(self.id, stmt, None, i)]
94+
def vln(stmt, i):
95+
return RuleViolation(self.id, stmt, None, i)
9996

10097
co_auth = "Co-authored-by:"
10198
sig = "Signed-off-by:"
10299

103100
message_iter = enumerate(commit.message.original.split("\n"))
104101

105-
# Checks commit message contains a `sig` string
106-
found = False
102+
# Skip ahead to the first signoff or co-author tag
103+
104+
# Checks commit message contains a `Signed-off-by` string
107105
for i, line in message_iter:
108-
# We check that no co-authors are declared before signatures.
109-
if line.startswith(co_auth):
110-
return rtn(f"'{co_auth}' found before '{sig}'", i)
111-
if line.startswith(sig):
112-
found = True
106+
if line.startswith(sig) or line.startswith(co_auth):
113107
break
114-
115-
# If no signature was found in the message
116-
# (before `message_iter` ended)
117-
if not found:
118-
return rtn(f"'{sig}' not found in commit message body", None)
119-
120-
# Checks lines following signature are
121-
# either signatures or co-authors
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.
122118
for i, line in message_iter:
123-
if line.startswith(sig) or not line.strip():
124-
continue
125-
126-
# Once we encounter the first co-author,
127-
# we no longer accept signatures
128119
if line.startswith(co_auth):
129-
break
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
130138

131-
return rtn(
132-
f"Non '{co_auth}' or '{sig}' string found " f"following 1st '{sig}'",
133-
i,
134-
)
139+
if line.startswith(sig) or not line.strip():
140+
continue
135141

136-
# Checks lines following co-author are only additional co-authors.
137-
for i, line in message_iter:
138-
if line and not line.startswith(co_auth):
139-
return rtn(
140-
f"Non '{co_auth}' string found after 1st '{co_auth}'",
142+
violations.append(
143+
vln(
144+
f"Non '{co_auth}' or '{sig}' string found following 1st '{sig}'",
141145
i,
142146
)
147+
)
143148

144-
# Return no errors
145-
return []
149+
# Return errors
150+
return violations

0 commit comments

Comments
 (0)