Flutter Engine
The Flutter Engine
PRESUBMIT.py
Go to the documentation of this file.
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
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
def GetApprovers(self)
Definition: PRESUBMIT.py:594
def __init__(self, input_api)
Definition: PRESUBMIT.py:576
def GetReviewers(self)
Definition: PRESUBMIT.py:589
def GetDescription(self)
Definition: PRESUBMIT.py:586
def GetSubject(self)
Definition: PRESUBMIT.py:583
def GetOwnerEmail(self)
Definition: PRESUBMIT.py:580
def __exit__(self, ex_type, ex_value, ex_traceback)
Definition: PRESUBMIT.py:220
def __init__(self, output_api)
Definition: PRESUBMIT.py:213
uint32_t uint32_t * format
def CheckChangeOnCommit(input_api, output_api)
Definition: PRESUBMIT.py:66
def CheckChangeOnUpload(input_api, output_api)
Definition: PRESUBMIT.py:70
def PostUploadHook(gerrit, change, output_api)
Definition: PRESUBMIT.py:658
SIT bool any(const Vec< 1, T > &x)
Definition: SkVx.h:530
static SkString join(const CommandLineFlags::StringArray &)
Definition: skpbench.cpp:741