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
8See http://dev.chromium.org/developers/how-tos/depottools/presubmit-scripts
9for more details about the presubmit API built into gcl.
10"""
11
12import fnmatch
13import os
14import re
15import sys
16import traceback
17
18
19REVERT_CL_SUBJECT_PREFIX = 'Revert '
20
21SKIA_TREE_STATUS_URL = 'http://skia-tree-status.appspot.com'
22
23# Please add the complete email address here (and not just 'xyz@' or 'xyz').
24PUBLIC_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
33AUTHORS_FILE_NAME = 'AUTHORS'
34
35
36def _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
52def _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
77def _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
93def 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
104def _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
142def _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
184def _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
242def 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