Home | History | Annotate | Download | only in skia
      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