More file types to check for (C).
[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 _CopyrightChecks(input_api, output_api, source_file_filter=None):
122   results = []
123   year_pattern = r'\d{4}'
124   year_range_pattern = r'%s(-%s)?' % (year_pattern, year_pattern)
125   years_pattern = r'%s(,%s)*,?' % (year_range_pattern, year_range_pattern)
126   copyright_pattern = (
127       r'Copyright (\([cC]\) )?%s \w+' % years_pattern)
128
129   for affected_file in input_api.AffectedSourceFiles(source_file_filter):
130     if 'third_party' in affected_file.LocalPath():
131       continue
132     contents = input_api.ReadFile(affected_file, 'rb')
133     if not re.search(copyright_pattern, contents):
134       results.append(output_api.PresubmitError(
135           '%s is missing a correct copyright header.' % affected_file))
136   return results
137
138
139 def _CommonChecks(input_api, output_api):
140   """Presubmit checks common to upload and commit."""
141   results = []
142   sources = lambda x: (x.LocalPath().endswith('.h') or
143                        x.LocalPath().endswith('.gypi') or
144                        x.LocalPath().endswith('.gyp') or
145                        x.LocalPath().endswith('.py') or
146                        x.LocalPath().endswith('.sh') or
147                        x.LocalPath().endswith('.m') or
148                        x.LocalPath().endswith('.mm') or
149                        x.LocalPath().endswith('.go') or
150                        x.LocalPath().endswith('.c') or
151                        x.LocalPath().endswith('.cc') or
152                        x.LocalPath().endswith('.cpp'))
153   results.extend(
154       _CheckChangeHasEol(
155           input_api, output_api, source_file_filter=sources))
156   results.extend(_PythonChecks(input_api, output_api))
157   results.extend(_IfDefChecks(input_api, output_api))
158   results.extend(_CopyrightChecks(input_api, output_api,
159                                   source_file_filter=sources))
160   return results
161
162
163 def CheckChangeOnUpload(input_api, output_api):
164   """Presubmit checks for the change on upload.
165
166   The following are the presubmit checks:
167   * Check change has one and only one EOL.
168   """
169   results = []
170   results.extend(_CommonChecks(input_api, output_api))
171   return results
172
173
174 def _CheckTreeStatus(input_api, output_api, json_url):
175   """Check whether to allow commit.
176
177   Args:
178     input_api: input related apis.
179     output_api: output related apis.
180     json_url: url to download json style status.
181   """
182   tree_status_results = input_api.canned_checks.CheckTreeIsOpen(
183       input_api, output_api, json_url=json_url)
184   if not tree_status_results:
185     # Check for caution state only if tree is not closed.
186     connection = input_api.urllib2.urlopen(json_url)
187     status = input_api.json.loads(connection.read())
188     connection.close()
189     if ('caution' in status['message'].lower() and
190         os.isatty(sys.stdout.fileno())):
191       # Display a prompt only if we are in an interactive shell. Without this
192       # check the commit queue behaves incorrectly because it considers
193       # prompts to be failures.
194       short_text = 'Tree state is: ' + status['general_state']
195       long_text = status['message'] + '\n' + json_url
196       tree_status_results.append(
197           output_api.PresubmitPromptWarning(
198               message=short_text, long_text=long_text))
199   else:
200     # Tree status is closed. Put in message about contacting sheriff.
201     connection = input_api.urllib2.urlopen(
202         SKIA_TREE_STATUS_URL + '/current-sheriff')
203     sheriff_details = input_api.json.loads(connection.read())
204     if sheriff_details:
205       tree_status_results[0]._message += (
206           '\n\nPlease contact the current Skia sheriff (%s) if you are trying '
207           'to submit a build fix\nand do not know how to submit because the '
208           'tree is closed') % sheriff_details['username']
209   return tree_status_results
210
211
212 def _CheckOwnerIsInAuthorsFile(input_api, output_api):
213   results = []
214   issue = input_api.change.issue
215   if issue and input_api.rietveld:
216     issue_properties = input_api.rietveld.get_issue_properties(
217         issue=int(issue), messages=False)
218     owner_email = issue_properties['owner_email']
219
220     try:
221       authors_content = ''
222       for line in open(AUTHORS_FILE_NAME):
223         if not line.startswith('#'):
224           authors_content += line
225       email_fnmatches = re.findall('<(.*)>', authors_content)
226       for email_fnmatch in email_fnmatches:
227         if fnmatch.fnmatch(owner_email, email_fnmatch):
228           # Found a match, the user is in the AUTHORS file break out of the loop
229           break
230       else:
231         # TODO(rmistry): Remove the below CLA messaging once a CLA checker has
232         # been added to the CQ.
233         results.append(
234           output_api.PresubmitError(
235             'The email %s is not in Skia\'s AUTHORS file.\n'
236             'Issue owner, this CL must include an addition to the Skia AUTHORS '
237             'file.\n'
238             'Googler reviewers, please check that the AUTHORS entry '
239             'corresponds to an email address in http://goto/cla-signers. If it '
240             'does not then ask the issue owner to sign the CLA at '
241             'https://developers.google.com/open-source/cla/individual '
242             '(individual) or '
243             'https://developers.google.com/open-source/cla/corporate '
244             '(corporate).'
245             % owner_email))
246     except IOError:
247       # Do not fail if authors file cannot be found.
248       traceback.print_exc()
249       input_api.logging.error('AUTHORS file not found!')
250
251   return results
252
253
254 def _CheckLGTMsForPublicAPI(input_api, output_api):
255   """Check LGTMs for public API changes.
256
257   For public API files make sure there is an LGTM from the list of owners in
258   PUBLIC_API_OWNERS.
259   """
260   results = []
261   requires_owner_check = False
262   for affected_file in input_api.AffectedFiles():
263     affected_file_path = affected_file.LocalPath()
264     file_path, file_ext = os.path.splitext(affected_file_path)
265     # We only care about files that end in .h and are under the top-level
266     # include dir.
267     if file_ext == '.h' and 'include' == file_path.split(os.path.sep)[0]:
268       requires_owner_check = True
269
270   if not requires_owner_check:
271     return results
272
273   lgtm_from_owner = False
274   issue = input_api.change.issue
275   if issue and input_api.rietveld:
276     issue_properties = input_api.rietveld.get_issue_properties(
277         issue=int(issue), messages=True)
278     if re.match(REVERT_CL_SUBJECT_PREFIX, issue_properties['subject'], re.I):
279       # It is a revert CL, ignore the public api owners check.
280       return results
281
282     if re.search(r'^COMMIT=false$', issue_properties['description'], re.M):
283       # Ignore public api owners check for COMMIT=false CLs since they are not
284       # going to be committed.
285       return results
286
287     match = re.search(r'^TBR=(.*)$', issue_properties['description'], re.M)
288     if match:
289       tbr_entries = match.group(1).strip().split(',')
290       for owner in PUBLIC_API_OWNERS:
291         if owner in tbr_entries or owner.split('@')[0] in tbr_entries:
292           # If an owner is specified in the TBR= line then ignore the public
293           # api owners check.
294           return results
295
296     if issue_properties['owner_email'] in PUBLIC_API_OWNERS:
297       # An owner created the CL that is an automatic LGTM.
298       lgtm_from_owner = True
299
300     messages = issue_properties.get('messages')
301     if messages:
302       for message in messages:
303         if (message['sender'] in PUBLIC_API_OWNERS and
304             'lgtm' in message['text'].lower()):
305           # Found an lgtm in a message from an owner.
306           lgtm_from_owner = True
307           break
308
309   if not lgtm_from_owner:
310     results.append(
311         output_api.PresubmitError(
312             'Since the CL is editing public API, you must have an LGTM from '
313             'one of: %s' % str(PUBLIC_API_OWNERS)))
314   return results
315
316
317 def PostUploadHook(cl, change, output_api):
318   """git cl upload will call this hook after the issue is created/modified.
319
320   This hook does the following:
321   * Adds a link to preview docs changes if there are any docs changes in the CL.
322   * Adds 'NOTRY=true' if the CL contains only docs changes.
323   * Adds 'NOTREECHECKS=true' for non master branch changes since they do not
324     need to be gated on the master branch's tree.
325   * Adds 'NOTRY=true' for non master branch changes since trybots do not yet
326     work on them.
327   """
328
329   results = []
330   atleast_one_docs_change = False
331   all_docs_changes = True
332   for affected_file in change.AffectedFiles():
333     affected_file_path = affected_file.LocalPath()
334     file_path, _ = os.path.splitext(affected_file_path)
335     if 'site' == file_path.split(os.path.sep)[0]:
336       atleast_one_docs_change = True
337     else:
338       all_docs_changes = False
339     if atleast_one_docs_change and not all_docs_changes:
340       break
341
342   issue = cl.issue
343   rietveld_obj = cl.RpcServer()
344   if issue and rietveld_obj:
345     original_description = rietveld_obj.get_description(issue)
346     new_description = original_description
347
348     # If the change includes only doc changes then add NOTRY=true in the
349     # CL's description if it does not exist yet.
350     if all_docs_changes and not re.search(
351         r'^NOTRY=true$', new_description, re.M | re.I):
352       new_description += '\nNOTRY=true'
353       results.append(
354           output_api.PresubmitNotifyResult(
355               'This change has only doc changes. Automatically added '
356               '\'NOTRY=true\' to the CL\'s description'))
357
358     # If there is atleast one docs change then add preview link in the CL's
359     # description if it does not already exist there.
360     if atleast_one_docs_change and not re.search(
361         r'^DOCS_PREVIEW=.*', new_description, re.M | re.I):
362       # Automatically add a link to where the docs can be previewed.
363       new_description += '\nDOCS_PREVIEW= %s%s' % (DOCS_PREVIEW_URL, issue)
364       results.append(
365           output_api.PresubmitNotifyResult(
366               'Automatically added a link to preview the docs changes to the '
367               'CL\'s description'))
368
369     # If the target ref is not master then add NOTREECHECKS=true and NOTRY=true
370     # to the CL's description if it does not already exist there.
371     target_ref = rietveld_obj.get_issue_properties(issue, False).get(
372         'target_ref', '')
373     if target_ref != 'refs/heads/master':
374       if not re.search(
375           r'^NOTREECHECKS=true$', new_description, re.M | re.I):
376         new_description += "\nNOTREECHECKS=true"
377         results.append(
378             output_api.PresubmitNotifyResult(
379                 'Branch changes do not need to rely on the master branch\'s '
380                 'tree status. Automatically added \'NOTREECHECKS=true\' to the '
381                 'CL\'s description'))
382       if not re.search(
383           r'^NOTRY=true$', new_description, re.M | re.I):
384         new_description += "\nNOTRY=true"
385         results.append(
386             output_api.PresubmitNotifyResult(
387                 'Trybots do not yet work for non-master branches. '
388                 'Automatically added \'NOTRY=true\' to the CL\'s description'))
389
390
391     # If the description has changed update it.
392     if new_description != original_description:
393       rietveld_obj.update_description(issue, new_description)
394
395     return results
396
397
398 def CheckChangeOnCommit(input_api, output_api):
399   """Presubmit checks for the change on commit.
400
401   The following are the presubmit checks:
402   * Check change has one and only one EOL.
403   * Ensures that the Skia tree is open in
404     http://skia-tree-status.appspot.com/. Shows a warning if it is in 'Caution'
405     state and an error if it is in 'Closed' state.
406   """
407   results = []
408   results.extend(_CommonChecks(input_api, output_api))
409   results.extend(
410       _CheckTreeStatus(input_api, output_api, json_url=(
411           SKIA_TREE_STATUS_URL + '/banner-status?format=json')))
412   results.extend(_CheckLGTMsForPublicAPI(input_api, output_api))
413   results.extend(_CheckOwnerIsInAuthorsFile(input_api, output_api))
414   return results