| OLD | NEW |
| 1 # Copyright (c) 2013 The Chromium Authors. All rights reserved. | 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 | 2 # Use of this source code is governed by a BSD-style license that can be |
| 3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
| 4 | 4 |
| 5 | 5 |
| 6 """Top-level presubmit script for Skia. | 6 """Top-level presubmit script for Skia. |
| 7 | 7 |
| 8 See http://dev.chromium.org/developers/how-tos/depottools/presubmit-scripts | 8 See http://dev.chromium.org/developers/how-tos/depottools/presubmit-scripts |
| 9 for more details about the presubmit API built into gcl. | 9 for more details about the presubmit API built into gcl. |
| 10 """ | 10 """ |
| 11 | 11 |
| 12 import collections |
| 12 import csv | 13 import csv |
| 13 import fnmatch | 14 import fnmatch |
| 14 import os | 15 import os |
| 15 import re | 16 import re |
| 16 import subprocess | 17 import subprocess |
| 17 import sys | 18 import sys |
| 18 import traceback | 19 import traceback |
| 19 | 20 |
| 20 | 21 |
| 21 REVERT_CL_SUBJECT_PREFIX = 'Revert ' | 22 REVERT_CL_SUBJECT_PREFIX = 'Revert ' |
| 22 | 23 |
| 23 SKIA_TREE_STATUS_URL = 'http://skia-tree-status.appspot.com' | 24 SKIA_TREE_STATUS_URL = 'http://skia-tree-status.appspot.com' |
| 24 | 25 |
| 25 CQ_KEYWORDS_THAT_NEED_APPENDING = ('CQ_INCLUDE_TRYBOTS', 'CQ_EXTRA_TRYBOTS', | |
| 26 'CQ_EXCLUDE_TRYBOTS', 'CQ_TRYBOTS') | |
| 27 | |
| 28 # Please add the complete email address here (and not just 'xyz@' or 'xyz'). | 26 # Please add the complete email address here (and not just 'xyz@' or 'xyz'). |
| 29 PUBLIC_API_OWNERS = ( | 27 PUBLIC_API_OWNERS = ( |
| 30 'reed@chromium.org', | 28 'reed@chromium.org', |
| 31 'reed@google.com', | 29 'reed@google.com', |
| 32 'bsalomon@chromium.org', | 30 'bsalomon@chromium.org', |
| 33 'bsalomon@google.com', | 31 'bsalomon@google.com', |
| 34 'djsollen@chromium.org', | 32 'djsollen@chromium.org', |
| 35 'djsollen@google.com', | 33 'djsollen@google.com', |
| 36 ) | 34 ) |
| 37 | 35 |
| 38 AUTHORS_FILE_NAME = 'AUTHORS' | 36 AUTHORS_FILE_NAME = 'AUTHORS' |
| 39 | 37 |
| 40 DOCS_PREVIEW_URL = 'https://skia.org/?cl=' | 38 DOCS_PREVIEW_URL = 'https://skia.org/?cl=' |
| 41 | 39 |
| 40 # Path to CQ bots feature is described in skbug.com/4364 |
| 41 PATH_PREFIX_TO_EXTRA_TRYBOTS = { |
| 42 # pylint: disable=line-too-long |
| 43 'cmake/': 'client.skia.compile:Build-Mac10.9-Clang-x86_64-Release-CMake-Tryb
ot,Build-Ubuntu-GCC-x86_64-Release-CMake-Trybot', |
| 44 # pylint: disable=line-too-long |
| 45 'src/opts/': 'client.skia:Test-Ubuntu-GCC-GCE-CPU-AVX2-x86_64-Release-SKNX_N
O_SIMD-Trybot', |
| 46 |
| 47 # Below are examples to show what is possible with this feature. |
| 48 # 'src/svg/': 'master1:abc;master2:def', |
| 49 # 'src/svg/parser/': 'master3:ghi,jkl;master4:mno', |
| 50 # 'src/image/SkImage_Base.h': 'master5:pqr,stu;master1:abc1;master2:def', |
| 51 } |
| 52 |
| 42 | 53 |
| 43 def _CheckChangeHasEol(input_api, output_api, source_file_filter=None): | 54 def _CheckChangeHasEol(input_api, output_api, source_file_filter=None): |
| 44 """Checks that files end with atleast one \n (LF).""" | 55 """Checks that files end with atleast one \n (LF).""" |
| 45 eof_files = [] | 56 eof_files = [] |
| 46 for f in input_api.AffectedSourceFiles(source_file_filter): | 57 for f in input_api.AffectedSourceFiles(source_file_filter): |
| 47 contents = input_api.ReadFile(f, 'rb') | 58 contents = input_api.ReadFile(f, 'rb') |
| 48 # Check that the file ends in atleast one newline character. | 59 # Check that the file ends in atleast one newline character. |
| 49 if len(contents) > 1 and contents[-1:] != '\n': | 60 if len(contents) > 1 and contents[-1:] != '\n': |
| 50 eof_files.append(f.LocalPath()) | 61 eof_files.append(f.LocalPath()) |
| 51 | 62 |
| (...skipping 280 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 332 | 343 |
| 333 This hook does the following: | 344 This hook does the following: |
| 334 * Adds a link to preview docs changes if there are any docs changes in the CL. | 345 * Adds a link to preview docs changes if there are any docs changes in the CL. |
| 335 * Adds 'NOTRY=true' if the CL contains only docs changes. | 346 * Adds 'NOTRY=true' if the CL contains only docs changes. |
| 336 * Adds 'NOTREECHECKS=true' for non master branch changes since they do not | 347 * Adds 'NOTREECHECKS=true' for non master branch changes since they do not |
| 337 need to be gated on the master branch's tree. | 348 need to be gated on the master branch's tree. |
| 338 * Adds 'NOTRY=true' for non master branch changes since trybots do not yet | 349 * Adds 'NOTRY=true' for non master branch changes since trybots do not yet |
| 339 work on them. | 350 work on them. |
| 340 * Adds 'NOPRESUBMIT=true' for non master branch changes since those don't | 351 * Adds 'NOPRESUBMIT=true' for non master branch changes since those don't |
| 341 run the presubmit checks. | 352 run the presubmit checks. |
| 353 * Adds extra trybots for the paths defined in PATH_TO_EXTRA_TRYBOTS. |
| 342 """ | 354 """ |
| 343 | 355 |
| 344 results = [] | 356 results = [] |
| 345 atleast_one_docs_change = False | 357 atleast_one_docs_change = False |
| 346 all_docs_changes = True | 358 all_docs_changes = True |
| 347 for affected_file in change.AffectedFiles(): | 359 for affected_file in change.AffectedFiles(): |
| 348 affected_file_path = affected_file.LocalPath() | 360 affected_file_path = affected_file.LocalPath() |
| 349 file_path, _ = os.path.splitext(affected_file_path) | 361 file_path, _ = os.path.splitext(affected_file_path) |
| 350 if 'site' == file_path.split(os.path.sep)[0]: | 362 if 'site' == file_path.split(os.path.sep)[0]: |
| 351 atleast_one_docs_change = True | 363 atleast_one_docs_change = True |
| (...skipping 49 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 401 output_api.PresubmitNotifyResult( | 413 output_api.PresubmitNotifyResult( |
| 402 'Trybots do not yet work for non-master branches. ' | 414 'Trybots do not yet work for non-master branches. ' |
| 403 'Automatically added \'NOTRY=true\' to the CL\'s description')) | 415 'Automatically added \'NOTRY=true\' to the CL\'s description')) |
| 404 if not re.search( | 416 if not re.search( |
| 405 r'^NOPRESUBMIT=true$', new_description, re.M | re.I): | 417 r'^NOPRESUBMIT=true$', new_description, re.M | re.I): |
| 406 new_description += "\nNOPRESUBMIT=true" | 418 new_description += "\nNOPRESUBMIT=true" |
| 407 results.append( | 419 results.append( |
| 408 output_api.PresubmitNotifyResult( | 420 output_api.PresubmitNotifyResult( |
| 409 'Branch changes do not run the presubmit checks.')) | 421 'Branch changes do not run the presubmit checks.')) |
| 410 | 422 |
| 411 # Read and process the HASHTAGS file. | 423 # Automatically set CQ_EXTRA_TRYBOTS if any of the changed files here begin |
| 412 hashtags_fullpath = os.path.join(change._local_root, 'HASHTAGS') | 424 # with the paths of interest. |
| 413 with open(hashtags_fullpath, 'rb') as hashtags_csv: | 425 cq_master_to_trybots = collections.defaultdict(set) |
| 414 hashtags_reader = csv.reader(hashtags_csv, delimiter=',') | 426 for affected_file in change.AffectedFiles(): |
| 415 for row in hashtags_reader: | 427 affected_file_path = affected_file.LocalPath() |
| 416 if not row or row[0].startswith('#'): | 428 for path_prefix, extra_bots in PATH_PREFIX_TO_EXTRA_TRYBOTS.iteritems(): |
| 417 # Ignore empty lines and comments | 429 if affected_file_path.startswith(path_prefix): |
| 418 continue | 430 results.append( |
| 419 hashtag = row[0] | 431 output_api.PresubmitNotifyResult( |
| 420 # Search for the hashtag in the description. | 432 'Your CL modifies the path %s.\nAutomatically adding %s to ' |
| 421 if re.search('#%s' % hashtag, new_description, re.M | re.I): | 433 'the CL description.' % (affected_file_path, extra_bots))) |
| 422 for mapped_text in row[1:]: | 434 _MergeCQExtraTrybotsMaps( |
| 423 # Special case handling for CQ_KEYWORDS_THAT_NEED_APPENDING. | 435 cq_master_to_trybots, _GetCQExtraTrybotsMap(extra_bots)) |
| 424 appended_description = _HandleAppendingCQKeywords( | 436 if cq_master_to_trybots: |
| 425 hashtag, mapped_text, new_description, results, output_api) | 437 new_description = _AddCQExtraTrybotsToDesc( |
| 426 if appended_description: | 438 cq_master_to_trybots, new_description) |
| 427 new_description = appended_description | |
| 428 continue | |
| 429 | |
| 430 # Add the mapped text if it does not already exist in the | |
| 431 # CL's description. | |
| 432 if not re.search( | |
| 433 r'^%s$' % mapped_text, new_description, re.M | re.I): | |
| 434 new_description += '\n%s' % mapped_text | |
| 435 results.append( | |
| 436 output_api.PresubmitNotifyResult( | |
| 437 'Found \'#%s\', automatically added \'%s\' to the CL\'s ' | |
| 438 'description' % (hashtag, mapped_text))) | |
| 439 | 439 |
| 440 # If the description has changed update it. | 440 # If the description has changed update it. |
| 441 if new_description != original_description: | 441 if new_description != original_description: |
| 442 rietveld_obj.update_description(issue, new_description) | 442 rietveld_obj.update_description(issue, new_description) |
| 443 | 443 |
| 444 return results | 444 return results |
| 445 | 445 |
| 446 | 446 |
| 447 def _HandleAppendingCQKeywords(hashtag, keyword_and_value, description, | 447 def _AddCQExtraTrybotsToDesc(cq_master_to_trybots, description): |
| 448 results, output_api): | 448 """Adds the specified master and trybots to the CQ_EXTRA_TRYBOTS keyword. |
| 449 """Handles the CQ keywords that need appending if specified in hashtags.""" | 449 |
| 450 keyword = keyword_and_value.split('=')[0] | 450 If the keyword already exists in the description then it appends to it only |
| 451 if keyword in CQ_KEYWORDS_THAT_NEED_APPENDING: | 451 if the specified values do not already exist. |
| 452 # If the keyword is already in the description then append to it. | 452 If the keyword does not exist then it creates a new section in the |
| 453 match = re.search( | 453 description. |
| 454 r'^%s=(.*)$' % keyword, description, re.M | re.I) | 454 """ |
| 455 if match: | 455 match = re.search(r'^CQ_EXTRA_TRYBOTS=(.*)$', description, re.M | re.I) |
| 456 old_values = match.group(1).split(';') | 456 if match: |
| 457 new_value = keyword_and_value.split('=')[1] | 457 original_trybots_map = _GetCQExtraTrybotsMap(match.group(1)) |
| 458 if new_value in old_values: | 458 _MergeCQExtraTrybotsMaps(cq_master_to_trybots, original_trybots_map) |
| 459 # Do not need to do anything here. | 459 new_description = description.replace( |
| 460 return description | 460 match.group(0), _GetCQExtraTrybotsStr(cq_master_to_trybots)) |
| 461 # Update the description with the new values. | 461 else: |
| 462 new_description = description.replace( | 462 new_description = description + "\n%s" % ( |
| 463 match.group(0), "%s;%s" % (match.group(0), new_value)) | 463 _GetCQExtraTrybotsStr(cq_master_to_trybots)) |
| 464 results.append( | 464 return new_description |
| 465 output_api.PresubmitNotifyResult( | 465 |
| 466 'Found \'#%s\', automatically appended \'%s\' to %s in ' | 466 |
| 467 'the CL\'s description' % (hashtag, new_value, keyword))) | 467 def _MergeCQExtraTrybotsMaps(dest_map, map_to_be_consumed): |
| 468 return new_description | 468 """Merges two maps of masters to trybots into one.""" |
| 469 return None | 469 for master, trybots in map_to_be_consumed.iteritems(): |
| 470 dest_map[master].update(trybots) |
| 471 return dest_map |
| 472 |
| 473 |
| 474 def _GetCQExtraTrybotsMap(cq_extra_trybots_str): |
| 475 """Parses the CQ_EXTRA_TRYBOTS str and returns a map of masters to trybots.""" |
| 476 cq_master_to_trybots = collections.defaultdict(set) |
| 477 for section in cq_extra_trybots_str.split(';'): |
| 478 if section: |
| 479 master, bots = section.split(':') |
| 480 cq_master_to_trybots[master].update(bots.split(',')) |
| 481 return cq_master_to_trybots |
| 482 |
| 483 |
| 484 def _GetCQExtraTrybotsStr(cq_master_to_trybots): |
| 485 """Constructs the CQ_EXTRA_TRYBOTS str from a map of masters to trybots.""" |
| 486 sections = [] |
| 487 for master, trybots in cq_master_to_trybots.iteritems(): |
| 488 sections.append('%s:%s' % (master, ','.join(trybots))) |
| 489 return 'CQ_EXTRA_TRYBOTS=%s' % ';'.join(sections) |
| 470 | 490 |
| 471 | 491 |
| 472 def CheckChangeOnCommit(input_api, output_api): | 492 def CheckChangeOnCommit(input_api, output_api): |
| 473 """Presubmit checks for the change on commit. | 493 """Presubmit checks for the change on commit. |
| 474 | 494 |
| 475 The following are the presubmit checks: | 495 The following are the presubmit checks: |
| 476 * Check change has one and only one EOL. | 496 * Check change has one and only one EOL. |
| 477 * Ensures that the Skia tree is open in | 497 * Ensures that the Skia tree is open in |
| 478 http://skia-tree-status.appspot.com/. Shows a warning if it is in 'Caution' | 498 http://skia-tree-status.appspot.com/. Shows a warning if it is in 'Caution' |
| 479 state and an error if it is in 'Closed' state. | 499 state and an error if it is in 'Closed' state. |
| 480 """ | 500 """ |
| 481 results = [] | 501 results = [] |
| 482 results.extend(_CommonChecks(input_api, output_api)) | 502 results.extend(_CommonChecks(input_api, output_api)) |
| 483 results.extend( | 503 results.extend( |
| 484 _CheckTreeStatus(input_api, output_api, json_url=( | 504 _CheckTreeStatus(input_api, output_api, json_url=( |
| 485 SKIA_TREE_STATUS_URL + '/banner-status?format=json'))) | 505 SKIA_TREE_STATUS_URL + '/banner-status?format=json'))) |
| 486 results.extend(_CheckLGTMsForPublicAPI(input_api, output_api)) | 506 results.extend(_CheckLGTMsForPublicAPI(input_api, output_api)) |
| 487 results.extend(_CheckOwnerIsInAuthorsFile(input_api, output_api)) | 507 results.extend(_CheckOwnerIsInAuthorsFile(input_api, output_api)) |
| 488 return results | 508 return results |
| OLD | NEW |