Fix GN formatting presubmit on Windows.
[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 collections
13 import csv
14 import fnmatch
15 import os
16 import re
17 import subprocess
18 import sys
19 import traceback
20
21
22 REVERT_CL_SUBJECT_PREFIX = 'Revert '
23
24 SKIA_TREE_STATUS_URL = 'http://skia-tree-status.appspot.com'
25
26 # Please add the complete email address here (and not just 'xyz@' or 'xyz').
27 PUBLIC_API_OWNERS = (
28     'reed@chromium.org',
29     'reed@google.com',
30     'bsalomon@chromium.org',
31     'bsalomon@google.com',
32     'djsollen@chromium.org',
33     'djsollen@google.com',
34 )
35
36 AUTHORS_FILE_NAME = 'AUTHORS'
37
38 DOCS_PREVIEW_URL = 'https://skia.org/?cl='
39 GOLD_TRYBOT_URL = 'https://gold.skia.org/search?issue='
40
41 # Path to CQ bots feature is described in https://bug.skia.org/4364
42 PATH_PREFIX_TO_EXTRA_TRYBOTS = {
43     # pylint: disable=line-too-long
44     'src/opts/': 'master.client.skia:Test-Ubuntu-GCC-GCE-CPU-AVX2-x86_64-Release-SKNX_NO_SIMD-Trybot',
45
46     'include/private/SkAtomics.h': ('master.client.skia:'
47       'Test-Ubuntu-Clang-GCE-CPU-AVX2-x86_64-Release-TSAN-Trybot,'
48       'Test-Ubuntu-Clang-Golo-GPU-GT610-x86_64-Release-TSAN-Trybot'
49     ),
50
51     # Below are examples to show what is possible with this feature.
52     # 'src/svg/': 'master1:abc;master2:def',
53     # 'src/svg/parser/': 'master3:ghi,jkl;master4:mno',
54     # 'src/image/SkImage_Base.h': 'master5:pqr,stu;master1:abc1;master2:def',
55 }
56
57
58 def _CheckChangeHasEol(input_api, output_api, source_file_filter=None):
59   """Checks that files end with atleast one \n (LF)."""
60   eof_files = []
61   for f in input_api.AffectedSourceFiles(source_file_filter):
62     contents = input_api.ReadFile(f, 'rb')
63     # Check that the file ends in atleast one newline character.
64     if len(contents) > 1 and contents[-1:] != '\n':
65       eof_files.append(f.LocalPath())
66
67   if eof_files:
68     return [output_api.PresubmitPromptWarning(
69       'These files should end in a newline character:',
70       items=eof_files)]
71   return []
72
73
74 def _PythonChecks(input_api, output_api):
75   """Run checks on any modified Python files."""
76   pylint_disabled_warnings = (
77       'F0401',  # Unable to import.
78       'E0611',  # No name in module.
79       'W0232',  # Class has no __init__ method.
80       'E1002',  # Use of super on an old style class.
81       'W0403',  # Relative import used.
82       'R0201',  # Method could be a function.
83       'E1003',  # Using class name in super.
84       'W0613',  # Unused argument.
85       'W0105',  # String statement has no effect.
86   )
87   # Run Pylint on only the modified python files. Unfortunately it still runs
88   # Pylint on the whole file instead of just the modified lines.
89   affected_python_files = []
90   for affected_file in input_api.AffectedSourceFiles(None):
91     affected_file_path = affected_file.LocalPath()
92     if affected_file_path.endswith('.py'):
93       affected_python_files.append(affected_file_path)
94   return input_api.canned_checks.RunPylint(
95       input_api, output_api,
96       disabled_warnings=pylint_disabled_warnings,
97       white_list=affected_python_files)
98
99
100 def _IfDefChecks(input_api, output_api):
101   """Ensures if/ifdef are not before includes. See skbug/3362 for details."""
102   comment_block_start_pattern = re.compile('^\s*\/\*.*$')
103   comment_block_middle_pattern = re.compile('^\s+\*.*')
104   comment_block_end_pattern = re.compile('^\s+\*\/.*$')
105   single_line_comment_pattern = re.compile('^\s*//.*$')
106   def is_comment(line):
107     return (comment_block_start_pattern.match(line) or
108             comment_block_middle_pattern.match(line) or
109             comment_block_end_pattern.match(line) or
110             single_line_comment_pattern.match(line))
111
112   empty_line_pattern = re.compile('^\s*$')
113   def is_empty_line(line):
114     return empty_line_pattern.match(line)
115
116   failing_files = []
117   for affected_file in input_api.AffectedSourceFiles(None):
118     affected_file_path = affected_file.LocalPath()
119     if affected_file_path.endswith('.cpp') or affected_file_path.endswith('.h'):
120       f = open(affected_file_path)
121       for line in f.xreadlines():
122         if is_comment(line) or is_empty_line(line):
123           continue
124         # The below will be the first real line after comments and newlines.
125         if line.startswith('#if 0 '):
126           pass
127         elif line.startswith('#if ') or line.startswith('#ifdef '):
128           failing_files.append(affected_file_path)
129         break
130
131   results = []
132   if failing_files:
133     results.append(
134         output_api.PresubmitError(
135             'The following files have #if or #ifdef before includes:\n%s\n\n'
136             'See https://bug.skia.org/3362 for why this should be fixed.' %
137                 '\n'.join(failing_files)))
138   return results
139
140
141 def _CopyrightChecks(input_api, output_api, source_file_filter=None):
142   results = []
143   year_pattern = r'\d{4}'
144   year_range_pattern = r'%s(-%s)?' % (year_pattern, year_pattern)
145   years_pattern = r'%s(,%s)*,?' % (year_range_pattern, year_range_pattern)
146   copyright_pattern = (
147       r'Copyright (\([cC]\) )?%s \w+' % years_pattern)
148
149   for affected_file in input_api.AffectedSourceFiles(source_file_filter):
150     if 'third_party' in affected_file.LocalPath():
151       continue
152     contents = input_api.ReadFile(affected_file, 'rb')
153     if not re.search(copyright_pattern, contents):
154       results.append(output_api.PresubmitError(
155           '%s is missing a correct copyright header.' % affected_file))
156   return results
157
158
159 def _ToolFlags(input_api, output_api):
160   """Make sure `{dm,nanobench}_flags.py test` passes if modified."""
161   results = []
162   sources = lambda x: ('dm_flags.py'        in x.LocalPath() or
163                        'nanobench_flags.py' in x.LocalPath())
164   for f in input_api.AffectedSourceFiles(sources):
165     if 0 != subprocess.call(['python', f.LocalPath(), 'test']):
166       results.append(output_api.PresubmitError('`python %s test` failed' % f))
167   return results
168
169
170 def _RecipeSimulationTest(input_api, output_api):
171   """Run the recipe simulation test."""
172   results = []
173   if not any(f.LocalPath().startswith('infra')
174              for f in input_api.AffectedFiles()):
175     return results
176
177   recipes_py = os.path.join('infra', 'bots', 'recipes.py')
178   cmd = ['python', recipes_py, 'simulation_test']
179   try:
180     subprocess.check_output(cmd)
181   except subprocess.CalledProcessError as e:
182     results.append(output_api.PresubmitError(
183         '`%s` failed:\n%s' % (' '.join(cmd), e.output)))
184   return results
185
186
187 def _GenTasksTest(input_api, output_api):
188   """Run gen_tasks.go test."""
189   results = []
190   if not any(f.LocalPath().startswith('infra')
191              for f in input_api.AffectedFiles()):
192     return results
193
194   gen_tasks = os.path.join('infra', 'bots', 'gen_tasks.go')
195   cmd = ['go', 'run', gen_tasks, '--test']
196   try:
197     subprocess.check_output(cmd)
198   except subprocess.CalledProcessError as e:
199     results.append(output_api.PresubmitError(
200         '`%s` failed:\n%s' % (' '.join(cmd), e.output)))
201   return results
202
203
204 def _CheckGNFormatted(input_api, output_api):
205   """Make sure any .gn files we're changing have been formatted."""
206   results = []
207   for f in input_api.AffectedFiles():
208     if not f.LocalPath().endswith('.gn'):
209       continue
210
211     gn = 'gn.bat' if 'win32' in sys.platform else 'gn'
212     cmd = [gn, 'format', '--dry-run', f.LocalPath()]
213     try:
214       subprocess.check_output(cmd)
215     except subprocess.CalledProcessError:
216       fix = 'gn format ' + f.LocalPath()
217       results.append(output_api.PresubmitError(
218           '`%s` failed, try\n\t%s' % (' '.join(cmd), fix)))
219   return results
220
221
222 def _CommonChecks(input_api, output_api):
223   """Presubmit checks common to upload and commit."""
224   results = []
225   sources = lambda x: (x.LocalPath().endswith('.h') or
226                        x.LocalPath().endswith('.gypi') or
227                        x.LocalPath().endswith('.gyp') or
228                        x.LocalPath().endswith('.py') or
229                        x.LocalPath().endswith('.sh') or
230                        x.LocalPath().endswith('.m') or
231                        x.LocalPath().endswith('.mm') or
232                        x.LocalPath().endswith('.go') or
233                        x.LocalPath().endswith('.c') or
234                        x.LocalPath().endswith('.cc') or
235                        x.LocalPath().endswith('.cpp'))
236   results.extend(
237       _CheckChangeHasEol(
238           input_api, output_api, source_file_filter=sources))
239   results.extend(_PythonChecks(input_api, output_api))
240   results.extend(_IfDefChecks(input_api, output_api))
241   results.extend(_CopyrightChecks(input_api, output_api,
242                                   source_file_filter=sources))
243   results.extend(_ToolFlags(input_api, output_api))
244   return results
245
246
247 def CheckChangeOnUpload(input_api, output_api):
248   """Presubmit checks for the change on upload.
249
250   The following are the presubmit checks:
251   * Check change has one and only one EOL.
252   """
253   results = []
254   results.extend(_CommonChecks(input_api, output_api))
255   # Run on upload, not commit, since the presubmit bot apparently doesn't have
256   # coverage or Go installed.
257   results.extend(_RecipeSimulationTest(input_api, output_api))
258   results.extend(_GenTasksTest(input_api, output_api))
259
260   results.extend(_CheckGNFormatted(input_api, output_api))
261   return results
262
263
264 def _CheckTreeStatus(input_api, output_api, json_url):
265   """Check whether to allow commit.
266
267   Args:
268     input_api: input related apis.
269     output_api: output related apis.
270     json_url: url to download json style status.
271   """
272   tree_status_results = input_api.canned_checks.CheckTreeIsOpen(
273       input_api, output_api, json_url=json_url)
274   if not tree_status_results:
275     # Check for caution state only if tree is not closed.
276     connection = input_api.urllib2.urlopen(json_url)
277     status = input_api.json.loads(connection.read())
278     connection.close()
279     if ('caution' in status['message'].lower() and
280         os.isatty(sys.stdout.fileno())):
281       # Display a prompt only if we are in an interactive shell. Without this
282       # check the commit queue behaves incorrectly because it considers
283       # prompts to be failures.
284       short_text = 'Tree state is: ' + status['general_state']
285       long_text = status['message'] + '\n' + json_url
286       tree_status_results.append(
287           output_api.PresubmitPromptWarning(
288               message=short_text, long_text=long_text))
289   else:
290     # Tree status is closed. Put in message about contacting sheriff.
291     connection = input_api.urllib2.urlopen(
292         SKIA_TREE_STATUS_URL + '/current-sheriff')
293     sheriff_details = input_api.json.loads(connection.read())
294     if sheriff_details:
295       tree_status_results[0]._message += (
296           '\n\nPlease contact the current Skia sheriff (%s) if you are trying '
297           'to submit a build fix\nand do not know how to submit because the '
298           'tree is closed') % sheriff_details['username']
299   return tree_status_results
300
301
302 class CodeReview(object):
303   """Abstracts which codereview tool is used for the specified issue."""
304
305   def __init__(self, input_api):
306     self._issue = input_api.change.issue
307     self._gerrit = input_api.gerrit
308     self._rietveld_properties = None
309     if not self._gerrit:
310       self._rietveld_properties = input_api.rietveld.get_issue_properties(
311           issue=int(self._issue), messages=True)
312
313   def GetOwnerEmail(self):
314     if self._gerrit:
315       return self._gerrit.GetChangeOwner(self._issue)
316     else:
317       return self._rietveld_properties['owner_email']
318
319   def GetSubject(self):
320     if self._gerrit:
321       return self._gerrit.GetChangeInfo(self._issue)['subject']
322     else:
323       return self._rietveld_properties['subject']
324
325   def GetDescription(self):
326     if self._gerrit:
327       return self._gerrit.GetChangeDescription(self._issue)
328     else:
329       return self._rietveld_properties['description']
330
331   def IsDryRun(self):
332     if self._gerrit:
333       return self._gerrit.GetChangeInfo(
334           self._issue)['labels']['Commit-Queue'].get('value', 0) == 1
335     else:
336       return self._rietveld_properties['cq_dry_run']
337
338   def GetReviewers(self):
339     if self._gerrit:
340       code_review_label = (
341           self._gerrit.GetChangeInfo(self._issue)['labels']['Code-Review'])
342       return [r['email'] for r in code_review_label.get('all', [])]
343     else:
344       return self._rietveld_properties['reviewers']
345
346   def GetApprovers(self):
347     approvers = []
348     if self._gerrit:
349       code_review_label = (
350           self._gerrit.GetChangeInfo(self._issue)['labels']['Code-Review'])
351       for m in code_review_label.get('all', []):
352         if m.get("value") == 1:
353           approvers.append(m["email"])
354     else:
355       for m in self._rietveld_properties.get('messages', []):
356         if 'lgtm' in m['text'].lower():
357           approvers.append(m['sender'])
358     return approvers
359
360
361 def _CheckOwnerIsInAuthorsFile(input_api, output_api):
362   results = []
363   if input_api.change.issue:
364     cr = CodeReview(input_api)
365
366     owner_email = cr.GetOwnerEmail()
367     try:
368       authors_content = ''
369       for line in open(AUTHORS_FILE_NAME):
370         if not line.startswith('#'):
371           authors_content += line
372       email_fnmatches = re.findall('<(.*)>', authors_content)
373       for email_fnmatch in email_fnmatches:
374         if fnmatch.fnmatch(owner_email, email_fnmatch):
375           # Found a match, the user is in the AUTHORS file break out of the loop
376           break
377       else:
378         results.append(
379           output_api.PresubmitError(
380             'The email %s is not in Skia\'s AUTHORS file.\n'
381             'Issue owner, this CL must include an addition to the Skia AUTHORS '
382             'file.'
383             % owner_email))
384     except IOError:
385       # Do not fail if authors file cannot be found.
386       traceback.print_exc()
387       input_api.logging.error('AUTHORS file not found!')
388
389   return results
390
391
392 def _CheckLGTMsForPublicAPI(input_api, output_api):
393   """Check LGTMs for public API changes.
394
395   For public API files make sure there is an LGTM from the list of owners in
396   PUBLIC_API_OWNERS.
397   """
398   results = []
399   requires_owner_check = False
400   for affected_file in input_api.AffectedFiles():
401     affected_file_path = affected_file.LocalPath()
402     file_path, file_ext = os.path.splitext(affected_file_path)
403     # We only care about files that end in .h and are under the top-level
404     # include dir, but not include/private.
405     if (file_ext == '.h' and
406         'include' == file_path.split(os.path.sep)[0] and
407         'private' not in file_path):
408       requires_owner_check = True
409
410   if not requires_owner_check:
411     return results
412
413   lgtm_from_owner = False
414   if input_api.change.issue:
415     cr = CodeReview(input_api)
416
417     if re.match(REVERT_CL_SUBJECT_PREFIX, cr.GetSubject(), re.I):
418       # It is a revert CL, ignore the public api owners check.
419       return results
420
421     if cr.IsDryRun():
422       # Ignore public api owners check for dry run CLs since they are not
423       # going to be committed.
424       return results
425
426     if input_api.gerrit:
427       for reviewer in cr.GetReviewers():
428         if reviewer in PUBLIC_API_OWNERS:
429           # If an owner is specified as an reviewer in Gerrit then ignore the
430           # public api owners check.
431           return results
432     else:
433       match = re.search(r'^TBR=(.*)$', cr.GetDescription(), re.M)
434       if match:
435         tbr_section = match.group(1).strip().split(' ')[0]
436         tbr_entries = tbr_section.split(',')
437         for owner in PUBLIC_API_OWNERS:
438           if owner in tbr_entries or owner.split('@')[0] in tbr_entries:
439             # If an owner is specified in the TBR= line then ignore the public
440             # api owners check.
441             return results
442
443     if cr.GetOwnerEmail() in PUBLIC_API_OWNERS:
444       # An owner created the CL that is an automatic LGTM.
445       lgtm_from_owner = True
446
447     for approver in cr.GetApprovers():
448       if approver in PUBLIC_API_OWNERS:
449         # Found an lgtm in a message from an owner.
450         lgtm_from_owner = True
451         break
452
453   if not lgtm_from_owner:
454     results.append(
455         output_api.PresubmitError(
456             "If this CL adds to or changes Skia's public API, you need an LGTM "
457             "from any of %s.  If this CL only removes from or doesn't change "
458             "Skia's public API, please add a short note to the CL saying so. "
459             "Add one of the owners as a reviewer to your CL. For Rietveld CLs "
460             "you also need to add one of those owners on a TBR= line.  If you "
461             "don't know if this CL affects Skia's public API, treat it like it "
462             "does." % str(PUBLIC_API_OWNERS)))
463   return results
464
465
466 def PostUploadHook(cl, change, output_api):
467   """git cl upload will call this hook after the issue is created/modified.
468
469   This hook does the following:
470   * Adds a link to the CL's Gold trybot results.
471   * Adds a link to preview docs changes if there are any docs changes in the CL.
472   * Adds 'NOTRY=true' if the CL contains only docs changes.
473   * Adds 'NOTREECHECKS=true' for non master branch changes since they do not
474     need to be gated on the master branch's tree.
475   * Adds 'NOTRY=true' for non master branch changes since trybots do not yet
476     work on them.
477   * Adds 'NOPRESUBMIT=true' for non master branch changes since those don't
478     run the presubmit checks.
479   * Adds extra trybots for the paths defined in PATH_TO_EXTRA_TRYBOTS.
480   """
481
482   results = []
483   atleast_one_docs_change = False
484   all_docs_changes = True
485   for affected_file in change.AffectedFiles():
486     affected_file_path = affected_file.LocalPath()
487     file_path, _ = os.path.splitext(affected_file_path)
488     if 'site' == file_path.split(os.path.sep)[0]:
489       atleast_one_docs_change = True
490     else:
491       all_docs_changes = False
492     if atleast_one_docs_change and not all_docs_changes:
493       break
494
495   issue = cl.issue
496   if issue:
497     original_description = cl.GetDescription()
498     changeIdLine = None
499     if cl.IsGerrit():
500       # Remove Change-Id from description and add it back at the end.
501       regex = re.compile(r'^(Change-Id: (\w+))(\n*)\Z', re.M | re.I)
502       changeIdLine = re.search(regex, original_description).group(0)
503       original_description = re.sub(regex, '', original_description)
504       original_description = re.sub('\n+\Z', '\n', original_description)
505
506     new_description = original_description
507
508     # Add GOLD_TRYBOT_URL if it does not exist yet.
509     if not re.search(r'^GOLD_TRYBOT_URL=', new_description, re.M | re.I):
510       new_description += '\nGOLD_TRYBOT_URL= %s%s' % (GOLD_TRYBOT_URL, issue)
511       results.append(
512           output_api.PresubmitNotifyResult(
513               'Added link to Gold trybot runs to the CL\'s description.\n'
514               'Note: Results may take sometime to be populated after trybots '
515               'complete.'))
516
517     # If the change includes only doc changes then add NOTRY=true in the
518     # CL's description if it does not exist yet.
519     if all_docs_changes and not re.search(
520         r'^NOTRY=true$', new_description, re.M | re.I):
521       new_description += '\nNOTRY=true'
522       results.append(
523           output_api.PresubmitNotifyResult(
524               'This change has only doc changes. Automatically added '
525               '\'NOTRY=true\' to the CL\'s description'))
526
527     # If there is atleast one docs change then add preview link in the CL's
528     # description if it does not already exist there.
529     if atleast_one_docs_change and not re.search(
530         r'^DOCS_PREVIEW=.*', new_description, re.M | re.I):
531       # Automatically add a link to where the docs can be previewed.
532       new_description += '\nDOCS_PREVIEW= %s%s' % (DOCS_PREVIEW_URL, issue)
533       results.append(
534           output_api.PresubmitNotifyResult(
535               'Automatically added a link to preview the docs changes to the '
536               'CL\'s description'))
537
538     # If the target ref is not master then add NOTREECHECKS=true and NOTRY=true
539     # to the CL's description if it does not already exist there.
540     target_ref = cl.GetRemoteBranch()[1]
541     if target_ref != 'refs/remotes/origin/master':
542       if not re.search(
543           r'^NOTREECHECKS=true$', new_description, re.M | re.I):
544         new_description += "\nNOTREECHECKS=true"
545         results.append(
546             output_api.PresubmitNotifyResult(
547                 'Branch changes do not need to rely on the master branch\'s '
548                 'tree status. Automatically added \'NOTREECHECKS=true\' to the '
549                 'CL\'s description'))
550       if not re.search(
551           r'^NOTRY=true$', new_description, re.M | re.I):
552         new_description += "\nNOTRY=true"
553         results.append(
554             output_api.PresubmitNotifyResult(
555                 'Trybots do not yet work for non-master branches. '
556                 'Automatically added \'NOTRY=true\' to the CL\'s description'))
557       if not re.search(
558           r'^NOPRESUBMIT=true$', new_description, re.M | re.I):
559         new_description += "\nNOPRESUBMIT=true"
560         results.append(
561             output_api.PresubmitNotifyResult(
562                 'Branch changes do not run the presubmit checks.'))
563
564     # Automatically set CQ_INCLUDE_TRYBOTS if any of the changed files here
565     # begin with the paths of interest.
566     cq_master_to_trybots = collections.defaultdict(set)
567     for affected_file in change.AffectedFiles():
568       affected_file_path = affected_file.LocalPath()
569       for path_prefix, extra_bots in PATH_PREFIX_TO_EXTRA_TRYBOTS.iteritems():
570         if affected_file_path.startswith(path_prefix):
571           results.append(
572               output_api.PresubmitNotifyResult(
573                   'Your CL modifies the path %s.\nAutomatically adding %s to '
574                   'the CL description.' % (affected_file_path, extra_bots)))
575           _MergeCQExtraTrybotsMaps(
576               cq_master_to_trybots, _GetCQExtraTrybotsMap(extra_bots))
577     if cq_master_to_trybots:
578       new_description = _AddCQExtraTrybotsToDesc(
579           cq_master_to_trybots, new_description)
580
581     # If the description has changed update it.
582     if new_description != original_description:
583       if changeIdLine:
584         # The Change-Id line must have two newlines before it.
585         new_description += '\n\n' + changeIdLine
586       cl.UpdateDescription(new_description)
587
588     return results
589
590
591 def _AddCQExtraTrybotsToDesc(cq_master_to_trybots, description):
592   """Adds the specified master and trybots to the CQ_INCLUDE_TRYBOTS keyword.
593
594   If the keyword already exists in the description then it appends to it only
595   if the specified values do not already exist.
596   If the keyword does not exist then it creates a new section in the
597   description.
598   """
599   match = re.search(r'^CQ_INCLUDE_TRYBOTS=(.*)$', description, re.M | re.I)
600   if match:
601     original_trybots_map = _GetCQExtraTrybotsMap(match.group(1))
602     _MergeCQExtraTrybotsMaps(cq_master_to_trybots, original_trybots_map)
603     new_description = description.replace(
604         match.group(0), _GetCQExtraTrybotsStr(cq_master_to_trybots))
605   else:
606     new_description = description + "\n%s" % (
607         _GetCQExtraTrybotsStr(cq_master_to_trybots))
608   return new_description
609
610
611 def _MergeCQExtraTrybotsMaps(dest_map, map_to_be_consumed):
612   """Merges two maps of masters to trybots into one."""
613   for master, trybots in map_to_be_consumed.iteritems():
614     dest_map[master].update(trybots)
615   return dest_map
616
617
618 def _GetCQExtraTrybotsMap(cq_extra_trybots_str):
619   """Parses CQ_INCLUDE_TRYBOTS str and returns a map of masters to trybots."""
620   cq_master_to_trybots = collections.defaultdict(set)
621   for section in cq_extra_trybots_str.split(';'):
622     if section:
623       master, bots = section.split(':')
624       cq_master_to_trybots[master].update(bots.split(','))
625   return cq_master_to_trybots
626
627
628 def _GetCQExtraTrybotsStr(cq_master_to_trybots):
629   """Constructs the CQ_INCLUDE_TRYBOTS str from a map of masters to trybots."""
630   sections = []
631   for master, trybots in cq_master_to_trybots.iteritems():
632     sections.append('%s:%s' % (master, ','.join(trybots)))
633   return 'CQ_INCLUDE_TRYBOTS=%s' % ';'.join(sections)
634
635
636 def CheckChangeOnCommit(input_api, output_api):
637   """Presubmit checks for the change on commit.
638
639   The following are the presubmit checks:
640   * Check change has one and only one EOL.
641   * Ensures that the Skia tree is open in
642     http://skia-tree-status.appspot.com/. Shows a warning if it is in 'Caution'
643     state and an error if it is in 'Closed' state.
644   """
645   results = []
646   results.extend(_CommonChecks(input_api, output_api))
647   results.extend(
648       _CheckTreeStatus(input_api, output_api, json_url=(
649           SKIA_TREE_STATUS_URL + '/banner-status?format=json')))
650   results.extend(_CheckLGTMsForPublicAPI(input_api, output_api))
651   results.extend(_CheckOwnerIsInAuthorsFile(input_api, output_api))
652   return results