2
2
# SPDX-License-Identifier: Apache-2.0
3
3
"""The user defined rules for gitlint."""
4
4
5
+ import re
6
+
5
7
from gitlint .rules import CommitRule , RuleViolation
6
8
7
9
@@ -22,129 +24,113 @@ class EndsSigned(CommitRule):
22
24
def validate (self , commit ):
23
25
r"""Validates Signed-off-by and Co-authored-by tags as Linux's scripts/checkpatch.pl
24
26
25
- >>> from gitlint.tests.base import BaseTestCase
27
+ >>> from gitlint.git import GitContext
26
28
>>> from gitlint.rules import RuleViolation
27
29
...
28
30
>>> 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"
29
35
...
30
36
>>> msg1 = (
31
37
... f"Title\n\nMessage.\n\n"
32
38
... f"Signed-off-by: name <email@domain>"
33
39
... )
34
- >>> commit1 = BaseTestCase.gitcommit (msg1)
40
+ >>> commit1 = GitContext.from_commit_msg (msg1).commits[0]
35
41
>>> ends_signed.validate(commit1)
36
42
[]
37
43
>>> msg2 = (
38
44
... f"Title\n\nMessage.\n\n"
39
45
... f"Co-authored-by: name <email>\n\n"
40
46
... f"Signed-off-by: name <email>"
41
47
... )
42
- >>> commit2 = BaseTestCase.gitcommit (msg2)
48
+ >>> commit2 = GitContext.from_commit_msg (msg2).commits[0]
43
49
>>> ends_signed.validate(commit2)
44
50
[]
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]
49
53
>>> 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)]
54
55
True
55
56
>>> msg4 = (
56
57
... f"Title\n\nMessage.\n\n"
57
58
... f"Signed-off-by: name <email@domain>\n\na sentence"
58
59
... )
59
- >>> commit4 = BaseTestCase.gitcommit (msg4)
60
+ >>> commit4 = GitContext.from_commit_msg (msg4).commits[0]
60
61
>>> 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)]
65
63
True
66
64
>>> msg5 = (
67
65
... f"Title\n\nMessage.\n\n"
68
66
... f"Co-authored-by: name <email@domain>"
69
67
... )
70
- >>> commit5 = BaseTestCase.gitcommit (msg5)
68
+ >>> commit5 = GitContext.from_commit_msg (msg5).commits[0]
71
69
>>> 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
+ ... ]
76
74
True
77
75
>>> msg6 = (
78
76
... f"Title\n\nMessage.\n\n"
79
77
... f"Co-authored-by: name <email@domain>\n\n"
80
78
... f"Signed-off-by: different name <email@domain>"
81
79
... )
82
- >>> commit6 = BaseTestCase.gitcommit (msg6)
80
+ >>> commit6 = GitContext.from_commit_msg (msg6).commits[0]
83
81
>>> 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)]
88
83
True
89
84
"""
90
85
91
86
violations = []
92
87
93
88
# Utilities
94
89
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 ))
99
91
100
- message_iter = enumerate (commit .message .original .split ("\n " ))
92
+ coab = "Co-authored-by"
93
+ sob = "Signed-off-by"
101
94
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 :
140
100
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 ))
141
109
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 :
143
120
vln (
144
- f"Non '{ co_auth } ' or '{ sig } ' string found following 1st '{ sig } '" ,
121
+ f"Non '{ coab } ' or '{ sob } ' string found following 1st '{ sob } '" ,
145
122
i ,
146
123
)
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
148
134
149
135
# Return errors
150
136
return violations
0 commit comments