2 # Verifies whether commit messages adhere to the standards.
3 # Checks the author name and email and invokes the tools/commit-msg script.
4 # Copy this into .git/hooks/post-commit
6 # Copyright (c) 2018 Peter Wu <peter@lekensteyn.nl>
8 # Wireshark - Network traffic analyzer
9 # By Gerald Combs <gerald@wireshark.org>
10 # Copyright 1998 Gerald Combs
12 # SPDX-License-Identifier: GPL-2.0-or-later
14 from __future__
import print_function
26 parser
= argparse
.ArgumentParser()
27 parser
.add_argument('commit', nargs
='?', default
='HEAD',
28 help='Commit ID to be checked (default %(default)s)')
29 parser
.add_argument('--commitmsg', help='commit-msg check', action
='store')
32 def print_git_user_instructions():
33 print('To configure your name and email for git, run:')
35 print(' git config --global user.name "Your Name"')
36 print(' git config --global user.email "you@example.com"')
38 print('After that update the author of your latest commit with:')
40 print(' git commit --amend --reset-author --no-edit')
44 def verify_name(name
):
45 name
= name
.lower().strip()
46 forbidden_names
= ('unknown', 'root', 'user', 'your name')
47 if name
in forbidden_names
:
49 # Warn about names without spaces. Sometimes it is a mistake where the
50 # developer accidentally committed using the system username.
52 print("WARNING: name '%s' does not contain a space." % (name
,))
53 print_git_user_instructions()
57 def verify_email(email
):
58 email
= email
.lower().strip()
60 user
, host
= email
.split('@')
62 # Lacks a '@' (e.g. a plain domain or "foo[AT]example.com")
64 tld
= host
.split('.')[-1]
66 # localhost, localhost.localdomain, my.local etc.
70 # Possibly an IP address
74 # forbid code.wireshark.org. Submissions could be submitted by other
75 # addresses if one would like to remain anonymous.
76 if host
.endswith('.wireshark.org'):
79 # For documentation purposes only.
80 if host
== 'example.com':
83 # 'peter-ubuntu32.(none)'
91 if __file__
.endswith('.py'):
92 # Assume direct invocation from tools directory
93 return os
.path
.dirname(__file__
)
94 # Otherwise it is a git hook. To support git worktrees, do not manually look
95 # for the .git directory, but query the actual top level instead.
96 cmd
= ['git', 'rev-parse', '--show-toplevel']
97 srcdir
= subprocess
.check_output(cmd
, universal_newlines
=True).strip()
98 return os
.path
.join(srcdir
, 'tools')
101 def extract_subject(subject
):
102 '''Extracts the original subject (ignoring the Revert prefix).'''
103 subject
= subject
.rstrip('\r\n')
106 while subject
.startswith(prefix
) and subject
.endswith(suffix
):
107 subject
= subject
[len(prefix
):-len(suffix
)]
111 def verify_body(body
):
112 bodynocomments
= re
.sub('^#.*$', '', body
, flags
=re
.MULTILINE
)
113 old_lines
= bodynocomments
.splitlines(True)
115 if len(old_lines
) >= 2 and old_lines
[1].strip():
116 print('ERROR: missing blank line after the first subject line.')
118 cleaned_subject
= extract_subject(old_lines
[0])
119 if len(cleaned_subject
) > 80:
120 # Note that this check is also invoked by the commit-msg hook.
121 print('Warning: keep lines in the commit message under 80 characters.')
125 Please rewrite your commit message to our standards, matching this format:
127 component: a very brief summary of the change
129 A commit message should start with a brief summary, followed by a single
130 blank line and an optional longer description. If the change is specific to
131 a single protocol, start the summary line with the abbreviated name of the
132 protocol and a colon.
134 Use paragraphs to improve readability. Limit each line to 80 characters.
137 if any(line
.startswith('Bug:') or line
.startswith('Ping-Bug:') for line
in old_lines
):
139 To close an issue, use "Closes #1234" or "Fixes #1234" instead of "Bug: 1234".
140 To reference an issue, use "related to #1234" instead of "Ping-Bug: 1234". See
141 https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically
146 # Cherry-picking can add an extra newline, which we'll allow.
147 cp_line
= '\n(cherry picked from commit'
148 body
= body
.replace('\n' + cp_line
, cp_line
)
151 cmd
= ['git', 'stripspace']
152 newbody
= subprocess
.check_output(cmd
, input=body
, universal_newlines
=True)
153 except OSError as ex
:
154 print('Warning: unable to invoke git stripspace: %s' % (ex
,))
157 new_lines
= newbody
.splitlines(True)
158 diff
= difflib
.unified_diff(old_lines
, new_lines
,
159 fromfile
='OLD/.git/COMMIT_EDITMSG',
160 tofile
='NEW/.git/COMMIT_EDITMSG')
161 # Clearly mark trailing whitespace (GNU patch supports such comments).
163 '# NOTE: trailing space on the next line\n%s' % (line
,)
164 if len(line
) > 2 and line
[-2].isspace() else line
167 print('The commit message does not follow our standards.')
168 print('Please rewrite it (there are likely whitespace issues):')
176 def verify_merge_request():
177 # Not needed if/when https://gitlab.com/gitlab-org/gitlab/-/issues/23308 is fixed.
178 gitlab_api_pfx
= "https://gitlab.com/api/v4"
179 # gitlab.com/wireshark/wireshark = 7898047
180 project_id
= os
.getenv('CI_MERGE_REQUEST_PROJECT_ID')
183 'black_white': ansi_csi
+ '30;47m',
184 'bold_red': ansi_csi
+ '31;1m', # gitlab-runner errors
185 'reset': ansi_csi
+ '0m'
187 m_r_iid
= os
.getenv('CI_MERGE_REQUEST_IID')
188 if project_id
is None or m_r_iid
is None:
189 print("This doesn't appear to be a merge request. CI_MERGE_REQUEST_PROJECT_ID={}, CI_MERGE_REQUEST_IID={}".format(project_id
, m_r_iid
))
192 m_r_sb_protected
= os
.getenv('CI_MERGE_REQUEST_SOURCE_BRANCH_PROTECTED')
193 if m_r_sb_protected
== 'true':
195 You're pushing from a protected branch ({os.getenv('CI_MERGE_REQUEST_SOURCE_BRANCH_NAME')}). You will probably
196 have to close this merge request and push from a different branch.\n
198 # Assume that the "Allow commits" test is about to fail.
200 m_r_url
= '{}/projects/{}/merge_requests/{}'.format(gitlab_api_pfx
, project_id
, m_r_iid
)
201 req
= urllib
.request
.Request(m_r_url
)
202 # print('req', repr(req), m_r_url)
203 with urllib
.request
.urlopen(req
) as resp
:
204 resp_json
= resp
.read().decode('utf-8')
205 # print('resp', resp_json)
206 m_r_attrs
= json
.loads(resp_json
)
208 if not m_r_attrs
['allow_collaboration']:
210 {bold_red}ERROR:{reset} Please edit your merge request and make sure the setting
211 {black_white}✅ Allow commits from members who can merge to the target branch{reset}
212 is checked so that maintainers can rebase your change and make minor edits.\
213 '''.format(**ansi_codes
))
216 sys
.stderr
.write('This appears to be a merge request, but we were not able to fetch the "Allow commits" status\n')
221 args
= parser
.parse_args()
224 # If called from commit-msg script, just validate that part and return.
227 with
open(args
.commitmsg
) as f
:
228 return 0 if verify_body(f
.read()) else 1
230 print("Couldn't verify body of message from file '", + args
.commitmsg
+ "'")
234 if(os
.getenv('CI_MERGE_REQUEST_EVENT_TYPE') == 'merge_train'):
235 print("If we were on the love train, people all over the world would be joining hands for this merge request.\nInstead, we're on a merge train so we're skipping commit validation checks. ")
238 cmd
= ['git', 'show', '--no-patch',
239 '--format=%h%n%an%n%ae%n%B', commit
, '--']
240 output
= subprocess
.check_output(cmd
, universal_newlines
=True)
241 # For some reason there is always an additional LF in the output, drop it.
242 if output
.endswith('\n\n'):
244 abbrev
, author_name
, author_email
, body
= output
.split('\n', 3)
245 subject
= body
.split('\n', 1)[0]
247 # If called directly (from the tools directory), print the commit that was
248 # being validated. If called from a git hook (without .py extension), try to
249 # remain silent unless there are issues.
250 if __file__
.endswith('.py'):
251 print('Checking commit: %s %s' % (abbrev
, subject
))
254 if not verify_name(author_name
):
255 print('Disallowed author name: {}'.format(author_name
))
258 if not verify_email(author_email
):
259 print('Disallowed author email address: {}'.format(author_email
))
263 print_git_user_instructions()
265 if not verify_body(body
):
268 if not verify_merge_request():
274 if __name__
== '__main__':
277 except subprocess
.CalledProcessError
as ex
:
279 sys
.exit(ex
.returncode
)
280 except KeyboardInterrupt: