@@ -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