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