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 PUBLIC_API_OWNERS = ( 24 'reed (at] chromium.org', 25 'reed (at] google.com', 26 'bsalomon (at] chromium.org', 27 'bsalomon (at] google.com', 28 ) 29 30 AUTHORS_FILE_NAME = 'AUTHORS' 31 32 33 def _CheckChangeHasEol(input_api, output_api, source_file_filter=None): 34 """Checks that files end with atleast one \n (LF).""" 35 eof_files = [] 36 for f in input_api.AffectedSourceFiles(source_file_filter): 37 contents = input_api.ReadFile(f, 'rb') 38 # Check that the file ends in atleast one newline character. 39 if len(contents) > 1 and contents[-1:] != '\n': 40 eof_files.append(f.LocalPath()) 41 42 if eof_files: 43 return [output_api.PresubmitPromptWarning( 44 'These files should end in a newline character:', 45 items=eof_files)] 46 return [] 47 48 49 def _CommonChecks(input_api, output_api): 50 """Presubmit checks common to upload and commit.""" 51 results = [] 52 sources = lambda x: (x.LocalPath().endswith('.h') or 53 x.LocalPath().endswith('.gypi') or 54 x.LocalPath().endswith('.gyp') or 55 x.LocalPath().endswith('.py') or 56 x.LocalPath().endswith('.sh') or 57 x.LocalPath().endswith('.cpp')) 58 results.extend( 59 _CheckChangeHasEol( 60 input_api, output_api, source_file_filter=sources)) 61 return results 62 63 64 def CheckChangeOnUpload(input_api, output_api): 65 """Presubmit checks for the change on upload. 66 67 The following are the presubmit checks: 68 * Check change has one and only one EOL. 69 """ 70 results = [] 71 results.extend(_CommonChecks(input_api, output_api)) 72 return results 73 74 75 def _CheckTreeStatus(input_api, output_api, json_url): 76 """Check whether to allow commit. 77 78 Args: 79 input_api: input related apis. 80 output_api: output related apis. 81 json_url: url to download json style status. 82 """ 83 tree_status_results = input_api.canned_checks.CheckTreeIsOpen( 84 input_api, output_api, json_url=json_url) 85 if not tree_status_results: 86 # Check for caution state only if tree is not closed. 87 connection = input_api.urllib2.urlopen(json_url) 88 status = input_api.json.loads(connection.read()) 89 connection.close() 90 if ('caution' in status['message'].lower() and 91 os.isatty(sys.stdout.fileno())): 92 # Display a prompt only if we are in an interactive shell. Without this 93 # check the commit queue behaves incorrectly because it considers 94 # prompts to be failures. 95 short_text = 'Tree state is: ' + status['general_state'] 96 long_text = status['message'] + '\n' + json_url 97 tree_status_results.append( 98 output_api.PresubmitPromptWarning( 99 message=short_text, long_text=long_text)) 100 else: 101 # Tree status is closed. Put in message about contacting sheriff. 102 connection = input_api.urllib2.urlopen( 103 SKIA_TREE_STATUS_URL + '/current-sheriff') 104 sheriff_details = input_api.json.loads(connection.read()) 105 if sheriff_details: 106 tree_status_results[0]._message += ( 107 '\n\nPlease contact the current Skia sheriff (%s) if you are trying ' 108 'to submit a build fix\nand do not know how to submit because the ' 109 'tree is closed') % sheriff_details['username'] 110 return tree_status_results 111 112 113 def _CheckOwnerIsInAuthorsFile(input_api, output_api): 114 results = [] 115 issue = input_api.change.issue 116 if issue and input_api.rietveld: 117 issue_properties = input_api.rietveld.get_issue_properties( 118 issue=int(issue), messages=False) 119 owner_email = issue_properties['owner_email'] 120 121 try: 122 authors_content = '' 123 for line in open(AUTHORS_FILE_NAME): 124 if not line.startswith('#'): 125 authors_content += line 126 email_fnmatches = re.findall('<(.*)>', authors_content) 127 for email_fnmatch in email_fnmatches: 128 if fnmatch.fnmatch(owner_email, email_fnmatch): 129 # Found a match, the user is in the AUTHORS file break out of the loop 130 break 131 else: 132 # TODO(rmistry): Remove the below CLA messaging once a CLA checker has 133 # been added to the CQ. 134 results.append( 135 output_api.PresubmitError( 136 'The email %s is not in Skia\'s AUTHORS file.\n' 137 'Issue owner, this CL must include an addition to the Skia AUTHORS ' 138 'file.\n' 139 'Googler reviewers, please check that the AUTHORS entry ' 140 'corresponds to an email address in http://goto/cla-signers. If it ' 141 'does not then ask the issue owner to sign the CLA at ' 142 'https://developers.google.com/open-source/cla/individual ' 143 '(individual) or ' 144 'https://developers.google.com/open-source/cla/corporate ' 145 '(corporate).' 146 % owner_email)) 147 except IOError: 148 # Do not fail if authors file cannot be found. 149 traceback.print_exc() 150 input_api.logging.error('AUTHORS file not found!') 151 152 return results 153 154 155 def _CheckLGTMsForPublicAPI(input_api, output_api): 156 """Check LGTMs for public API changes. 157 158 For public API files make sure there is an LGTM from the list of owners in 159 PUBLIC_API_OWNERS. 160 """ 161 results = [] 162 requires_owner_check = False 163 for affected_svn_file in input_api.AffectedFiles(): 164 affected_file_path = affected_svn_file.AbsoluteLocalPath() 165 file_path, file_ext = os.path.splitext(affected_file_path) 166 # We only care about files that end in .h and are under the include dir. 167 if file_ext == '.h' and 'include' in file_path.split(os.path.sep): 168 requires_owner_check = True 169 170 if not requires_owner_check: 171 return results 172 173 lgtm_from_owner = False 174 issue = input_api.change.issue 175 if issue and input_api.rietveld: 176 issue_properties = input_api.rietveld.get_issue_properties( 177 issue=int(issue), messages=True) 178 if re.match(REVERT_CL_SUBJECT_PREFIX, issue_properties['subject'], re.I): 179 # It is a revert CL, ignore the public api owners check. 180 return results 181 if issue_properties['owner_email'] in PUBLIC_API_OWNERS: 182 # An owner created the CL that is an automatic LGTM. 183 lgtm_from_owner = True 184 185 messages = issue_properties.get('messages') 186 if messages: 187 for message in messages: 188 if (message['sender'] in PUBLIC_API_OWNERS and 189 'lgtm' in message['text'].lower()): 190 # Found an lgtm in a message from an owner. 191 lgtm_from_owner = True 192 break; 193 194 if not lgtm_from_owner: 195 results.append( 196 output_api.PresubmitError( 197 'Since the CL is editing public API, you must have an LGTM from ' 198 'one of: %s' % str(PUBLIC_API_OWNERS))) 199 return results 200 201 202 def CheckChangeOnCommit(input_api, output_api): 203 """Presubmit checks for the change on commit. 204 205 The following are the presubmit checks: 206 * Check change has one and only one EOL. 207 * Ensures that the Skia tree is open in 208 http://skia-tree-status.appspot.com/. Shows a warning if it is in 'Caution' 209 state and an error if it is in 'Closed' state. 210 """ 211 results = [] 212 results.extend(_CommonChecks(input_api, output_api)) 213 results.extend( 214 _CheckTreeStatus(input_api, output_api, json_url=( 215 SKIA_TREE_STATUS_URL + '/banner-status?format=json'))) 216 results.extend(_CheckLGTMsForPublicAPI(input_api, output_api)) 217 results.extend(_CheckOwnerIsInAuthorsFile(input_api, output_api)) 218 return results 219