blob: 43675ccb30bc7eeb97e5a3a5a46feef1a7b6e7b0 [file] [log] [blame]
rmistry@google.com8e3ff8c2013-01-17 12:55:34 +00001# 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
8See http://dev.chromium.org/developers/how-tos/depottools/presubmit-scripts
9for more details about the presubmit API built into gcl.
10"""
11
rmistry58276532015-10-01 08:24:03 -070012import collections
rmistry3cfd1ad2015-03-25 12:53:35 -070013import csv
commit-bot@chromium.org745e08c2014-02-03 14:18:32 +000014import fnmatch
rmistry@google.comf6c5f752013-03-29 17:26:00 +000015import os
commit-bot@chromium.orgcfdc5962014-01-31 17:33:04 +000016import re
rmistryd223fb22015-02-26 10:16:13 -080017import subprocess
rmistry@google.comf6c5f752013-03-29 17:26:00 +000018import sys
commit-bot@chromium.org745e08c2014-02-03 14:18:32 +000019import traceback
rmistry@google.comf6c5f752013-03-29 17:26:00 +000020
rmistry@google.comc2993442013-01-23 14:35:58 +000021
commit-bot@chromium.orgcfdc5962014-01-31 17:33:04 +000022REVERT_CL_SUBJECT_PREFIX = 'Revert '
23
rmistryf2d83ca2014-08-26 10:30:29 -070024# Please add the complete email address here (and not just 'xyz@' or 'xyz').
rmistry@google.comfb4a68d2013-08-12 14:51:20 +000025PUBLIC_API_OWNERS = (
Mike Klein3f041f82021-02-03 09:36:14 -060026 'brianosman@google.com',
rmistry@google.comfb4a68d2013-08-12 14:51:20 +000027 'bsalomon@google.com',
rmistry83fab472014-07-18 05:25:56 -070028 'djsollen@chromium.org',
29 'djsollen@google.com',
Ravi Mistryfbff3292017-01-19 12:00:08 -050030 'hcm@chromium.org',
31 'hcm@google.com',
Mike Klein3f041f82021-02-03 09:36:14 -060032 'reed@chromium.org',
33 'reed@google.com',
rmistry@google.comfb4a68d2013-08-12 14:51:20 +000034)
35
commit-bot@chromium.org745e08c2014-02-03 14:18:32 +000036AUTHORS_FILE_NAME = 'AUTHORS'
Ravi Mistry57735162019-07-25 13:45:15 -040037RELEASE_NOTES_FILE_NAME = 'RELEASE_NOTES.txt'
commit-bot@chromium.org745e08c2014-02-03 14:18:32 +000038
Ravi Mistry27095f22021-04-22 12:51:49 +000039DOCS_PREVIEW_URL = 'https://skia.org/?cl={issue}'
rmistryd88b0be2016-05-20 03:50:01 -070040GOLD_TRYBOT_URL = 'https://gold.skia.org/search?issue='
rmistryd223fb22015-02-26 10:16:13 -080041
Eric Boren1eec99c2018-04-26 13:09:48 -040042SERVICE_ACCOUNT_SUFFIX = [
Eric Boren47ed6f12018-04-26 14:02:43 -040043 '@%s.iam.gserviceaccount.com' % project for project in [
Eric Boren6ad3ca42018-09-07 14:22:16 -040044 'skia-buildbots.google.com', 'skia-swarming-bots', 'skia-public',
Ravi Mistry53c44232019-03-12 08:51:42 -040045 'skia-corp.google.com', 'chops-service-accounts']]
Eric Borendd988292018-01-02 13:29:21 -050046
rmistry@google.com547012d2013-04-12 19:45:46 +000047
rmistry@google.com713276b2013-01-25 18:27:34 +000048def _CheckChangeHasEol(input_api, output_api, source_file_filter=None):
Edward Lemur2b7876c2020-01-17 18:48:13 -050049 """Checks that files end with at least one \n (LF)."""
rmistry@google.com713276b2013-01-25 18:27:34 +000050 eof_files = []
51 for f in input_api.AffectedSourceFiles(source_file_filter):
52 contents = input_api.ReadFile(f, 'rb')
Edward Lemur2b7876c2020-01-17 18:48:13 -050053 # Check that the file ends in at least one newline character.
rmistry@google.com713276b2013-01-25 18:27:34 +000054 if len(contents) > 1 and contents[-1:] != '\n':
55 eof_files.append(f.LocalPath())
56
57 if eof_files:
58 return [output_api.PresubmitPromptWarning(
59 'These files should end in a newline character:',
60 items=eof_files)]
61 return []
62
63
Ben Wagnercf42e982018-02-09 17:41:20 -050064def _JsonChecks(input_api, output_api):
65 """Run checks on any modified json files."""
66 failing_files = []
67 for affected_file in input_api.AffectedFiles(None):
68 affected_file_path = affected_file.LocalPath()
69 is_json = affected_file_path.endswith('.json')
70 is_metadata = (affected_file_path.startswith('site/') and
71 affected_file_path.endswith('/METADATA'))
72 if is_json or is_metadata:
73 try:
74 input_api.json.load(open(affected_file_path, 'r'))
75 except ValueError:
76 failing_files.append(affected_file_path)
77
78 results = []
79 if failing_files:
80 results.append(
81 output_api.PresubmitError(
82 'The following files contain invalid json:\n%s\n\n' %
83 '\n'.join(failing_files)))
84 return results
85
86
rmistry01cbf6c2015-03-12 07:48:40 -070087def _IfDefChecks(input_api, output_api):
88 """Ensures if/ifdef are not before includes. See skbug/3362 for details."""
89 comment_block_start_pattern = re.compile('^\s*\/\*.*$')
90 comment_block_middle_pattern = re.compile('^\s+\*.*')
91 comment_block_end_pattern = re.compile('^\s+\*\/.*$')
92 single_line_comment_pattern = re.compile('^\s*//.*$')
93 def is_comment(line):
94 return (comment_block_start_pattern.match(line) or
95 comment_block_middle_pattern.match(line) or
96 comment_block_end_pattern.match(line) or
97 single_line_comment_pattern.match(line))
98
99 empty_line_pattern = re.compile('^\s*$')
100 def is_empty_line(line):
101 return empty_line_pattern.match(line)
102
103 failing_files = []
104 for affected_file in input_api.AffectedSourceFiles(None):
105 affected_file_path = affected_file.LocalPath()
106 if affected_file_path.endswith('.cpp') or affected_file_path.endswith('.h'):
107 f = open(affected_file_path)
108 for line in f.xreadlines():
109 if is_comment(line) or is_empty_line(line):
110 continue
111 # The below will be the first real line after comments and newlines.
112 if line.startswith('#if 0 '):
113 pass
114 elif line.startswith('#if ') or line.startswith('#ifdef '):
115 failing_files.append(affected_file_path)
116 break
117
118 results = []
119 if failing_files:
120 results.append(
121 output_api.PresubmitError(
122 'The following files have #if or #ifdef before includes:\n%s\n\n'
halcanary6950de62015-11-07 05:29:00 -0800123 'See https://bug.skia.org/3362 for why this should be fixed.' %
rmistry01cbf6c2015-03-12 07:48:40 -0700124 '\n'.join(failing_files)))
125 return results
126
127
borenetc7c91802015-03-25 04:47:02 -0700128def _CopyrightChecks(input_api, output_api, source_file_filter=None):
129 results = []
130 year_pattern = r'\d{4}'
131 year_range_pattern = r'%s(-%s)?' % (year_pattern, year_pattern)
132 years_pattern = r'%s(,%s)*,?' % (year_range_pattern, year_range_pattern)
133 copyright_pattern = (
134 r'Copyright (\([cC]\) )?%s \w+' % years_pattern)
135
136 for affected_file in input_api.AffectedSourceFiles(source_file_filter):
John Stilesd836f842020-09-14 10:21:44 -0400137 if ('third_party/' in affected_file.LocalPath() or
138 'tests/sksl/' in affected_file.LocalPath()):
borenetc7c91802015-03-25 04:47:02 -0700139 continue
140 contents = input_api.ReadFile(affected_file, 'rb')
141 if not re.search(copyright_pattern, contents):
142 results.append(output_api.PresubmitError(
143 '%s is missing a correct copyright header.' % affected_file))
144 return results
145
146
borenet2dbbfa52016-10-14 06:32:09 -0700147def _InfraTests(input_api, output_api):
148 """Run the infra tests."""
borenet1ed2ae42016-07-26 11:52:17 -0700149 results = []
mtklein3da80f52016-07-27 04:14:07 -0700150 if not any(f.LocalPath().startswith('infra')
151 for f in input_api.AffectedFiles()):
152 return results
153
borenet2dbbfa52016-10-14 06:32:09 -0700154 cmd = ['python', os.path.join('infra', 'bots', 'infra_tests.py')]
borenet60b0a2d2016-10-04 12:45:41 -0700155 try:
156 subprocess.check_output(cmd)
157 except subprocess.CalledProcessError as e:
158 results.append(output_api.PresubmitError(
159 '`%s` failed:\n%s' % (' '.join(cmd), e.output)))
160 return results
161
162
mtklein4db3b792016-08-03 14:18:22 -0700163def _CheckGNFormatted(input_api, output_api):
164 """Make sure any .gn files we're changing have been formatted."""
Ben Wagner3c4a9d32020-02-14 14:28:33 -0500165 files = []
Corentin Wallez6a5187a2020-04-08 10:24:04 +0200166 for f in input_api.AffectedFiles(include_deletes=False):
Ben Wagner3c4a9d32020-02-14 14:28:33 -0500167 if (f.LocalPath().endswith('.gn') or
168 f.LocalPath().endswith('.gni')):
169 files.append(f)
170 if not files:
171 return []
mtklein4db3b792016-08-03 14:18:22 -0700172
Ben Wagner3c4a9d32020-02-14 14:28:33 -0500173 cmd = ['python', os.path.join('bin', 'fetch-gn')]
174 try:
175 subprocess.check_output(cmd)
176 except subprocess.CalledProcessError as e:
177 return [output_api.PresubmitError(
178 '`%s` failed:\n%s' % (' '.join(cmd), e.output))]
179
180 results = []
181 for f in files:
Brian Osman70f24af2020-02-18 15:08:27 -0500182 gn = 'gn.exe' if 'win32' in sys.platform else 'gn'
Ben Wagner06265e02020-02-13 19:02:46 -0500183 gn = os.path.join(input_api.PresubmitLocalPath(), 'bin', gn)
Mike Klein7a1c53d2016-10-11 14:03:06 -0400184 cmd = [gn, 'format', '--dry-run', f.LocalPath()]
mtklein4db3b792016-08-03 14:18:22 -0700185 try:
186 subprocess.check_output(cmd)
187 except subprocess.CalledProcessError:
Ben Wagner06265e02020-02-13 19:02:46 -0500188 fix = 'bin/gn format ' + f.LocalPath()
mtklein4db3b792016-08-03 14:18:22 -0700189 results.append(output_api.PresubmitError(
mtkleind434b012016-08-10 07:30:58 -0700190 '`%s` failed, try\n\t%s' % (' '.join(cmd), fix)))
mtklein4db3b792016-08-03 14:18:22 -0700191 return results
192
Ravi Mistry6eca5792020-12-16 11:42:29 -0500193
194def _CheckGitConflictMarkers(input_api, output_api):
195 pattern = input_api.re.compile('^(?:<<<<<<<|>>>>>>>) |^=======$')
196 results = []
197 for f in input_api.AffectedFiles():
198 for line_num, line in f.ChangedContents():
199 if f.LocalPath().endswith('.md'):
200 # First-level headers in markdown look a lot like version control
201 # conflict markers. http://daringfireball.net/projects/markdown/basics
202 continue
203 if pattern.match(line):
204 results.append(
205 output_api.PresubmitError(
206 'Git conflict markers found in %s:%d %s' % (
207 f.LocalPath(), line_num, line)))
208 return results
209
210
Mike Kleinbb413432019-07-26 11:55:40 -0500211def _CheckIncludesFormatted(input_api, output_api):
212 """Make sure #includes in files we're changing have been formatted."""
Mike Kleinf9ad5ba2019-07-29 12:34:39 -0500213 files = [str(f) for f in input_api.AffectedFiles() if f.Action() != 'D']
Mike Kleinbb413432019-07-26 11:55:40 -0500214 cmd = ['python',
215 'tools/rewrite_includes.py',
Mike Kleinf9ad5ba2019-07-29 12:34:39 -0500216 '--dry-run'] + files
Hal Canary4df3d532019-07-30 13:49:45 -0400217 if 0 != subprocess.call(cmd):
Mike Kleinbb413432019-07-26 11:55:40 -0500218 return [output_api.PresubmitError('`%s` failed' % ' '.join(cmd))]
219 return []
borenet1ed2ae42016-07-26 11:52:17 -0700220
Eric Boren58d1f762019-07-19 08:07:44 -0400221
Ben Wagner88855502017-10-12 17:55:19 -0400222class _WarningsAsErrors():
223 def __init__(self, output_api):
224 self.output_api = output_api
225 self.old_warning = None
226 def __enter__(self):
227 self.old_warning = self.output_api.PresubmitPromptWarning
228 self.output_api.PresubmitPromptWarning = self.output_api.PresubmitError
229 return self.output_api
230 def __exit__(self, ex_type, ex_value, ex_traceback):
231 self.output_api.PresubmitPromptWarning = self.old_warning
232
233
Eric Boren6dc00212019-07-24 15:15:43 -0400234def _CheckDEPSValid(input_api, output_api):
235 """Ensure that DEPS contains valid entries."""
236 results = []
237 script = os.path.join('infra', 'bots', 'check_deps.py')
238 relevant_files = ('DEPS', script)
239 for f in input_api.AffectedFiles():
240 if f.LocalPath() in relevant_files:
241 break
242 else:
243 return results
244 cmd = ['python', script]
245 try:
246 subprocess.check_output(cmd, stderr=subprocess.STDOUT)
247 except subprocess.CalledProcessError as e:
248 results.append(output_api.PresubmitError(e.output))
249 return results
250
251
Kevin Lubick2cd80672021-07-01 11:03:36 -0400252def _RegenerateAllExamplesCPP(input_api, output_api):
253 """Regenerates all_examples.cpp if an example was added or deleted."""
254 if not any(f.LocalPath().startswith('docs/examples/')
255 for f in input_api.AffectedFiles()):
256 return []
257 command_str = 'tools/fiddle/make_all_examples_cpp.py'
258 cmd = ['python', command_str]
259 if 0 != subprocess.call(cmd):
260 return [output_api.PresubmitError('`%s` failed' % ' '.join(cmd))]
261
262 results = []
263 git_diff_output = input_api.subprocess.check_output(
264 ['git', 'diff', '--no-ext-diff'])
265 if git_diff_output:
266 results += [output_api.PresubmitError(
267 'Diffs found after running "%s":\n\n%s\n'
268 'Please commit or discard the above changes.' % (
269 command_str,
270 git_diff_output,
271 )
272 )]
273 return results
274
rmistry@google.com6be0b4c2013-01-17 14:50:59 +0000275def _CommonChecks(input_api, output_api):
276 """Presubmit checks common to upload and commit."""
277 results = []
278 sources = lambda x: (x.LocalPath().endswith('.h') or
rmistry@google.com6be0b4c2013-01-17 14:50:59 +0000279 x.LocalPath().endswith('.py') or
280 x.LocalPath().endswith('.sh') or
mtklein18e55802015-03-25 07:21:20 -0700281 x.LocalPath().endswith('.m') or
282 x.LocalPath().endswith('.mm') or
283 x.LocalPath().endswith('.go') or
284 x.LocalPath().endswith('.c') or
285 x.LocalPath().endswith('.cc') or
rmistry@google.com6be0b4c2013-01-17 14:50:59 +0000286 x.LocalPath().endswith('.cpp'))
Ben Wagner88855502017-10-12 17:55:19 -0400287 results.extend(_CheckChangeHasEol(
288 input_api, output_api, source_file_filter=sources))
289 with _WarningsAsErrors(output_api):
290 results.extend(input_api.canned_checks.CheckChangeHasNoCR(
291 input_api, output_api, source_file_filter=sources))
292 results.extend(input_api.canned_checks.CheckChangeHasNoStrayWhitespace(
293 input_api, output_api, source_file_filter=sources))
Ben Wagnercf42e982018-02-09 17:41:20 -0500294 results.extend(_JsonChecks(input_api, output_api))
rmistry01cbf6c2015-03-12 07:48:40 -0700295 results.extend(_IfDefChecks(input_api, output_api))
borenetc7c91802015-03-25 04:47:02 -0700296 results.extend(_CopyrightChecks(input_api, output_api,
297 source_file_filter=sources))
Eric Boren6dc00212019-07-24 15:15:43 -0400298 results.extend(_CheckDEPSValid(input_api, output_api))
Mike Kleinbb413432019-07-26 11:55:40 -0500299 results.extend(_CheckIncludesFormatted(input_api, output_api))
Mike Klein96f64012020-04-03 10:59:37 -0500300 results.extend(_CheckGNFormatted(input_api, output_api))
Ravi Mistry6eca5792020-12-16 11:42:29 -0500301 results.extend(_CheckGitConflictMarkers(input_api, output_api))
Kevin Lubick2cd80672021-07-01 11:03:36 -0400302 results.extend(_RegenerateAllExamplesCPP(input_api, output_api))
rmistry@google.com6be0b4c2013-01-17 14:50:59 +0000303 return results
304
rmistry@google.com8e3ff8c2013-01-17 12:55:34 +0000305
306def CheckChangeOnUpload(input_api, output_api):
Ravi Mistry4c0ffe72020-03-02 13:19:02 -0500307 """Presubmit checks for the change on upload."""
rmistry@google.com6be0b4c2013-01-17 14:50:59 +0000308 results = []
309 results.extend(_CommonChecks(input_api, output_api))
borenet1ed2ae42016-07-26 11:52:17 -0700310 # Run on upload, not commit, since the presubmit bot apparently doesn't have
borenet60b0a2d2016-10-04 12:45:41 -0700311 # coverage or Go installed.
borenet2dbbfa52016-10-14 06:32:09 -0700312 results.extend(_InfraTests(input_api, output_api))
Ravi Mistry57735162019-07-25 13:45:15 -0400313 results.extend(_CheckReleaseNotesForPublicAPI(input_api, output_api))
rmistry@google.com6be0b4c2013-01-17 14:50:59 +0000314 return results
rmistry@google.com8e3ff8c2013-01-17 12:55:34 +0000315
316
rmistryb398ecc2016-08-29 08:13:29 -0700317class CodeReview(object):
318 """Abstracts which codereview tool is used for the specified issue."""
319
320 def __init__(self, input_api):
321 self._issue = input_api.change.issue
322 self._gerrit = input_api.gerrit
rmistryb398ecc2016-08-29 08:13:29 -0700323
324 def GetOwnerEmail(self):
Aaron Gablea49909a2017-10-09 12:50:52 -0700325 return self._gerrit.GetChangeOwner(self._issue)
rmistryb398ecc2016-08-29 08:13:29 -0700326
327 def GetSubject(self):
Aaron Gablea49909a2017-10-09 12:50:52 -0700328 return self._gerrit.GetChangeInfo(self._issue)['subject']
rmistryb398ecc2016-08-29 08:13:29 -0700329
330 def GetDescription(self):
Aaron Gablea49909a2017-10-09 12:50:52 -0700331 return self._gerrit.GetChangeDescription(self._issue)
rmistryb398ecc2016-08-29 08:13:29 -0700332
Ravi Mistry39eabb62016-10-05 08:41:12 -0400333 def GetReviewers(self):
Aaron Gablea49909a2017-10-09 12:50:52 -0700334 code_review_label = (
335 self._gerrit.GetChangeInfo(self._issue)['labels']['Code-Review'])
336 return [r['email'] for r in code_review_label.get('all', [])]
Ravi Mistry39eabb62016-10-05 08:41:12 -0400337
rmistryb398ecc2016-08-29 08:13:29 -0700338 def GetApprovers(self):
339 approvers = []
Aaron Gablea49909a2017-10-09 12:50:52 -0700340 code_review_label = (
341 self._gerrit.GetChangeInfo(self._issue)['labels']['Code-Review'])
342 for m in code_review_label.get('all', []):
343 if m.get("value") == 1:
344 approvers.append(m["email"])
rmistryb398ecc2016-08-29 08:13:29 -0700345 return approvers
346
347
commit-bot@chromium.org745e08c2014-02-03 14:18:32 +0000348def _CheckOwnerIsInAuthorsFile(input_api, output_api):
349 results = []
rmistryb398ecc2016-08-29 08:13:29 -0700350 if input_api.change.issue:
351 cr = CodeReview(input_api)
commit-bot@chromium.org745e08c2014-02-03 14:18:32 +0000352
rmistryb398ecc2016-08-29 08:13:29 -0700353 owner_email = cr.GetOwnerEmail()
Eric Borendd988292018-01-02 13:29:21 -0500354
355 # Service accounts don't need to be in AUTHORS.
Eric Boren1eec99c2018-04-26 13:09:48 -0400356 for suffix in SERVICE_ACCOUNT_SUFFIX:
357 if owner_email.endswith(suffix):
358 return results
Eric Borendd988292018-01-02 13:29:21 -0500359
commit-bot@chromium.org745e08c2014-02-03 14:18:32 +0000360 try:
361 authors_content = ''
362 for line in open(AUTHORS_FILE_NAME):
363 if not line.startswith('#'):
364 authors_content += line
365 email_fnmatches = re.findall('<(.*)>', authors_content)
366 for email_fnmatch in email_fnmatches:
367 if fnmatch.fnmatch(owner_email, email_fnmatch):
368 # Found a match, the user is in the AUTHORS file break out of the loop
369 break
370 else:
commit-bot@chromium.org745e08c2014-02-03 14:18:32 +0000371 results.append(
372 output_api.PresubmitError(
373 'The email %s is not in Skia\'s AUTHORS file.\n'
374 'Issue owner, this CL must include an addition to the Skia AUTHORS '
rmistry9806d4d2015-10-01 08:10:54 -0700375 'file.'
rmistry83fab472014-07-18 05:25:56 -0700376 % owner_email))
commit-bot@chromium.org745e08c2014-02-03 14:18:32 +0000377 except IOError:
378 # Do not fail if authors file cannot be found.
379 traceback.print_exc()
380 input_api.logging.error('AUTHORS file not found!')
381
382 return results
383
384
Ravi Mistry57735162019-07-25 13:45:15 -0400385def _CheckReleaseNotesForPublicAPI(input_api, output_api):
386 """Checks to see if release notes file is updated with public API changes."""
387 results = []
388 public_api_changed = False
389 release_file_changed = False
390 for affected_file in input_api.AffectedFiles():
391 affected_file_path = affected_file.LocalPath()
392 file_path, file_ext = os.path.splitext(affected_file_path)
393 # We only care about files that end in .h and are under the top-level
394 # include dir, but not include/private.
395 if (file_ext == '.h' and
396 file_path.split(os.path.sep)[0] == 'include' and
397 'private' not in file_path):
398 public_api_changed = True
399 elif affected_file_path == RELEASE_NOTES_FILE_NAME:
400 release_file_changed = True
401
402 if public_api_changed and not release_file_changed:
403 results.append(output_api.PresubmitPromptWarning(
404 'If this change affects a client API, please add a summary line '
405 'to the %s file.' % RELEASE_NOTES_FILE_NAME))
406 return results
407
408
409
rmistry@google.comfb4a68d2013-08-12 14:51:20 +0000410def _CheckLGTMsForPublicAPI(input_api, output_api):
411 """Check LGTMs for public API changes.
412
413 For public API files make sure there is an LGTM from the list of owners in
414 PUBLIC_API_OWNERS.
415 """
416 results = []
417 requires_owner_check = False
rmistry9407ece2014-08-26 14:00:54 -0700418 for affected_file in input_api.AffectedFiles():
419 affected_file_path = affected_file.LocalPath()
rmistry@google.comfb4a68d2013-08-12 14:51:20 +0000420 file_path, file_ext = os.path.splitext(affected_file_path)
rmistry9407ece2014-08-26 14:00:54 -0700421 # We only care about files that end in .h and are under the top-level
mtkleinbda12672015-07-28 08:54:12 -0700422 # include dir, but not include/private.
423 if (file_ext == '.h' and
424 'include' == file_path.split(os.path.sep)[0] and
425 'private' not in file_path):
rmistry@google.comfb4a68d2013-08-12 14:51:20 +0000426 requires_owner_check = True
427
428 if not requires_owner_check:
429 return results
430
431 lgtm_from_owner = False
rmistryb398ecc2016-08-29 08:13:29 -0700432 if input_api.change.issue:
433 cr = CodeReview(input_api)
434
435 if re.match(REVERT_CL_SUBJECT_PREFIX, cr.GetSubject(), re.I):
commit-bot@chromium.orgcfdc5962014-01-31 17:33:04 +0000436 # It is a revert CL, ignore the public api owners check.
437 return results
rmistryf2d83ca2014-08-26 10:30:29 -0700438
Ravi Mistry39eabb62016-10-05 08:41:12 -0400439 if input_api.gerrit:
440 for reviewer in cr.GetReviewers():
441 if reviewer in PUBLIC_API_OWNERS:
442 # If an owner is specified as an reviewer in Gerrit then ignore the
443 # public api owners check.
rmistryf2d83ca2014-08-26 10:30:29 -0700444 return results
Ravi Mistry39eabb62016-10-05 08:41:12 -0400445 else:
446 match = re.search(r'^TBR=(.*)$', cr.GetDescription(), re.M)
447 if match:
448 tbr_section = match.group(1).strip().split(' ')[0]
449 tbr_entries = tbr_section.split(',')
450 for owner in PUBLIC_API_OWNERS:
451 if owner in tbr_entries or owner.split('@')[0] in tbr_entries:
452 # If an owner is specified in the TBR= line then ignore the public
453 # api owners check.
454 return results
rmistryf2d83ca2014-08-26 10:30:29 -0700455
rmistryb398ecc2016-08-29 08:13:29 -0700456 if cr.GetOwnerEmail() in PUBLIC_API_OWNERS:
rmistry@google.comfb4a68d2013-08-12 14:51:20 +0000457 # An owner created the CL that is an automatic LGTM.
458 lgtm_from_owner = True
459
rmistryb398ecc2016-08-29 08:13:29 -0700460 for approver in cr.GetApprovers():
461 if approver in PUBLIC_API_OWNERS:
462 # Found an lgtm in a message from an owner.
463 lgtm_from_owner = True
464 break
commit-bot@chromium.orgcfdc5962014-01-31 17:33:04 +0000465
rmistry@google.comfb4a68d2013-08-12 14:51:20 +0000466 if not lgtm_from_owner:
467 results.append(
468 output_api.PresubmitError(
mtkleinbda12672015-07-28 08:54:12 -0700469 "If this CL adds to or changes Skia's public API, you need an LGTM "
470 "from any of %s. If this CL only removes from or doesn't change "
Ravi Mistrydbb84c22016-10-05 12:47:44 -0400471 "Skia's public API, please add a short note to the CL saying so. "
Aaron Gablea49909a2017-10-09 12:50:52 -0700472 "Add one of the owners as a reviewer to your CL as well as to the "
473 "TBR= line. If you don't know if this CL affects Skia's public "
474 "API, treat it like it does." % str(PUBLIC_API_OWNERS)))
rmistry@google.comfb4a68d2013-08-12 14:51:20 +0000475 return results
476
477
Edward Lemur2b7876c2020-01-17 18:48:13 -0500478def PostUploadHook(gerrit, change, output_api):
rmistryd223fb22015-02-26 10:16:13 -0800479 """git cl upload will call this hook after the issue is created/modified.
480
481 This hook does the following:
482 * Adds a link to preview docs changes if there are any docs changes in the CL.
Ravi Mistry355feab2017-05-23 14:24:08 -0400483 * Adds 'No-Try: true' if the CL contains only docs changes.
rmistryd223fb22015-02-26 10:16:13 -0800484 """
Edward Lemur2b7876c2020-01-17 18:48:13 -0500485 if not change.issue:
486 return []
487
488 # Skip PostUploadHooks for all auto-commit service account bots. New
489 # patchsets (caused due to PostUploadHooks) invalidates the CQ+2 vote from
490 # the "--use-commit-queue" flag to "git cl upload".
491 for suffix in SERVICE_ACCOUNT_SUFFIX:
492 if change.author_email.endswith(suffix):
493 return []
rmistryd223fb22015-02-26 10:16:13 -0800494
495 results = []
Ravi Mistry27095f22021-04-22 12:51:49 +0000496 at_least_one_docs_change = False
rmistryd223fb22015-02-26 10:16:13 -0800497 all_docs_changes = True
498 for affected_file in change.AffectedFiles():
499 affected_file_path = affected_file.LocalPath()
500 file_path, _ = os.path.splitext(affected_file_path)
Ravi Mistry27095f22021-04-22 12:51:49 +0000501 if 'site' == file_path.split(os.path.sep)[0]:
502 at_least_one_docs_change = True
rmistryd223fb22015-02-26 10:16:13 -0800503 else:
504 all_docs_changes = False
Ravi Mistry27095f22021-04-22 12:51:49 +0000505 if at_least_one_docs_change and not all_docs_changes:
506 break
rmistryd223fb22015-02-26 10:16:13 -0800507
Edward Lemur2b7876c2020-01-17 18:48:13 -0500508 footers = change.GitFootersFromDescription()
509 description_changed = False
Ravi Mistryb5e2acc2017-12-07 11:10:11 -0500510
Edward Lemur2b7876c2020-01-17 18:48:13 -0500511 # If the change includes only doc changes then add No-Try: true in the
512 # CL's description if it does not exist yet.
513 if all_docs_changes and 'true' not in footers.get('No-Try', []):
514 description_changed = True
Edward Lemurc631b7c2020-02-04 15:30:18 -0500515 change.AddDescriptionFooter('No-Try', 'true')
Edward Lemur2b7876c2020-01-17 18:48:13 -0500516 results.append(
517 output_api.PresubmitNotifyResult(
518 'This change has only doc changes. Automatically added '
519 '\'No-Try: true\' to the CL\'s description'))
rmistryd223fb22015-02-26 10:16:13 -0800520
Ravi Mistry27095f22021-04-22 12:51:49 +0000521 # If there is at least one docs change then add preview link in the CL's
522 # description if it does not already exist there.
523 docs_preview_link = DOCS_PREVIEW_URL.format(issue=change.issue)
524 if (at_least_one_docs_change
525 and docs_preview_link not in footers.get('Docs-Preview', [])):
526 # Automatically add a link to where the docs can be previewed.
527 description_changed = True
528 change.AddDescriptionFooter('Docs-Preview', docs_preview_link)
529 results.append(
530 output_api.PresubmitNotifyResult(
531 'Automatically added a link to preview the docs changes to the '
532 'CL\'s description'))
rmistryd223fb22015-02-26 10:16:13 -0800533
Edward Lemur2b7876c2020-01-17 18:48:13 -0500534 # If the description has changed update it.
535 if description_changed:
536 gerrit.UpdateDescription(
537 change.FullDescriptionText(), change.issue)
rmistryd223fb22015-02-26 10:16:13 -0800538
Edward Lemur2b7876c2020-01-17 18:48:13 -0500539 return results
rmistryd223fb22015-02-26 10:16:13 -0800540
541
rmistry@google.com8e3ff8c2013-01-17 12:55:34 +0000542def CheckChangeOnCommit(input_api, output_api):
Ravi Mistry4c0ffe72020-03-02 13:19:02 -0500543 """Presubmit checks for the change on commit."""
rmistry@google.com8e3ff8c2013-01-17 12:55:34 +0000544 results = []
rmistry@google.com6be0b4c2013-01-17 14:50:59 +0000545 results.extend(_CommonChecks(input_api, output_api))
rmistry@google.comfb4a68d2013-08-12 14:51:20 +0000546 results.extend(_CheckLGTMsForPublicAPI(input_api, output_api))
commit-bot@chromium.org745e08c2014-02-03 14:18:32 +0000547 results.extend(_CheckOwnerIsInAuthorsFile(input_api, output_api))
Ravi Mistrya70cb8a2017-09-12 13:52:05 -0400548 # Checks for the presence of 'DO NOT''SUBMIT' in CL description and in
549 # content of files.
550 results.extend(
551 input_api.canned_checks.CheckDoNotSubmit(input_api, output_api))
rmistry@google.com8e3ff8c2013-01-17 12:55:34 +0000552 return results