1 # 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 8 See http://dev.chromium.org/developers/how-tos/depottools/presubmit-scripts 9 for more details about the presubmit API built into gcl. 10 """ 11 12 import fnmatch 13 import os 14 import re 15 import sys 16 import traceback 17 18 19 REVERT_CL_SUBJECT_PREFIX = 'Revert ' 20 21 SKIA_TREE_STATUS_URL = 'http://skia-tree-status.appspot.com' 22 23 # Please add the complete email address here (and not just 'xyz@' or 'xyz'). 24 PUBLIC_API_OWNERS = ( 25 'reed (at] chromium.org', 26 'reed (at] google.com', 27 'bsalomon (at] chromium.org', 28 'bsalomon (at] google.com', 29 'djsollen (at] chromium.org', 30 'djsollen (at] google.com', 31 ) 32 33 AUTHORS_FILE_NAME = 'AUTHORS' 34 35 36 def _CheckChangeHasEol(input_api, output_api, source_file_filter=None): 37 """Checks that files end with atleast one \n (LF).""" 38 eof_files = [] 39 for f in input_api.AffectedSourceFiles(source_file_filter): 40 contents = input_api.ReadFile(f, 'rb') 41 # Check that the file ends in atleast one newline character. 42 if len(contents) > 1 and contents[-1:] != '\n': 43 eof_files.append(f.LocalPath()) 44 45 if eof_files: 46 return [output_api.PresubmitPromptWarning( 47 'These files should end in a newline character:', 48 items=eof_files)] 49 return [] 50 51 52 def _PythonChecks(input_api, output_api): 53 """Run checks on any modified Python files.""" 54 pylint_disabled_warnings = ( 55 'F0401', # Unable to import. 56 'E0611', # No name in module. 57 'W0232', # Class has no __init__ method. 58 'E1002', # Use of super on an old style class. 59 'W0403', # Relative import used. 60 'R0201', # Method could be a function. 61 'E1003', # Using class name in super. 62 'W0613', # Unused argument. 63 ) 64 # Run Pylint on only the modified python files. Unfortunately it still runs 65 # Pylint on the whole file instead of just the modified lines. 66 affected_python_files = [] 67 for affected_file in input_api.AffectedSourceFiles(None): 68 affected_file_path = affected_file.LocalPath() 69 if affected_file_path.endswith('.py'): 70 affected_python_files.append(affected_file_path) 71 return input_api.canned_checks.RunPylint( 72 input_api, output_api, 73 disabled_warnings=pylint_disabled_warnings, 74 white_list=affected_python_files) 75 76 77 def _CommonChecks(input_api, output_api): 78 """Presubmit checks common to upload and commit.""" 79 results = [] 80 sources = lambda x: (x.LocalPath().endswith('.h') or 81 x.LocalPath().endswith('.gypi') or 82 x.LocalPath().endswith('.gyp') or 83 x.LocalPath().endswith('.py') or 84 x.LocalPath().endswith('.sh') or 85 x.LocalPath().endswith('.cpp')) 86 results.extend( 87 _CheckChangeHasEol( 88 input_api, output_api, source_file_filter=sources)) 89 results.extend(_PythonChecks(input_api, output_api)) 90 return results 91 92 93 def CheckChangeOnUpload(input_api, output_api): 94 """Presubmit checks for the change on upload. 95 96 The following are the presubmit checks: 97 * Check change has one and only one EOL. 98 """ 99 results = [] 100 results.extend(_CommonChecks(input_api, output_api)) 101 return results 102 103 104 def _CheckTreeStatus(input_api, output_api, json_url): 105 """Check whether to allow commit. 106 107 Args: 108 input_api: input related apis. 109 output_api: output related apis. 110 json_url: url to download json style status. 111 """ 112 tree_status_results = input_api.canned_checks.CheckTreeIsOpen( 113 input_api, output_api, json_url=json_url) 114 if not tree_status_results: 115 # Check for caution state only if tree is not closed. 116 connection = input_api.urllib2.urlopen(json_url) 117 status = input_api.json.loads(connection.read()) 118 connection.close() 119 if ('caution' in status['message'].lower() and 120 os.isatty(sys.stdout.fileno())): 121 # Display a prompt only if we are in an interactive shell. Without this 122 # check the commit queue behaves incorrectly because it considers 123 # prompts to be failures. 124 short_text = 'Tree state is: ' + status['general_state'] 125 long_text = status['message'] + '\n' + json_url 126 tree_status_results.append( 127 output_api.PresubmitPromptWarning( 128 message=short_text, long_text=long_text)) 129 else: 130 # Tree status is closed. Put in message about contacting sheriff. 131 connection = input_api.urllib2.urlopen( 132 SKIA_TREE_STATUS_URL + '/current-sheriff') 133 sheriff_details = input_api.json.loads(connection.read()) 134 if sheriff_details: 135 tree_status_results[0]._message += ( 136 '\n\nPlease contact the current Skia sheriff (%s) if you are trying ' 137 'to submit a build fix\nand do not know how to submit because the ' 138 'tree is closed') % sheriff_details['username'] 139 return tree_status_results 140 141 142 def _CheckOwnerIsInAuthorsFile(input_api, output_api): 143 results = [] 144 issue = input_api.change.issue 145 if issue and input_api.rietveld: 146 issue_properties = input_api.rietveld.get_issue_properties( 147 issue=int(issue), messages=False) 148 owner_email = issue_properties['owner_email'] 149 150 try: 151 authors_content = '' 152 for line in open(AUTHORS_FILE_NAME): 153 if not line.startswith('#'): 154 authors_content += line 155 email_fnmatches = re.findall('<(.*)>', authors_content) 156 for email_fnmatch in email_fnmatches: 157 if fnmatch.fnmatch(owner_email, email_fnmatch): 158 # Found a match, the user is in the AUTHORS file break out of the loop 159 break 160 else: 161 # TODO(rmistry): Remove the below CLA messaging once a CLA checker has 162 # been added to the CQ. 163 results.append( 164 output_api.PresubmitError( 165 'The email %s is not in Skia\'s AUTHORS file.\n' 166 'Issue owner, this CL must include an addition to the Skia AUTHORS ' 167 'file.\n' 168 'Googler reviewers, please check that the AUTHORS entry ' 169 'corresponds to an email address in http://goto/cla-signers. If it ' 170 'does not then ask the issue owner to sign the CLA at ' 171 'https://developers.google.com/open-source/cla/individual ' 172 '(individual) or ' 173 'https://developers.google.com/open-source/cla/corporate ' 174 '(corporate).' 175 % owner_email)) 176 except IOError: 177 # Do not fail if authors file cannot be found. 178 traceback.print_exc() 179 input_api.logging.error('AUTHORS file not found!') 180 181 return results 182 183 184 def _CheckLGTMsForPublicAPI(input_api, output_api): 185 """Check LGTMs for public API changes. 186 187 For public API files make sure there is an LGTM from the list of owners in 188 PUBLIC_API_OWNERS. 189 """ 190 results = [] 191 requires_owner_check = False 192 for affected_file in input_api.AffectedFiles(): 193 affected_file_path = affected_file.LocalPath() 194 file_path, file_ext = os.path.splitext(affected_file_path) 195 # We only care about files that end in .h and are under the top-level 196 # include dir. 197 if file_ext == '.h' and 'include' == file_path.split(os.path.sep)[0]: 198 requires_owner_check = True 199 200 if not requires_owner_check: 201 return results 202 203 lgtm_from_owner = False 204 issue = input_api.change.issue 205 if issue and input_api.rietveld: 206 issue_properties = input_api.rietveld.get_issue_properties( 207 issue=int(issue), messages=True) 208 if re.match(REVERT_CL_SUBJECT_PREFIX, issue_properties['subject'], re.I): 209 # It is a revert CL, ignore the public api owners check. 210 return results 211 212 match = re.search(r'^TBR=(.*)$', issue_properties['description'], re.M) 213 if match: 214 tbr_entries = match.group(1).strip().split(',') 215 for owner in PUBLIC_API_OWNERS: 216 if owner in tbr_entries or owner.split('@')[0] in tbr_entries: 217 # If an owner is specified in the TBR= line then ignore the public 218 # api owners check. 219 return results 220 221 if issue_properties['owner_email'] in PUBLIC_API_OWNERS: 222 # An owner created the CL that is an automatic LGTM. 223 lgtm_from_owner = True 224 225 messages = issue_properties.get('messages') 226 if messages: 227 for message in messages: 228 if (message['sender'] in PUBLIC_API_OWNERS and 229 'lgtm' in message['text'].lower()): 230 # Found an lgtm in a message from an owner. 231 lgtm_from_owner = True 232 break 233 234 if not lgtm_from_owner: 235 results.append( 236 output_api.PresubmitError( 237 'Since the CL is editing public API, you must have an LGTM from ' 238 'one of: %s' % str(PUBLIC_API_OWNERS))) 239 return results 240 241 242 def CheckChangeOnCommit(input_api, output_api): 243 """Presubmit checks for the change on commit. 244 245 The following are the presubmit checks: 246 * Check change has one and only one EOL. 247 * Ensures that the Skia tree is open in 248 http://skia-tree-status.appspot.com/. Shows a warning if it is in 'Caution' 249 state and an error if it is in 'Closed' state. 250 """ 251 results = [] 252 results.extend(_CommonChecks(input_api, output_api)) 253 results.extend( 254 _CheckTreeStatus(input_api, output_api, json_url=( 255 SKIA_TREE_STATUS_URL + '/banner-status?format=json'))) 256 results.extend(_CheckLGTMsForPublicAPI(input_api, output_api)) 257 results.extend(_CheckOwnerIsInAuthorsFile(input_api, output_api)) 258 return results 259