]>
Commit | Line | Data |
---|---|---|
a7e173b0 JJ |
1 | ## @file\r |
2 | # Check a patch for various format issues\r | |
3 | #\r | |
b112ec22 | 4 | # Copyright (c) 2015 - 2020, Intel Corporation. All rights reserved.<BR>\r |
8ffa47fb | 5 | # Copyright (C) 2020, Red Hat, Inc.<BR>\r |
01356d29 | 6 | # Copyright (c) 2020, ARM Ltd. All rights reserved.<BR>\r |
a7e173b0 | 7 | #\r |
2e351cbe | 8 | # SPDX-License-Identifier: BSD-2-Clause-Patent\r |
a7e173b0 JJ |
9 | #\r |
10 | \r | |
11 | from __future__ import print_function\r | |
12 | \r | |
13 | VersionNumber = '0.1'\r | |
e6140670 | 14 | __copyright__ = "Copyright (c) 2015 - 2016, Intel Corporation All rights reserved."\r |
a7e173b0 JJ |
15 | \r |
16 | import email\r | |
17 | import argparse\r | |
18 | import os\r | |
19 | import re\r | |
20 | import subprocess\r | |
21 | import sys\r | |
22 | \r | |
f56d52c7 LL |
23 | import email.header\r |
24 | \r | |
a7e173b0 JJ |
25 | class Verbose:\r |
26 | SILENT, ONELINE, NORMAL = range(3)\r | |
27 | level = NORMAL\r | |
28 | \r | |
8ffa47fb PMD |
29 | class EmailAddressCheck:\r |
30 | """Checks an email address."""\r | |
31 | \r | |
8120390a | 32 | def __init__(self, email, description):\r |
8ffa47fb PMD |
33 | self.ok = True\r |
34 | \r | |
35 | if email is None:\r | |
36 | self.error('Email address is missing!')\r | |
37 | return\r | |
8120390a PMD |
38 | if description is None:\r |
39 | self.error('Email description is missing!')\r | |
40 | return\r | |
8ffa47fb | 41 | \r |
8120390a | 42 | self.description = "'" + description + "'"\r |
8ffa47fb PMD |
43 | self.check_email_address(email)\r |
44 | \r | |
45 | def error(self, *err):\r | |
46 | if self.ok and Verbose.level > Verbose.ONELINE:\r | |
8120390a | 47 | print('The ' + self.description + ' email address is not valid:')\r |
8ffa47fb PMD |
48 | self.ok = False\r |
49 | if Verbose.level < Verbose.NORMAL:\r | |
50 | return\r | |
51 | count = 0\r | |
52 | for line in err:\r | |
53 | prefix = (' *', ' ')[count > 0]\r | |
54 | print(prefix, line)\r | |
55 | count += 1\r | |
56 | \r | |
57 | email_re1 = re.compile(r'(?:\s*)(.*?)(\s*)<(.+)>\s*$',\r | |
58 | re.MULTILINE|re.IGNORECASE)\r | |
59 | \r | |
60 | def check_email_address(self, email):\r | |
61 | email = email.strip()\r | |
62 | mo = self.email_re1.match(email)\r | |
63 | if mo is None:\r | |
64 | self.error("Email format is invalid: " + email.strip())\r | |
65 | return\r | |
66 | \r | |
67 | name = mo.group(1).strip()\r | |
68 | if name == '':\r | |
69 | self.error("Name is not provided with email address: " +\r | |
70 | email)\r | |
71 | else:\r | |
72 | quoted = len(name) > 2 and name[0] == '"' and name[-1] == '"'\r | |
73 | if name.find(',') >= 0 and not quoted:\r | |
74 | self.error('Add quotes (") around name with a comma: ' +\r | |
75 | name)\r | |
76 | \r | |
77 | if mo.group(2) == '':\r | |
78 | self.error("There should be a space between the name and " +\r | |
79 | "email address: " + email)\r | |
80 | \r | |
81 | if mo.group(3).find(' ') >= 0:\r | |
82 | self.error("The email address cannot contain a space: " +\r | |
83 | mo.group(3))\r | |
84 | \r | |
38ed2ff3 PMD |
85 | if ' via Groups.Io' in name and mo.group(3).endswith('@groups.io'):\r |
86 | self.error("Email rewritten by lists DMARC / DKIM / SPF: " +\r | |
87 | email)\r | |
88 | \r | |
a7e173b0 JJ |
89 | class CommitMessageCheck:\r |
90 | """Checks the contents of a git commit message."""\r | |
91 | \r | |
92 | def __init__(self, subject, message):\r | |
93 | self.ok = True\r | |
94 | \r | |
95 | if subject is None and message is None:\r | |
96 | self.error('Commit message is missing!')\r | |
97 | return\r | |
98 | \r | |
99 | self.subject = subject\r | |
100 | self.msg = message\r | |
101 | \r | |
b112ec22 MK |
102 | print (subject)\r |
103 | \r | |
a7e173b0 JJ |
104 | self.check_contributed_under()\r |
105 | self.check_signed_off_by()\r | |
106 | self.check_misc_signatures()\r | |
107 | self.check_overall_format()\r | |
108 | self.report_message_result()\r | |
109 | \r | |
110 | url = 'https://github.com/tianocore/tianocore.github.io/wiki/Commit-Message-Format'\r | |
111 | \r | |
112 | def report_message_result(self):\r | |
113 | if Verbose.level < Verbose.NORMAL:\r | |
114 | return\r | |
115 | if self.ok:\r | |
116 | # All checks passed\r | |
117 | return_code = 0\r | |
118 | print('The commit message format passed all checks.')\r | |
119 | else:\r | |
120 | return_code = 1\r | |
121 | if not self.ok:\r | |
122 | print(self.url)\r | |
123 | \r | |
124 | def error(self, *err):\r | |
125 | if self.ok and Verbose.level > Verbose.ONELINE:\r | |
126 | print('The commit message format is not valid:')\r | |
127 | self.ok = False\r | |
128 | if Verbose.level < Verbose.NORMAL:\r | |
129 | return\r | |
130 | count = 0\r | |
131 | for line in err:\r | |
132 | prefix = (' *', ' ')[count > 0]\r | |
133 | print(prefix, line)\r | |
134 | count += 1\r | |
135 | \r | |
a2813610 MK |
136 | # Find 'contributed-under:' at the start of a line ignoring case and\r |
137 | # requires ':' to be present. Matches if there is white space before\r | |
138 | # the tag or between the tag and the ':'.\r | |
139 | contributed_under_re = \\r | |
140 | re.compile(r'^\s*contributed-under\s*:', re.MULTILINE|re.IGNORECASE)\r | |
141 | \r | |
a7e173b0 | 142 | def check_contributed_under(self):\r |
a2813610 MK |
143 | match = self.contributed_under_re.search(self.msg)\r |
144 | if match is not None:\r | |
145 | self.error('Contributed-under! (Note: this must be ' +\r | |
146 | 'removed by the code contributor!)')\r | |
a7e173b0 JJ |
147 | \r |
148 | @staticmethod\r | |
149 | def make_signature_re(sig, re_input=False):\r | |
150 | if re_input:\r | |
151 | sub_re = sig\r | |
152 | else:\r | |
153 | sub_re = sig.replace('-', r'[-\s]+')\r | |
154 | re_str = (r'^(?P<tag>' + sub_re +\r | |
155 | r')(\s*):(\s*)(?P<value>\S.*?)(?:\s*)$')\r | |
156 | try:\r | |
157 | return re.compile(re_str, re.MULTILINE|re.IGNORECASE)\r | |
158 | except Exception:\r | |
159 | print("Tried to compile re:", re_str)\r | |
160 | raise\r | |
161 | \r | |
162 | sig_block_re = \\r | |
163 | re.compile(r'''^\r | |
164 | (?: (?P<tag>[^:]+) \s* : \s*\r | |
165 | (?P<value>\S.*?) )\r | |
166 | |\r | |
167 | (?: \[ (?P<updater>[^:]+) \s* : \s*\r | |
168 | (?P<note>.+?) \s* \] )\r | |
169 | \s* $''',\r | |
170 | re.VERBOSE | re.MULTILINE)\r | |
171 | \r | |
172 | def find_signatures(self, sig):\r | |
173 | if not sig.endswith('-by') and sig != 'Cc':\r | |
174 | sig += '-by'\r | |
175 | regex = self.make_signature_re(sig)\r | |
176 | \r | |
177 | sigs = regex.findall(self.msg)\r | |
178 | \r | |
179 | bad_case_sigs = filter(lambda m: m[0] != sig, sigs)\r | |
180 | for s in bad_case_sigs:\r | |
181 | self.error("'" +s[0] + "' should be '" + sig + "'")\r | |
182 | \r | |
183 | for s in sigs:\r | |
184 | if s[1] != '':\r | |
185 | self.error('There should be no spaces between ' + sig +\r | |
186 | " and the ':'")\r | |
187 | if s[2] != ' ':\r | |
188 | self.error("There should be a space after '" + sig + ":'")\r | |
189 | \r | |
8120390a | 190 | EmailAddressCheck(s[3], sig)\r |
a7e173b0 JJ |
191 | \r |
192 | return sigs\r | |
193 | \r | |
a7e173b0 JJ |
194 | def check_signed_off_by(self):\r |
195 | sob='Signed-off-by'\r | |
196 | if self.msg.find(sob) < 0:\r | |
197 | self.error('Missing Signed-off-by! (Note: this must be ' +\r | |
198 | 'added by the code contributor!)')\r | |
199 | return\r | |
200 | \r | |
201 | sobs = self.find_signatures('Signed-off')\r | |
202 | \r | |
203 | if len(sobs) == 0:\r | |
204 | self.error('Invalid Signed-off-by format!')\r | |
205 | return\r | |
206 | \r | |
207 | sig_types = (\r | |
208 | 'Reviewed',\r | |
209 | 'Reported',\r | |
210 | 'Tested',\r | |
211 | 'Suggested',\r | |
212 | 'Acked',\r | |
213 | 'Cc'\r | |
214 | )\r | |
215 | \r | |
216 | def check_misc_signatures(self):\r | |
217 | for sig in self.sig_types:\r | |
218 | self.find_signatures(sig)\r | |
219 | \r | |
b112ec22 MK |
220 | cve_re = re.compile('CVE-[0-9]{4}-[0-9]{5}[^0-9]')\r |
221 | \r | |
a7e173b0 JJ |
222 | def check_overall_format(self):\r |
223 | lines = self.msg.splitlines()\r | |
224 | \r | |
225 | if len(lines) >= 1 and lines[0].endswith('\r\n'):\r | |
226 | empty_line = '\r\n'\r | |
227 | else:\r | |
228 | empty_line = '\n'\r | |
229 | \r | |
230 | lines.insert(0, empty_line)\r | |
231 | lines.insert(0, self.subject + empty_line)\r | |
232 | \r | |
233 | count = len(lines)\r | |
234 | \r | |
235 | if count <= 0:\r | |
236 | self.error('Empty commit message!')\r | |
237 | return\r | |
238 | \r | |
b112ec22 MK |
239 | if count >= 1 and re.search(self.cve_re, lines[0]):\r |
240 | #\r | |
241 | # If CVE-xxxx-xxxxx is present in subject line, then limit length of\r | |
242 | # subject line to 92 characters\r | |
243 | #\r | |
244 | if len(lines[0].rstrip()) >= 93:\r | |
245 | self.error(\r | |
246 | 'First line of commit message (subject line) is too long (%d >= 93).' %\r | |
247 | (len(lines[0].rstrip()))\r | |
248 | )\r | |
249 | else:\r | |
250 | #\r | |
251 | # If CVE-xxxx-xxxxx is not present in subject line, then limit\r | |
252 | # length of subject line to 75 characters\r | |
253 | #\r | |
254 | if len(lines[0].rstrip()) >= 76:\r | |
255 | self.error(\r | |
256 | 'First line of commit message (subject line) is too long (%d >= 76).' %\r | |
257 | (len(lines[0].rstrip()))\r | |
258 | )\r | |
a7e173b0 JJ |
259 | \r |
260 | if count >= 1 and len(lines[0].strip()) == 0:\r | |
261 | self.error('First line of commit message (subject line) ' +\r | |
262 | 'is empty.')\r | |
263 | \r | |
264 | if count >= 2 and lines[1].strip() != '':\r | |
265 | self.error('Second line of commit message should be ' +\r | |
266 | 'empty.')\r | |
267 | \r | |
268 | for i in range(2, count):\r | |
e6140670 | 269 | if (len(lines[i]) >= 76 and\r |
a7e173b0 | 270 | len(lines[i].split()) > 1 and\r |
3d9d66ad SZ |
271 | not lines[i].startswith('git-svn-id:') and\r |
272 | not lines[i].startswith('Reviewed-by') and\r | |
273 | not lines[i].startswith('Acked-by:') and\r | |
274 | not lines[i].startswith('Tested-by:') and\r | |
275 | not lines[i].startswith('Reported-by:') and\r | |
276 | not lines[i].startswith('Suggested-by:') and\r | |
277 | not lines[i].startswith('Signed-off-by:') and\r | |
278 | not lines[i].startswith('Cc:')):\r | |
b112ec22 MK |
279 | #\r |
280 | # Print a warning if body line is longer than 75 characters\r | |
281 | #\r | |
282 | print(\r | |
283 | 'WARNING - Line %d of commit message is too long (%d >= 76).' %\r | |
284 | (i + 1, len(lines[i]))\r | |
285 | )\r | |
286 | print(lines[i])\r | |
a7e173b0 JJ |
287 | \r |
288 | last_sig_line = None\r | |
289 | for i in range(count - 1, 0, -1):\r | |
290 | line = lines[i]\r | |
291 | mo = self.sig_block_re.match(line)\r | |
292 | if mo is None:\r | |
293 | if line.strip() == '':\r | |
294 | break\r | |
295 | elif last_sig_line is not None:\r | |
296 | err2 = 'Add empty line before "%s"?' % last_sig_line\r | |
297 | self.error('The line before the signature block ' +\r | |
298 | 'should be empty', err2)\r | |
299 | else:\r | |
300 | self.error('The signature block was not found')\r | |
301 | break\r | |
302 | last_sig_line = line.strip()\r | |
303 | \r | |
304 | (START, PRE_PATCH, PATCH) = range(3)\r | |
305 | \r | |
306 | class GitDiffCheck:\r | |
307 | """Checks the contents of a git diff."""\r | |
308 | \r | |
309 | def __init__(self, diff):\r | |
310 | self.ok = True\r | |
311 | self.format_ok = True\r | |
312 | self.lines = diff.splitlines(True)\r | |
313 | self.count = len(self.lines)\r | |
314 | self.line_num = 0\r | |
315 | self.state = START\r | |
6a69dd49 | 316 | self.new_bin = []\r |
a4cfb842 | 317 | self.LicenseCheck(self.lines, self.count)\r |
a7e173b0 JJ |
318 | while self.line_num < self.count and self.format_ok:\r |
319 | line_num = self.line_num\r | |
320 | self.run()\r | |
321 | assert(self.line_num > line_num)\r | |
322 | self.report_message_result()\r | |
323 | \r | |
a4cfb842 SZ |
324 | def LicenseCheck(self, lines, count):\r |
325 | self.ok = True\r | |
326 | self.startcheck = False\r | |
327 | self.license = True\r | |
328 | line_index = 0\r | |
329 | for line in lines:\r | |
330 | if line.startswith('--- /dev/null'):\r | |
331 | nextline = lines[line_index + 1]\r | |
332 | added_file = self.Readdedfileformat.search(nextline).group(1)\r | |
333 | added_file_extension = os.path.splitext(added_file)[1]\r | |
334 | if added_file_extension in self.file_extension_list:\r | |
335 | self.startcheck = True\r | |
336 | self.license = False\r | |
337 | if self.startcheck and self.license_format_preflix in line:\r | |
338 | if self.bsd2_patent in line or self.bsd3_patent in line:\r | |
339 | self.license = True\r | |
340 | else:\r | |
341 | for optional_license in self.license_optional_list:\r | |
342 | if optional_license in line:\r | |
343 | self.license = True\r | |
344 | self.warning(added_file)\r | |
345 | if line_index + 1 == count or lines[line_index + 1].startswith('diff --') and self.startcheck:\r | |
346 | if not self.license:\r | |
347 | error_message = "Invalid License in: " + added_file\r | |
348 | self.error(error_message)\r | |
349 | self.startcheck = False\r | |
350 | self.license = True\r | |
351 | line_index = line_index + 1\r | |
352 | \r | |
353 | def warning(self, *err):\r | |
354 | count = 0\r | |
355 | for line in err:\r | |
356 | warning_format = 'Warning: License accepted but not BSD plus patent license in'\r | |
357 | print(warning_format, line)\r | |
358 | count += 1\r | |
359 | \r | |
a7e173b0 JJ |
360 | def report_message_result(self):\r |
361 | if Verbose.level < Verbose.NORMAL:\r | |
362 | return\r | |
363 | if self.ok:\r | |
364 | print('The code passed all checks.')\r | |
6a69dd49 HW |
365 | if self.new_bin:\r |
366 | print('\nWARNING - The following binary files will be added ' +\r | |
367 | 'into the repository:')\r | |
368 | for binary in self.new_bin:\r | |
369 | print(' ' + binary)\r | |
a7e173b0 JJ |
370 | \r |
371 | def run(self):\r | |
372 | line = self.lines[self.line_num]\r | |
373 | \r | |
374 | if self.state in (PRE_PATCH, PATCH):\r | |
375 | if line.startswith('diff --git'):\r | |
376 | self.state = START\r | |
377 | if self.state == PATCH:\r | |
378 | if line.startswith('@@ '):\r | |
379 | self.state = PRE_PATCH\r | |
380 | elif len(line) >= 1 and line[0] not in ' -+' and \\r | |
05579e51 | 381 | not line.startswith('\r\n') and \\r |
aab57eff | 382 | not line.startswith(r'\ No newline ') and not self.binary:\r |
a7e173b0 JJ |
383 | for line in self.lines[self.line_num + 1:]:\r |
384 | if line.startswith('diff --git'):\r | |
385 | self.format_error('diff found after end of patch')\r | |
386 | break\r | |
387 | self.line_num = self.count\r | |
388 | return\r | |
389 | \r | |
390 | if self.state == START:\r | |
391 | if line.startswith('diff --git'):\r | |
392 | self.state = PRE_PATCH\r | |
ccaa7754 | 393 | self.filename = line[13:].split(' ', 1)[0]\r |
6a69dd49 | 394 | self.is_newfile = False\r |
c8b8157e MK |
395 | self.force_crlf = True\r |
396 | self.force_notabs = True\r | |
01356d29 PG |
397 | if self.filename.endswith('.sh') or \\r |
398 | self.filename.startswith('BaseTools/BinWrappers/PosixLike/') or \\r | |
399 | self.filename.startswith('BaseTools/Bin/CYGWIN_NT-5.1-i686/') or \\r | |
400 | self.filename == 'BaseTools/BuildEnv':\r | |
c8b8157e MK |
401 | #\r |
402 | # Do not enforce CR/LF line endings for linux shell scripts.\r | |
01356d29 PG |
403 | # Some linux shell scripts don't end with the ".sh" extension,\r |
404 | # they are identified by their path.\r | |
c8b8157e MK |
405 | #\r |
406 | self.force_crlf = False\r | |
c267eb88 LL |
407 | if self.filename == '.gitmodules' or \\r |
408 | self.filename == 'BaseTools/Conf/diff.order':\r | |
c8b8157e | 409 | #\r |
c267eb88 LL |
410 | # .gitmodules and diff orderfiles are used internally by git\r |
411 | # use tabs and LF line endings. Do not enforce no tabs and\r | |
412 | # do not enforce CR/LF line endings.\r | |
c8b8157e MK |
413 | #\r |
414 | self.force_crlf = False\r | |
415 | self.force_notabs = False\r | |
a7e173b0 JJ |
416 | elif len(line.rstrip()) != 0:\r |
417 | self.format_error("didn't find diff command")\r | |
418 | self.line_num += 1\r | |
419 | elif self.state == PRE_PATCH:\r | |
a7e173b0 JJ |
420 | if line.startswith('@@ '):\r |
421 | self.state = PATCH\r | |
103733f8 | 422 | self.binary = False\r |
6a69dd49 HW |
423 | elif line.startswith('GIT binary patch') or \\r |
424 | line.startswith('Binary files'):\r | |
103733f8 JJ |
425 | self.state = PATCH\r |
426 | self.binary = True\r | |
6a69dd49 HW |
427 | if self.is_newfile:\r |
428 | self.new_bin.append(self.filename)\r | |
c8b8157e MK |
429 | elif line.startswith('new file mode 160000'):\r |
430 | #\r | |
431 | # New submodule. Do not enforce CR/LF line endings\r | |
432 | #\r | |
433 | self.force_crlf = False\r | |
a7e173b0 JJ |
434 | else:\r |
435 | ok = False\r | |
6a69dd49 | 436 | self.is_newfile = self.newfile_prefix_re.match(line)\r |
a7e173b0 JJ |
437 | for pfx in self.pre_patch_prefixes:\r |
438 | if line.startswith(pfx):\r | |
439 | ok = True\r | |
440 | if not ok:\r | |
441 | self.format_error("didn't find diff hunk marker (@@)")\r | |
442 | self.line_num += 1\r | |
443 | elif self.state == PATCH:\r | |
103733f8 JJ |
444 | if self.binary:\r |
445 | pass\r | |
aab57eff | 446 | elif line.startswith('-'):\r |
a7e173b0 JJ |
447 | pass\r |
448 | elif line.startswith('+'):\r | |
449 | self.check_added_line(line[1:])\r | |
05579e51 MK |
450 | elif line.startswith('\r\n'):\r |
451 | pass\r | |
a7e173b0 JJ |
452 | elif line.startswith(r'\ No newline '):\r |
453 | pass\r | |
454 | elif not line.startswith(' '):\r | |
455 | self.format_error("unexpected patch line")\r | |
456 | self.line_num += 1\r | |
457 | \r | |
458 | pre_patch_prefixes = (\r | |
459 | '--- ',\r | |
460 | '+++ ',\r | |
461 | 'index ',\r | |
462 | 'new file ',\r | |
463 | 'deleted file ',\r | |
464 | 'old mode ',\r | |
465 | 'new mode ',\r | |
466 | 'similarity index ',\r | |
1eeb5ff1 MK |
467 | 'copy from ',\r |
468 | 'copy to ',\r | |
a7e173b0 | 469 | 'rename ',\r |
a7e173b0 JJ |
470 | )\r |
471 | \r | |
472 | line_endings = ('\r\n', '\n\r', '\n', '\r')\r | |
473 | \r | |
6a69dd49 HW |
474 | newfile_prefix_re = \\r |
475 | re.compile(r'''^\r | |
476 | index\ 0+\.\.\r | |
477 | ''',\r | |
478 | re.VERBOSE)\r | |
a7e173b0 JJ |
479 | \r |
480 | def added_line_error(self, msg, line):\r | |
481 | lines = [ msg ]\r | |
6a69dd49 HW |
482 | if self.filename is not None:\r |
483 | lines.append('File: ' + self.filename)\r | |
a7e173b0 JJ |
484 | lines.append('Line: ' + line)\r |
485 | \r | |
486 | self.error(*lines)\r | |
487 | \r | |
c3926cdb YZ |
488 | old_debug_re = \\r |
489 | re.compile(r'''\r | |
490 | DEBUG \s* \( \s* \( \s*\r | |
491 | (?: DEBUG_[A-Z_]+ \s* \| \s*)*\r | |
492 | EFI_D_ ([A-Z_]+)\r | |
493 | ''',\r | |
494 | re.VERBOSE)\r | |
495 | \r | |
a7e173b0 JJ |
496 | def check_added_line(self, line):\r |
497 | eol = ''\r | |
498 | for an_eol in self.line_endings:\r | |
499 | if line.endswith(an_eol):\r | |
500 | eol = an_eol\r | |
501 | line = line[:-len(eol)]\r | |
502 | \r | |
503 | stripped = line.rstrip()\r | |
504 | \r | |
ff2655d1 | 505 | if self.force_crlf and eol != '\r\n' and (line.find('Subproject commit') == -1):\r |
a7e173b0 JJ |
506 | self.added_line_error('Line ending (%s) is not CRLF' % repr(eol),\r |
507 | line)\r | |
c8b8157e | 508 | if self.force_notabs and '\t' in line:\r |
a7e173b0 JJ |
509 | self.added_line_error('Tab character used', line)\r |
510 | if len(stripped) < len(line):\r | |
511 | self.added_line_error('Trailing whitespace found', line)\r | |
512 | \r | |
c3926cdb YZ |
513 | mo = self.old_debug_re.search(line)\r |
514 | if mo is not None:\r | |
515 | self.added_line_error('EFI_D_' + mo.group(1) + ' was used, '\r | |
516 | 'but DEBUG_' + mo.group(1) +\r | |
517 | ' is now recommended', line)\r | |
518 | \r | |
a7e173b0 JJ |
519 | split_diff_re = re.compile(r'''\r |
520 | (?P<cmd>\r | |
521 | ^ diff \s+ --git \s+ a/.+ \s+ b/.+ $\r | |
522 | )\r | |
523 | (?P<index>\r | |
524 | ^ index \s+ .+ $\r | |
525 | )\r | |
526 | ''',\r | |
527 | re.IGNORECASE | re.VERBOSE | re.MULTILINE)\r | |
528 | \r | |
529 | def format_error(self, err):\r | |
530 | self.format_ok = False\r | |
531 | err = 'Patch format error: ' + err\r | |
532 | err2 = 'Line: ' + self.lines[self.line_num].rstrip()\r | |
533 | self.error(err, err2)\r | |
534 | \r | |
535 | def error(self, *err):\r | |
536 | if self.ok and Verbose.level > Verbose.ONELINE:\r | |
537 | print('Code format is not valid:')\r | |
538 | self.ok = False\r | |
539 | if Verbose.level < Verbose.NORMAL:\r | |
540 | return\r | |
541 | count = 0\r | |
542 | for line in err:\r | |
543 | prefix = (' *', ' ')[count > 0]\r | |
544 | print(prefix, line)\r | |
545 | count += 1\r | |
546 | \r | |
a4cfb842 SZ |
547 | license_format_preflix = 'SPDX-License-Identifier'\r |
548 | \r | |
549 | bsd2_patent = 'BSD-2-Clause-Patent'\r | |
550 | \r | |
551 | bsd3_patent = 'BSD-3-Clause-Patent'\r | |
552 | \r | |
553 | license_optional_list = ['BSD-2-Clause', 'BSD-3-Clause', 'MIT', 'Python-2.0', 'Zlib']\r | |
554 | \r | |
555 | Readdedfileformat = re.compile(r'\+\+\+ b\/(.*)\n')\r | |
556 | \r | |
557 | file_extension_list = [".c", ".h", ".inf", ".dsc", ".dec", ".py", ".bat", ".sh", ".uni", ".yaml", ".fdf", ".inc", "yml", ".asm", \\r | |
558 | ".asm16", ".asl", ".vfr", ".s", ".S", ".aslc", ".nasm", ".nasmb", ".idf", ".Vfr", ".H"]\r | |
559 | \r | |
a7e173b0 JJ |
560 | class CheckOnePatch:\r |
561 | """Checks the contents of a git email formatted patch.\r | |
562 | \r | |
563 | Various checks are performed on both the commit message and the\r | |
564 | patch content.\r | |
565 | """\r | |
566 | \r | |
567 | def __init__(self, name, patch):\r | |
568 | self.patch = patch\r | |
569 | self.find_patch_pieces()\r | |
570 | \r | |
c0328cf3 PMD |
571 | email_check = EmailAddressCheck(self.author_email, 'Author')\r |
572 | email_ok = email_check.ok\r | |
573 | \r | |
a7e173b0 JJ |
574 | msg_check = CommitMessageCheck(self.commit_subject, self.commit_msg)\r |
575 | msg_ok = msg_check.ok\r | |
576 | \r | |
577 | diff_ok = True\r | |
578 | if self.diff is not None:\r | |
579 | diff_check = GitDiffCheck(self.diff)\r | |
580 | diff_ok = diff_check.ok\r | |
581 | \r | |
c0328cf3 | 582 | self.ok = email_ok and msg_ok and diff_ok\r |
a7e173b0 JJ |
583 | \r |
584 | if Verbose.level == Verbose.ONELINE:\r | |
585 | if self.ok:\r | |
586 | result = 'ok'\r | |
587 | else:\r | |
588 | result = list()\r | |
589 | if not msg_ok:\r | |
590 | result.append('commit message')\r | |
591 | if not diff_ok:\r | |
592 | result.append('diff content')\r | |
593 | result = 'bad ' + ' and '.join(result)\r | |
594 | print(name, result)\r | |
595 | \r | |
596 | \r | |
597 | git_diff_re = re.compile(r'''\r | |
598 | ^ diff \s+ --git \s+ a/.+ \s+ b/.+ $\r | |
599 | ''',\r | |
600 | re.IGNORECASE | re.VERBOSE | re.MULTILINE)\r | |
601 | \r | |
602 | stat_re = \\r | |
603 | re.compile(r'''\r | |
604 | (?P<commit_message> [\s\S\r\n]* )\r | |
605 | (?P<stat>\r | |
606 | ^ --- $ [\r\n]+\r | |
607 | (?: ^ \s+ .+ \s+ \| \s+ \d+ \s+ \+* \-*\r | |
608 | $ [\r\n]+ )+\r | |
609 | [\s\S\r\n]+\r | |
610 | )\r | |
611 | ''',\r | |
612 | re.IGNORECASE | re.VERBOSE | re.MULTILINE)\r | |
613 | \r | |
e709bbb1 YZ |
614 | subject_prefix_re = \\r |
615 | re.compile(r'''^\r | |
616 | \s* (\[\r | |
617 | [^\[\]]* # Allow all non-brackets\r | |
618 | \])* \s*\r | |
619 | ''',\r | |
620 | re.VERBOSE)\r | |
621 | \r | |
a7e173b0 JJ |
622 | def find_patch_pieces(self):\r |
623 | if sys.version_info < (3, 0):\r | |
624 | patch = self.patch.encode('ascii', 'ignore')\r | |
625 | else:\r | |
626 | patch = self.patch\r | |
627 | \r | |
628 | self.commit_msg = None\r | |
629 | self.stat = None\r | |
630 | self.commit_subject = None\r | |
631 | self.commit_prefix = None\r | |
632 | self.diff = None\r | |
633 | \r | |
634 | if patch.startswith('diff --git'):\r | |
635 | self.diff = patch\r | |
636 | return\r | |
637 | \r | |
638 | pmail = email.message_from_string(patch)\r | |
639 | parts = list(pmail.walk())\r | |
640 | assert(len(parts) == 1)\r | |
641 | assert(parts[0].get_content_type() == 'text/plain')\r | |
642 | content = parts[0].get_payload(decode=True).decode('utf-8', 'ignore')\r | |
643 | \r | |
644 | mo = self.git_diff_re.search(content)\r | |
645 | if mo is not None:\r | |
646 | self.diff = content[mo.start():]\r | |
647 | content = content[:mo.start()]\r | |
648 | \r | |
649 | mo = self.stat_re.search(content)\r | |
650 | if mo is None:\r | |
651 | self.commit_msg = content\r | |
652 | else:\r | |
653 | self.stat = mo.group('stat')\r | |
654 | self.commit_msg = mo.group('commit_message')\r | |
b112ec22 MK |
655 | #\r |
656 | # Parse subject line from email header. The subject line may be\r | |
657 | # composed of multiple parts with different encodings. Decode and\r | |
658 | # combine all the parts to produce a single string with the contents of\r | |
659 | # the decoded subject line.\r | |
660 | #\r | |
661 | parts = email.header.decode_header(pmail.get('subject'))\r | |
662 | subject = ''\r | |
663 | for (part, encoding) in parts:\r | |
664 | if encoding:\r | |
665 | part = part.decode(encoding)\r | |
666 | else:\r | |
667 | try:\r | |
668 | part = part.decode()\r | |
669 | except:\r | |
670 | pass\r | |
671 | subject = subject + part\r | |
a7e173b0 | 672 | \r |
b112ec22 | 673 | self.commit_subject = subject.replace('\r\n', '')\r |
a7e173b0 | 674 | self.commit_subject = self.commit_subject.replace('\n', '')\r |
e709bbb1 | 675 | self.commit_subject = self.subject_prefix_re.sub('', self.commit_subject, 1)\r |
a7e173b0 | 676 | \r |
c0328cf3 PMD |
677 | self.author_email = pmail['from']\r |
678 | \r | |
a7e173b0 JJ |
679 | class CheckGitCommits:\r |
680 | """Reads patches from git based on the specified git revision range.\r | |
681 | \r | |
682 | The patches are read from git, and then checked.\r | |
683 | """\r | |
684 | \r | |
685 | def __init__(self, rev_spec, max_count):\r | |
686 | commits = self.read_commit_list_from_git(rev_spec, max_count)\r | |
687 | if len(commits) == 1 and Verbose.level > Verbose.ONELINE:\r | |
688 | commits = [ rev_spec ]\r | |
689 | self.ok = True\r | |
690 | blank_line = False\r | |
691 | for commit in commits:\r | |
692 | if Verbose.level > Verbose.ONELINE:\r | |
693 | if blank_line:\r | |
694 | print()\r | |
695 | else:\r | |
696 | blank_line = True\r | |
697 | print('Checking git commit:', commit)\r | |
8f38b08b PMD |
698 | email = self.read_committer_email_address_from_git(commit)\r |
699 | self.ok &= EmailAddressCheck(email, 'Committer').ok\r | |
a7e173b0 JJ |
700 | patch = self.read_patch_from_git(commit)\r |
701 | self.ok &= CheckOnePatch(commit, patch).ok\r | |
5ac4548c JC |
702 | if not commits:\r |
703 | print("Couldn't find commit matching: '{}'".format(rev_spec))\r | |
a7e173b0 JJ |
704 | \r |
705 | def read_commit_list_from_git(self, rev_spec, max_count):\r | |
706 | # Run git to get the commit patch\r | |
707 | cmd = [ 'rev-list', '--abbrev-commit', '--no-walk' ]\r | |
708 | if max_count is not None:\r | |
709 | cmd.append('--max-count=' + str(max_count))\r | |
710 | cmd.append(rev_spec)\r | |
711 | out = self.run_git(*cmd)\r | |
5ac4548c | 712 | return out.split() if out else []\r |
a7e173b0 JJ |
713 | \r |
714 | def read_patch_from_git(self, commit):\r | |
715 | # Run git to get the commit patch\r | |
c37cce7a PMD |
716 | return self.run_git('show', '--pretty=email', '--no-textconv',\r |
717 | '--no-use-mailmap', commit)\r | |
a7e173b0 | 718 | \r |
8f38b08b PMD |
719 | def read_committer_email_address_from_git(self, commit):\r |
720 | # Run git to get the committer email\r | |
c37cce7a PMD |
721 | return self.run_git('show', '--pretty=%cn <%ce>', '--no-patch',\r |
722 | '--no-use-mailmap', commit)\r | |
8f38b08b | 723 | \r |
a7e173b0 JJ |
724 | def run_git(self, *args):\r |
725 | cmd = [ 'git' ]\r | |
726 | cmd += args\r | |
727 | p = subprocess.Popen(cmd,\r | |
728 | stdout=subprocess.PIPE,\r | |
729 | stderr=subprocess.STDOUT)\r | |
5ac4548c | 730 | Result = p.communicate()\r |
6460513a | 731 | return Result[0].decode('utf-8', 'ignore') if Result[0] and Result[0].find(b"fatal")!=0 else None\r |
a7e173b0 JJ |
732 | \r |
733 | class CheckOnePatchFile:\r | |
734 | """Performs a patch check for a single file.\r | |
735 | \r | |
736 | stdin is used when the filename is '-'.\r | |
737 | """\r | |
738 | \r | |
739 | def __init__(self, patch_filename):\r | |
740 | if patch_filename == '-':\r | |
741 | patch = sys.stdin.read()\r | |
742 | patch_filename = 'stdin'\r | |
743 | else:\r | |
744 | f = open(patch_filename, 'rb')\r | |
745 | patch = f.read().decode('utf-8', 'ignore')\r | |
746 | f.close()\r | |
747 | if Verbose.level > Verbose.ONELINE:\r | |
748 | print('Checking patch file:', patch_filename)\r | |
749 | self.ok = CheckOnePatch(patch_filename, patch).ok\r | |
750 | \r | |
751 | class CheckOneArg:\r | |
752 | """Performs a patch check for a single command line argument.\r | |
753 | \r | |
754 | The argument will be handed off to a file or git-commit based\r | |
755 | checker.\r | |
756 | """\r | |
757 | \r | |
758 | def __init__(self, param, max_count=None):\r | |
759 | self.ok = True\r | |
760 | if param == '-' or os.path.exists(param):\r | |
761 | checker = CheckOnePatchFile(param)\r | |
762 | else:\r | |
763 | checker = CheckGitCommits(param, max_count)\r | |
764 | self.ok = checker.ok\r | |
765 | \r | |
766 | class PatchCheckApp:\r | |
767 | """Checks patches based on the command line arguments."""\r | |
768 | \r | |
769 | def __init__(self):\r | |
770 | self.parse_options()\r | |
771 | patches = self.args.patches\r | |
772 | \r | |
773 | if len(patches) == 0:\r | |
774 | patches = [ 'HEAD' ]\r | |
775 | \r | |
776 | self.ok = True\r | |
777 | self.count = None\r | |
778 | for patch in patches:\r | |
779 | self.process_one_arg(patch)\r | |
780 | \r | |
781 | if self.count is not None:\r | |
782 | self.process_one_arg('HEAD')\r | |
783 | \r | |
784 | if self.ok:\r | |
785 | self.retval = 0\r | |
786 | else:\r | |
787 | self.retval = -1\r | |
788 | \r | |
789 | def process_one_arg(self, arg):\r | |
790 | if len(arg) >= 2 and arg[0] == '-':\r | |
791 | try:\r | |
792 | self.count = int(arg[1:])\r | |
793 | return\r | |
794 | except ValueError:\r | |
795 | pass\r | |
796 | self.ok &= CheckOneArg(arg, self.count).ok\r | |
797 | self.count = None\r | |
798 | \r | |
799 | def parse_options(self):\r | |
800 | parser = argparse.ArgumentParser(description=__copyright__)\r | |
801 | parser.add_argument('--version', action='version',\r | |
802 | version='%(prog)s ' + VersionNumber)\r | |
803 | parser.add_argument('patches', nargs='*',\r | |
804 | help='[patch file | git rev list]')\r | |
805 | group = parser.add_mutually_exclusive_group()\r | |
806 | group.add_argument("--oneline",\r | |
807 | action="store_true",\r | |
808 | help="Print one result per line")\r | |
809 | group.add_argument("--silent",\r | |
810 | action="store_true",\r | |
811 | help="Print nothing")\r | |
812 | self.args = parser.parse_args()\r | |
813 | if self.args.oneline:\r | |
814 | Verbose.level = Verbose.ONELINE\r | |
815 | if self.args.silent:\r | |
816 | Verbose.level = Verbose.SILENT\r | |
817 | \r | |
818 | if __name__ == "__main__":\r | |
819 | sys.exit(PatchCheckApp().retval)\r |