Revert "drsuapi_dissect_element_DsGetNCChangesCtr6TS_ctr6 dissect_krb5_PAC_NDRHEADERBLOB"
[wireshark-sm.git] / tools / validate-commit.py
blobb5d85afe5c0b4be4628898ef571d63c0438e0153
1 #!/usr/bin/env python3
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
16 import argparse
17 import difflib
18 import json
19 import os
20 import subprocess
21 import sys
22 import urllib.request
23 import re
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:')
34 print('')
35 print(' git config --global user.name "Your Name"')
36 print(' git config --global user.email "you@example.com"')
37 print('')
38 print('After that update the author of your latest commit with:')
39 print('')
40 print(' git commit --amend --reset-author --no-edit')
41 print('')
44 def verify_name(name):
45 name = name.lower().strip()
46 forbidden_names = ('unknown', 'root', 'user', 'your name')
47 if name in forbidden_names:
48 return False
49 # Warn about names without spaces. Sometimes it is a mistake where the
50 # developer accidentally committed using the system username.
51 if ' ' not in name:
52 print("WARNING: name '%s' does not contain a space." % (name,))
53 print_git_user_instructions()
54 return True
57 def verify_email(email):
58 email = email.lower().strip()
59 try:
60 user, host = email.split('@')
61 except ValueError:
62 # Lacks a '@' (e.g. a plain domain or "foo[AT]example.com")
63 return False
64 tld = host.split('.')[-1]
66 # localhost, localhost.localdomain, my.local etc.
67 if 'local' in tld:
68 return False
70 # Possibly an IP address
71 if tld.isdigit():
72 return False
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'):
77 return False
79 # For documentation purposes only.
80 if host == 'example.com':
81 return False
83 # 'peter-ubuntu32.(none)'
84 if '(none)' in host:
85 return False
87 return True
90 def tools_dir():
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')
104 prefix = 'Revert "'
105 suffix = '"'
106 while subject.startswith(prefix) and subject.endswith(suffix):
107 subject = subject[len(prefix):-len(suffix)]
108 return subject
111 def verify_body(body):
112 bodynocomments = re.sub('^#.*$', '', body, flags=re.MULTILINE)
113 old_lines = bodynocomments.splitlines(True)
114 is_good = True
115 if len(old_lines) >= 2 and old_lines[1].strip():
116 print('ERROR: missing blank line after the first subject line.')
117 is_good = False
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.')
122 is_good = False
123 if not is_good:
124 print('''
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.
136 ''')
137 if any(line.startswith('Bug:') or line.startswith('Ping-Bug:') for line in old_lines):
138 sys.stderr.write('''
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
142 for details.
143 ''')
144 return False
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)
150 try:
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,))
155 return is_good
156 if newbody != body:
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).
162 diff = [
163 '# NOTE: trailing space on the next line\n%s' % (line,)
164 if len(line) > 2 and line[-2].isspace() else line
165 for line in diff
167 print('The commit message does not follow our standards.')
168 print('Please rewrite it (there are likely whitespace issues):')
169 print('')
170 print(''.join(diff))
171 return False
172 return is_good
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')
181 ansi_csi = '\x1b['
182 ansi_codes = {
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))
190 return True
192 m_r_sb_protected = os.getenv('CI_MERGE_REQUEST_SOURCE_BRANCH_PROTECTED')
193 if m_r_sb_protected == 'true':
194 print(f'''\
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
197 ''')
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)
207 try:
208 if not m_r_attrs['allow_collaboration']:
209 print('''\
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))
214 return False
215 except KeyError:
216 sys.stderr.write('This appears to be a merge request, but we were not able to fetch the "Allow commits" status\n')
217 return True
220 def main():
221 args = parser.parse_args()
222 commit = args.commit
224 # If called from commit-msg script, just validate that part and return.
225 if args.commitmsg:
226 try:
227 with open(args.commitmsg) as f:
228 return 0 if verify_body(f.read()) else 1
229 except Exception:
230 print("Couldn't verify body of message from file '", + args.commitmsg + "'")
231 return 1
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. ")
236 return 0
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'):
243 output = output[:-1]
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))
253 exit_code = 0
254 if not verify_name(author_name):
255 print('Disallowed author name: {}'.format(author_name))
256 exit_code = 1
258 if not verify_email(author_email):
259 print('Disallowed author email address: {}'.format(author_email))
260 exit_code = 1
262 if exit_code:
263 print_git_user_instructions()
265 if not verify_body(body):
266 exit_code = 1
268 if not verify_merge_request():
269 exit_code = 1
271 return exit_code
274 if __name__ == '__main__':
275 try:
276 sys.exit(main())
277 except subprocess.CalledProcessError as ex:
278 print('\n%s' % ex)
279 sys.exit(ex.returncode)
280 except KeyboardInterrupt:
281 sys.exit(130)