Ignore public API checks if COMMIT=false is in the description
[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 subprocess
16 import sys
17 import traceback
18
19
20 REVERT_CL_SUBJECT_PREFIX = 'Revert '
21
22 SKIA_TREE_STATUS_URL = 'http://skia-tree-status.appspot.com'
23
24 # Please add the complete email address here (and not just 'xyz@' or 'xyz').
25 PUBLIC_API_OWNERS = (
26     'reed@chromium.org',
27     'reed@google.com',
28     'bsalomon@chromium.org',
29     'bsalomon@google.com',
30     'djsollen@chromium.org',
31     'djsollen@google.com',
32 )
33
34 AUTHORS_FILE_NAME = 'AUTHORS'
35
36 DOCS_PREVIEW_URL = 'https://skia.org/?cl='
37
38
39 def _CheckChangeHasEol(input_api, output_api, source_file_filter=None):
40   """Checks that files end with atleast one \n (LF)."""
41   eof_files = []
42   for f in input_api.AffectedSourceFiles(source_file_filter):
43     contents = input_api.ReadFile(f, 'rb')
44     # Check that the file ends in atleast one newline character.
45     if len(contents) > 1 and contents[-1:] != '\n':
46       eof_files.append(f.LocalPath())
47
48   if eof_files:
49     return [output_api.PresubmitPromptWarning(
50       'These files should end in a newline character:',
51       items=eof_files)]
52   return []
53
54
55 def _PythonChecks(input_api, output_api):
56   """Run checks on any modified Python files."""
57   pylint_disabled_warnings = (
58       'F0401',  # Unable to import.
59       'E0611',  # No name in module.
60       'W0232',  # Class has no __init__ method.
61       'E1002',  # Use of super on an old style class.
62       'W0403',  # Relative import used.
63       'R0201',  # Method could be a function.
64       'E1003',  # Using class name in super.
65       'W0613',  # Unused argument.
66   )
67   # Run Pylint on only the modified python files. Unfortunately it still runs
68   # Pylint on the whole file instead of just the modified lines.
69   affected_python_files = []
70   for affected_file in input_api.AffectedSourceFiles(None):
71     affected_file_path = affected_file.LocalPath()
72     if affected_file_path.endswith('.py'):
73       affected_python_files.append(affected_file_path)
74   return input_api.canned_checks.RunPylint(
75       input_api, output_api,
76       disabled_warnings=pylint_disabled_warnings,
77       white_list=affected_python_files)
78
79
80 def _IfDefChecks(input_api, output_api):
81   """Ensures if/ifdef are not before includes. See skbug/3362 for details."""
82   comment_block_start_pattern = re.compile('^\s*\/\*.*$')
83   comment_block_middle_pattern = re.compile('^\s+\*.*')
84   comment_block_end_pattern = re.compile('^\s+\*\/.*$')
85   single_line_comment_pattern = re.compile('^\s*//.*$')
86   def is_comment(line):
87     return (comment_block_start_pattern.match(line) or
88             comment_block_middle_pattern.match(line) or
89             comment_block_end_pattern.match(line) or
90             single_line_comment_pattern.match(line))
91
92   empty_line_pattern = re.compile('^\s*$')
93   def is_empty_line(line):
94     return empty_line_pattern.match(line)
95
96   failing_files = []
97   for affected_file in input_api.AffectedSourceFiles(None):
98     affected_file_path = affected_file.LocalPath()
99     if affected_file_path.endswith('.cpp') or affected_file_path.endswith('.h'):
100       f = open(affected_file_path)
101       for line in f.xreadlines():
102         if is_comment(line) or is_empty_line(line):
103           continue
104         # The below will be the first real line after comments and newlines.
105         if line.startswith('#if 0 '):
106           pass
107         elif line.startswith('#if ') or line.startswith('#ifdef '):
108           failing_files.append(affected_file_path)
109         break
110
111   results = []
112   if failing_files:
113     results.append(
114         output_api.PresubmitError(
115             'The following files have #if or #ifdef before includes:\n%s\n\n'
116             'See skbug.com/3362 for why this should be fixed.' %
117                 '\n'.join(failing_files)))
118   return results
119
120
121 def _CommonChecks(input_api, output_api):
122   """Presubmit checks common to upload and commit."""
123   results = []
124   sources = lambda x: (x.LocalPath().endswith('.h') or
125                        x.LocalPath().endswith('.gypi') or
126                        x.LocalPath().endswith('.gyp') or
127                        x.LocalPath().endswith('.py') or
128                        x.LocalPath().endswith('.sh') or
129                        x.LocalPath().endswith('.cpp'))
130   results.extend(
131       _CheckChangeHasEol(
132           input_api, output_api, source_file_filter=sources))
133   results.extend(_PythonChecks(input_api, output_api))
134   results.extend(_IfDefChecks(input_api, output_api))
135   return results
136
137
138 def CheckChangeOnUpload(input_api, output_api):
139   """Presubmit checks for the change on upload.
140
141   The following are the presubmit checks:
142   * Check change has one and only one EOL.
143   """
144   results = []
145   results.extend(_CommonChecks(input_api, output_api))
146   # TODO(rmistry): Remove the below it is only for testing!!!
147   results.extend(_CheckLGTMsForPublicAPI(input_api, output_api))
148   return results
149
150
151 def _CheckTreeStatus(input_api, output_api, json_url):
152   """Check whether to allow commit.
153
154   Args:
155     input_api: input related apis.
156     output_api: output related apis.
157     json_url: url to download json style status.
158   """
159   tree_status_results = input_api.canned_checks.CheckTreeIsOpen(
160       input_api, output_api, json_url=json_url)
161   if not tree_status_results:
162     # Check for caution state only if tree is not closed.
163     connection = input_api.urllib2.urlopen(json_url)
164     status = input_api.json.loads(connection.read())
165     connection.close()
166     if ('caution' in status['message'].lower() and
167         os.isatty(sys.stdout.fileno())):
168       # Display a prompt only if we are in an interactive shell. Without this
169       # check the commit queue behaves incorrectly because it considers
170       # prompts to be failures.
171       short_text = 'Tree state is: ' + status['general_state']
172       long_text = status['message'] + '\n' + json_url
173       tree_status_results.append(
174           output_api.PresubmitPromptWarning(
175               message=short_text, long_text=long_text))
176   else:
177     # Tree status is closed. Put in message about contacting sheriff.
178     connection = input_api.urllib2.urlopen(
179         SKIA_TREE_STATUS_URL + '/current-sheriff')
180     sheriff_details = input_api.json.loads(connection.read())
181     if sheriff_details:
182       tree_status_results[0]._message += (
183           '\n\nPlease contact the current Skia sheriff (%s) if you are trying '
184           'to submit a build fix\nand do not know how to submit because the '
185           'tree is closed') % sheriff_details['username']
186   return tree_status_results
187
188
189 def _CheckOwnerIsInAuthorsFile(input_api, output_api):
190   results = []
191   issue = input_api.change.issue
192   if issue and input_api.rietveld:
193     issue_properties = input_api.rietveld.get_issue_properties(
194         issue=int(issue), messages=False)
195     owner_email = issue_properties['owner_email']
196
197     try:
198       authors_content = ''
199       for line in open(AUTHORS_FILE_NAME):
200         if not line.startswith('#'):
201           authors_content += line
202       email_fnmatches = re.findall('<(.*)>', authors_content)
203       for email_fnmatch in email_fnmatches:
204         if fnmatch.fnmatch(owner_email, email_fnmatch):
205           # Found a match, the user is in the AUTHORS file break out of the loop
206           break
207       else:
208         # TODO(rmistry): Remove the below CLA messaging once a CLA checker has
209         # been added to the CQ.
210         results.append(
211           output_api.PresubmitError(
212             'The email %s is not in Skia\'s AUTHORS file.\n'
213             'Issue owner, this CL must include an addition to the Skia AUTHORS '
214             'file.\n'
215             'Googler reviewers, please check that the AUTHORS entry '
216             'corresponds to an email address in http://goto/cla-signers. If it '
217             'does not then ask the issue owner to sign the CLA at '
218             'https://developers.google.com/open-source/cla/individual '
219             '(individual) or '
220             'https://developers.google.com/open-source/cla/corporate '
221             '(corporate).'
222             % owner_email))
223     except IOError:
224       # Do not fail if authors file cannot be found.
225       traceback.print_exc()
226       input_api.logging.error('AUTHORS file not found!')
227
228   return results
229
230
231 def _CheckLGTMsForPublicAPI(input_api, output_api):
232   """Check LGTMs for public API changes.
233
234   For public API files make sure there is an LGTM from the list of owners in
235   PUBLIC_API_OWNERS.
236   """
237   results = []
238   requires_owner_check = False
239   for affected_file in input_api.AffectedFiles():
240     affected_file_path = affected_file.LocalPath()
241     file_path, file_ext = os.path.splitext(affected_file_path)
242     # We only care about files that end in .h and are under the top-level
243     # include dir.
244     if file_ext == '.h' and 'include' == file_path.split(os.path.sep)[0]:
245       requires_owner_check = True
246
247   if not requires_owner_check:
248     return results
249
250   lgtm_from_owner = False
251   issue = input_api.change.issue
252   if issue and input_api.rietveld:
253     issue_properties = input_api.rietveld.get_issue_properties(
254         issue=int(issue), messages=True)
255     if re.match(REVERT_CL_SUBJECT_PREFIX, issue_properties['subject'], re.I):
256       # It is a revert CL, ignore the public api owners check.
257       return results
258
259     if re.search(r'^COMMIT=false$', issue_properties['description'], re.M):
260       # Ignore public api owners check for COMMIT=false CLs since they are not
261       # going to be committed.
262       return results
263
264     match = re.search(r'^TBR=(.*)$', issue_properties['description'], re.M)
265     if match:
266       tbr_entries = match.group(1).strip().split(',')
267       for owner in PUBLIC_API_OWNERS:
268         if owner in tbr_entries or owner.split('@')[0] in tbr_entries:
269           # If an owner is specified in the TBR= line then ignore the public
270           # api owners check.
271           return results
272
273     if issue_properties['owner_email'] in PUBLIC_API_OWNERS:
274       # An owner created the CL that is an automatic LGTM.
275       lgtm_from_owner = True
276
277     messages = issue_properties.get('messages')
278     if messages:
279       for message in messages:
280         if (message['sender'] in PUBLIC_API_OWNERS and
281             'lgtm' in message['text'].lower()):
282           # Found an lgtm in a message from an owner.
283           lgtm_from_owner = True
284           break
285
286   if not lgtm_from_owner:
287     results.append(
288         output_api.PresubmitError(
289             'Since the CL is editing public API, you must have an LGTM from '
290             'one of: %s' % str(PUBLIC_API_OWNERS)))
291   return results
292
293
294 def PostUploadHook(cl, change, output_api):
295   """git cl upload will call this hook after the issue is created/modified.
296
297   This hook does the following:
298   * Adds a link to preview docs changes if there are any docs changes in the CL.
299   * Adds 'NOTRY=true' if the CL contains only docs changes.
300   * Adds 'NOTREECHECKS=true' for non master branch changes since they do not
301     need to be gated on the master branch's tree.
302   * Adds 'NOTRY=true' for non master branch changes since trybots do not yet
303     work on them.
304   """
305
306   results = []
307   atleast_one_docs_change = False
308   all_docs_changes = True
309   for affected_file in change.AffectedFiles():
310     affected_file_path = affected_file.LocalPath()
311     file_path, _ = os.path.splitext(affected_file_path)
312     if 'site' == file_path.split(os.path.sep)[0]:
313       atleast_one_docs_change = True
314     else:
315       all_docs_changes = False
316     if atleast_one_docs_change and not all_docs_changes:
317       break
318
319   issue = cl.issue
320   rietveld_obj = cl.RpcServer()
321   if issue and rietveld_obj:
322     original_description = rietveld_obj.get_description(issue)
323     new_description = original_description
324
325     # If the change includes only doc changes then add NOTRY=true in the
326     # CL's description if it does not exist yet.
327     if all_docs_changes and not re.search(
328         r'^NOTRY=true$', new_description, re.M | re.I):
329       new_description += '\nNOTRY=true'
330       results.append(
331           output_api.PresubmitNotifyResult(
332               'This change has only doc changes. Automatically added '
333               '\'NOTRY=true\' to the CL\'s description'))
334
335     # If there is atleast one docs change then add preview link in the CL's
336     # description if it does not already exist there.
337     if atleast_one_docs_change and not re.search(
338         r'^DOCS_PREVIEW=.*', new_description, re.M | re.I):
339       # Automatically add a link to where the docs can be previewed.
340       new_description += '\nDOCS_PREVIEW= %s%s' % (DOCS_PREVIEW_URL, issue)
341       results.append(
342           output_api.PresubmitNotifyResult(
343               'Automatically added a link to preview the docs changes to the '
344               'CL\'s description'))
345
346     # If the target ref is not master then add NOTREECHECKS=true and NOTRY=true
347     # to the CL's description if it does not already exist there.
348     target_ref = rietveld_obj.get_issue_properties(issue, False).get(
349         'target_ref', '')
350     if target_ref != 'refs/heads/master':
351       if not re.search(
352           r'^NOTREECHECKS=true$', new_description, re.M | re.I):
353         new_description += "\nNOTREECHECKS=true"
354         results.append(
355             output_api.PresubmitNotifyResult(
356                 'Branch changes do not need to rely on the master branch\'s '
357                 'tree status. Automatically added \'NOTREECHECKS=true\' to the '
358                 'CL\'s description'))
359       if not re.search(
360           r'^NOTRY=true$', new_description, re.M | re.I):
361         new_description += "\nNOTRY=true"
362         results.append(
363             output_api.PresubmitNotifyResult(
364                 'Trybots do not yet work for non-master branches. '
365                 'Automatically added \'NOTRY=true\' to the CL\'s description'))
366
367
368     # If the description has changed update it.
369     if new_description != original_description:
370       rietveld_obj.update_description(issue, new_description)
371
372     return results
373
374
375 def CheckChangeOnCommit(input_api, output_api):
376   """Presubmit checks for the change on commit.
377
378   The following are the presubmit checks:
379   * Check change has one and only one EOL.
380   * Ensures that the Skia tree is open in
381     http://skia-tree-status.appspot.com/. Shows a warning if it is in 'Caution'
382     state and an error if it is in 'Closed' state.
383   """
384   results = []
385   results.extend(_CommonChecks(input_api, output_api))
386   results.extend(
387       _CheckTreeStatus(input_api, output_api, json_url=(
388           SKIA_TREE_STATUS_URL + '/banner-status?format=json')))
389   results.extend(_CheckLGTMsForPublicAPI(input_api, output_api))
390   results.extend(_CheckOwnerIsInAuthorsFile(input_api, output_api))
391   return results