1#!/usr/bin/env python3 2# Copyright (c) 2013 The Chromium Authors. All rights reserved. 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6 7"""Top-level presubmit script for Skia. 8 9See http://dev.chromium.org/developers/how-tos/depottools/presubmit-scripts 10for more details about the presubmit API built into gcl. 11""" 12 13import fnmatch 14import os 15import re 16import subprocess 17import sys 18import traceback 19 20 21RELEASE_NOTES_DIR = 'relnotes' 22RELEASE_NOTES_FILE_NAME = 'RELEASE_NOTES.md' 23RELEASE_NOTES_README = '//relnotes/README.md' 24 25GOLD_TRYBOT_URL = 'https://gold.skia.org/search?issue=' 26 27SERVICE_ACCOUNT_SUFFIX = [ 28 '@%s.iam.gserviceaccount.com' % project for project in [ 29 'skia-buildbots.google.com', 'skia-swarming-bots', 'skia-public', 30 'skia-corp.google.com', 'chops-service-accounts']] 31 32USE_PYTHON3 = True 33 34 35def _CheckChangeHasEol(input_api, output_api, source_file_filter=None): 36 """Checks that files end with at least one \n (LF).""" 37 eof_files = [] 38 for f in input_api.AffectedSourceFiles(source_file_filter): 39 contents = input_api.ReadFile(f, 'rb') 40 # Check that the file ends in at least one newline character. 41 if len(contents) > 1 and contents[-1:] != '\n': 42 eof_files.append(f.LocalPath()) 43 44 if eof_files: 45 return [output_api.PresubmitPromptWarning( 46 'These files should end in a newline character:', 47 items=eof_files)] 48 return [] 49 50 51def _JsonChecks(input_api, output_api): 52 """Run checks on any modified json files.""" 53 failing_files = [] 54 for affected_file in input_api.AffectedFiles(None): 55 affected_file_path = affected_file.LocalPath() 56 is_json = affected_file_path.endswith('.json') 57 is_metadata = (affected_file_path.startswith('site/') and 58 affected_file_path.endswith('/METADATA')) 59 if is_json or is_metadata: 60 try: 61 input_api.json.load(open(affected_file_path, 'r')) 62 except ValueError: 63 failing_files.append(affected_file_path) 64 65 results = [] 66 if failing_files: 67 results.append( 68 output_api.PresubmitError( 69 'The following files contain invalid json:\n%s\n\n' % 70 '\n'.join(failing_files))) 71 return results 72 73 74def _IfDefChecks(input_api, output_api): 75 """Ensures if/ifdef are not before includes. See skbug/3362 for details.""" 76 comment_block_start_pattern = re.compile('^\s*\/\*.*$') 77 comment_block_middle_pattern = re.compile('^\s+\*.*') 78 comment_block_end_pattern = re.compile('^\s+\*\/.*$') 79 single_line_comment_pattern = re.compile('^\s*//.*$') 80 def is_comment(line): 81 return (comment_block_start_pattern.match(line) or 82 comment_block_middle_pattern.match(line) or 83 comment_block_end_pattern.match(line) or 84 single_line_comment_pattern.match(line)) 85 86 empty_line_pattern = re.compile('^\s*$') 87 def is_empty_line(line): 88 return empty_line_pattern.match(line) 89 90 failing_files = [] 91 for affected_file in input_api.AffectedSourceFiles(None): 92 affected_file_path = affected_file.LocalPath() 93 if affected_file_path.endswith('.cpp') or affected_file_path.endswith('.h'): 94 f = open(affected_file_path) 95 for line in f: 96 if is_comment(line) or is_empty_line(line): 97 continue 98 # The below will be the first real line after comments and newlines. 99 if line.startswith('#if 0 '): 100 pass 101 elif line.startswith('#if ') or line.startswith('#ifdef '): 102 failing_files.append(affected_file_path) 103 break 104 105 results = [] 106 if failing_files: 107 results.append( 108 output_api.PresubmitError( 109 'The following files have #if or #ifdef before includes:\n%s\n\n' 110 'See https://bug.skia.org/3362 for why this should be fixed.' % 111 '\n'.join(failing_files))) 112 return results 113 114 115def _CopyrightChecks(input_api, output_api, source_file_filter=None): 116 results = [] 117 year_pattern = r'\d{4}' 118 year_range_pattern = r'%s(-%s)?' % (year_pattern, year_pattern) 119 years_pattern = r'%s(,%s)*,?' % (year_range_pattern, year_range_pattern) 120 copyright_pattern = ( 121 r'Copyright (\([cC]\) )?%s \w+' % years_pattern) 122 123 for affected_file in input_api.AffectedSourceFiles(source_file_filter): 124 if ('third_party/' in affected_file.LocalPath() or 125 'tests/sksl/' in affected_file.LocalPath() or 126 'bazel/rbe/' in affected_file.LocalPath() or 127 'bazel/external/' in affected_file.LocalPath() or 128 'bazel/exporter/interfaces/mocks/' in affected_file.LocalPath()): 129 continue 130 contents = input_api.ReadFile(affected_file, 'rb') 131 if not re.search(copyright_pattern, contents): 132 results.append(output_api.PresubmitError( 133 '%s is missing a correct copyright header.' % affected_file)) 134 return results 135 136 137def _InfraTests(input_api, output_api): 138 """Run the infra tests.""" 139 results = [] 140 if not any(f.LocalPath().startswith('infra') 141 for f in input_api.AffectedFiles()): 142 return results 143 144 cmd = ['python3', os.path.join('infra', 'bots', 'infra_tests.py')] 145 try: 146 subprocess.check_output(cmd) 147 except subprocess.CalledProcessError as e: 148 results.append(output_api.PresubmitError( 149 '`%s` failed:\n%s' % (' '.join(cmd), e.output))) 150 return results 151 152 153def _CheckGNFormatted(input_api, output_api): 154 """Make sure any .gn files we're changing have been formatted.""" 155 files = [] 156 for f in input_api.AffectedFiles(include_deletes=False): 157 if (f.LocalPath().endswith('.gn') or 158 f.LocalPath().endswith('.gni')): 159 files.append(f) 160 if not files: 161 return [] 162 163 cmd = ['python3', os.path.join('bin', 'fetch-gn')] 164 try: 165 subprocess.check_output(cmd) 166 except subprocess.CalledProcessError as e: 167 return [output_api.PresubmitError( 168 '`%s` failed:\n%s' % (' '.join(cmd), e.output))] 169 170 results = [] 171 for f in files: 172 gn = 'gn.exe' if 'win32' in sys.platform else 'gn' 173 gn = os.path.join(input_api.PresubmitLocalPath(), 'bin', gn) 174 cmd = [gn, 'format', '--dry-run', f.LocalPath()] 175 try: 176 subprocess.check_output(cmd) 177 except subprocess.CalledProcessError: 178 fix = 'bin/gn format ' + f.LocalPath() 179 results.append(output_api.PresubmitError( 180 '`%s` failed, try\n\t%s' % (' '.join(cmd), fix))) 181 return results 182 183 184def _CheckGitConflictMarkers(input_api, output_api): 185 pattern = input_api.re.compile('^(?:<<<<<<<|>>>>>>>) |^=======$') 186 results = [] 187 for f in input_api.AffectedFiles(): 188 for line_num, line in f.ChangedContents(): 189 if f.LocalPath().endswith('.md'): 190 # First-level headers in markdown look a lot like version control 191 # conflict markers. http://daringfireball.net/projects/markdown/basics 192 continue 193 if pattern.match(line): 194 results.append( 195 output_api.PresubmitError( 196 'Git conflict markers found in %s:%d %s' % ( 197 f.LocalPath(), line_num, line))) 198 return results 199 200 201def _CheckIncludesFormatted(input_api, output_api): 202 """Make sure #includes in files we're changing have been formatted.""" 203 files = [str(f) for f in input_api.AffectedFiles() if f.Action() != 'D'] 204 cmd = ['python3', 205 'tools/rewrite_includes.py', 206 '--dry-run'] + files 207 if 0 != subprocess.call(cmd): 208 return [output_api.PresubmitError('`%s` failed' % ' '.join(cmd))] 209 return [] 210 211 212class _WarningsAsErrors(): 213 def __init__(self, output_api): 214 self.output_api = output_api 215 self.old_warning = None 216 def __enter__(self): 217 self.old_warning = self.output_api.PresubmitPromptWarning 218 self.output_api.PresubmitPromptWarning = self.output_api.PresubmitError 219 return self.output_api 220 def __exit__(self, ex_type, ex_value, ex_traceback): 221 self.output_api.PresubmitPromptWarning = self.old_warning 222 223 224def _RegenerateAllExamplesCPP(input_api, output_api): 225 """Regenerates all_examples.cpp if an example was added or deleted.""" 226 if not any(f.LocalPath().startswith('docs/examples/') 227 for f in input_api.AffectedFiles()): 228 return [] 229 command_str = 'tools/fiddle/make_all_examples_cpp.py' 230 cmd = ['python3', command_str] 231 if 0 != subprocess.call(cmd): 232 return [output_api.PresubmitError('`%s` failed' % ' '.join(cmd))] 233 234 results = [] 235 git_diff_output = input_api.subprocess.check_output( 236 ['git', 'diff', '--no-ext-diff']) 237 if git_diff_output: 238 results += [output_api.PresubmitError( 239 'Diffs found after running "%s":\n\n%s\n' 240 'Please commit or discard the above changes.' % ( 241 command_str, 242 git_diff_output, 243 ) 244 )] 245 return results 246 247 248def _CheckExamplesForPrivateAPIs(input_api, output_api): 249 """We only want our checked-in examples (aka fiddles) to show public API.""" 250 banned_includes = [ 251 input_api.re.compile(r'#\s*include\s+("src/.*)'), 252 input_api.re.compile(r'#\s*include\s+("include/private/.*)'), 253 ] 254 file_filter = lambda x: (x.LocalPath().startswith('docs/examples/')) 255 errors = [] 256 for affected_file in input_api.AffectedSourceFiles(file_filter): 257 affected_filepath = affected_file.LocalPath() 258 for (line_num, line) in affected_file.ChangedContents(): 259 for re in banned_includes: 260 match = re.search(line) 261 if match: 262 errors.append('%s:%s: Fiddles should not use private/internal API like %s.' % ( 263 affected_filepath, line_num, match.group(1))) 264 265 if errors: 266 return [output_api.PresubmitError('\n'.join(errors))] 267 return [] 268 269 270def _CheckGeneratedBazelBUILDFiles(input_api, output_api): 271 if 'win32' in sys.platform: 272 # TODO(crbug.com/skia/12541): Remove when Bazel builds work on Windows. 273 # Note: `make` is not installed on Windows by default. 274 return [] 275 if 'darwin' in sys.platform: 276 # This takes too long on Mac with default settings. Probably due to sandboxing. 277 return [] 278 for affected_file in input_api.AffectedFiles(include_deletes=True): 279 affected_file_path = affected_file.LocalPath() 280 if (affected_file_path.endswith('.go') or 281 affected_file_path.endswith('BUILD.bazel')): 282 return _RunCommandAndCheckGitDiff(output_api, 283 ['make', '-C', 'bazel', 'generate_go']) 284 return [] # No modified Go source files. 285 286 287def _CheckBazelBUILDFiles(input_api, output_api): 288 """Makes sure our BUILD.bazel files are compatible with G3.""" 289 results = [] 290 for affected_file in input_api.AffectedFiles(include_deletes=False): 291 affected_file_path = affected_file.LocalPath() 292 is_bazel = affected_file_path.endswith('BUILD.bazel') 293 # This list lines up with the one in autoroller_lib.py (see G3). 294 excluded_paths = ["infra/", "bazel/rbe/", "bazel/external/", "bazel/common_config_settings/", 295 "modules/canvaskit/go/", "experimental/", "bazel/platform", "third_party/", 296 "tests/", "resources/", "bazel/deps_parser/", "bazel/exporter_tool/", 297 "tools/gpu/gl/interface/", "bazel/utils/", "include/config/", 298 "bench/", "example/external_client/"] 299 is_excluded = any(affected_file_path.startswith(n) for n in excluded_paths) 300 if is_bazel and not is_excluded: 301 with open(affected_file_path, 'r') as file: 302 contents = file.read() 303 if 'exports_files_legacy(' not in contents: 304 results.append(output_api.PresubmitError( 305 ('%s needs to call exports_files_legacy() to support legacy G3 ' + 306 'rules.\nPut this near the top of the file, beneath ' + 307 'licenses(["notice"]).') % affected_file_path 308 )) 309 if 'licenses(["notice"])' not in contents: 310 results.append(output_api.PresubmitError( 311 ('%s needs to have\nlicenses(["notice"])\nimmediately after ' + 312 'the load() calls to comply with G3 policies.') % affected_file_path 313 )) 314 if 'cc_library(' in contents and '"skia_cc_library"' not in contents: 315 results.append(output_api.PresubmitError( 316 ('%s needs to load skia_cc_library from macros.bzl instead of using the ' + 317 'native one. This allows us to build differently for G3.\n' + 318 'Add "skia_cc_library" to load("//bazel:macros.bzl", ...)') 319 % affected_file_path 320 )) 321 if 'default_applicable_licenses' not in contents: 322 # See https://opensource.google/documentation/reference/thirdparty/new_license_rules 323 results.append(output_api.PresubmitError( 324 ('%s needs to have\npackage(default_applicable_licenses = ["//:license"])\n'+ 325 'to comply with G3 policies') % affected_file_path 326 )) 327 return results 328 329 330def _RunCommandAndCheckGitDiff(output_api, command): 331 """Run an arbitrary command. Fail if it produces any diffs.""" 332 command_str = ' '.join(command) 333 results = [] 334 335 try: 336 output = subprocess.check_output( 337 command, 338 stderr=subprocess.STDOUT, encoding='utf-8') 339 except subprocess.CalledProcessError as e: 340 results += [output_api.PresubmitError( 341 'Command "%s" returned non-zero exit code %d. Output: \n\n%s' % ( 342 command_str, 343 e.returncode, 344 e.output, 345 ) 346 )] 347 348 git_diff_output = subprocess.check_output( 349 ['git', 'diff', '--no-ext-diff'], encoding='utf-8') 350 if git_diff_output: 351 results += [output_api.PresubmitError( 352 'Diffs found after running "%s":\n\n%s\n' 353 'Please commit or discard the above changes.' % ( 354 command_str, 355 git_diff_output, 356 ) 357 )] 358 359 return results 360 361 362def _CheckGNIGenerated(input_api, output_api): 363 """Ensures that the generated *.gni files are current. 364 365 The Bazel project files are authoritative and some *.gni files are 366 generated from them using the exporter_tool. This check ensures they 367 are still current. 368 """ 369 if 'win32' in sys.platform: 370 # TODO(crbug.com/skia/12541): Remove when Bazel builds work on Windows. 371 # Note: `make` is not installed on Windows by default. 372 return [ 373 output_api.PresubmitPromptWarning( 374 'Skipping Bazel=>GNI export check on Windows (unsupported platform).' 375 ) 376 ] 377 if 'darwin' in sys.platform: 378 # This takes too long on Mac with default settings. Probably due to sandboxing. 379 return [] 380 should_run = False 381 for affected_file in input_api.AffectedFiles(include_deletes=True): 382 affected_file_path = affected_file.LocalPath() 383 if affected_file_path.endswith('BUILD.bazel') or affected_file_path.endswith('.gni'): 384 should_run = True 385 # Generate GNI files and verify no changes. 386 if should_run: 387 return _RunCommandAndCheckGitDiff(output_api, 388 ['make', '-C', 'bazel', 'generate_gni']) 389 390 # No Bazel build files changed. 391 return [] 392 393 394def _CheckBuildifier(input_api, output_api): 395 """Runs Buildifier and fails on linting errors, or if it produces any diffs. 396 397 This check only runs if the affected files include any WORKSPACE, BUILD, 398 BUILD.bazel or *.bzl files. 399 """ 400 files = [] 401 # Please keep the below exclude patterns in sync with those in the //:buildifier rule definition. 402 for affected_file in input_api.AffectedFiles(include_deletes=False): 403 affected_file_path = affected_file.LocalPath() 404 if affected_file_path.endswith('BUILD.bazel') or affected_file_path.endswith('.bzl'): 405 if not affected_file_path.endswith('public.bzl') and \ 406 not affected_file_path.endswith('go_repositories.bzl') and \ 407 not "bazel/rbe/gce_linux/" in affected_file_path and \ 408 not affected_file_path.startswith("third_party/externals/") and \ 409 not "node_modules/" in affected_file_path: # Skip generated files. 410 files.append(affected_file_path) 411 if not files: 412 return [] 413 try: 414 subprocess.check_output( 415 ['buildifier', '--version'], 416 stderr=subprocess.STDOUT) 417 except: 418 return [output_api.PresubmitNotifyResult( 419 'Skipping buildifier check because it is not on PATH. \n' + 420 'You can download it from https://github.com/bazelbuild/buildtools/releases')] 421 422 return _RunCommandAndCheckGitDiff( 423 # One can change --lint=warn to --lint=fix to have things automatically fixed where possible. 424 # However, --lint=fix will not cause a presubmit error if there are things that require 425 # manual intervention, so we leave --lint=warn on by default. 426 # 427 # Please keep the below arguments in sync with those in the //:buildifier rule definition. 428 output_api, [ 429 'buildifier', 430 '--mode=fix', 431 '--lint=warn', 432 '--warnings', 433 ','.join([ 434 '-native-android', 435 '-native-cc', 436 '-native-py', 437 ]) 438 ] + files) 439 440 441def _CheckBannedAPIs(input_api, output_api): 442 """Check source code for functions and packages that should not be used.""" 443 444 # A list of tuples of a regex to match an API and a suggested replacement for 445 # that API. There is an optional third parameter for files which *can* use this 446 # API without warning. 447 banned_replacements = [ 448 (r'std::stof\(', 'std::strtof(), which does not throw'), 449 (r'std::stod\(', 'std::strtod(), which does not throw'), 450 (r'std::stold\(', 'std::strtold(), which does not throw'), 451 ] 452 453 # These defines are either there or not, and using them with just an #if is a 454 # subtle, frustrating bug. 455 existence_defines = ['SK_GANESH', 'SK_GRAPHITE', 'SK_GL', 'SK_VULKAN', 'SK_DAWN', 'SK_METAL', 456 'SK_DIRECT3D', 'SK_DEBUG', 'GR_TEST_UTILS', 'GRAPHITE_TEST_UTILS'] 457 for d in existence_defines: 458 banned_replacements.append(('#if {}'.format(d), 459 '#if defined({})'.format(d))) 460 compiled_replacements = [] 461 for rep in banned_replacements: 462 exceptions = [] 463 if len(rep) == 3: 464 (re, replacement, exceptions) = rep 465 else: 466 (re, replacement) = rep 467 468 compiled_re = input_api.re.compile(re) 469 compiled_exceptions = [input_api.re.compile(exc) for exc in exceptions] 470 compiled_replacements.append( 471 (compiled_re, replacement, compiled_exceptions)) 472 473 errors = [] 474 file_filter = lambda x: (x.LocalPath().endswith('.h') or 475 x.LocalPath().endswith('.cpp') or 476 x.LocalPath().endswith('.cc') or 477 x.LocalPath().endswith('.m') or 478 x.LocalPath().endswith('.mm')) 479 for affected_file in input_api.AffectedSourceFiles(file_filter): 480 affected_filepath = affected_file.LocalPath() 481 for (line_num, line) in affected_file.ChangedContents(): 482 for (re, replacement, exceptions) in compiled_replacements: 483 match = re.search(line) 484 if match: 485 for exc in exceptions: 486 if exc.search(affected_filepath): 487 break 488 else: 489 errors.append('%s:%s: Instead of %s, please use %s.' % ( 490 affected_filepath, line_num, match.group(), replacement)) 491 492 if errors: 493 return [output_api.PresubmitError('\n'.join(errors))] 494 495 return [] 496 497 498def _CheckDEPS(input_api, output_api): 499 """If DEPS was modified, run the deps_parser to update bazel/deps.bzl""" 500 needs_running = False 501 for affected_file in input_api.AffectedFiles(include_deletes=False): 502 affected_file_path = affected_file.LocalPath() 503 if affected_file_path.endswith('DEPS') or affected_file_path.endswith('deps.bzl'): 504 needs_running = True 505 break 506 if not needs_running: 507 return [] 508 try: 509 subprocess.check_output( 510 ['bazelisk', '--version'], 511 stderr=subprocess.STDOUT) 512 except: 513 return [output_api.PresubmitNotifyResult( 514 'Skipping DEPS check because bazelisk is not on PATH. \n' + 515 'You can download it from https://github.com/bazelbuild/bazelisk/releases/tag/v1.14.0')] 516 517 return _RunCommandAndCheckGitDiff( 518 output_api, ['bazelisk', 'run', '//bazel/deps_parser']) 519 520 521def _CommonChecks(input_api, output_api): 522 """Presubmit checks common to upload and commit.""" 523 results = [] 524 sources = lambda x: (x.LocalPath().endswith('.h') or 525 x.LocalPath().endswith('.py') or 526 x.LocalPath().endswith('.sh') or 527 x.LocalPath().endswith('.m') or 528 x.LocalPath().endswith('.mm') or 529 x.LocalPath().endswith('.go') or 530 x.LocalPath().endswith('.c') or 531 x.LocalPath().endswith('.cc') or 532 x.LocalPath().endswith('.cpp')) 533 results.extend(_CheckChangeHasEol( 534 input_api, output_api, source_file_filter=sources)) 535 with _WarningsAsErrors(output_api): 536 results.extend(input_api.canned_checks.CheckChangeHasNoCR( 537 input_api, output_api, source_file_filter=sources)) 538 results.extend(input_api.canned_checks.CheckChangeHasNoStrayWhitespace( 539 input_api, output_api, source_file_filter=sources)) 540 results.extend(_JsonChecks(input_api, output_api)) 541 results.extend(_IfDefChecks(input_api, output_api)) 542 results.extend(_CopyrightChecks(input_api, output_api, 543 source_file_filter=sources)) 544 results.extend(_CheckIncludesFormatted(input_api, output_api)) 545 results.extend(_CheckGNFormatted(input_api, output_api)) 546 results.extend(_CheckGitConflictMarkers(input_api, output_api)) 547 results.extend(_RegenerateAllExamplesCPP(input_api, output_api)) 548 results.extend(_CheckExamplesForPrivateAPIs(input_api, output_api)) 549 results.extend(_CheckBazelBUILDFiles(input_api, output_api)) 550 results.extend(_CheckBannedAPIs(input_api, output_api)) 551 return results 552 553 554def CheckChangeOnUpload(input_api, output_api): 555 """Presubmit checks for the change on upload.""" 556 results = [] 557 results.extend(_CommonChecks(input_api, output_api)) 558 # Run on upload, not commit, since the presubmit bot apparently doesn't have 559 # coverage or Go installed. 560 results.extend(_InfraTests(input_api, output_api)) 561 results.extend(_CheckTopReleaseNotesChanged(input_api, output_api)) 562 results.extend(_CheckReleaseNotesForPublicAPI(input_api, output_api)) 563 # Buildifier might not be on the CI machines. 564 results.extend(_CheckBuildifier(input_api, output_api)) 565 # We don't want this to block the CQ (for now). 566 results.extend(_CheckDEPS(input_api, output_api)) 567 # Bazelisk is not yet included in the Presubmit job. 568 results.extend(_CheckGeneratedBazelBUILDFiles(input_api, output_api)) 569 results.extend(_CheckGNIGenerated(input_api, output_api)) 570 return results 571 572 573class CodeReview(object): 574 """Abstracts which codereview tool is used for the specified issue.""" 575 576 def __init__(self, input_api): 577 self._issue = input_api.change.issue 578 self._gerrit = input_api.gerrit 579 580 def GetOwnerEmail(self): 581 return self._gerrit.GetChangeOwner(self._issue) 582 583 def GetSubject(self): 584 return self._gerrit.GetChangeInfo(self._issue)['subject'] 585 586 def GetDescription(self): 587 return self._gerrit.GetChangeDescription(self._issue) 588 589 def GetReviewers(self): 590 code_review_label = ( 591 self._gerrit.GetChangeInfo(self._issue)['labels']['Code-Review']) 592 return [r['email'] for r in code_review_label.get('all', [])] 593 594 def GetApprovers(self): 595 approvers = [] 596 code_review_label = ( 597 self._gerrit.GetChangeInfo(self._issue)['labels']['Code-Review']) 598 for m in code_review_label.get('all', []): 599 if m.get("value") == 1: 600 approvers.append(m["email"]) 601 return approvers 602 603 604def _CheckReleaseNotesForPublicAPI(input_api, output_api): 605 """Checks to see if a release notes file is added or edited with public API changes.""" 606 results = [] 607 public_api_changed = False 608 release_file_changed = False 609 for affected_file in input_api.AffectedFiles(): 610 affected_file_path = affected_file.LocalPath() 611 file_path, file_ext = os.path.splitext(affected_file_path) 612 # We only care about files that end in .h and are under the top-level 613 # include dir, but not include/private. 614 if (file_ext == '.h' and 615 file_path.split(os.path.sep)[0] == 'include' and 616 'private' not in file_path): 617 public_api_changed = True 618 elif os.path.dirname(file_path) == RELEASE_NOTES_DIR: 619 release_file_changed = True 620 621 if public_api_changed and not release_file_changed: 622 results.append(output_api.PresubmitPromptWarning( 623 'If this change affects a client API, please add a new summary ' 624 'file in the %s directory. More information can be found in ' 625 '%s.' % (RELEASE_NOTES_DIR, RELEASE_NOTES_README))) 626 return results 627 628 629def _CheckTopReleaseNotesChanged(input_api, output_api): 630 """Warns if the top level release notes file was changed. 631 632 The top level file is now auto-edited, and new release notes should 633 be added to the RELEASE_NOTES_DIR directory""" 634 results = [] 635 top_relnotes_changed = False 636 release_file_changed = False 637 for affected_file in input_api.AffectedFiles(): 638 affected_file_path = affected_file.LocalPath() 639 file_path, file_ext = os.path.splitext(affected_file_path) 640 if affected_file_path == RELEASE_NOTES_FILE_NAME: 641 top_relnotes_changed = True 642 elif os.path.dirname(file_path) == RELEASE_NOTES_DIR: 643 release_file_changed = True 644 # When relnotes_util is run it will modify RELEASE_NOTES_FILE_NAME 645 # and delete the individual note files in RELEASE_NOTES_DIR. 646 # So, if both paths are modified do not emit a warning. 647 if top_relnotes_changed and not release_file_changed: 648 results.append(output_api.PresubmitPromptWarning( 649 'Do not edit %s directly. %s is automatically edited during the ' 650 'release process. Release notes should be added as new files in ' 651 'the %s directory. More information can be found in %s.' % (RELEASE_NOTES_FILE_NAME, 652 RELEASE_NOTES_FILE_NAME, 653 RELEASE_NOTES_DIR, 654 RELEASE_NOTES_README))) 655 return results 656 657 658def PostUploadHook(gerrit, change, output_api): 659 """git cl upload will call this hook after the issue is created/modified. 660 661 This hook does the following: 662 * Adds a link to preview docs changes if there are any docs changes in the CL. 663 * Adds 'No-Try: true' if the CL contains only docs changes. 664 """ 665 if not change.issue: 666 return [] 667 668 # Skip PostUploadHooks for all auto-commit service account bots. New 669 # patchsets (caused due to PostUploadHooks) invalidates the CQ+2 vote from 670 # the "--use-commit-queue" flag to "git cl upload". 671 for suffix in SERVICE_ACCOUNT_SUFFIX: 672 if change.author_email.endswith(suffix): 673 return [] 674 675 results = [] 676 at_least_one_docs_change = False 677 all_docs_changes = True 678 for affected_file in change.AffectedFiles(): 679 affected_file_path = affected_file.LocalPath() 680 file_path, _ = os.path.splitext(affected_file_path) 681 if 'site' == file_path.split(os.path.sep)[0]: 682 at_least_one_docs_change = True 683 else: 684 all_docs_changes = False 685 if at_least_one_docs_change and not all_docs_changes: 686 break 687 688 footers = change.GitFootersFromDescription() 689 description_changed = False 690 691 # If the change includes only doc changes then add No-Try: true in the 692 # CL's description if it does not exist yet. 693 if all_docs_changes and 'true' not in footers.get('No-Try', []): 694 description_changed = True 695 change.AddDescriptionFooter('No-Try', 'true') 696 results.append( 697 output_api.PresubmitNotifyResult( 698 'This change has only doc changes. Automatically added ' 699 '\'No-Try: true\' to the CL\'s description')) 700 701 # If the description has changed update it. 702 if description_changed: 703 gerrit.UpdateDescription( 704 change.FullDescriptionText(), change.issue) 705 706 return results 707 708 709def CheckChangeOnCommit(input_api, output_api): 710 """Presubmit checks for the change on commit.""" 711 results = [] 712 results.extend(_CommonChecks(input_api, output_api)) 713 # Checks for the presence of 'DO NOT''SUBMIT' in CL description and in 714 # content of files. 715 results.extend( 716 input_api.canned_checks.CheckDoNotSubmit(input_api, output_api)) 717 return results 718