Public API changes are allowed if an owner is TBR'ed.
[platform/upstream/libSkiaSharp.git] / PRESUBMIT.py
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@chromium.org',
26     'reed@google.com',
27     'bsalomon@chromium.org',
28     'bsalomon@google.com',
29     'djsollen@chromium.org',
30     'djsollen@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_svn_file in input_api.AffectedFiles():
193     affected_file_path = affected_svn_file.AbsoluteLocalPath()
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 include dir.
196     if file_ext == '.h' and 'include' in file_path.split(os.path.sep):
197       requires_owner_check = True
198
199   if not requires_owner_check:
200     return results
201
202   lgtm_from_owner = False
203   issue = input_api.change.issue
204   if issue and input_api.rietveld:
205     issue_properties = input_api.rietveld.get_issue_properties(
206         issue=int(issue), messages=True)
207     if re.match(REVERT_CL_SUBJECT_PREFIX, issue_properties['subject'], re.I):
208       # It is a revert CL, ignore the public api owners check.
209       return results
210
211     match = re.search(r'^TBR=(.*)$', issue_properties['description'], re.M)
212     if match:
213       tbr_entries = match.group(1).strip().split(',')
214       for owner in PUBLIC_API_OWNERS:
215         if owner in tbr_entries or owner.split('@')[0] in tbr_entries:
216           # If an owner is specified in the TBR= line then ignore the public
217           # api owners check.
218           return results
219
220     if issue_properties['owner_email'] in PUBLIC_API_OWNERS:
221       # An owner created the CL that is an automatic LGTM.
222       lgtm_from_owner = True
223
224     messages = issue_properties.get('messages')
225     if messages:
226       for message in messages:
227         if (message['sender'] in PUBLIC_API_OWNERS and
228             'lgtm' in message['text'].lower()):
229           # Found an lgtm in a message from an owner.
230           lgtm_from_owner = True
231           break
232
233   if not lgtm_from_owner:
234     results.append(
235         output_api.PresubmitError(
236             'Since the CL is editing public API, you must have an LGTM from '
237             'one of: %s' % str(PUBLIC_API_OWNERS)))
238   return results
239
240
241 def CheckChangeOnCommit(input_api, output_api):
242   """Presubmit checks for the change on commit.
243
244   The following are the presubmit checks:
245   * Check change has one and only one EOL.
246   * Ensures that the Skia tree is open in
247     http://skia-tree-status.appspot.com/. Shows a warning if it is in 'Caution'
248     state and an error if it is in 'Closed' state.
249   """
250   results = []
251   results.extend(_CommonChecks(input_api, output_api))
252   results.extend(
253       _CheckTreeStatus(input_api, output_api, json_url=(
254           SKIA_TREE_STATUS_URL + '/banner-status?format=json')))
255   results.extend(_CheckLGTMsForPublicAPI(input_api, output_api))
256   results.extend(_CheckOwnerIsInAuthorsFile(input_api, output_api))
257   return results