blob: 8b60774de5fff6f67ad7f40b3181ac2176a8052c [file] [log] [blame]
rmistry@google.com8e3ff8c2013-01-17 12:55:34 +00001# Copyright (c) 2013 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5
6"""Top-level presubmit script for Skia.
7
8See http://dev.chromium.org/developers/how-tos/depottools/presubmit-scripts
9for more details about the presubmit API built into gcl.
10"""
11
rmistry3cfd1ad2015-03-25 12:53:35 -070012import csv
commit-bot@chromium.org745e08c2014-02-03 14:18:32 +000013import fnmatch
rmistry@google.comf6c5f752013-03-29 17:26:00 +000014import os
commit-bot@chromium.orgcfdc5962014-01-31 17:33:04 +000015import re
rmistryd223fb22015-02-26 10:16:13 -080016import subprocess
rmistry@google.comf6c5f752013-03-29 17:26:00 +000017import sys
commit-bot@chromium.org745e08c2014-02-03 14:18:32 +000018import traceback
rmistry@google.comf6c5f752013-03-29 17:26:00 +000019
rmistry@google.comc2993442013-01-23 14:35:58 +000020
commit-bot@chromium.orgcfdc5962014-01-31 17:33:04 +000021REVERT_CL_SUBJECT_PREFIX = 'Revert '
22
rmistry@google.com547012d2013-04-12 19:45:46 +000023SKIA_TREE_STATUS_URL = 'http://skia-tree-status.appspot.com'
24
rmistry3cfd1ad2015-03-25 12:53:35 -070025CQ_KEYWORDS_THAT_NEED_APPENDING = ('CQ_INCLUDE_TRYBOTS', 'CQ_EXTRA_TRYBOTS',
26 'CQ_EXCLUDE_TRYBOTS', 'CQ_TRYBOTS')
27
rmistryf2d83ca2014-08-26 10:30:29 -070028# Please add the complete email address here (and not just 'xyz@' or 'xyz').
rmistry@google.comfb4a68d2013-08-12 14:51:20 +000029PUBLIC_API_OWNERS = (
30 'reed@chromium.org',
31 'reed@google.com',
32 'bsalomon@chromium.org',
33 'bsalomon@google.com',
rmistry83fab472014-07-18 05:25:56 -070034 'djsollen@chromium.org',
35 'djsollen@google.com',
rmistry@google.comfb4a68d2013-08-12 14:51:20 +000036)
37
commit-bot@chromium.org745e08c2014-02-03 14:18:32 +000038AUTHORS_FILE_NAME = 'AUTHORS'
39
rmistryd223fb22015-02-26 10:16:13 -080040DOCS_PREVIEW_URL = 'https://skia.org/?cl='
41
rmistry@google.com547012d2013-04-12 19:45:46 +000042
rmistry@google.com713276b2013-01-25 18:27:34 +000043def _CheckChangeHasEol(input_api, output_api, source_file_filter=None):
44 """Checks that files end with atleast one \n (LF)."""
45 eof_files = []
46 for f in input_api.AffectedSourceFiles(source_file_filter):
47 contents = input_api.ReadFile(f, 'rb')
48 # Check that the file ends in atleast one newline character.
49 if len(contents) > 1 and contents[-1:] != '\n':
50 eof_files.append(f.LocalPath())
51
52 if eof_files:
53 return [output_api.PresubmitPromptWarning(
54 'These files should end in a newline character:',
55 items=eof_files)]
56 return []
57
58
Eric Borenbb0ef0a2014-06-25 11:13:27 -040059def _PythonChecks(input_api, output_api):
60 """Run checks on any modified Python files."""
61 pylint_disabled_warnings = (
62 'F0401', # Unable to import.
63 'E0611', # No name in module.
64 'W0232', # Class has no __init__ method.
65 'E1002', # Use of super on an old style class.
66 'W0403', # Relative import used.
67 'R0201', # Method could be a function.
68 'E1003', # Using class name in super.
69 'W0613', # Unused argument.
70 )
71 # Run Pylint on only the modified python files. Unfortunately it still runs
72 # Pylint on the whole file instead of just the modified lines.
73 affected_python_files = []
74 for affected_file in input_api.AffectedSourceFiles(None):
75 affected_file_path = affected_file.LocalPath()
76 if affected_file_path.endswith('.py'):
77 affected_python_files.append(affected_file_path)
78 return input_api.canned_checks.RunPylint(
79 input_api, output_api,
80 disabled_warnings=pylint_disabled_warnings,
81 white_list=affected_python_files)
82
83
rmistry01cbf6c2015-03-12 07:48:40 -070084def _IfDefChecks(input_api, output_api):
85 """Ensures if/ifdef are not before includes. See skbug/3362 for details."""
86 comment_block_start_pattern = re.compile('^\s*\/\*.*$')
87 comment_block_middle_pattern = re.compile('^\s+\*.*')
88 comment_block_end_pattern = re.compile('^\s+\*\/.*$')
89 single_line_comment_pattern = re.compile('^\s*//.*$')
90 def is_comment(line):
91 return (comment_block_start_pattern.match(line) or
92 comment_block_middle_pattern.match(line) or
93 comment_block_end_pattern.match(line) or
94 single_line_comment_pattern.match(line))
95
96 empty_line_pattern = re.compile('^\s*$')
97 def is_empty_line(line):
98 return empty_line_pattern.match(line)
99
100 failing_files = []
101 for affected_file in input_api.AffectedSourceFiles(None):
102 affected_file_path = affected_file.LocalPath()
103 if affected_file_path.endswith('.cpp') or affected_file_path.endswith('.h'):
104 f = open(affected_file_path)
105 for line in f.xreadlines():
106 if is_comment(line) or is_empty_line(line):
107 continue
108 # The below will be the first real line after comments and newlines.
109 if line.startswith('#if 0 '):
110 pass
111 elif line.startswith('#if ') or line.startswith('#ifdef '):
112 failing_files.append(affected_file_path)
113 break
114
115 results = []
116 if failing_files:
117 results.append(
118 output_api.PresubmitError(
119 'The following files have #if or #ifdef before includes:\n%s\n\n'
120 'See skbug.com/3362 for why this should be fixed.' %
121 '\n'.join(failing_files)))
122 return results
123
124
borenetc7c91802015-03-25 04:47:02 -0700125def _CopyrightChecks(input_api, output_api, source_file_filter=None):
126 results = []
127 year_pattern = r'\d{4}'
128 year_range_pattern = r'%s(-%s)?' % (year_pattern, year_pattern)
129 years_pattern = r'%s(,%s)*,?' % (year_range_pattern, year_range_pattern)
130 copyright_pattern = (
131 r'Copyright (\([cC]\) )?%s \w+' % years_pattern)
132
133 for affected_file in input_api.AffectedSourceFiles(source_file_filter):
134 if 'third_party' in affected_file.LocalPath():
135 continue
136 contents = input_api.ReadFile(affected_file, 'rb')
137 if not re.search(copyright_pattern, contents):
138 results.append(output_api.PresubmitError(
139 '%s is missing a correct copyright header.' % affected_file))
140 return results
141
142
rmistry@google.com6be0b4c2013-01-17 14:50:59 +0000143def _CommonChecks(input_api, output_api):
144 """Presubmit checks common to upload and commit."""
145 results = []
146 sources = lambda x: (x.LocalPath().endswith('.h') or
147 x.LocalPath().endswith('.gypi') or
148 x.LocalPath().endswith('.gyp') or
149 x.LocalPath().endswith('.py') or
150 x.LocalPath().endswith('.sh') or
mtklein18e55802015-03-25 07:21:20 -0700151 x.LocalPath().endswith('.m') or
152 x.LocalPath().endswith('.mm') or
153 x.LocalPath().endswith('.go') or
154 x.LocalPath().endswith('.c') or
155 x.LocalPath().endswith('.cc') or
rmistry@google.com6be0b4c2013-01-17 14:50:59 +0000156 x.LocalPath().endswith('.cpp'))
157 results.extend(
rmistry@google.com713276b2013-01-25 18:27:34 +0000158 _CheckChangeHasEol(
rmistry@google.com6be0b4c2013-01-17 14:50:59 +0000159 input_api, output_api, source_file_filter=sources))
Eric Borenbb0ef0a2014-06-25 11:13:27 -0400160 results.extend(_PythonChecks(input_api, output_api))
rmistry01cbf6c2015-03-12 07:48:40 -0700161 results.extend(_IfDefChecks(input_api, output_api))
borenetc7c91802015-03-25 04:47:02 -0700162 results.extend(_CopyrightChecks(input_api, output_api,
163 source_file_filter=sources))
rmistry@google.com6be0b4c2013-01-17 14:50:59 +0000164 return results
165
rmistry@google.com8e3ff8c2013-01-17 12:55:34 +0000166
167def CheckChangeOnUpload(input_api, output_api):
rmistry@google.com6be0b4c2013-01-17 14:50:59 +0000168 """Presubmit checks for the change on upload.
169
170 The following are the presubmit checks:
171 * Check change has one and only one EOL.
172 """
173 results = []
174 results.extend(_CommonChecks(input_api, output_api))
175 return results
rmistry@google.com8e3ff8c2013-01-17 12:55:34 +0000176
177
rmistry@google.comc2993442013-01-23 14:35:58 +0000178def _CheckTreeStatus(input_api, output_api, json_url):
179 """Check whether to allow commit.
180
181 Args:
182 input_api: input related apis.
183 output_api: output related apis.
184 json_url: url to download json style status.
185 """
186 tree_status_results = input_api.canned_checks.CheckTreeIsOpen(
187 input_api, output_api, json_url=json_url)
188 if not tree_status_results:
189 # Check for caution state only if tree is not closed.
190 connection = input_api.urllib2.urlopen(json_url)
191 status = input_api.json.loads(connection.read())
192 connection.close()
rmistry@google.comf6c5f752013-03-29 17:26:00 +0000193 if ('caution' in status['message'].lower() and
194 os.isatty(sys.stdout.fileno())):
195 # Display a prompt only if we are in an interactive shell. Without this
196 # check the commit queue behaves incorrectly because it considers
197 # prompts to be failures.
rmistry@google.comc2993442013-01-23 14:35:58 +0000198 short_text = 'Tree state is: ' + status['general_state']
199 long_text = status['message'] + '\n' + json_url
200 tree_status_results.append(
201 output_api.PresubmitPromptWarning(
202 message=short_text, long_text=long_text))
rmistry@google.com547012d2013-04-12 19:45:46 +0000203 else:
204 # Tree status is closed. Put in message about contacting sheriff.
205 connection = input_api.urllib2.urlopen(
206 SKIA_TREE_STATUS_URL + '/current-sheriff')
207 sheriff_details = input_api.json.loads(connection.read())
208 if sheriff_details:
209 tree_status_results[0]._message += (
210 '\n\nPlease contact the current Skia sheriff (%s) if you are trying '
211 'to submit a build fix\nand do not know how to submit because the '
212 'tree is closed') % sheriff_details['username']
rmistry@google.comc2993442013-01-23 14:35:58 +0000213 return tree_status_results
214
215
commit-bot@chromium.org745e08c2014-02-03 14:18:32 +0000216def _CheckOwnerIsInAuthorsFile(input_api, output_api):
217 results = []
218 issue = input_api.change.issue
219 if issue and input_api.rietveld:
rmistry83fab472014-07-18 05:25:56 -0700220 issue_properties = input_api.rietveld.get_issue_properties(
commit-bot@chromium.org745e08c2014-02-03 14:18:32 +0000221 issue=int(issue), messages=False)
222 owner_email = issue_properties['owner_email']
223
224 try:
225 authors_content = ''
226 for line in open(AUTHORS_FILE_NAME):
227 if not line.startswith('#'):
228 authors_content += line
229 email_fnmatches = re.findall('<(.*)>', authors_content)
230 for email_fnmatch in email_fnmatches:
231 if fnmatch.fnmatch(owner_email, email_fnmatch):
232 # Found a match, the user is in the AUTHORS file break out of the loop
233 break
234 else:
235 # TODO(rmistry): Remove the below CLA messaging once a CLA checker has
236 # been added to the CQ.
237 results.append(
238 output_api.PresubmitError(
239 'The email %s is not in Skia\'s AUTHORS file.\n'
240 'Issue owner, this CL must include an addition to the Skia AUTHORS '
241 'file.\n'
242 'Googler reviewers, please check that the AUTHORS entry '
243 'corresponds to an email address in http://goto/cla-signers. If it '
244 'does not then ask the issue owner to sign the CLA at '
245 'https://developers.google.com/open-source/cla/individual '
246 '(individual) or '
247 'https://developers.google.com/open-source/cla/corporate '
248 '(corporate).'
rmistry83fab472014-07-18 05:25:56 -0700249 % owner_email))
commit-bot@chromium.org745e08c2014-02-03 14:18:32 +0000250 except IOError:
251 # Do not fail if authors file cannot be found.
252 traceback.print_exc()
253 input_api.logging.error('AUTHORS file not found!')
254
255 return results
256
257
rmistry@google.comfb4a68d2013-08-12 14:51:20 +0000258def _CheckLGTMsForPublicAPI(input_api, output_api):
259 """Check LGTMs for public API changes.
260
261 For public API files make sure there is an LGTM from the list of owners in
262 PUBLIC_API_OWNERS.
263 """
264 results = []
265 requires_owner_check = False
rmistry9407ece2014-08-26 14:00:54 -0700266 for affected_file in input_api.AffectedFiles():
267 affected_file_path = affected_file.LocalPath()
rmistry@google.comfb4a68d2013-08-12 14:51:20 +0000268 file_path, file_ext = os.path.splitext(affected_file_path)
rmistry9407ece2014-08-26 14:00:54 -0700269 # We only care about files that end in .h and are under the top-level
270 # include dir.
271 if file_ext == '.h' and 'include' == file_path.split(os.path.sep)[0]:
rmistry@google.comfb4a68d2013-08-12 14:51:20 +0000272 requires_owner_check = True
273
274 if not requires_owner_check:
275 return results
276
277 lgtm_from_owner = False
278 issue = input_api.change.issue
279 if issue and input_api.rietveld:
280 issue_properties = input_api.rietveld.get_issue_properties(
281 issue=int(issue), messages=True)
commit-bot@chromium.orgcfdc5962014-01-31 17:33:04 +0000282 if re.match(REVERT_CL_SUBJECT_PREFIX, issue_properties['subject'], re.I):
283 # It is a revert CL, ignore the public api owners check.
284 return results
rmistryf2d83ca2014-08-26 10:30:29 -0700285
rmistryf91b7172015-03-12 09:48:10 -0700286 if re.search(r'^COMMIT=false$', issue_properties['description'], re.M):
287 # Ignore public api owners check for COMMIT=false CLs since they are not
288 # going to be committed.
289 return results
290
rmistryf2d83ca2014-08-26 10:30:29 -0700291 match = re.search(r'^TBR=(.*)$', issue_properties['description'], re.M)
292 if match:
293 tbr_entries = match.group(1).strip().split(',')
294 for owner in PUBLIC_API_OWNERS:
295 if owner in tbr_entries or owner.split('@')[0] in tbr_entries:
296 # If an owner is specified in the TBR= line then ignore the public
297 # api owners check.
298 return results
299
rmistry@google.comfb4a68d2013-08-12 14:51:20 +0000300 if issue_properties['owner_email'] in PUBLIC_API_OWNERS:
301 # An owner created the CL that is an automatic LGTM.
302 lgtm_from_owner = True
303
304 messages = issue_properties.get('messages')
305 if messages:
306 for message in messages:
307 if (message['sender'] in PUBLIC_API_OWNERS and
308 'lgtm' in message['text'].lower()):
309 # Found an lgtm in a message from an owner.
310 lgtm_from_owner = True
Eric Borenbb0ef0a2014-06-25 11:13:27 -0400311 break
commit-bot@chromium.orgcfdc5962014-01-31 17:33:04 +0000312
rmistry@google.comfb4a68d2013-08-12 14:51:20 +0000313 if not lgtm_from_owner:
314 results.append(
315 output_api.PresubmitError(
316 'Since the CL is editing public API, you must have an LGTM from '
317 'one of: %s' % str(PUBLIC_API_OWNERS)))
318 return results
319
320
rmistryd223fb22015-02-26 10:16:13 -0800321def PostUploadHook(cl, change, output_api):
322 """git cl upload will call this hook after the issue is created/modified.
323
324 This hook does the following:
325 * Adds a link to preview docs changes if there are any docs changes in the CL.
326 * Adds 'NOTRY=true' if the CL contains only docs changes.
rmistry896f3932015-02-26 11:52:05 -0800327 * Adds 'NOTREECHECKS=true' for non master branch changes since they do not
328 need to be gated on the master branch's tree.
329 * Adds 'NOTRY=true' for non master branch changes since trybots do not yet
330 work on them.
rmistryd223fb22015-02-26 10:16:13 -0800331 """
332
333 results = []
334 atleast_one_docs_change = False
335 all_docs_changes = True
336 for affected_file in change.AffectedFiles():
337 affected_file_path = affected_file.LocalPath()
338 file_path, _ = os.path.splitext(affected_file_path)
339 if 'site' == file_path.split(os.path.sep)[0]:
340 atleast_one_docs_change = True
341 else:
342 all_docs_changes = False
343 if atleast_one_docs_change and not all_docs_changes:
344 break
345
346 issue = cl.issue
347 rietveld_obj = cl.RpcServer()
348 if issue and rietveld_obj:
349 original_description = rietveld_obj.get_description(issue)
350 new_description = original_description
351
352 # If the change includes only doc changes then add NOTRY=true in the
353 # CL's description if it does not exist yet.
354 if all_docs_changes and not re.search(
rmistry896f3932015-02-26 11:52:05 -0800355 r'^NOTRY=true$', new_description, re.M | re.I):
rmistryd223fb22015-02-26 10:16:13 -0800356 new_description += '\nNOTRY=true'
357 results.append(
358 output_api.PresubmitNotifyResult(
359 'This change has only doc changes. Automatically added '
360 '\'NOTRY=true\' to the CL\'s description'))
361
362 # If there is atleast one docs change then add preview link in the CL's
363 # description if it does not already exist there.
364 if atleast_one_docs_change and not re.search(
rmistry896f3932015-02-26 11:52:05 -0800365 r'^DOCS_PREVIEW=.*', new_description, re.M | re.I):
rmistryd223fb22015-02-26 10:16:13 -0800366 # Automatically add a link to where the docs can be previewed.
367 new_description += '\nDOCS_PREVIEW= %s%s' % (DOCS_PREVIEW_URL, issue)
368 results.append(
369 output_api.PresubmitNotifyResult(
370 'Automatically added a link to preview the docs changes to the '
371 'CL\'s description'))
372
rmistry896f3932015-02-26 11:52:05 -0800373 # If the target ref is not master then add NOTREECHECKS=true and NOTRY=true
374 # to the CL's description if it does not already exist there.
375 target_ref = rietveld_obj.get_issue_properties(issue, False).get(
376 'target_ref', '')
377 if target_ref != 'refs/heads/master':
378 if not re.search(
379 r'^NOTREECHECKS=true$', new_description, re.M | re.I):
380 new_description += "\nNOTREECHECKS=true"
381 results.append(
382 output_api.PresubmitNotifyResult(
383 'Branch changes do not need to rely on the master branch\'s '
384 'tree status. Automatically added \'NOTREECHECKS=true\' to the '
385 'CL\'s description'))
386 if not re.search(
387 r'^NOTRY=true$', new_description, re.M | re.I):
388 new_description += "\nNOTRY=true"
389 results.append(
390 output_api.PresubmitNotifyResult(
391 'Trybots do not yet work for non-master branches. '
392 'Automatically added \'NOTRY=true\' to the CL\'s description'))
393
rmistry3cfd1ad2015-03-25 12:53:35 -0700394 # Read and process the HASHTAGS file.
395 with open('HASHTAGS', 'rb') as hashtags_csv:
396 hashtags_reader = csv.reader(hashtags_csv, delimiter=',')
397 for row in hashtags_reader:
398 if not row or row[0].startswith('#'):
399 # Ignore empty lines and comments
400 continue
401 hashtag = row[0]
402 # Search for the hashtag in the description.
403 if re.search('#%s' % hashtag, new_description, re.M | re.I):
404 for mapped_text in row[1:]:
405 # Special case handling for CQ_KEYWORDS_THAT_NEED_APPENDING.
406 appended_description = _HandleAppendingCQKeywords(
407 hashtag, mapped_text, new_description, results, output_api)
408 if appended_description:
409 new_description = appended_description
410 continue
411
412 # Add the mapped text if it does not already exist in the
413 # CL's description.
414 if not re.search(
415 r'^%s$' % mapped_text, new_description, re.M | re.I):
416 new_description += '\n%s' % mapped_text
417 results.append(
418 output_api.PresubmitNotifyResult(
419 'Found \'#%s\', automatically added \'%s\' to the CL\'s '
420 'description' % (hashtag, mapped_text)))
rmistry896f3932015-02-26 11:52:05 -0800421
rmistryd223fb22015-02-26 10:16:13 -0800422 # If the description has changed update it.
423 if new_description != original_description:
424 rietveld_obj.update_description(issue, new_description)
425
426 return results
427
428
rmistry3cfd1ad2015-03-25 12:53:35 -0700429def _HandleAppendingCQKeywords(hashtag, keyword_and_value, description,
430 results, output_api):
431 """Handles the CQ keywords that need appending if specified in hashtags."""
432 keyword = keyword_and_value.split('=')[0]
433 if keyword in CQ_KEYWORDS_THAT_NEED_APPENDING:
434 # If the keyword is already in the description then append to it.
435 match = re.search(
436 r'^%s=(.*)$' % keyword, description, re.M | re.I)
437 if match:
438 old_values = match.group(1).split(';')
439 new_value = keyword_and_value.split('=')[1]
440 if new_value in old_values:
441 # Do not need to do anything here.
442 return description
443 # Update the description with the new values.
444 new_description = description.replace(
445 match.group(0), "%s;%s" % (match.group(0), new_value))
446 results.append(
447 output_api.PresubmitNotifyResult(
448 'Found \'#%s\', automatically appended \'%s\' to %s in '
449 'the CL\'s description' % (hashtag, new_value, keyword)))
450 return new_description
451 return None
452
453
rmistry@google.com8e3ff8c2013-01-17 12:55:34 +0000454def CheckChangeOnCommit(input_api, output_api):
455 """Presubmit checks for the change on commit.
456
457 The following are the presubmit checks:
rmistry@google.com6be0b4c2013-01-17 14:50:59 +0000458 * Check change has one and only one EOL.
rmistry@google.comc2993442013-01-23 14:35:58 +0000459 * Ensures that the Skia tree is open in
460 http://skia-tree-status.appspot.com/. Shows a warning if it is in 'Caution'
461 state and an error if it is in 'Closed' state.
rmistry@google.com8e3ff8c2013-01-17 12:55:34 +0000462 """
463 results = []
rmistry@google.com6be0b4c2013-01-17 14:50:59 +0000464 results.extend(_CommonChecks(input_api, output_api))
rmistry@google.com8e3ff8c2013-01-17 12:55:34 +0000465 results.extend(
rmistry@google.comc2993442013-01-23 14:35:58 +0000466 _CheckTreeStatus(input_api, output_api, json_url=(
rmistry@google.com547012d2013-04-12 19:45:46 +0000467 SKIA_TREE_STATUS_URL + '/banner-status?format=json')))
rmistry@google.comfb4a68d2013-08-12 14:51:20 +0000468 results.extend(_CheckLGTMsForPublicAPI(input_api, output_api))
commit-bot@chromium.org745e08c2014-02-03 14:18:32 +0000469 results.extend(_CheckOwnerIsInAuthorsFile(input_api, output_api))
rmistry@google.com8e3ff8c2013-01-17 12:55:34 +0000470 return results