22# SPDX-License-Identifier: Apache-2.0
33"""The user defined rules for gitlint."""
44
5+ import re
6+
57from gitlint .rules import CommitRule , RuleViolation
68
79
@@ -26,125 +28,109 @@ def validate(self, commit):
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 ((i + 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